@cyclonedx/cdxgen 12.4.2 → 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 (43) 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 +320 -172
  8. package/lib/cli/index.poku.js +81 -0
  9. package/lib/evinser/evinser.js +31 -9
  10. package/lib/helpers/analyzer.js +890 -0
  11. package/lib/helpers/analyzer.poku.js +341 -0
  12. package/lib/helpers/atomUtils.js +445 -0
  13. package/lib/helpers/atomUtils.poku.js +137 -0
  14. package/lib/helpers/bomUtils.js +71 -0
  15. package/lib/helpers/bomUtils.poku.js +45 -0
  16. package/lib/helpers/depsUtils.js +146 -0
  17. package/lib/helpers/depsUtils.poku.js +183 -0
  18. package/lib/helpers/display.js +12 -6
  19. package/lib/helpers/display.poku.js +38 -0
  20. package/lib/helpers/utils.js +653 -191
  21. package/lib/helpers/utils.poku.js +414 -4
  22. package/lib/managers/binary.js +18 -9
  23. package/lib/stages/postgen/postgen.js +215 -0
  24. package/lib/stages/postgen/postgen.poku.js +218 -3
  25. package/lib/validator/bomValidator.js +11 -2
  26. package/package.json +8 -8
  27. package/types/lib/audit/index.d.ts.map +1 -1
  28. package/types/lib/cli/index.d.ts.map +1 -1
  29. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  30. package/types/lib/helpers/atomUtils.d.ts +18 -0
  31. package/types/lib/helpers/atomUtils.d.ts.map +1 -0
  32. package/types/lib/helpers/bomUtils.d.ts +10 -0
  33. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  34. package/types/lib/helpers/depsUtils.d.ts +9 -0
  35. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  36. package/types/lib/helpers/display.d.ts.map +1 -1
  37. package/types/lib/helpers/dosaiParsers.d.ts.map +1 -1
  38. package/types/lib/helpers/utils.d.ts +19 -0
  39. package/types/lib/helpers/utils.d.ts.map +1 -1
  40. package/types/lib/managers/binary.d.ts +2 -1
  41. package/types/lib/managers/binary.d.ts.map +1 -1
  42. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  43. package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/README.md CHANGED
@@ -403,6 +403,12 @@ The default specification used by cdxgen is 1.7. To generate BOM for a different
403
403
  cdxgen -r -o bom.json --spec-version 1.6
404
404
  ```
405
405
 
406
+ Use repeated `--component-type` values to include only selected CycloneDX component types. This is a filter, not an inventory enabler: passing a type such as `machine-learning-model` does not enable machine-learning model discovery, and filtering to a type that the scan does not generate can produce an empty component list. The allowed values are validated against `--spec-version`; for example, `cryptographic-asset` is rejected for CycloneDX 1.5 and unsupported component types are pruned during compatibility downgrades. The dedicated `cbom` command does not accept `--component-type`; use `cdxgen --include-crypto` for normal generation plus filtering.
407
+
408
+ ```shell
409
+ cdxgen -t docker alpine:3.20 --spec-version 1.5 --component-type library --component-type data
410
+ ```
411
+
406
412
  To generate SBOM for C or Python, ensure Java >= 21 is installed.
407
413
 
408
414
  ```shell
package/bin/audit.js CHANGED
@@ -109,6 +109,12 @@ const args = yargs(hideBin(process.argv))
109
109
  "Optional JSON array or newline-delimited file of purl prefixes to exclude from predictive audit target selection in addition to the built-in well-known allowlist.",
110
110
  type: "string",
111
111
  })
112
+ .option("skip-default-branch-recheck", {
113
+ default: false,
114
+ description:
115
+ "Skip rechecking critical/high findings against the default branch to detect issues already fixed upstream.",
116
+ type: "boolean",
117
+ })
112
118
  .check((argv) => {
113
119
  if (!argv.bom && !argv.bomDir) {
114
120
  throw new Error("Specify --bom or --bom-dir.");
@@ -187,6 +193,7 @@ function writeOrPrint(output, outputPath) {
187
193
  reportsDir: args.reportsDir,
188
194
  rulesDir: args.rulesDir,
189
195
  scope: args.scope === "required" ? "required" : undefined,
196
+ skipDefaultBranchRecheck: args.skipDefaultBranchRecheck,
190
197
  trusted: args.onlyTrusted
191
198
  ? "only"
192
199
  : args.includeTrusted
package/bin/cdxgen.js CHANGED
@@ -20,7 +20,13 @@ import { hideBin } from "yargs/helpers";
20
20
 
21
21
  import { createBom, submitBom } from "../lib/cli/index.js";
22
22
  import { signBom, verifyBom } from "../lib/helpers/bomSigner.js";
23
- import { isCycloneDxBom } from "../lib/helpers/bomUtils.js";
23
+ import {
24
+ getSupportedCycloneDxComponentTypes,
25
+ isCycloneDxBom,
26
+ isCycloneDxComponentTypeEnabled,
27
+ normalizeCycloneDxComponentTypeFilter,
28
+ toCycloneDxSpecVersionString,
29
+ } from "../lib/helpers/bomUtils.js";
24
30
  import {
25
31
  displaySelfThreatModel,
26
32
  printActivitySummary,
@@ -135,6 +141,7 @@ const invokedCommandName = basename(process.argv[1] || "cdxgen").replace(
135
141
  /\.(?:[cm]?js|exe)$/u,
136
142
  "",
137
143
  );
144
+ const defaultComponentTypeChoices = getSupportedCycloneDxComponentTypes(1.7);
138
145
 
139
146
  const args = _yargs
140
147
  .env("CDXGEN")
@@ -408,6 +415,7 @@ const args = _yargs
408
415
  .option("exclude", {
409
416
  alias: "exclude-regex",
410
417
  description: "Additional glob pattern(s) to ignore",
418
+ nargs: 1,
411
419
  type: "array",
412
420
  })
413
421
  .option("export-proto", {
@@ -483,6 +491,12 @@ const args = _yargs
483
491
  "filename",
484
492
  ],
485
493
  })
494
+ .option("component-type", {
495
+ description:
496
+ "CycloneDX component type(s) to include. Choices are validated against --spec-version.",
497
+ choices: defaultComponentTypeChoices,
498
+ type: "string",
499
+ })
486
500
  .option("tlp-classification", {
487
501
  description:
488
502
  "Traffic Light Protocol (TLP) is a classification system for identifying the potential risk associated with an artefact, including whether it is subject to certain types of legal, financial, or technical threats. Refer to [https://www.first.org/tlp/](https://www.first.org/tlp/) for further information.",
@@ -567,6 +581,29 @@ const args = _yargs
567
581
  .array("standard")
568
582
  .array("feature-flags")
569
583
  .array("technique")
584
+ .array("componentType")
585
+ .check((argv) => {
586
+ const requestedComponentTypes = normalizeCycloneDxComponentTypeFilter(
587
+ argv.componentType,
588
+ );
589
+ if (!requestedComponentTypes.length) {
590
+ return true;
591
+ }
592
+ const normalizedSpecVersion =
593
+ toCycloneDxSpecVersionString(argv.specVersion) || "1.7";
594
+ const supportedComponentTypes = getSupportedCycloneDxComponentTypes(
595
+ normalizedSpecVersion,
596
+ );
597
+ const unsupportedComponentTypes = requestedComponentTypes.filter(
598
+ (componentType) => !supportedComponentTypes.includes(componentType),
599
+ );
600
+ if (unsupportedComponentTypes.length) {
601
+ throw new Error(
602
+ `Unsupported --component-type value(s) for CycloneDX ${normalizedSpecVersion}: ${unsupportedComponentTypes.join(", ")}. Supported values: ${supportedComponentTypes.join(", ")}`,
603
+ );
604
+ }
605
+ return true;
606
+ })
570
607
  .option("auto-compositions", {
571
608
  type: "boolean",
572
609
  default: true,
@@ -740,6 +777,12 @@ if (!options.projectType) {
740
777
  // Handle dedicated cbom and saasbom commands
741
778
  if (["cbom", "saasbom"].includes(invokedCommandName)) {
742
779
  if (invokedCommandName.includes("cbom")) {
780
+ if (normalizeCycloneDxComponentTypeFilter(options.componentType).length) {
781
+ console.error(
782
+ "The cbom command does not support --component-type. Use cdxgen with --include-crypto when you need component-type filtering.",
783
+ );
784
+ process.exit(1);
785
+ }
743
786
  thoughtLog(
744
787
  "Ok, the user wants to generate Cryptographic Bill-of-Materials (CBOM).",
745
788
  );
@@ -1676,7 +1719,10 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1676
1719
  reachablesSlicesFile: options.reachablesSlicesFile,
1677
1720
  semanticsSlicesFile: options.semanticsSlicesFile,
1678
1721
  openapiSpecFile: options.openapiSpecFile,
1679
- includeCrypto: options.includeCrypto,
1722
+ componentType: options.componentType,
1723
+ includeCrypto:
1724
+ options.includeCrypto &&
1725
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", options),
1680
1726
  specVersion: options.specVersion,
1681
1727
  profile: options.profile,
1682
1728
  jsonPretty: options.jsonPretty,
package/bin/evinse.js CHANGED
@@ -113,6 +113,13 @@ const args = yargs(hideBin(process.argv))
113
113
  default: false,
114
114
  type: "boolean",
115
115
  })
116
+ .option("exclude", {
117
+ alias: "exclude-regex",
118
+ description:
119
+ "Additional glob pattern(s) to ignore during Atom evidence generation.",
120
+ nargs: 1,
121
+ type: "array",
122
+ })
116
123
  .option("usages-slices-file", {
117
124
  description: "Use an existing usages slices file.",
118
125
  default: "usages.slices.json",
@@ -1597,6 +1597,131 @@ function buildChildOptions(options, target) {
1597
1597
  };
1598
1598
  }
1599
1599
 
1600
+ function normalizeFindingFile(filePath) {
1601
+ return String(filePath)
1602
+ .replace(/\\/g, "/")
1603
+ .replace(/\/+/g, "/")
1604
+ .replace(/^\.\//, "");
1605
+ }
1606
+
1607
+ function sourceFindingKey(finding) {
1608
+ if (!finding?.ruleId || !finding?.location?.file) {
1609
+ return undefined;
1610
+ }
1611
+ return `${finding.ruleId}|${normalizeFindingFile(finding.location.file)}`;
1612
+ }
1613
+
1614
+ /**
1615
+ * Recheck high/critical findings against the default branch to identify issues
1616
+ * that have already been fixed upstream. This reduces false positives caused by
1617
+ * scanning a version-specific tag where an issue existed but has since been
1618
+ * resolved in the project's main development line.
1619
+ *
1620
+ * @param {object[]} findings findings from version-specific scan
1621
+ * @param {object} target audit target
1622
+ * @param {object} resolution repository resolution metadata
1623
+ * @param {string[]} categories active audit categories
1624
+ * @param {object} options CLI options
1625
+ * @returns {Promise<{findings: object[], fixedInDefaultBranch: number}|undefined>}
1626
+ */
1627
+ async function recheckFindingsAgainstDefaultBranch(
1628
+ findings,
1629
+ target,
1630
+ resolution,
1631
+ categories,
1632
+ options,
1633
+ ) {
1634
+ let defaultBranchDir;
1635
+ try {
1636
+ defaultBranchDir = safeMkdtempSync(
1637
+ join(getTmpDir(), `${targetSlug(target)}-default-`),
1638
+ );
1639
+ await cloneRepositoryToDirWithRetry(
1640
+ resolution.repoUrl,
1641
+ defaultBranchDir,
1642
+ undefined,
1643
+ );
1644
+ const defaultSourceSelection = resolveTargetSourceDirectory(
1645
+ defaultBranchDir,
1646
+ target,
1647
+ resolution,
1648
+ );
1649
+ const childOptions = buildChildOptions(options, target);
1650
+ const bomNSData =
1651
+ (await createBom(defaultSourceSelection.scanDir, childOptions)) || {};
1652
+ if (!bomNSData?.bomJson) {
1653
+ return undefined;
1654
+ }
1655
+ const processedBomNSData = postProcess(
1656
+ bomNSData,
1657
+ childOptions,
1658
+ defaultSourceSelection.scanDir,
1659
+ );
1660
+ const defaultBranchFindings = await auditBom(processedBomNSData.bomJson, {
1661
+ bomAuditCategories: categories.join(","),
1662
+ bomAuditMinSeverity: options.minSeverity || "low",
1663
+ bomAuditRulesDir: options.rulesDir,
1664
+ });
1665
+ const defaultBranchSourceFindings = defaultBranchFindings.concat(
1666
+ buildPythonSourceHeuristicFindings(
1667
+ defaultSourceSelection.scanDir,
1668
+ target,
1669
+ ),
1670
+ );
1671
+ // Build a set of finding signatures present in the default branch
1672
+ const defaultBranchFindingKeys = new Set();
1673
+ for (const finding of defaultBranchSourceFindings) {
1674
+ const key = sourceFindingKey(finding);
1675
+ if (key) {
1676
+ defaultBranchFindingKeys.add(key);
1677
+ }
1678
+ }
1679
+ // Retain findings that are still present in the default branch or are
1680
+ // contextual/heuristic findings (not from rule evaluation against source).
1681
+ // Source-derived findings have a location.file pointing to a repo file,
1682
+ // while contextual findings reference purls or bomRefs only.
1683
+ const retainedFindings = [];
1684
+ let fixedCount = 0;
1685
+ for (const finding of findings) {
1686
+ const isSourceDerivedFinding = Boolean(finding?.location?.file);
1687
+ if (!isSourceDerivedFinding) {
1688
+ // Contextual/heuristic findings are not source-dependent
1689
+ retainedFindings.push(finding);
1690
+ continue;
1691
+ }
1692
+ if (defaultBranchFindingKeys.has(sourceFindingKey(finding))) {
1693
+ retainedFindings.push(finding);
1694
+ } else {
1695
+ fixedCount += 1;
1696
+ thoughtLog("Finding fixed in default branch, removing.", {
1697
+ ruleId: finding.ruleId,
1698
+ file: finding?.location?.file,
1699
+ purl: target.purl,
1700
+ });
1701
+ }
1702
+ }
1703
+ if (fixedCount === 0) {
1704
+ return undefined;
1705
+ }
1706
+ return {
1707
+ findings: retainedFindings,
1708
+ fixedInDefaultBranch: fixedCount,
1709
+ };
1710
+ } catch (error) {
1711
+ const errorMessage = error?.message ?? String(error);
1712
+ thoughtLog("Default branch recheck failed, keeping original findings.", {
1713
+ error: errorMessage,
1714
+ errorType: error?.errorType,
1715
+ purl: target.purl,
1716
+ });
1717
+ return undefined;
1718
+ } finally {
1719
+ if (defaultBranchDir) {
1720
+ cleanupSourceDir(defaultBranchDir);
1721
+ }
1722
+ }
1723
+ }
1724
+
1600
1725
  /**
1601
1726
  * Analyze a single purl target by generating a child SBOM and auditing it.
1602
1727
  *
@@ -1756,17 +1881,55 @@ export async function auditTarget(target, options) {
1756
1881
  sourceSelection.scanDir,
1757
1882
  target,
1758
1883
  );
1759
- const predictiveFindings = findings.concat(
1884
+ let predictiveFindings = findings.concat(
1760
1885
  contextualFindings,
1761
1886
  pythonSourceFindings,
1762
1887
  );
1763
- const assessment = scoreTargetRisk(predictiveFindings, target, {
1888
+ let assessment = scoreTargetRisk(predictiveFindings, target, {
1764
1889
  bomJson: processedBomJson,
1765
1890
  repoReused: Boolean(checkout?.reused || cacheHit),
1766
1891
  resolution,
1767
1892
  sourceDirectoryConfidence: sourceSelection.confidence,
1768
1893
  versionMatched,
1769
1894
  });
1895
+ // When we scanned a version-specific ref and the severity is high or
1896
+ // critical, recheck against the default branch to reduce false positives
1897
+ // from issues already fixed upstream.
1898
+ if (
1899
+ versionMatched &&
1900
+ !options.skipDefaultBranchRecheck &&
1901
+ severityMeetsThreshold(assessment.severity, "high") &&
1902
+ resolution?.repoUrl
1903
+ ) {
1904
+ emitProgress(options, {
1905
+ index: targetIndex,
1906
+ label: targetLabel,
1907
+ target,
1908
+ total: targetTotal,
1909
+ type: "target:stage",
1910
+ stage: "rechecking findings against default branch",
1911
+ });
1912
+ const recheckResult = await recheckFindingsAgainstDefaultBranch(
1913
+ predictiveFindings,
1914
+ target,
1915
+ resolution,
1916
+ categories,
1917
+ options,
1918
+ );
1919
+ if (recheckResult) {
1920
+ predictiveFindings = recheckResult.findings;
1921
+ assessment = scoreTargetRisk(predictiveFindings, target, {
1922
+ bomJson: processedBomJson,
1923
+ repoReused: Boolean(checkout?.reused || cacheHit),
1924
+ resolution,
1925
+ sourceDirectoryConfidence: sourceSelection.confidence,
1926
+ versionMatched,
1927
+ });
1928
+ assessment.defaultBranchRechecked = true;
1929
+ assessment.findingsFixedInDefaultBranch =
1930
+ recheckResult.fixedInDefaultBranch;
1931
+ }
1932
+ }
1770
1933
  return persistAuditArtifacts(
1771
1934
  {
1772
1935
  assessment,