165 lines
4.5 KiB
JavaScript
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),
|
|
}));
|