@cyclonedx/cdxgen 12.4.3 → 12.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +6 -0
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +48 -2
  4. package/bin/evinse.js +7 -0
  5. package/lib/audit/index.js +165 -2
  6. package/lib/audit/index.poku.js +462 -0
  7. package/lib/cli/index.js +317 -169
  8. package/lib/evinser/evinser.js +31 -9
  9. package/lib/helpers/analyzer.js +890 -0
  10. package/lib/helpers/analyzer.poku.js +341 -0
  11. package/lib/helpers/atomUtils.js +445 -0
  12. package/lib/helpers/atomUtils.poku.js +137 -0
  13. package/lib/helpers/bomUtils.js +71 -0
  14. package/lib/helpers/bomUtils.poku.js +45 -0
  15. package/lib/helpers/depsUtils.js +146 -0
  16. package/lib/helpers/depsUtils.poku.js +183 -0
  17. package/lib/helpers/utils.js +585 -191
  18. package/lib/helpers/utils.poku.js +357 -4
  19. package/lib/managers/binary.js +18 -9
  20. package/lib/stages/postgen/postgen.js +215 -0
  21. package/lib/stages/postgen/postgen.poku.js +218 -3
  22. package/lib/validator/bomValidator.js +11 -2
  23. package/package.json +8 -8
  24. package/types/lib/audit/index.d.ts.map +1 -1
  25. package/types/lib/cli/index.d.ts.map +1 -1
  26. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  27. package/types/lib/helpers/atomUtils.d.ts +18 -0
  28. package/types/lib/helpers/atomUtils.d.ts.map +1 -0
  29. package/types/lib/helpers/bomUtils.d.ts +10 -0
  30. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  31. package/types/lib/helpers/depsUtils.d.ts +9 -0
  32. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  33. package/types/lib/helpers/utils.d.ts +19 -0
  34. package/types/lib/helpers/utils.d.ts.map +1 -1
  35. package/types/lib/managers/binary.d.ts +2 -1
  36. package/types/lib/managers/binary.d.ts.map +1 -1
  37. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  38. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -2058,3 +2058,465 @@ describe("auditTarget() cache resume", () => {
2058
2058
  }
2059
2059
  });
2060
2060
  });
2061
+
2062
+ describe("auditTarget() default branch recheck", () => {
2063
+ it("downgrades severity when findings are fixed in default branch", async () => {
2064
+ const workspaceDir = mkdtempSync(
2065
+ path.join(os.tmpdir(), "cdx-audit-recheck-"),
2066
+ );
2067
+ const target = {
2068
+ bomRefs: ["pkg:pypi/typer@0.9.0"],
2069
+ name: "typer",
2070
+ purl: "pkg:pypi/typer@0.9.0",
2071
+ properties: [],
2072
+ type: "pypi",
2073
+ version: "0.9.0",
2074
+ };
2075
+ const targetDir = path.join(workspaceDir, auditTargetSlug(target));
2076
+ const cacheDir = path.join(targetDir, ".cdx-audit");
2077
+ const cachedBom = {
2078
+ bomFormat: "CycloneDX",
2079
+ specVersion: "1.7",
2080
+ version: 1,
2081
+ components: [],
2082
+ formulation: [{ components: [] }],
2083
+ };
2084
+ writeJson(path.join(cacheDir, "source-bom.json"), cachedBom);
2085
+ writeJson(path.join(cacheDir, "source-bom.meta.json"), {
2086
+ repoUrl: "https://github.com/tiangolo/typer.git",
2087
+ resolution: {
2088
+ name: "typer",
2089
+ repoUrl: "https://github.com/tiangolo/typer.git",
2090
+ type: "pypi",
2091
+ version: "0.9.0",
2092
+ },
2093
+ scanDirRelative: ".",
2094
+ sourceDirectoryConfidence: "high",
2095
+ versionMatched: true,
2096
+ });
2097
+
2098
+ // First call returns critical findings; second call returns the same rule in
2099
+ // a different default-branch file, which should not retain the fixed finding.
2100
+ const auditBomStub = sinon.stub();
2101
+ auditBomStub.onFirstCall().resolves([
2102
+ {
2103
+ category: "ci-permission",
2104
+ location: { file: ".github/workflows/ci.yml" },
2105
+ message: "Workflow uses pull_request_target trigger",
2106
+ ruleId: "CI-007",
2107
+ severity: "critical",
2108
+ },
2109
+ {
2110
+ category: "ci-permission",
2111
+ location: { file: ".github/workflows/ci.yml" },
2112
+ message: "Checkout step pulls PR head inside pull_request_target",
2113
+ ruleId: "CI-019",
2114
+ severity: "critical",
2115
+ },
2116
+ {
2117
+ category: "dependency-source",
2118
+ location: { file: "requirements.txt" },
2119
+ message: "Dependency pinned to mutable git reference",
2120
+ ruleId: "DEP-001",
2121
+ severity: "high",
2122
+ },
2123
+ ]);
2124
+ auditBomStub.onSecondCall().resolves([
2125
+ {
2126
+ category: "ci-permission",
2127
+ location: { file: ".github\\workflows\\release.yml" },
2128
+ message: "Workflow uses pull_request_target trigger elsewhere",
2129
+ ruleId: "CI-007",
2130
+ severity: "high",
2131
+ },
2132
+ ]);
2133
+
2134
+ const cloneStub = sinon.stub();
2135
+ const createBomStub = sinon.stub().resolves({ bomJson: cachedBom });
2136
+ const { auditTarget } = await esmock("./index.js", {
2137
+ "../cli/index.js": { createBom: createBomStub },
2138
+ "../helpers/logger.js": { thoughtLog: sinon.stub() },
2139
+ "../helpers/source.js": {
2140
+ cleanupSourceDir: sinon.stub(),
2141
+ findGitRefForPurlVersion: sinon.stub().returns("0.9.0"),
2142
+ hardenedGitCommand: cloneStub.returns({ status: 0 }),
2143
+ resolveGitUrlFromPurl: sinon.stub().resolves({
2144
+ name: "typer",
2145
+ repoUrl: "https://github.com/tiangolo/typer.git",
2146
+ type: "pypi",
2147
+ version: "0.9.0",
2148
+ }),
2149
+ resolvePurlSourceDirectory: sinon.stub().returns(undefined),
2150
+ sanitizeRemoteUrlForLogs: (value) => value,
2151
+ },
2152
+ "../helpers/utils.js": {
2153
+ dirNameStr: path.resolve("."),
2154
+ getTmpDir: () => os.tmpdir(),
2155
+ isDryRun: false,
2156
+ safeExistsSync: (filePath) => existsSync(filePath),
2157
+ safeMkdirSync: (filePath, options) => mkdirSync(filePath, options),
2158
+ safeMkdtempSync: (prefix) => mkdtempSync(prefix),
2159
+ safeRmSync: (filePath, options) => rmSync(filePath, options),
2160
+ },
2161
+ "../stages/postgen/auditBom.js": { auditBom: auditBomStub },
2162
+ "../stages/postgen/postgen.js": {
2163
+ postProcess: sinon.stub().callsFake((bomNSData) => bomNSData),
2164
+ },
2165
+ });
2166
+
2167
+ try {
2168
+ const result = await auditTarget(target, {
2169
+ maxTargets: 1,
2170
+ minSeverity: "low",
2171
+ workspaceDir,
2172
+ });
2173
+
2174
+ assert.strictEqual(result.status, "audited");
2175
+ // Source-derived findings should be removed since they're fixed in default branch
2176
+ // Contextual PROV-002 finding (no location.file) is retained
2177
+ assert.strictEqual(result.findings.length, 1);
2178
+ assert.strictEqual(result.findings[0].ruleId, "PROV-002");
2179
+ assert.strictEqual(result.assessment.defaultBranchRechecked, true);
2180
+ assert.strictEqual(result.assessment.findingsFixedInDefaultBranch, 3);
2181
+ // Severity should be downgraded from critical/high to something lower
2182
+ assert.ok(
2183
+ result.assessment.severity !== "critical" &&
2184
+ result.assessment.severity !== "high",
2185
+ "severity should be downgraded after default branch recheck",
2186
+ );
2187
+ } finally {
2188
+ rmSync(workspaceDir, { force: true, recursive: true });
2189
+ }
2190
+ });
2191
+
2192
+ it("retains Python source heuristic findings still present on default branch", async () => {
2193
+ const workspaceDir = mkdtempSync(
2194
+ path.join(os.tmpdir(), "cdx-audit-recheck-pysrc-"),
2195
+ );
2196
+ const target = {
2197
+ bomRefs: ["pkg:pypi/demo@1.0.0"],
2198
+ name: "demo",
2199
+ purl: "pkg:pypi/demo@1.0.0",
2200
+ properties: [],
2201
+ type: "pypi",
2202
+ version: "1.0.0",
2203
+ };
2204
+ const targetDir = path.join(workspaceDir, auditTargetSlug(target));
2205
+ const cacheDir = path.join(targetDir, ".cdx-audit");
2206
+ const suspiciousSetup = [
2207
+ "from setuptools import setup",
2208
+ "import base64",
2209
+ "import os",
2210
+ "payload = base64.b64decode('bHM=')",
2211
+ "os.system(payload.decode())",
2212
+ "setup(name='demo')",
2213
+ ].join("\n");
2214
+ mkdirSync(targetDir, { recursive: true });
2215
+ writeFileSync(path.join(targetDir, "setup.py"), suspiciousSetup);
2216
+ const cachedBom = {
2217
+ bomFormat: "CycloneDX",
2218
+ specVersion: "1.7",
2219
+ version: 1,
2220
+ components: [],
2221
+ formulation: [{ components: [] }],
2222
+ };
2223
+ writeJson(path.join(cacheDir, "source-bom.json"), cachedBom);
2224
+ writeJson(path.join(cacheDir, "source-bom.meta.json"), {
2225
+ repoUrl: "https://github.com/acme/demo.git",
2226
+ resolution: {
2227
+ name: "demo",
2228
+ repoUrl: "https://github.com/acme/demo.git",
2229
+ type: "pypi",
2230
+ version: "1.0.0",
2231
+ },
2232
+ scanDirRelative: ".",
2233
+ sourceDirectoryConfidence: "high",
2234
+ versionMatched: true,
2235
+ });
2236
+
2237
+ const auditBomStub = sinon.stub().resolves([]);
2238
+ const cloneStub = sinon.stub().callsFake((args) => {
2239
+ const cloneDir = args.at(-1);
2240
+ mkdirSync(cloneDir, { recursive: true });
2241
+ writeFileSync(path.join(cloneDir, "setup.py"), suspiciousSetup);
2242
+ return { status: 0 };
2243
+ });
2244
+ const { auditTarget } = await esmock("./index.js", {
2245
+ "../cli/index.js": {
2246
+ createBom: sinon.stub().resolves({ bomJson: cachedBom }),
2247
+ },
2248
+ "../helpers/logger.js": { thoughtLog: sinon.stub() },
2249
+ "../helpers/source.js": {
2250
+ cleanupSourceDir: sinon.stub(),
2251
+ findGitRefForPurlVersion: sinon.stub().returns("1.0.0"),
2252
+ hardenedGitCommand: cloneStub,
2253
+ resolveGitUrlFromPurl: sinon.stub().resolves({
2254
+ name: "demo",
2255
+ repoUrl: "https://github.com/acme/demo.git",
2256
+ type: "pypi",
2257
+ version: "1.0.0",
2258
+ }),
2259
+ resolvePurlSourceDirectory: sinon.stub().returns(undefined),
2260
+ sanitizeRemoteUrlForLogs: (value) => value,
2261
+ },
2262
+ "../helpers/utils.js": {
2263
+ dirNameStr: path.resolve("."),
2264
+ getTmpDir: () => os.tmpdir(),
2265
+ isDryRun: false,
2266
+ safeExistsSync: (filePath) => existsSync(filePath),
2267
+ safeMkdirSync: (filePath, options) => mkdirSync(filePath, options),
2268
+ safeMkdtempSync: (prefix) => mkdtempSync(prefix),
2269
+ safeRmSync: (filePath, options) => rmSync(filePath, options),
2270
+ },
2271
+ "../stages/postgen/auditBom.js": { auditBom: auditBomStub },
2272
+ "../stages/postgen/postgen.js": {
2273
+ postProcess: sinon.stub().callsFake((bomNSData) => bomNSData),
2274
+ },
2275
+ });
2276
+
2277
+ try {
2278
+ const result = await auditTarget(target, {
2279
+ maxTargets: 1,
2280
+ minSeverity: "low",
2281
+ workspaceDir,
2282
+ });
2283
+
2284
+ assert.strictEqual(result.status, "audited");
2285
+ assert.ok(
2286
+ result.findings.some((finding) => finding.ruleId === "PYSRC-001"),
2287
+ );
2288
+ assert.strictEqual(result.assessment.defaultBranchRechecked, undefined);
2289
+ } finally {
2290
+ rmSync(workspaceDir, { force: true, recursive: true });
2291
+ }
2292
+ });
2293
+
2294
+ it("logs non-Error default-branch recheck failures safely", async () => {
2295
+ const workspaceDir = mkdtempSync(
2296
+ path.join(os.tmpdir(), "cdx-audit-recheck-log-"),
2297
+ );
2298
+ const target = {
2299
+ bomRefs: ["pkg:npm/acme/demo@1.0.0"],
2300
+ name: "demo",
2301
+ namespace: "acme",
2302
+ purl: "pkg:npm/acme/demo@1.0.0",
2303
+ properties: [],
2304
+ type: "npm",
2305
+ version: "1.0.0",
2306
+ };
2307
+ const targetDir = path.join(workspaceDir, auditTargetSlug(target));
2308
+ const cacheDir = path.join(targetDir, ".cdx-audit");
2309
+ const cachedBom = {
2310
+ bomFormat: "CycloneDX",
2311
+ specVersion: "1.7",
2312
+ version: 1,
2313
+ components: [],
2314
+ formulation: [{ components: [] }],
2315
+ };
2316
+ writeJson(path.join(cacheDir, "source-bom.json"), cachedBom);
2317
+ writeJson(path.join(cacheDir, "source-bom.meta.json"), {
2318
+ repoUrl: "https://github.com/acme/demo.git",
2319
+ resolution: {
2320
+ name: "demo",
2321
+ repoUrl: "https://github.com/acme/demo.git",
2322
+ type: "npm",
2323
+ version: "1.0.0",
2324
+ },
2325
+ scanDirRelative: ".",
2326
+ sourceDirectoryConfidence: "high",
2327
+ versionMatched: true,
2328
+ });
2329
+
2330
+ const auditBomStub = sinon.stub().resolves([
2331
+ {
2332
+ category: "ci-permission",
2333
+ location: { file: ".github/workflows/ci.yml" },
2334
+ message: "Workflow uses pull_request_target trigger",
2335
+ ruleId: "CI-007",
2336
+ severity: "critical",
2337
+ },
2338
+ {
2339
+ category: "ci-permission",
2340
+ location: { file: ".github/workflows/ci.yml" },
2341
+ message: "Checkout step pulls PR head inside pull_request_target",
2342
+ ruleId: "CI-019",
2343
+ severity: "critical",
2344
+ },
2345
+ {
2346
+ category: "dependency-source",
2347
+ location: { file: "package.json" },
2348
+ message: "Dependency pinned to mutable git reference",
2349
+ ruleId: "DEP-001",
2350
+ severity: "high",
2351
+ },
2352
+ ]);
2353
+ const thoughtLogStub = sinon.stub();
2354
+ const { auditTarget } = await esmock("./index.js", {
2355
+ "../cli/index.js": {
2356
+ createBom: sinon.stub().callsFake(() => {
2357
+ throw "default branch failure";
2358
+ }),
2359
+ },
2360
+ "../helpers/logger.js": { thoughtLog: thoughtLogStub },
2361
+ "../helpers/source.js": {
2362
+ cleanupSourceDir: sinon.stub(),
2363
+ findGitRefForPurlVersion: sinon.stub().returns("1.0.0"),
2364
+ hardenedGitCommand: sinon.stub().returns({ status: 0 }),
2365
+ resolveGitUrlFromPurl: sinon.stub().resolves({
2366
+ name: "demo",
2367
+ repoUrl: "https://github.com/acme/demo.git",
2368
+ type: "npm",
2369
+ version: "1.0.0",
2370
+ }),
2371
+ resolvePurlSourceDirectory: sinon.stub().returns(undefined),
2372
+ sanitizeRemoteUrlForLogs: (value) => value,
2373
+ },
2374
+ "../helpers/utils.js": {
2375
+ dirNameStr: path.resolve("."),
2376
+ getTmpDir: () => os.tmpdir(),
2377
+ isDryRun: false,
2378
+ safeExistsSync: (filePath) => existsSync(filePath),
2379
+ safeMkdirSync: (filePath, options) => mkdirSync(filePath, options),
2380
+ safeMkdtempSync: (prefix) => mkdtempSync(prefix),
2381
+ safeRmSync: (filePath, options) => rmSync(filePath, options),
2382
+ },
2383
+ "../stages/postgen/auditBom.js": { auditBom: auditBomStub },
2384
+ "../stages/postgen/postgen.js": {
2385
+ postProcess: sinon.stub().callsFake((bomNSData) => bomNSData),
2386
+ },
2387
+ });
2388
+
2389
+ try {
2390
+ const result = await auditTarget(target, {
2391
+ maxTargets: 1,
2392
+ minSeverity: "low",
2393
+ workspaceDir,
2394
+ });
2395
+ assert.strictEqual(result.status, "audited");
2396
+ assert.ok(result.findings.some((finding) => finding.ruleId === "CI-007"));
2397
+ assert.ok(
2398
+ thoughtLogStub.calledWithMatch(
2399
+ "Default branch recheck failed, keeping original findings.",
2400
+ {
2401
+ error: "default branch failure",
2402
+ purl: target.purl,
2403
+ },
2404
+ ),
2405
+ JSON.stringify(thoughtLogStub.getCalls().map((call) => call.args)),
2406
+ );
2407
+ } finally {
2408
+ rmSync(workspaceDir, { force: true, recursive: true });
2409
+ }
2410
+ });
2411
+
2412
+ it("skips recheck when skipDefaultBranchRecheck option is set", async () => {
2413
+ const workspaceDir = mkdtempSync(
2414
+ path.join(os.tmpdir(), "cdx-audit-recheck-skip-"),
2415
+ );
2416
+ const target = {
2417
+ bomRefs: ["pkg:pypi/typer@0.9.0"],
2418
+ name: "typer",
2419
+ purl: "pkg:pypi/typer@0.9.0",
2420
+ properties: [],
2421
+ type: "pypi",
2422
+ version: "0.9.0",
2423
+ };
2424
+ const targetDir = path.join(workspaceDir, auditTargetSlug(target));
2425
+ const cacheDir = path.join(targetDir, ".cdx-audit");
2426
+ const cachedBom = {
2427
+ bomFormat: "CycloneDX",
2428
+ specVersion: "1.7",
2429
+ version: 1,
2430
+ components: [],
2431
+ formulation: [{ components: [] }],
2432
+ };
2433
+ writeJson(path.join(cacheDir, "source-bom.json"), cachedBom);
2434
+ writeJson(path.join(cacheDir, "source-bom.meta.json"), {
2435
+ repoUrl: "https://github.com/tiangolo/typer.git",
2436
+ resolution: {
2437
+ name: "typer",
2438
+ repoUrl: "https://github.com/tiangolo/typer.git",
2439
+ type: "pypi",
2440
+ version: "0.9.0",
2441
+ },
2442
+ scanDirRelative: ".",
2443
+ sourceDirectoryConfidence: "high",
2444
+ versionMatched: true,
2445
+ });
2446
+
2447
+ const auditBomStub = sinon.stub().resolves([
2448
+ {
2449
+ category: "ci-permission",
2450
+ location: { file: ".github/workflows/ci.yml" },
2451
+ message: "Workflow uses pull_request_target trigger",
2452
+ ruleId: "CI-007",
2453
+ severity: "high",
2454
+ },
2455
+ {
2456
+ category: "ci-permission",
2457
+ location: { file: ".github/workflows/ci.yml" },
2458
+ message: "Checkout step pulls PR head inside pull_request_target",
2459
+ ruleId: "CI-019",
2460
+ severity: "critical",
2461
+ },
2462
+ {
2463
+ category: "dependency-source",
2464
+ location: { file: "requirements.txt" },
2465
+ message: "Dependency pinned to mutable git reference",
2466
+ ruleId: "DEP-001",
2467
+ severity: "high",
2468
+ },
2469
+ ]);
2470
+
2471
+ const { auditTarget } = await esmock("./index.js", {
2472
+ "../cli/index.js": {
2473
+ createBom: sinon.stub().resolves({ bomJson: cachedBom }),
2474
+ },
2475
+ "../helpers/logger.js": { thoughtLog: sinon.stub() },
2476
+ "../helpers/source.js": {
2477
+ cleanupSourceDir: sinon.stub(),
2478
+ findGitRefForPurlVersion: sinon.stub().returns("0.9.0"),
2479
+ hardenedGitCommand: sinon.stub().returns({ status: 0 }),
2480
+ resolveGitUrlFromPurl: sinon.stub().resolves({
2481
+ name: "typer",
2482
+ repoUrl: "https://github.com/tiangolo/typer.git",
2483
+ type: "pypi",
2484
+ version: "0.9.0",
2485
+ }),
2486
+ resolvePurlSourceDirectory: sinon.stub().returns(undefined),
2487
+ sanitizeRemoteUrlForLogs: (value) => value,
2488
+ },
2489
+ "../helpers/utils.js": {
2490
+ dirNameStr: path.resolve("."),
2491
+ getTmpDir: () => os.tmpdir(),
2492
+ isDryRun: false,
2493
+ safeExistsSync: (filePath) => existsSync(filePath),
2494
+ safeMkdirSync: (filePath, options) => mkdirSync(filePath, options),
2495
+ safeMkdtempSync: (prefix) => mkdtempSync(prefix),
2496
+ safeRmSync: (filePath, options) => rmSync(filePath, options),
2497
+ },
2498
+ "../stages/postgen/auditBom.js": { auditBom: auditBomStub },
2499
+ "../stages/postgen/postgen.js": {
2500
+ postProcess: sinon.stub().callsFake((bomNSData) => bomNSData),
2501
+ },
2502
+ });
2503
+
2504
+ try {
2505
+ const result = await auditTarget(target, {
2506
+ maxTargets: 1,
2507
+ minSeverity: "low",
2508
+ skipDefaultBranchRecheck: true,
2509
+ workspaceDir,
2510
+ });
2511
+
2512
+ assert.strictEqual(result.status, "audited");
2513
+ // auditBom should only be called once (no recheck)
2514
+ assert.strictEqual(auditBomStub.callCount, 1);
2515
+ // Findings should remain unchanged (3 from auditBom + 1 contextual PROV-002)
2516
+ assert.strictEqual(result.findings.length, 4);
2517
+ assert.strictEqual(result.assessment.defaultBranchRechecked, undefined);
2518
+ } finally {
2519
+ rmSync(workspaceDir, { force: true, recursive: true });
2520
+ }
2521
+ });
2522
+ });