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), }));