File: //snap/google-cloud-cli/current/lib/surface/artifacts/sbom/load.py
# -*- coding: utf-8 -*- #
# Copyright 2023 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Implements the command to upload an SBOM file."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.artifacts import endpoint_util
from googlecloudsdk.command_lib.artifacts import flags
from googlecloudsdk.command_lib.artifacts import sbom_util
from googlecloudsdk.command_lib.artifacts import util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Load(base.Command):
"""Upload an SBOM file and create a reference occurrence."""
detailed_help = {
'DESCRIPTION': '{description}',
'EXAMPLES': """\
To upload an SBOM file at /path/to/sbom.json for a Docker image in Artifact Registry:
$ {command} --source=/path/to/sbom.json \
--uri=us-west1-docker.pkg.dev/my-project/my-repository/busy-box@sha256:abcxyz
To upload an SBOM file at /path/to/sbom.json for a Docker image with a KMS key version to sign the created SBOM reference:
$ {command} --source=/path/to/sbom.json \
--uri=us-west1-docker.pkg.dev/my-project/my-repository/busy-box@sha256:abcxyz \
--kms-key-version=projects/my-project/locations/us-west1/keyRings/my-key-ring/cryptoKeys/my-key/cryptoKeyVersions/1
To upload an SBOM file at /path/to/sbom.json for a Docker image from a Docker registry:
$ {command} --source=/path/to/sbom.json \
--uri=my-docker-registry/my-image@sha256:abcxyz \
--destination=gs://my-cloud-storage-bucket
""",
}
@staticmethod
def Args(parser):
"""Set up arguments for this command.
Args:
parser: An argparse.ArgumentPaser.
"""
parser.add_argument(
'--source',
metavar='SOURCE',
required=True,
default='.',
help='The SBOM file for uploading.',
)
parser.add_argument(
'--uri',
metavar='ARTIFACT_URI',
required=True,
help="""\
The URI of the artifact the SBOM is generated from.
The URI can be a Docker image from any Docker registries. A URI provided with a tag (e.g. `[IMAGE]:[TAG]`) will be resolved into a URI with a digest (`[IMAGE]@sha256:[DIGEST]`).
When passing an image which is not from Artifact Registry or Container Registry with a tag, only public images can be resolved.
Also, when passing an image which is not from Artifact Registry or Container Registry, the `--destination` flag is required.
""",
)
parser.add_argument(
'--kms-key-version',
default=None,
help="""\
Cloud KMS key version to sign the SBOM reference.
The key version provided should be the resource ID in the format of
`projects/[KEY_PROJECT_ID]/locations/[LOCATION]/keyRings/[RING_NAME]/cryptoKeys/[KEY_NAME]/cryptoKeyVersions/[KEY_VERSION]`.
""",
required=False,
type=arg_parsers.RegexpValidator(
r'^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+/cryptoKeyVersions/[^/]+$',
(
'Must be in format of'
" 'projects/[KEY_PROJECT_ID]/locations/[LOCATION]/keyRings/[RING_NAME]/cryptoKeys/[KEY_NAME]/cryptoKeyVersions/[KEY_VERSION]'"
),
),
)
parser.add_argument(
'--destination',
metavar='DESTINATION',
default=None,
required=False,
help="""\
The storage path will be used to store the SBOM file.
Currently only supports Cloud Storage paths start with 'gs://'.
""",
type=arg_parsers.RegexpValidator(
r'^gs://.*$', 'Must be in format of gs://[STORAGE_PATH]'
),
)
flags.GetOptionalAALocationFlag().AddToParser(parser)
def Run(self, args):
"""Run the load command."""
# Parse file and get the version.
s = sbom_util.ParseJsonSbom(args.source)
log.info(
'Successfully loaded the SBOM file. Format: {0}-{1}.'.format(
s.sbom_format, s.version
)
)
if not args.uri:
raise exceptions.InvalidArgumentException(
'ARTIFACT_URI',
'--uri is required.',
)
# Get information from the artifact.
a = sbom_util.ProcessArtifact(args.uri)
log.info(
(
'Processed artifact. '
+ 'Project: {0}, Location: {1}, URI: {2}, Digest {3}.'
).format(
a.project, a.location, a.resource_uri, a.digests.get('sha256', '')
)
)
if (
sbom_util.ARTIFACT_TYPE_OTHER == a.artifact_type
and not args.destination
):
raise exceptions.InvalidArgumentException(
'DESTINATION',
(
'--destination is required for images not in Artifact Registry or'
' Container Registry.'
),
)
# Upload SBOM to a GCS bucket.
remote_path = sbom_util.UploadSbomToGCS(
source=args.source,
artifact=a,
sbom=s,
gcs_path=args.destination,
)
log.info('Uploaded the SBOM file at {0}'.format(remote_path))
# Create occurrence in the same project as the artifact's project.
# If artifact doesn't have a project (e.g. non AR/GCR images), we will use
# the current working project.
project_id = a.project
if not project_id:
project_id = args.project or properties.VALUES.core.project.GetOrFail()
log.info(
(
'Failed to get project_id from the artifact URI {0}. Using'
' project {1} for occurrence.'
).format(args.uri, project_id)
)
parent = util.GetParent(project_id, args.location)
# Write reference occurrence.
with endpoint_util.WithRegion(args.location):
occurrence_id = sbom_util.WriteReferenceOccurrence(
artifact=a,
project_id=parent,
storage=remote_path,
sbom=s,
kms_key_version=args.kms_key_version,
)
log.info('Wrote reference occurrence {0}.'.format(occurrence_id))
log.status.Print(
'Uploaded the SBOM file under the resource URI {}.'.format(
a.GetOccurrenceResourceUri()
)
)