# -*- 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 list vulnerabilities from Artifact Registry.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import re from googlecloudsdk.api_lib.artifacts import exceptions as ar_exceptions from googlecloudsdk.api_lib.artifacts.vulnerabilities import GetLatestScan from googlecloudsdk.api_lib.artifacts.vulnerabilities import GetVulnerabilities from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.artifacts import docker_util from googlecloudsdk.command_lib.artifacts import endpoint_util from googlecloudsdk.command_lib.artifacts import flags from googlecloudsdk.command_lib.artifacts import format_util DEFAULT_LIST_FORMAT = """\ table[box, title="%TITLE%"]( occurrence.vulnerability.shortDescription:label=CVE, occurrence.vulnerability.effectiveSeverity:label=EFFECTIVE_SEVERITY, occurrence.vulnerability.cvssScore:label=CVSS:sort=-1:reverse, occurrence.vulnerability.packageIssue.fixAvailable:label=FIX_AVAILABLE, occurrence.vulnerability.vexAssessment.state:label=VEX_STATUS, occurrence.vulnerability.packageIssue.affectedPackage:sort=3:label=PACKAGE, occurrence.vulnerability.packageIssue.packageType:label=PACKAGE_TYPE, vexScope, {} ) """.format(format_util.CONTAINER_ANALYSIS_METADATA_FORMAT) @base.ReleaseTracks(base.ReleaseTrack.GA) @base.DefaultUniverseOnly class List(base.ListCommand): """Command for listing vulnerabilities. To see all fields, use --format=json. """ detailed_help = { 'DESCRIPTION': '{description}', 'EXAMPLES': """\ To list vulnerabilities for an artifact, run: $ {command} us-east1-docker.pkg.dev/project123/repository123/someimage@sha256:49765698074d6d7baa82f """, } @staticmethod def Args(parser): flags.GetListURIArg().AddToParser(parser) flags.GetOptionalAALocationFlag().AddToParser(parser) flags.GetVulnerabilitiesOccurrenceFilterFlag().AddToParser(parser) parser.display_info.AddFlatten(['occurrence.vulnerability.packageIssue']) return def Run(self, args): location = args.location occurrence_filter = args.occurrence_filter resource, project = self.replaceTags(args.URI) with endpoint_util.WithRegion(location): if location is not None: project = '{}/locations/{}'.format(project, location) latest_scan = GetLatestScan(project, resource) occurrences = GetVulnerabilities(project, resource, occurrence_filter) self.setTitle(args, latest_scan) occurrences = list(occurrences) results = [] if len(occurrences) < 1: return {} for occ in occurrences: vex_scope = '' if ( occ.vulnerability and occ.vulnerability.vexAssessment and occ.vulnerability.vexAssessment.noteName ): tokens = occ.vulnerability.vexAssessment.noteName.split('/') if tokens[-1].startswith('image-'): vex_scope = 'IMAGE' else: vex_scope = 'DIGEST' results.append(VulnerabilityEntry(occ, vex_scope)) return results def replaceTags(self, original_uri): updated_uri = original_uri if not updated_uri.startswith('https://'): updated_uri = 'https://{}'.format(updated_uri) found = re.findall(docker_util.DOCKER_URI_REGEX, updated_uri) if found: resource_uri_str = found[0][0] is_gcr = 'gcr.io' in found[0][1] if is_gcr: resource_uri_str, _, _ = docker_util.ConvertGCRImageString( resource_uri_str, ) image, version = docker_util.DockerUrlToVersion(resource_uri_str) if is_gcr: version = docker_util.GcrDockerVersion( image.docker_repo.project, version.GetDockerString().replace( image.docker_repo.GetDockerString(), '{}/{}'.format( image.docker_repo.repo, # AR repo name is the gcr_host image.docker_repo.project, ), ), ) project = image.project docker_html_str_digest = 'https://{}'.format(version.GetDockerString()) updated_uri = re.sub( docker_util.DOCKER_URI_REGEX, docker_html_str_digest, updated_uri, 1, ) return updated_uri, project raise ar_exceptions.InvalidInputValueError( 'Received invalid URI {}'.format(original_uri) ) def setTitle(self, args, latest_scan): title = '' if ( not latest_scan or latest_scan.discovery is None or latest_scan.discovery.lastScanTime is None ): title = 'Scan status unknown' else: last_scan_time = latest_scan.discovery.lastScanTime[:-11] title = 'Latest scan was at {}'.format(last_scan_time) list_format = DEFAULT_LIST_FORMAT.replace('%TITLE%', title) args.GetDisplayInfo().AddFormat(list_format) class VulnerabilityEntry(object): """Holder for an entry of vulnerability list results. Properties: occurrence: Vulnerability occurrence. vex_scope: Scope of the VEX statement. """ def __init__(self, occurrence, vex_scope): self._occurrence = occurrence self._vex_scope = vex_scope @property def occurrence(self): return self._occurrence @property def vex_scope(self): return self._vex_scope