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:
180
dist/index.js
generated
vendored
180
dist/index.js
generated
vendored
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user