aps-agent/scripts/generate-sbom.mjs

165 lines
4.5 KiB
JavaScript

import { spawnSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputDir = path.join(repoRoot, 'dist', 'sbom');
const uvxExecutable = process.platform === 'win32' ? 'uvx.exe' : 'uvx';
const npxCli = [
process.env.npm_execpath
? path.join(path.dirname(process.env.npm_execpath), 'npx-cli.js')
: null,
path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js'),
].find((candidate) => candidate && fs.existsSync(candidate));
if (!npxCli) throw new Error('Could not locate npx-cli.js for the current Node installation');
const tools = {
cyclonedxNpm: '6.0.0',
cyclonedxPython: '7.3.1',
};
const sboms = [
{
name: 'web-node',
file: 'web-node.cdx.json',
command: process.execPath,
args: [
npxCli,
'--yes',
`@cyclonedx/cyclonedx-npm@${tools.cyclonedxNpm}`,
'--package-lock-only',
'--output-reproducible',
'--validate',
'--spec-version',
'1.6',
'--output-file',
path.join(outputDir, 'web-node.cdx.json'),
path.join(repoRoot, 'apps', 'web', 'package.json'),
],
},
{
name: 'desktop-node',
file: 'desktop-node.cdx.json',
command: process.execPath,
args: [
npxCli,
'--yes',
`@cyclonedx/cyclonedx-npm@${tools.cyclonedxNpm}`,
'--package-lock-only',
'--output-reproducible',
'--validate',
'--spec-version',
'1.6',
'--output-file',
path.join(outputDir, 'desktop-node.cdx.json'),
path.join(repoRoot, 'apps', 'desktop', 'package.json'),
],
},
{
name: 'sidecar-python',
file: 'sidecar-python.cdx.json',
command: uvxExecutable,
args: [
'--from',
`cyclonedx-bom==${tools.cyclonedxPython}`,
'cyclonedx-py',
'requirements',
'--pyproject',
path.join(repoRoot, 'pyproject.toml'),
'--mc-type',
'application',
'--spec-version',
'1.6',
'--output-reproducible',
'--validate',
'--output-format',
'JSON',
'--output-file',
path.join(outputDir, 'sidecar-python.cdx.json'),
path.join(repoRoot, 'packaging', 'requirements-sidecar.lock'),
],
},
];
function run(command, args) {
const result = spawnSync(command, args, {
cwd: repoRoot,
env: { ...process.env, PYTHONUTF8: '1' },
stdio: 'inherit',
windowsHide: true,
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${command} failed with exit code ${result.status}`);
}
}
function sha256(filePath) {
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}
function validateBom(filePath) {
const bom = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (bom.bomFormat !== 'CycloneDX' || bom.specVersion !== '1.6') {
throw new Error(`${filePath} is not a CycloneDX 1.6 BOM`);
}
if (!Array.isArray(bom.components) || bom.components.length === 0) {
throw new Error(`${filePath} contains no components`);
}
return bom.components.length;
}
fs.mkdirSync(outputDir, { recursive: true });
for (const sbom of sboms) run(sbom.command, sbom.args);
const sbomEvidence = sboms.map((sbom) => {
const filePath = path.join(outputDir, sbom.file);
return {
name: sbom.name,
path: path.relative(repoRoot, filePath).replaceAll('\\', '/'),
sha256: sha256(filePath),
components: validateBom(filePath),
};
});
const artifactCandidates = [
path.join(repoRoot, 'apps', 'desktop', 'release', 'aps-agent-desktop-0.1.0.exe'),
path.join(repoRoot, 'apps', 'desktop', 'release', 'win-unpacked', '工业智核 APS.exe'),
path.join(
repoRoot,
'apps',
'desktop',
'release',
'win-unpacked',
'resources',
'sidecar',
'aps-sidecar.exe',
),
];
const artifacts = artifactCandidates
.filter((filePath) => fs.existsSync(filePath))
.map((filePath) => ({
path: path.relative(repoRoot, filePath).replaceAll('\\', '/'),
sha256: sha256(filePath),
bytes: fs.statSync(filePath).size,
}));
const manifestPath = path.join(outputDir, 'release-manifest.json');
fs.writeFileSync(manifestPath, `${JSON.stringify({
schemaVersion: 1,
signed: false,
note: 'Local integrity manifest only; this is not a signed provenance attestation.',
tools,
sboms: sbomEvidence,
artifacts,
}, null, 2)}\n`, 'utf8');
console.log(JSON.stringify({
outputDir,
sboms: sbomEvidence,
artifacts,
manifestSha256: sha256(manifestPath),
}));