UNPKG

snyk-docker-plugin

Version:
331 lines (286 loc) 8.65 kB
import * as Debug from "debug"; import { analyseDockerfile } from "../dockerfile"; import { DockerFileAnalysis } from "../dockerfile/types"; import { InTotoStatement, ResolvedAttestationManifest } from "./types"; const debug = Debug("snyk"); const MAX_ATTESTATIONS_PER_IMAGE = 10; type SlsaProvenanceVersion = | "https://slsa.dev/provenance/v0.2" | "https://slsa.dev/provenance/v1"; export interface DockerfileMetadata { name: string; analysis: DockerFileAnalysis | null; } export interface ProvenanceMetadata { buildTimestamp: string | null; buildConfigDigest: string | null; buildConfigDigestSource: "remote" | "local" | null; attestedManifestDigest: string; attestationManifestDigest: string; buildConfigSourceUri: string | null; builderId: string; buildType: string; dockerfileMetadata: DockerfileMetadata; } interface BuildkitSourceInfo { filename?: string; data?: string; } interface SlsaPredicateV0_2 { builder?: { id?: string }; buildType?: string; metadata?: { buildStartedOn?: string; "https://mobyproject.org/buildkit@v1#metadata"?: { vcs?: { source?: string; revision?: string }; source?: { infos?: BuildkitSourceInfo[]; }; }; }; invocation?: { configSource?: { uri?: string; digest?: { [algorithm: string]: string }; entryPoint?: string; }; }; } interface SlsaPredicateV1_0 { buildDefinition?: { buildType?: string; externalParameters?: { configSource?: { uri?: string; digest?: { [algorithm: string]: string }; path?: string; }; }; }; runDetails?: { builder?: { id?: string }; metadata?: { startedOn?: string; buildkit_metadata?: { vcs?: { revision?: string }; source?: { infos?: BuildkitSourceInfo[]; }; }; }; }; } function getDecodedDockerfileContents( infos: BuildkitSourceInfo[] | undefined, dockerfileName: string, ): string | null { const match = infos?.find((info) => info.filename === dockerfileName); if (!match?.data) { return null; } return Buffer.from(match.data, "base64").toString("utf8"); } async function analyseDecodedDockerfile( infos: BuildkitSourceInfo[] | undefined, dockerfileName: string, ): Promise<DockerFileAnalysis | null> { const contents = getDecodedDockerfileContents(infos, dockerfileName); if (contents === null) { return null; } return analyseDockerfile(contents); } function getConfigSourceCommit(digest?: { [algorithm: string]: string; }): string | null { if (!digest) { return null; } const [algorithm, value] = Object.entries(digest)[0] ?? []; if (!algorithm || !value) { return null; } return `${algorithm}:${value}`; } async function extractFieldsSlsaV0_2( predicate: SlsaPredicateV0_2, attestedManifestDigest: string, attestationManifestDigest: string, ): Promise<ProvenanceMetadata> { const buildTimestamp = predicate.metadata?.buildStartedOn || null; const buildkitMeta = predicate.metadata?.["https://mobyproject.org/buildkit@v1#metadata"]; const remoteCommit = getConfigSourceCommit( predicate.invocation?.configSource?.digest, ); const localCommit = buildkitMeta?.vcs?.revision; const buildConfigDigest = remoteCommit || localCommit || null; const buildConfigDigestSource = remoteCommit ? "remote" : localCommit ? "local" : null; const buildConfigSourceUri = predicate.invocation?.configSource?.uri || null; const builderId = predicate.builder?.id || ""; const buildType = predicate.buildType || ""; const dockerfileName = predicate.invocation?.configSource?.entryPoint || "Dockerfile"; const dockerfileAnalysis = await analyseDecodedDockerfile( buildkitMeta?.source?.infos, dockerfileName, ); return { buildTimestamp, buildConfigDigest, buildConfigDigestSource, attestedManifestDigest, attestationManifestDigest, buildConfigSourceUri, builderId, buildType, dockerfileMetadata: { name: dockerfileName, analysis: dockerfileAnalysis, }, }; } async function extractFieldsSlsaV1_0( predicate: SlsaPredicateV1_0, attestedManifestDigest: string, attestationManifestDigest: string, ): Promise<ProvenanceMetadata> { const runDetails = predicate.runDetails; const buildDefinition = predicate.buildDefinition; const buildTimestamp = runDetails?.metadata?.startedOn || null; const remoteCommit = getConfigSourceCommit( buildDefinition?.externalParameters?.configSource?.digest, ); const localCommit = runDetails?.metadata?.buildkit_metadata?.vcs?.revision; const buildConfigDigest = remoteCommit || localCommit || null; const buildConfigDigestSource = remoteCommit ? "remote" : localCommit ? "local" : null; const buildConfigSourceUri = buildDefinition?.externalParameters?.configSource?.uri || null; const builderId = runDetails?.builder?.id || ""; const buildType = buildDefinition?.buildType || ""; const dockerfileName = buildDefinition?.externalParameters?.configSource?.path || "Dockerfile"; const dockerfileAnalysis = await analyseDecodedDockerfile( runDetails?.metadata?.buildkit_metadata?.source?.infos, dockerfileName, ); return { buildTimestamp, buildConfigDigest, buildConfigDigestSource, attestedManifestDigest, attestationManifestDigest, buildConfigSourceUri, builderId, buildType, dockerfileMetadata: { name: dockerfileName, analysis: dockerfileAnalysis, }, }; } function getAttestedManifestDigest(statement: InTotoStatement): string | null { const digest = statement.subject?.[0]?.digest; if (!digest) { return null; } const [algorithm, value] = Object.entries(digest)[0] ?? []; if (!algorithm || !value) { return null; } return `${algorithm}:${value}`; } function getSlsaProvenanceVersion( predicateType?: string, ): SlsaProvenanceVersion | null { switch (predicateType) { case "https://slsa.dev/provenance/v0.2": case "https://slsa.dev/provenance/v1": return predicateType; default: return null; } } async function parseStatement( statement: InTotoStatement, attestationManifestDigest: string, attestedManifestDigest: string | undefined, ): Promise<ProvenanceMetadata | null> { const predicate = statement.predicate; if (!predicate) { debug("[provenance] No predicate found in in-toto statement"); return null; } const resolvedAttestedManifestDigest = attestedManifestDigest || getAttestedManifestDigest(statement); if (!resolvedAttestedManifestDigest) { debug("[provenance] Could not determine the attested manifest digest"); return null; } const predicateType = statement.predicateType; const version = getSlsaProvenanceVersion(predicateType); if (version === null) { debug(`[provenance] Unsupported SLSA predicate type: ${predicateType}`); return null; } switch (version) { case "https://slsa.dev/provenance/v0.2": return extractFieldsSlsaV0_2( predicate as SlsaPredicateV0_2, resolvedAttestedManifestDigest, attestationManifestDigest, ); case "https://slsa.dev/provenance/v1": return extractFieldsSlsaV1_0( predicate as SlsaPredicateV1_0, resolvedAttestedManifestDigest, attestationManifestDigest, ); default: { const _exhaustiveCheck: never = version; return _exhaustiveCheck; } } } export async function parseProvenanceAttestations( attestationManifests: ResolvedAttestationManifest[], ): Promise<ProvenanceMetadata[]> { const results: ProvenanceMetadata[] = []; const sortedManifests = [...attestationManifests].sort((a, b) => a.manifestDigest.localeCompare(b.manifestDigest), ); for (const attestation of sortedManifests) { const sortedLayers = [...attestation.manifest.layers].sort((a, b) => a.digest.localeCompare(b.digest), ); for (const layer of sortedLayers) { if (results.length >= MAX_ATTESTATIONS_PER_IMAGE) { debug( `[provenance] Reached max attestation limit (${MAX_ATTESTATIONS_PER_IMAGE}), skipping remaining`, ); return results; } const inTotoStatement = attestation.inTotoStatements[layer.digest]; if (!inTotoStatement) { continue; } const parsed = await parseStatement( inTotoStatement, attestation.manifestDigest, attestation.attestedManifestDigest, ); if (parsed) { results.push(parsed); } } } return results; }