Consolidate attestation actions (#346)

* consolidate attestation actions

Signed-off-by: Brian DeHamer <bdehamer@github.com>

* better errors

Signed-off-by: Brian DeHamer <bdehamer@github.com>

* Update src/sbom.ts

Co-authored-by: Austin Beattie <ajbeattie@github.com>

* clarify dedupe comment

Signed-off-by: Brian DeHamer <bdehamer@github.com>

---------

Signed-off-by: Brian DeHamer <bdehamer@github.com>
Co-authored-by: Austin Beattie <ajbeattie@github.com>
This commit is contained in:
Brian DeHamer
2026-02-13 11:23:24 -08:00
committed by GitHub
parent a82737a684
commit dc4ad3cc6c
15 changed files with 1297 additions and 61 deletions

180
dist/index.js generated vendored
View File

@@ -107016,6 +107016,43 @@ function getRegistryURL(subjectName) {
}
/***/ }),
/***/ 41052:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.validateAttestationInputs = exports.detectAttestationType = void 0;
const detectAttestationType = (inputs) => {
const { sbomPath, predicateType, predicate, predicatePath } = inputs;
// SBOM mode takes priority
if (sbomPath) {
return 'sbom';
}
// Custom mode when any predicate inputs are provided
if (predicateType || predicate || predicatePath) {
return 'custom';
}
// Default to provenance mode
return 'provenance';
};
exports.detectAttestationType = detectAttestationType;
const validateAttestationInputs = (inputs) => {
const { sbomPath, predicateType, predicate, predicatePath } = inputs;
// Cannot combine sbom-path with predicate inputs
if (sbomPath && (predicateType || predicate || predicatePath)) {
throw new Error('Cannot specify sbom-path together with predicate-type, predicate, or predicate-path');
}
// Custom mode requires predicate-type
if ((predicate || predicatePath) && !predicateType) {
throw new Error('predicate-type is required when using predicate or predicate-path');
}
};
exports.validateAttestationInputs = validateAttestationInputs;
/***/ }),
/***/ 7437:
@@ -107079,6 +107116,7 @@ const inputs = {
subjectName: core.getInput('subject-name'),
subjectDigest: core.getInput('subject-digest'),
subjectChecksums: core.getInput('subject-checksums'),
sbomPath: core.getInput('sbom-path'),
predicateType: core.getInput('predicate-type'),
predicate: core.getInput('predicate'),
predicatePath: core.getInput('predicate-path'),
@@ -107144,8 +107182,11 @@ const fs_1 = __importDefault(__nccwpck_require__(79896));
const os_1 = __importDefault(__nccwpck_require__(70857));
const path_1 = __importDefault(__nccwpck_require__(16928));
const attest_1 = __nccwpck_require__(93738);
const detect_1 = __nccwpck_require__(41052);
const endpoints_1 = __nccwpck_require__(7437);
const predicate_1 = __nccwpck_require__(84982);
const provenance_1 = __nccwpck_require__(83628);
const sbom_1 = __nccwpck_require__(20594);
const style = __importStar(__nccwpck_require__(64542));
const subject_1 = __nccwpck_require__(36303);
const ATTESTATION_FILE_NAME = 'attestation.json';
@@ -107174,11 +107215,22 @@ async function run(inputs) {
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
throw new Error('missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.');
}
// Detect attestation type and validate inputs
const detectionInputs = {
sbomPath: inputs.sbomPath,
predicateType: inputs.predicateType,
predicate: inputs.predicate,
predicatePath: inputs.predicatePath
};
(0, detect_1.validateAttestationInputs)(detectionInputs);
const attestationType = (0, detect_1.detectAttestationType)(detectionInputs);
logAttestationType(attestationType);
const subjects = await (0, subject_1.subjectFromInputs)({
...inputs,
downcaseName: inputs.pushToRegistry
});
const predicate = (0, predicate_1.predicateFromInputs)(inputs);
// Generate predicate based on attestation type
const predicate = await getPredicateForType(attestationType, inputs);
const outputPath = path_1.default.join(tempDir(), ATTESTATION_FILE_NAME);
core.setOutput('bundle-path', outputPath);
const att = await (0, attest_1.createAttestation)(subjects, predicate, {
@@ -107283,6 +107335,28 @@ const tempDir = () => {
return fs_1.default.mkdtempSync(path_1.default.join(basePath, path_1.default.sep));
};
const attestationURL = (id) => `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`;
// Log the detected attestation type
const logAttestationType = (type) => {
const typeLabels = {
provenance: 'Build Provenance',
sbom: 'SBOM',
custom: 'Custom'
};
core.info(`Attestation type: ${typeLabels[type]}`);
};
// Generate predicate based on attestation type
const getPredicateForType = async (type, inputs) => {
switch (type) {
case 'provenance':
return (0, provenance_1.generateProvenancePredicate)();
case 'sbom': {
const sbom = await (0, sbom_1.parseSBOMFromPath)(inputs.sbomPath);
return (0, sbom_1.generateSBOMPredicate)(sbom);
}
case 'custom':
return (0, predicate_1.predicateFromInputs)(inputs);
}
};
/***/ }),
@@ -107334,6 +107408,96 @@ const predicateFromInputs = (inputs) => {
exports.predicateFromInputs = predicateFromInputs;
/***/ }),
/***/ 83628:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateProvenancePredicate = void 0;
const attest_1 = __nccwpck_require__(11485);
const generateProvenancePredicate = async () => {
return (0, attest_1.buildSLSAProvenancePredicate)();
};
exports.generateProvenancePredicate = generateProvenancePredicate;
/***/ }),
/***/ 20594:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateSBOMPredicate = exports.parseSBOMFromPath = void 0;
const fs_1 = __importDefault(__nccwpck_require__(79896));
// SBOMs cannot exceed 16MB.
const MAX_SBOM_SIZE_BYTES = 16 * 1024 * 1024;
const parseSBOMFromPath = async (filePath) => {
if (!fs_1.default.existsSync(filePath)) {
throw new Error(`SBOM file not found: ${filePath}`);
}
const stats = fs_1.default.statSync(filePath);
if (stats.size > MAX_SBOM_SIZE_BYTES) {
throw new Error(`SBOM file exceeds maximum allowed size: ${MAX_SBOM_SIZE_BYTES} bytes`);
}
const fileContent = await fs_1.default.promises.readFile(filePath, 'utf8');
const sbom = JSON.parse(fileContent);
if (checkIsSPDX(sbom)) {
return { type: 'spdx', object: sbom };
}
else if (checkIsCycloneDX(sbom)) {
return { type: 'cyclonedx', object: sbom };
}
throw new Error('Unsupported SBOM format. Must be valid SPDX or CycloneDX JSON.');
};
exports.parseSBOMFromPath = parseSBOMFromPath;
const checkIsSPDX = (sbomObject) => {
return !!(sbomObject?.spdxVersion && sbomObject?.SPDXID);
};
const checkIsCycloneDX = (sbomObject) => {
return !!(sbomObject?.bomFormat &&
sbomObject?.serialNumber &&
sbomObject?.specVersion);
};
const generateSBOMPredicate = (sbom) => {
switch (sbom.type) {
case 'spdx':
return generateSPDXPredicate(sbom.object);
case 'cyclonedx':
return generateCycloneDXPredicate(sbom.object);
default:
throw new Error('Unsupported SBOM format');
}
};
exports.generateSBOMPredicate = generateSBOMPredicate;
// ref: https://github.com/in-toto/attestation/blob/main/spec/predicates/spdx.md
const generateSPDXPredicate = (sbom) => {
const spdxVersion = sbom?.['spdxVersion'];
if (!spdxVersion) {
throw new Error('Cannot find spdxVersion in the SBOM');
}
const version = spdxVersion.split('-')[1];
return {
type: `https://spdx.dev/Document/v${version}`,
params: sbom
};
};
// ref: https://github.com/in-toto/attestation/blob/main/spec/predicates/cyclonedx.md
const generateCycloneDXPredicate = (sbom) => {
return {
type: 'https://cyclonedx.org/bom',
params: sbom
};
};
/***/ }),
/***/ 64542:
@@ -107461,7 +107625,7 @@ const getSubjectFromPath = async (subjectPath, subjectName) => {
// Filter path list to just the files (not directories)
const files = paths.filter(p => fs_1.default.statSync(p).isFile());
if (files.length > MAX_SUBJECT_COUNT) {
throw new Error(`Too many subjects specified. The maximum number of subjects is ${MAX_SUBJECT_COUNT}.`);
throw new Error(`Too many subjects specified (${files.length}). The maximum number of subjects is ${MAX_SUBJECT_COUNT}.`);
}
for (const file of files) {
const name = subjectName || path_1.default.parse(file).base;
@@ -107528,10 +107692,14 @@ const getSubjectFromChecksumsString = (checksums) => {
if (!HEX_STRING_RE.test(digest)) {
throw new Error(`Invalid digest: ${digest}`);
}
subjects.push({
name,
digest: { [digestAlgorithm(digest)]: digest }
});
const alg = digestAlgorithm(digest);
// Only add the subject if it is not already in the list (deduplicate by name & digest)
if (!subjects.some(s => s.name === name && s.digest[alg] === digest)) {
subjects.push({
name,
digest: { [alg]: digest }
});
}
}
return subjects;
};