77 lines
5.5 KiB
TypeScript
77 lines
5.5 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
// @ts-expect-error The production release checker is an executable ESM module.
|
|
import { verifySupplyChainEvidence } from '../../scripts/supply-chain-check.mjs';
|
|
|
|
const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex');
|
|
|
|
async function evidenceFixture(overrides: Record<string, unknown> = {}) {
|
|
const root = await mkdtemp(join(tmpdir(), 'portfolio-supply-chain-'));
|
|
const evidence = join(root, 'evidence');
|
|
await mkdir(evidence);
|
|
const packageJson = JSON.stringify({ name: 'portfolio', version: '1.0.0', devDependencies: { '@cyclonedx/cyclonedx-npm': '6.0.1', 'license-checker-rseidelsohn': '4.4.2' } });
|
|
const packageLock = JSON.stringify({ name: 'portfolio', version: '1.0.0', lockfileVersion: 3, packages: {
|
|
'': { name: 'portfolio', version: '1.0.0' },
|
|
'node_modules/@cyclonedx/cyclonedx-npm': { version: '6.0.1', integrity: 'sha512-test' },
|
|
'node_modules/license-checker-rseidelsohn': { version: '4.4.2', integrity: 'sha512-test' },
|
|
} });
|
|
await writeFile(join(root, 'package.json'), packageJson);
|
|
await writeFile(join(root, 'package-lock.json'), packageLock);
|
|
const reports = {
|
|
'npm-audit-production.json': JSON.stringify(overrides.productionAudit ?? { auditReportVersion: 2, vulnerabilities: {}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 } } }),
|
|
'npm-audit-development.json': JSON.stringify(overrides.developmentAudit ?? { auditReportVersion: 2, vulnerabilities: {}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 } } }),
|
|
'licenses.json': JSON.stringify(overrides.licenses ?? { 'portfolio@1.0.0': { licenses: 'ISC' } }),
|
|
'sbom.cdx.json': JSON.stringify(overrides.sbom ?? { bomFormat: 'CycloneDX', specVersion: '1.6', version: 1, metadata: { component: { type: 'application', name: 'portfolio', version: '1.0.0' } }, components: [] }),
|
|
'medium-triage.json': JSON.stringify(overrides.triage ?? { schema: 'portfolio-medium-vulnerability-triage/v1', findings: [] }),
|
|
};
|
|
for (const [name, contents] of Object.entries(reports)) await writeFile(join(evidence, name), contents);
|
|
const manifest = {
|
|
schema: 'portfolio-supply-chain-evidence/v1',
|
|
packageJsonSha256: sha256(packageJson),
|
|
packageLockSha256: sha256(packageLock),
|
|
reports: Object.fromEntries(Object.entries(reports).map(([name, contents]) => [name, sha256(contents)])),
|
|
toolVersions: { npm: '11.0.0', cyclonedxNpm: '6.0.1', licenseChecker: '4.4.2' },
|
|
};
|
|
await writeFile(join(evidence, 'evidence-manifest.json'), JSON.stringify(manifest));
|
|
return { root, evidence };
|
|
}
|
|
|
|
describe('supply-chain evidence', () => {
|
|
it('accepts a digest-bound clean audit, allowlisted licenses, and CycloneDX SBOM', async () => {
|
|
const fixture = await evidenceFixture();
|
|
await expect(verifySupplyChainEvidence({ projectRoot: fixture.root, evidenceDir: fixture.evidence, requireTrackedTriage: false })).resolves.toMatchObject({
|
|
critical: 0, high: 0, medium: 0, componentCount: 0,
|
|
});
|
|
});
|
|
|
|
it('fails closed on altered evidence, Critical/High findings, or forbidden licenses', async () => {
|
|
const fixture = await evidenceFixture();
|
|
await writeFile(join(fixture.evidence, 'licenses.json'), JSON.stringify({ 'portfolio@1.0.0': { licenses: 'AGPL-3.0' } }));
|
|
await expect(verifySupplyChainEvidence({ projectRoot: fixture.root, evidenceDir: fixture.evidence, requireTrackedTriage: false })).rejects.toThrow(/digest/u);
|
|
|
|
const high = await evidenceFixture({
|
|
productionAudit: { auditReportVersion: 2, vulnerabilities: { unsafe: { name: 'unsafe', severity: 'high', via: [], effects: [], range: '*', nodes: [] } }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 1, critical: 0, total: 1 } } },
|
|
});
|
|
await expect(verifySupplyChainEvidence({ projectRoot: high.root, evidenceDir: high.evidence, requireTrackedTriage: false })).rejects.toThrow(/High|high/u);
|
|
|
|
const license = await evidenceFixture({ licenses: { 'portfolio@1.0.0': { licenses: 'AGPL-3.0-only' } } });
|
|
await expect(verifySupplyChainEvidence({ projectRoot: license.root, evidenceDir: license.evidence, requireTrackedTriage: false })).rejects.toThrow(/license/u);
|
|
});
|
|
|
|
it('requires an exact, current triage record for every Medium finding', async () => {
|
|
const audit = { auditReportVersion: 2, vulnerabilities: { packageA: { name: 'packageA', severity: 'moderate', via: [], effects: [], range: '<2', nodes: ['node_modules/packageA'] } }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 1, high: 0, critical: 0, total: 1 } } };
|
|
const missing = await evidenceFixture({ developmentAudit: audit });
|
|
await expect(verifySupplyChainEvidence({ projectRoot: missing.root, evidenceDir: missing.evidence, requireTrackedTriage: false })).rejects.toThrow(/triage/u);
|
|
|
|
const accepted = await evidenceFixture({
|
|
developmentAudit: audit,
|
|
triage: { schema: 'portfolio-medium-vulnerability-triage/v1', findings: [{ scope: 'development', package: 'packageA', severity: 'moderate', range: '<2', disposition: 'accepted', rationale: 'Build-only tool; no deployed runtime exposure.', owner: 'Luca Pîslaru', reviewBy: '2027-01-01' }] },
|
|
});
|
|
await expect(verifySupplyChainEvidence({ projectRoot: accepted.root, evidenceDir: accepted.evidence, requireTrackedTriage: false, today: '2026-09-02' })).resolves.toMatchObject({ medium: 1 });
|
|
});
|
|
});
|