703 lines
23 KiB
Python
703 lines
23 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2020 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.
|
|
"""Utility for interacting with containeranalysis API."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import collections
|
|
import re
|
|
|
|
from googlecloudsdk.api_lib.containeranalysis import filter_util
|
|
from googlecloudsdk.api_lib.containeranalysis import requests as ca_requests
|
|
from googlecloudsdk.api_lib.services import enable_api
|
|
from googlecloudsdk.api_lib.services import exceptions as serviceusage_exceptions
|
|
import six
|
|
|
|
|
|
class ContainerAnalysisMetadata:
|
|
"""ContainerAnalysisMetadata defines metadata retrieved from containeranalysis API.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.vulnerability = PackageVulnerabilitySummary()
|
|
self.image = ImageBasisSummary()
|
|
self.discovery = DiscoverySummary()
|
|
self.deployment = DeploymentSummary()
|
|
self.build = BuildSummary()
|
|
self.provenance = ProvenanceSummary()
|
|
self.package = PackageSummary()
|
|
self.attestation = AttestationSummary()
|
|
self.upgrade = UpgradeSummary()
|
|
self.compliance = ComplianceSummary()
|
|
self.dsse_attestation = DsseAttestaionSummary()
|
|
self.sbom_reference = SbomReferenceSummary()
|
|
|
|
def AddOccurrence(self, occ, include_build=True):
|
|
"""Adds occurrences retrieved from containeranalysis API.
|
|
|
|
Generally we have a 1-1 correspondence between type and summary it's added
|
|
to. The exceptions (due to backwards compatibility issues) are:
|
|
BUILD: If you pass in --show-provenance, there will be a provenance
|
|
section (for intoto builds) but no build section. If you pass in
|
|
--show-all-metadata or --show-build-details, there will be a provenance
|
|
section (for intoto builds) and a builds section (for every build). That
|
|
does mean an occurrence may be in both provenance_summary and build_summary.
|
|
DSSE_ATTESTATION: We always return it in both the DSSE_ATTESTATION section
|
|
and the provenance section.
|
|
|
|
Args:
|
|
occ: the occurrence retrieved from the API.
|
|
include_build: whether build-kind occurrences should be added to build.
|
|
"""
|
|
messages = ca_requests.GetMessages()
|
|
if occ.kind == messages.Occurrence.KindValueValuesEnum.VULNERABILITY:
|
|
self.vulnerability.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.IMAGE:
|
|
self.image.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.DEPLOYMENT:
|
|
self.deployment.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.DISCOVERY:
|
|
self.discovery.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.DSSE_ATTESTATION:
|
|
self.provenance.AddOccurrence(occ)
|
|
elif (
|
|
occ.kind == messages.Occurrence.KindValueValuesEnum.BUILD
|
|
and occ.build
|
|
and (occ.build.intotoStatement or occ.build.inTotoSlsaProvenanceV1)
|
|
):
|
|
self.provenance.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.PACKAGE:
|
|
self.package.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.ATTESTATION:
|
|
self.attestation.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.UPGRADE:
|
|
self.upgrade.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.COMPLIANCE:
|
|
self.compliance.AddOccurrence(occ)
|
|
elif occ.kind == messages.Occurrence.KindValueValuesEnum.SBOM_REFERENCE:
|
|
self.sbom_reference.AddOccurrence(occ)
|
|
# DSSEAttestation should also have its own section, even if it was already
|
|
# added to the provenance section, as a user can make a non-provenance dsse.
|
|
if occ.kind == messages.Occurrence.KindValueValuesEnum.DSSE_ATTESTATION:
|
|
self.dsse_attestation.AddOccurrence(occ)
|
|
# BUILD should also have its own section, even if it was already
|
|
# added to the provenance section.
|
|
if (
|
|
occ.kind == messages.Occurrence.KindValueValuesEnum.BUILD
|
|
and include_build
|
|
):
|
|
self.build.AddOccurrence(occ)
|
|
|
|
def ImagesListView(self):
|
|
"""Returns a dictionary representing the metadata.
|
|
|
|
The returned dictionary is used by artifacts docker images list command.
|
|
"""
|
|
view = {}
|
|
if self.image.base_images:
|
|
view['IMAGE'] = self.image.base_images
|
|
if self.deployment.deployments:
|
|
view['DEPLOYMENT'] = self.deployment.deployments
|
|
if self.discovery.discovery:
|
|
view['DISCOVERY'] = self.discovery.discovery
|
|
if self.build.build_details:
|
|
view['BUILD'] = self.build.build_details
|
|
if self.package.packages:
|
|
view['PACKAGE'] = self.package.packages
|
|
if self.attestation.attestations:
|
|
view['ATTESTATION'] = self.attestation.attestations
|
|
if self.upgrade.upgrades:
|
|
view['UPGRADE'] = self.upgrade.upgrades
|
|
if self.compliance.compliances:
|
|
view['COMPLIANCE'] = self.compliance.compliances
|
|
if self.dsse_attestation.dsse_attestations:
|
|
view['DSSE_ATTESTATION'] = self.dsse_attestation.dsse_attestations
|
|
if self.sbom_reference.sbom_references:
|
|
view['SBOM_REFERENCE'] = self.sbom_reference.sbom_references
|
|
view.update(self.vulnerability.ImagesListView())
|
|
return view
|
|
|
|
def ArtifactsDescribeView(self):
|
|
"""Returns a dictionary representing the metadata.
|
|
|
|
The returned dictionary is used by artifacts docker images describe command.
|
|
"""
|
|
view = {}
|
|
if self.image.base_images:
|
|
view['image_basis_summary'] = self.image
|
|
if self.deployment.deployments:
|
|
view['deployment_summary'] = self.deployment
|
|
if self.discovery.discovery:
|
|
view['discovery_summary'] = self.discovery
|
|
if self.build.build_details:
|
|
view['build_details_summary'] = self.build
|
|
vuln = self.vulnerability.ArtifactsDescribeView()
|
|
if vuln:
|
|
view['package_vulnerability_summary'] = vuln
|
|
if self.provenance.provenance:
|
|
view['provenance_summary'] = self.provenance
|
|
if self.package.packages:
|
|
view['package_summary'] = self.package
|
|
if self.attestation.attestations:
|
|
view['attestation_summary'] = self.attestation
|
|
if self.upgrade.upgrades:
|
|
view['upgrade_summary'] = self.upgrade
|
|
if self.compliance.compliances:
|
|
view['compliance_summary'] = self.compliance
|
|
if self.dsse_attestation.dsse_attestations:
|
|
view['dsse_attestation_summary'] = self.dsse_attestation
|
|
if self.sbom_reference.sbom_references:
|
|
view['sbom_summary'] = self.sbom_reference
|
|
return view
|
|
|
|
def SLSABuildLevel(self):
|
|
"""Returns SLSA build level 0-3 or unknown."""
|
|
if self.provenance.provenance:
|
|
return _ComputeSLSABuildLevel(self.provenance.provenance)
|
|
return 'unknown'
|
|
|
|
def SbomLocations(self):
|
|
return [
|
|
sbom_ref.sbomReference.payload.predicate.location
|
|
for sbom_ref in self.sbom_reference.sbom_references
|
|
]
|
|
|
|
|
|
class PackageVulnerabilitySummary:
|
|
"""PackageVulnerabilitySummary holds package vulnerability information."""
|
|
|
|
def __init__(self):
|
|
self.vulnerabilities = {}
|
|
self.counts = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
sev = six.text_type(occ.vulnerability.effectiveSeverity)
|
|
self.vulnerabilities.setdefault(sev, []).append(occ)
|
|
|
|
def AddSummary(self, summary):
|
|
self.counts += summary.counts
|
|
|
|
def AddCount(self, count):
|
|
self.counts.append(count)
|
|
|
|
def ArtifactsDescribeView(self):
|
|
"""Returns a dictionary representing package vulnerability metadata.
|
|
|
|
The returned dictionary is used by artifacts docker images describe command.
|
|
"""
|
|
messages = ca_requests.GetMessages()
|
|
view = {}
|
|
if self.vulnerabilities:
|
|
view['vulnerabilities'] = self.vulnerabilities
|
|
for count in self.counts:
|
|
# SEVERITY_UNSPECIFIED represents total counts across all serverities
|
|
if (count.severity == messages.FixableTotalByDigest
|
|
.SeverityValueValuesEnum.SEVERITY_UNSPECIFIED):
|
|
view['not_fixed_vulnerability_count'] = (
|
|
count.totalCount - count.fixableCount)
|
|
view['total_vulnerability_count'] = count.totalCount
|
|
break
|
|
return view
|
|
|
|
def ImagesListView(self):
|
|
"""Returns a dictionary representing package vulnerability metadata.
|
|
|
|
The returned dictionary is used by artifacts docker images list command.
|
|
"""
|
|
messages = ca_requests.GetMessages()
|
|
view = {}
|
|
if self.vulnerabilities:
|
|
view['PACKAGE_VULNERABILITY'] = self.vulnerabilities
|
|
vuln_counts = {}
|
|
for count in self.counts:
|
|
# SEVERITY_UNSPECIFIED represents total counts across all serverities
|
|
sev = count.severity
|
|
if (sev and sev != messages.FixableTotalByDigest.SeverityValueValuesEnum
|
|
.SEVERITY_UNSPECIFIED):
|
|
vuln_counts.update({sev: vuln_counts.get(sev, 0) + count.totalCount})
|
|
if vuln_counts:
|
|
view['vuln_counts'] = vuln_counts
|
|
return view
|
|
|
|
|
|
class ImageBasisSummary:
|
|
"""ImageBasisSummary holds image basis information."""
|
|
|
|
def __init__(self):
|
|
self.base_images = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.base_images.append(occ)
|
|
|
|
|
|
class BuildSummary:
|
|
"""BuildSummary holds image build information."""
|
|
|
|
def __init__(self):
|
|
self.build_details = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.build_details.append(occ)
|
|
|
|
|
|
class DeploymentSummary:
|
|
"""DeploymentSummary holds image deployment information."""
|
|
|
|
def __init__(self):
|
|
self.deployments = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.deployments.append(occ)
|
|
|
|
|
|
class DiscoverySummary:
|
|
"""DiscoverySummary holds image vulnerability discovery information."""
|
|
|
|
def __init__(self):
|
|
self.discovery = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.discovery.append(occ)
|
|
|
|
|
|
class ProvenanceSummary:
|
|
"""ProvenanceSummary holds image provenance information."""
|
|
|
|
def __init__(self):
|
|
self.provenance = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.provenance.append(occ)
|
|
|
|
|
|
class PackageSummary:
|
|
"""PackageSummary holds image package information."""
|
|
|
|
def __init__(self):
|
|
self.packages = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.packages.append(occ)
|
|
|
|
|
|
class AttestationSummary:
|
|
"""AttestationSummary holds image attestation information."""
|
|
|
|
def __init__(self):
|
|
self.attestations = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.attestations.append(occ)
|
|
|
|
|
|
class UpgradeSummary:
|
|
"""UpgradeSummary holds image upgrade information."""
|
|
|
|
def __init__(self):
|
|
self.upgrades = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.upgrades.append(occ)
|
|
|
|
|
|
class ComplianceSummary:
|
|
"""ComplianceSummary holds image compliance information."""
|
|
|
|
def __init__(self):
|
|
self.compliances = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.compliances.append(occ)
|
|
|
|
|
|
class DsseAttestaionSummary:
|
|
"""DsseAttestaionSummary holds image dsse_attestation information."""
|
|
|
|
def __init__(self):
|
|
self.dsse_attestations = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.dsse_attestations.append(occ)
|
|
|
|
|
|
class SbomReferenceSummary:
|
|
"""SbomReferenceSummary holds image SBOM reference information."""
|
|
|
|
def __init__(self):
|
|
self.sbom_references = []
|
|
|
|
def AddOccurrence(self, occ):
|
|
self.sbom_references.append(occ)
|
|
|
|
|
|
def GetContainerAnalysisMetadata(docker_version, args):
|
|
"""Retrieves metadata for a docker image."""
|
|
metadata = ContainerAnalysisMetadata()
|
|
docker_urls = [
|
|
'https://{}'.format(docker_version.GetDockerString()),
|
|
docker_version.GetDockerString(),
|
|
]
|
|
occ_filter = _CreateFilterFromImagesDescribeArgs(docker_urls, args)
|
|
if occ_filter is None:
|
|
return metadata
|
|
occurrences = ca_requests.ListOccurrences(docker_version.project, occ_filter)
|
|
include_build = (
|
|
args.show_build_details or args.show_all_metadata or args.metadata_filter
|
|
)
|
|
for occ in occurrences:
|
|
metadata.AddOccurrence(occ, include_build)
|
|
|
|
if metadata.vulnerability.vulnerabilities:
|
|
vuln_summary = ca_requests.GetVulnerabilitySummary(
|
|
docker_version.project,
|
|
filter_util.ContainerAnalysisFilter().WithResources(
|
|
docker_urls).GetFilter())
|
|
metadata.vulnerability.AddSummary(vuln_summary)
|
|
return metadata
|
|
|
|
|
|
def GetImageSummaryMetadata(docker_version):
|
|
"""Retrieves build and SBOM metadata for a docker image.
|
|
|
|
This function is used only for SLSA build level computation and retrieving
|
|
SBOM locations. If the containeranalysis API is disabled for the project, no
|
|
request will be sent and it returns empty metadata resulting in 'unknown' SLSA
|
|
level.
|
|
|
|
Args:
|
|
docker_version: docker info about image and project.
|
|
|
|
Returns:
|
|
The build and SBOM metadata for the given image.
|
|
"""
|
|
metadata = ContainerAnalysisMetadata()
|
|
try:
|
|
ca_enabled = enable_api.IsServiceEnabled(
|
|
docker_version.project, 'containeranalysis.googleapis.com'
|
|
)
|
|
except serviceusage_exceptions.GetServicePermissionDeniedException:
|
|
# Do not raise the exception, as it will break the command.
|
|
ca_enabled = False
|
|
|
|
if not ca_enabled:
|
|
return metadata
|
|
|
|
docker_urls = [
|
|
'https://{}'.format(docker_version.GetDockerString()),
|
|
docker_version.GetDockerString(),
|
|
]
|
|
occ_filter = _CreateFilterForImageSummaryOccurrences(docker_urls)
|
|
occurrences = ca_requests.ListOccurrences(docker_version.project, occ_filter)
|
|
for occ in occurrences:
|
|
metadata.AddOccurrence(occ, False)
|
|
|
|
return metadata
|
|
|
|
|
|
def GetArtifactOccurrences(project, artifact_resource):
|
|
"""Retrieves occurrences for Maven artifacts."""
|
|
metadata = ContainerAnalysisMetadata()
|
|
|
|
occ_filter = _CreateFilterForMaven(artifact_resource)
|
|
|
|
occurrences = ca_requests.ListOccurrences(project, occ_filter)
|
|
for occ in occurrences:
|
|
metadata.AddOccurrence(occ, False)
|
|
|
|
return metadata
|
|
|
|
|
|
def GetContainerAnalysisMetadataForImages(repo_or_image, occurrence_filter,
|
|
images):
|
|
"""Retrieves metadata for all images with a given path prefix.
|
|
|
|
The prefix may initially be used to resolve to a list of images if
|
|
--show-occurrences-from is used.
|
|
To account for cases where there is or isn't a list of images,
|
|
this always filters on both prefix and the list of images. In both of
|
|
those cases, the lookup is for both the case where there is and isn't
|
|
an https prefix, in both the prefixes and in the images list.
|
|
|
|
Args:
|
|
repo_or_image: The repository originally given by the user.
|
|
occurrence_filter: The repository originally given by the user.
|
|
images: The list of images that matched the prefix, without https prepended.
|
|
|
|
Returns:
|
|
The metadata about the given images.
|
|
"""
|
|
metadata = collections.defaultdict(ContainerAnalysisMetadata)
|
|
prefixes = [
|
|
'https://{}'.format(repo_or_image.GetDockerString()),
|
|
repo_or_image.GetDockerString()
|
|
]
|
|
image_urls = images + ['https://{}'.format(img) for img in images]
|
|
occ_filters = _CreateFilterForImages(prefixes, occurrence_filter, image_urls)
|
|
occurrences = ca_requests.ListOccurrencesWithFilters(repo_or_image.project,
|
|
occ_filters)
|
|
for occ in occurrences:
|
|
metadata.setdefault(occ.resourceUri,
|
|
ContainerAnalysisMetadata()).AddOccurrence(occ)
|
|
|
|
summary_filters = filter_util.ContainerAnalysisFilter().WithResourcePrefixes(
|
|
prefixes).WithResources(image_urls).GetChunkifiedFilters()
|
|
summaries = ca_requests.GetVulnerabilitySummaryWithFilters(
|
|
repo_or_image.project, summary_filters)
|
|
for summary in summaries:
|
|
for count in summary.counts:
|
|
metadata.setdefault(
|
|
count.resourceUri,
|
|
ContainerAnalysisMetadata()).vulnerability.AddCount(count)
|
|
|
|
return metadata
|
|
|
|
|
|
def _CreateFilterForMaven(maven_resource):
|
|
"""Builds filters for containeranalysis APIs for Maven Artifacts."""
|
|
occ_filter = filter_util.ContainerAnalysisFilter()
|
|
|
|
filter_kinds = ['VULNERABILITY', 'DISCOVERY']
|
|
|
|
occ_filter.WithKinds(filter_kinds)
|
|
occ_filter.WithResources([maven_resource])
|
|
return occ_filter.GetFilter()
|
|
|
|
|
|
def _CreateFilterForImageSummaryOccurrences(images):
|
|
"""Builds filters for containeranalysis APIs for build and SBOM occurrences."""
|
|
occ_filter = filter_util.ContainerAnalysisFilter()
|
|
|
|
filter_kinds = ['BUILD', 'SBOM_REFERENCE']
|
|
|
|
occ_filter.WithKinds(filter_kinds)
|
|
occ_filter.WithResources(images)
|
|
return occ_filter.GetFilter()
|
|
|
|
|
|
def _CreateFilterFromImagesDescribeArgs(images, args):
|
|
r"""Parses `docker images describe` arguments into a filter to send to containeranalysis API.
|
|
|
|
The returned filter will combine the user-provided filter specified by
|
|
the --metadata-filter flag and occurrence kind filters specified by flags
|
|
such as --show-package-vulnerability.
|
|
|
|
Returns None if there is no information to fetch from containeranalysis API.
|
|
|
|
Args:
|
|
images: list, the fully-qualified path of docker images.
|
|
args: user provided command line arguments.
|
|
|
|
Returns:
|
|
A filter string to send to the containeranalysis API.
|
|
|
|
For example, given a user input:
|
|
gcloud docker images describe \
|
|
us-west1-docker.pkg.dev/my-project/my-repo/ubuntu@sha256:abc \
|
|
--show-package-vulnerability \
|
|
--show-image-basis \
|
|
--metadata-filter='createTime>"2019-04-10T"'
|
|
|
|
this method will create a filter:
|
|
|
|
'''
|
|
((kind="VULNERABILITY") OR (kind="IMAGE")) AND
|
|
(createTime>"2019-04-10T") AND
|
|
(resourceUrl=us-west1-docker.pkg.dev/my-project/my-repo/ubuntu@sha256:abc' OR
|
|
resourceUrl=https://us-west1-docker.pkg.dev/my-project/my-repo/ubuntu@sha256:abc'))
|
|
'''
|
|
"""
|
|
|
|
occ_filter = filter_util.ContainerAnalysisFilter()
|
|
filter_kinds = []
|
|
# We don't need to filter on kinds when showing all metadata
|
|
if not args.show_all_metadata:
|
|
if args.show_build_details:
|
|
filter_kinds.append('BUILD')
|
|
if args.show_package_vulnerability:
|
|
filter_kinds.append('VULNERABILITY')
|
|
filter_kinds.append('DISCOVERY')
|
|
if args.show_image_basis:
|
|
filter_kinds.append('IMAGE')
|
|
if args.show_deployment:
|
|
filter_kinds.append('DEPLOYMENT')
|
|
if args.show_provenance:
|
|
filter_kinds.append('DSSE_ATTESTATION')
|
|
filter_kinds.append('BUILD')
|
|
if args.show_sbom_references:
|
|
filter_kinds.append('SBOM_REFERENCE')
|
|
|
|
# args include none of the occurrence types, there's no need to call the
|
|
# containeranalysis API.
|
|
# The exception to this is where there is a user provided filter.
|
|
if not filter_kinds and not args.metadata_filter:
|
|
return None
|
|
|
|
occ_filter.WithKinds(filter_kinds)
|
|
occ_filter.WithCustomFilter(args.metadata_filter)
|
|
occ_filter.WithResources(images)
|
|
return occ_filter.GetFilter()
|
|
|
|
|
|
def _CreateFilterForImages(prefixes, custom_filter, images):
|
|
"""Creates a list of filters from a docker image prefix, a custom filter and fully-qualified image URLs.
|
|
|
|
Args:
|
|
prefixes: URL prefixes. Only metadata of images with any of these prefixes
|
|
will be retrieved.
|
|
custom_filter: user provided filter string.
|
|
images: fully-qualified docker image URLs. Only metadata of these images
|
|
will be retrieved.
|
|
|
|
Returns:
|
|
A filter string to send to the containeranalysis API.
|
|
"""
|
|
occ_filter = filter_util.ContainerAnalysisFilter()
|
|
occ_filter.WithResourcePrefixes(prefixes)
|
|
occ_filter.WithResources(images)
|
|
occ_filter.WithCustomFilter(custom_filter)
|
|
return occ_filter.GetChunkifiedFilters()
|
|
|
|
|
|
def _ComputeSLSABuildLevel(provenance):
|
|
"""Computes SLSA build level from a build provenance.
|
|
|
|
Determines SLSA Level based on a list of occurrences,
|
|
preferring data from SLSA v1.0 occurrences over others.
|
|
|
|
Args:
|
|
provenance: build provenance list containing build occurrences.
|
|
|
|
Returns:
|
|
A string `unknown` if build provenance doesn't exist, otherwise
|
|
an integer from 0 to 3 indicating SLSA build level.
|
|
"""
|
|
if not provenance:
|
|
return 'unknown'
|
|
|
|
builder_id_v1 = 'https://cloudbuild.googleapis.com/GoogleHostedWorker'
|
|
builds_v1 = [
|
|
p for p in provenance if p.build and p.build.inTotoSlsaProvenanceV1
|
|
]
|
|
for build_v1 in builds_v1:
|
|
provenance_v1 = build_v1.build.inTotoSlsaProvenanceV1
|
|
# GCB Build Occurrences that populate SLSA v1.0 data
|
|
# always have SLSA Level 3.
|
|
if (
|
|
provenance_v1.predicate
|
|
and provenance_v1.predicate.runDetails
|
|
and provenance_v1.predicate.runDetails.builder
|
|
and provenance_v1.predicate.runDetails.builder.id
|
|
and provenance_v1.predicate.runDetails.builder.id == builder_id_v1
|
|
):
|
|
return 3
|
|
|
|
# No SLSA v1.0 data was found, just compute the SLSA level from
|
|
# the first occurrence found with defined slsaProvenance.
|
|
builds_v0_1 = [
|
|
p for p in provenance if p.build and p.build.intotoStatement
|
|
]
|
|
if not builds_v0_1:
|
|
return 'unknown'
|
|
provenance = builds_v0_1[0]
|
|
intoto = provenance.build.intotoStatement
|
|
|
|
if _HasSteps(intoto):
|
|
if _HasValidKey(provenance):
|
|
if _HasLevel3BuildVersion(intoto):
|
|
return 3
|
|
return 2
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _HasSteps(intoto):
|
|
"""Check whether a build provenance contains build steps.
|
|
|
|
Args:
|
|
intoto: intoto statement in build occurrence.
|
|
|
|
Returns:
|
|
A boolean value indicating whether intoto contains build steps.
|
|
"""
|
|
if (
|
|
intoto
|
|
and hasattr(intoto, 'slsaProvenance')
|
|
and hasattr(intoto.slsaProvenance, 'recipe')
|
|
and hasattr(intoto.slsaProvenance.recipe, 'arguments')
|
|
and hasattr(
|
|
intoto.slsaProvenance.recipe.arguments, 'additionalProperties'
|
|
)
|
|
):
|
|
properties = intoto.slsaProvenance.recipe.arguments.additionalProperties
|
|
return any(p.key == 'steps' and p.value for p in properties)
|
|
return False
|
|
|
|
|
|
def _HasValidKey(build):
|
|
"""Check whether a build provenance contains valid signature and key id.
|
|
|
|
Args:
|
|
build: container analysis build occurrence.
|
|
|
|
Returns:
|
|
A boolean value indicating whether build occurrence contains valid signature
|
|
and key id.
|
|
"""
|
|
if (
|
|
build
|
|
and hasattr(build, 'envelope')
|
|
and hasattr(build.envelope, 'signatures')
|
|
and build.envelope.signatures
|
|
):
|
|
key_id_pattern = '^projects/verified-builder/locations/.+/keyRings/attestor/cryptoKeys/builtByGCB/cryptoKeyVersions/1$'
|
|
|
|
def CheckSignature(signature):
|
|
return (hasattr(signature, 'sig') and
|
|
signature.sig and
|
|
hasattr(signature, 'keyid') and
|
|
re.match(key_id_pattern, signature.keyid))
|
|
filtered = filter(CheckSignature, build.envelope.signatures)
|
|
if filtered:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _HasLevel3BuildVersion(intoto):
|
|
"""Check whether a build provenance contains level 3 build version.
|
|
|
|
Args:
|
|
intoto: intoto statement in build occurrence.
|
|
|
|
Returns:
|
|
A boolean value indicating whether intoto contains level 3 build version.
|
|
"""
|
|
if (
|
|
intoto
|
|
and hasattr(intoto, 'slsaProvenance')
|
|
and hasattr(intoto.slsaProvenance, 'builder')
|
|
and hasattr(intoto.slsaProvenance.builder, 'id')
|
|
and intoto.slsaProvenance.builder.id
|
|
):
|
|
[uri, version] = intoto.slsaProvenance.builder.id.split('@v')
|
|
if (
|
|
uri == 'https://cloudbuild.googleapis.com/GoogleHostedWorker'
|
|
and version
|
|
):
|
|
[major_version, minor_version] = version.split('.')
|
|
return int(major_version) > 0 or int(minor_version) >= 3
|
|
return False
|