snyk-docker-plugin
Version:
Snyk CLI docker plugin
331 lines (286 loc) • 8.65 kB
text/typescript
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;
}