@cdxgen/cdxgen 13.4.0 → 13.5.0

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 (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,10 +1,12 @@
1
1
  import { platform } from "node:os";
2
- import { extname } from "node:path";
2
+ import { basename, dirname, extname } from "node:path";
3
3
 
4
4
  import { build } from "@cdxgen/cdx-purl";
5
5
 
6
6
  import { safeExistsSync } from "../core/fs.js";
7
7
  import { thoughtLog } from "../core/logger.js";
8
+ import { sanitizeBomUrl } from "../core/propertySanitizer.js";
9
+ import { isAgentSkillSpecValid } from "./agentSkillSpec.js";
8
10
  import {
9
11
  addDosaiReachabilityProperties,
10
12
  readDosaiMethodsReport,
@@ -20,16 +22,26 @@ import {
20
22
  } from "./dosaiParsers.js";
21
23
  import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
22
24
  import { createLolbasProperties } from "./lolbas.js";
25
+ import { isOsPackagePurlType } from "./osPackagePurl.js";
23
26
  import {
24
27
  createOsQueryFallbackBomRef,
28
+ createOsQueryPackageIdentity,
25
29
  createOsQueryPurl,
26
30
  deriveOsQueryDescription,
27
31
  deriveOsQueryName,
28
32
  deriveOsQueryPublisher,
29
33
  deriveOsQueryVersion,
34
+ OS_PACKAGE_LICENSE_COLUMNS,
35
+ osPackagePropertyName,
30
36
  sanitizeOsQueryIdentity,
31
37
  shouldCreateOsQueryPurl,
32
38
  } from "./osqueryTransform.js";
39
+ import {
40
+ adjustLicenseInformation,
41
+ findLicenseId,
42
+ isSpdxLicenseExpression,
43
+ spdxLicenses,
44
+ } from "./spdx.js";
33
45
 
34
46
  /**
35
47
  * Build a CycloneDX occurrence evidence object from a location string.
@@ -540,20 +552,55 @@ export function convertOSQueryResults(
540
552
  enhance = false,
541
553
  osPackageListers = undefined,
542
554
  ) {
555
+ if (queryCategory === "ai_agent_skills") {
556
+ return convertAgentSkillRows(results);
557
+ }
543
558
  const pkgList = [];
559
+ // deb, rpm, apk, pacman, Homebrew, MacPorts, snap and flatpak rows describe a
560
+ // package, not a file or an app: the package manager names them, and nothing
561
+ // in such a row is a path inside the package.
562
+ const isOsPackage = isOsPackagePurlType(queryObj.purlType);
563
+ const isLocatedPackage =
564
+ LOCATED_PACKAGE_PURL_TYPES.has(queryObj.purlType) ||
565
+ LOCATED_PACKAGE_CATEGORIES.has(queryCategory);
544
566
  if (results?.length) {
545
567
  for (const res of results) {
546
- const version = deriveOsQueryVersion(res);
568
+ let version = deriveOsQueryVersion(res);
547
569
  let name = deriveOsQueryName(res, results.length === 1, queryObj.name);
548
570
  if (queryObj.purlType === "chrome-extension") {
549
- name = (res.identifier || res.extension_id || name || "").toLowerCase();
571
+ // osquery computes identifier from the manifest key, which an
572
+ // unpacked extension may lack; the profile still knows its ID.
573
+ name = (
574
+ res.identifier ||
575
+ res.referenced_identifier ||
576
+ res.extension_id ||
577
+ name ||
578
+ ""
579
+ ).toLowerCase();
580
+ }
581
+ let npmScope;
582
+ if (queryObj.purlType === "npm") {
583
+ const scoped = /^@([^/]+)\/(.+)$/.exec(`${name || ""}`);
584
+ if (scoped) {
585
+ [, npmScope, name] = scoped;
586
+ }
550
587
  }
551
588
  let group = "";
552
- const subpath = res.path || res.admindir || res.source;
589
+ const subpath =
590
+ isOsPackage || isLocatedPackage
591
+ ? undefined
592
+ : res.path || res.admindir || res.source;
553
593
  const publisher = deriveOsQueryPublisher(res);
554
- // For vscode-extension purl type, the publisher is used as the namespace
555
- if (queryObj.purlType === "vscode-extension" && publisher) {
556
- group = publisher.toLowerCase();
594
+ if (queryObj.purlType === "vscode-extension") {
595
+ // osquery names a VS Code extension by its ID, <publisher>.<name>,
596
+ // which the vscode-extension type splits into namespace and name. Its
597
+ // publisher column is the publisher's display name.
598
+ const extensionId = /^([^.]+)\.(.+)$/u.exec(`${name || ""}`);
599
+ if (extensionId) {
600
+ [, group, name] = extensionId;
601
+ } else if (publisher) {
602
+ group = publisher.toLowerCase();
603
+ }
557
604
  }
558
605
  let scope;
559
606
  const compScope = res.priority;
@@ -568,25 +615,53 @@ export function convertOSQueryResults(
568
615
  qualifiers = {
569
616
  tag_id: res.identifying_number.replace("{", "").replace("}", ""),
570
617
  };
618
+ } else if (queryCategory === "firefox_addons" && res.identifier) {
619
+ // The profile an add-on was found in is not part of its purl, so the
620
+ // add-on ID is what tells apart two add-ons that share a name.
621
+ qualifiers = { tag_id: firefoxAddonTagId(res.identifier) };
571
622
  }
572
623
  if (name) {
573
624
  name = sanitizeOsQueryIdentity(name);
574
- group = sanitizeOsQueryIdentity(group);
625
+ // sanitizeOsQueryIdentity drops a leading @, which an npm scope keeps.
626
+ group = npmScope
627
+ ? `@${sanitizeOsQueryIdentity(npmScope)}`
628
+ : sanitizeOsQueryIdentity(group);
575
629
  const isCryptoAsset = queryObj.componentType === "cryptographic-asset";
576
- const purl = shouldCreateOsQueryPurl(queryObj.componentType)
577
- ? createOsQueryPurl(
578
- queryObj.purlType,
579
- group,
580
- name,
581
- version,
582
- qualifiers,
583
- subpath,
584
- )
585
- : undefined;
630
+ let purl;
631
+ if (isOsPackage) {
632
+ // The version of such a row is part of its identity: an rpm's is its
633
+ // version and release, behind its epoch.
634
+ const identity = createOsQueryPackageIdentity(
635
+ queryObj.purlType,
636
+ res,
637
+ name,
638
+ );
639
+ version = identity.version;
640
+ purl = shouldCreateOsQueryPurl(queryObj.componentType)
641
+ ? identity.purl
642
+ : undefined;
643
+ } else if (shouldCreateOsQueryPurl(queryObj.componentType)) {
644
+ purl = createOsQueryPurl(
645
+ queryObj.purlType,
646
+ group,
647
+ name,
648
+ version,
649
+ qualifiers,
650
+ subpath,
651
+ );
652
+ }
586
653
  const props = [{ name: "cdx:osquery:category", value: queryCategory }];
587
654
  props.push(...createLolbasProperties(queryCategory, res));
588
655
  if (platform() === "linux") {
589
- props.push(...createGtfoBinsPropertiesFromRow(queryCategory, res));
656
+ // The GTFOBins matcher reads a description for command names, and
657
+ // the free text of a package's description matches by chance: zlib's
658
+ // "the deflate compression method found in gzip" is no use of gzip.
659
+ props.push(
660
+ ...createGtfoBinsPropertiesFromRow(
661
+ queryCategory,
662
+ isOsPackage ? { ...res, description: undefined } : res,
663
+ ),
664
+ );
590
665
  }
591
666
  let providesList;
592
667
  if (enhance) {
@@ -620,10 +695,12 @@ export function convertOSQueryResults(
620
695
  version: version || "",
621
696
  description,
622
697
  publisher,
698
+ // Without a purl, the bom-ref is built from the row, and the path of
699
+ // a located package is not part of what it is.
623
700
  "bom-ref": createOsQueryBomRef(
624
701
  queryCategory,
625
702
  queryObj.componentType,
626
- res,
703
+ isLocatedPackage ? { ...res, path: undefined } : res,
627
704
  name,
628
705
  version,
629
706
  purl,
@@ -644,25 +721,361 @@ export function convertOSQueryResults(
644
721
  if (cryptoProperties) {
645
722
  apkg.cryptoProperties = cryptoProperties;
646
723
  }
724
+ const licenseColumns = isOsPackage
725
+ ? OS_PACKAGE_LICENSE_COLUMNS[queryObj.purlType]
726
+ : undefined;
727
+ if (licenseColumns) {
728
+ const licenses = osPackageLicenses(
729
+ res[licenseColumns.license],
730
+ licenseColumns.separator,
731
+ );
732
+ if (licenses.length) {
733
+ apkg.licenses = licenses;
734
+ }
735
+ const website = licenseColumns.website
736
+ ? osPackageWebsite(res[licenseColumns.website])
737
+ : undefined;
738
+ if (website) {
739
+ apkg.externalReferences = [{ type: "website", url: website }];
740
+ }
741
+ }
647
742
  for (const k of Object.keys(res).filter((p) => {
648
743
  if (["version", "description", "publisher"].includes(p)) {
649
744
  return false;
650
745
  }
651
746
  return !(queryObj.purlType !== "chrome-extension" && p === "name");
652
747
  })) {
653
- if (res[k] && res[k] !== "null") {
748
+ const propertyName = isOsPackage
749
+ ? osPackagePropertyName(queryObj.purlType, k)
750
+ : k;
751
+ // (none) is what rpm's database says for a tag a record lacks, such
752
+ // as the source rpm of a key record.
753
+ if (
754
+ propertyName &&
755
+ res[k] &&
756
+ res[k] !== "null" &&
757
+ !(isOsPackage && res[k] === "(none)")
758
+ ) {
654
759
  props.push({
655
- name: k,
760
+ name: propertyName,
656
761
  value: res[k],
657
762
  });
658
763
  }
659
764
  }
765
+ if (isLocatedPackage) {
766
+ // osquery writes a JSON null as "null", which is the path Firefox
767
+ // gives its default theme.
768
+ const occurrence = createOccurrenceEvidence(
769
+ [res.path, res.directory].find(
770
+ (value) => value && value !== "null",
771
+ ),
772
+ );
773
+ if (occurrence) {
774
+ apkg.evidence = { occurrences: [occurrence] };
775
+ }
776
+ }
660
777
  apkg.properties = props;
661
778
  pkgList.push(apkg);
662
779
  }
663
780
  }
664
781
  }
665
- return pkgList;
782
+ return isLocatedPackage ? mergeLocatedPackages(pkgList) : pkgList;
783
+ }
784
+
785
+ // npm and pypi rows name a package found at a path on the host, Chromium and
786
+ // VS Code extension rows an extension found in a browser profile or an editor's
787
+ // extensions folder, and a firefox_addons row an add-on found in a Firefox
788
+ // profile. Where it was found is not part of what it is, so the path is an
789
+ // occurrence rather than a subpath: a home directory stays out of the purl, and
790
+ // one package installed in two places is one component with two occurrences.
791
+ const LOCATED_PACKAGE_PURL_TYPES = new Set([
792
+ "npm",
793
+ "pypi",
794
+ "chrome-extension",
795
+ "vscode-extension",
796
+ ]);
797
+ const LOCATED_PACKAGE_CATEGORIES = new Set(["firefox_addons"]);
798
+
799
+ const FIREFOX_ADDON_GUID =
800
+ /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/iu;
801
+
802
+ // A Firefox add-on ID is an email-like string or a GUID in braces. A swid
803
+ // tag_id that is a GUID must be in lower case, and is written without braces.
804
+ function firefoxAddonTagId(identifier) {
805
+ const id = `${identifier}`.trim();
806
+ const guid = FIREFOX_ADDON_GUID.exec(id);
807
+ return guid ? guid[1].toLowerCase() : id;
808
+ }
809
+
810
+ function mergeLocatedPackages(components) {
811
+ const byRef = new Map();
812
+ for (const component of components) {
813
+ const existing = byRef.get(component["bom-ref"]);
814
+ if (!existing) {
815
+ byRef.set(component["bom-ref"], component);
816
+ continue;
817
+ }
818
+ const occurrences = existing.evidence?.occurrences || [];
819
+ for (const occurrence of component.evidence?.occurrences || []) {
820
+ if (
821
+ !occurrences.some((known) => known.location === occurrence.location)
822
+ ) {
823
+ occurrences.push(occurrence);
824
+ }
825
+ }
826
+ if (occurrences.length) {
827
+ existing.evidence = { ...existing.evidence, occurrences };
828
+ }
829
+ for (const property of component.properties) {
830
+ if (
831
+ !existing.properties.some(
832
+ (known) =>
833
+ known.name === property.name && known.value === property.value,
834
+ )
835
+ ) {
836
+ existing.properties.push(property);
837
+ }
838
+ }
839
+ }
840
+ for (const component of byRef.values()) {
841
+ component.evidence?.occurrences?.sort((a, b) =>
842
+ a.location.localeCompare(b.location),
843
+ );
844
+ }
845
+ return [...byRef.values()];
846
+ }
847
+
848
+ // The licence an OS package declares. An apk `L:` field is a list of SPDX ids
849
+ // separated by spaces on older packages and an SPDX expression on newer ones;
850
+ // pacman keeps one licence per entry, each an SPDX id or expression, which
851
+ // osquery joins with commas; a snap or a flatpak declares one expression. Only
852
+ // an explicit operator or a parenthesis makes a text an expression: the SPDX
853
+ // parser's own check takes the `or` inside `LGPL-2.0-or-later` for an operator,
854
+ // and would turn a plain list into one expression. A token is an id only when
855
+ // the SPDX list has what it maps to; any other is kept by name rather than
856
+ // dropped.
857
+ const MAX_OS_PACKAGE_LICENSES = 64;
858
+ const SPDX_OPERATORS = new Set(["AND", "OR", "WITH"]);
859
+
860
+ function isExplicitSpdxExpression(text) {
861
+ // No SPDX expression holds a comma, but the expression parser lets one
862
+ // through, which would take a whole pacman licence list for one expression.
863
+ return (
864
+ !text.includes(",") &&
865
+ (text.split(/\s+/u).some((token) => SPDX_OPERATORS.has(token)) ||
866
+ text.includes("(")) &&
867
+ isSpdxLicenseExpression(text)
868
+ );
869
+ }
870
+
871
+ function osPackageLicenses(text, separator) {
872
+ const license = `${text ?? ""}`.split(/\s+/u).filter(Boolean).join(" ");
873
+ if (!license) {
874
+ return [];
875
+ }
876
+ if (isExplicitSpdxExpression(license)) {
877
+ return [{ expression: license }];
878
+ }
879
+ const entries = [];
880
+ const items = separator ? license.split(separator) : [license];
881
+ for (const item of items.slice(0, MAX_OS_PACKAGE_LICENSES)) {
882
+ const entry = item.trim();
883
+ if (!entry || SPDX_OPERATORS.has(entry)) {
884
+ continue;
885
+ }
886
+ if (isExplicitSpdxExpression(entry)) {
887
+ entries.push({ expression: entry });
888
+ continue;
889
+ }
890
+ const id = findLicenseId(entry);
891
+ entries.push(
892
+ spdxLicenses.includes(id)
893
+ ? { license: { id } }
894
+ : { license: { name: entry } },
895
+ );
896
+ }
897
+ // A list that holds an expression must become one expression, since a
898
+ // component's licenses are either licences or a single expression. Every
899
+ // entry applies, so they are joined with AND.
900
+ if (entries.length > 1 && entries.some((entry) => entry.expression)) {
901
+ const parts = entries.map((entry) =>
902
+ entry.expression ? `(${entry.expression})` : entry.license.id,
903
+ );
904
+ const expression = parts.join(" AND ");
905
+ if (parts.every(Boolean) && isSpdxLicenseExpression(expression)) {
906
+ return [{ expression }];
907
+ }
908
+ }
909
+ return adjustLicenseInformation(entries);
910
+ }
911
+
912
+ // The home page of a package. A URL a package carries is untrusted text: only
913
+ // an http(s) URL is kept, without credentials, query or fragment.
914
+ function osPackageWebsite(value) {
915
+ const cleaned = sanitizeBomUrl(`${value ?? ""}`.trim());
916
+ try {
917
+ const { protocol } = new URL(cleaned);
918
+ return protocol === "http:" || protocol === "https:" ? cleaned : undefined;
919
+ } catch {
920
+ return undefined;
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Convert the packages of an apk installed database into components.
926
+ *
927
+ * The packages are named as the container scan names an Alpine, Wolfi or
928
+ * Chainguard package, and grouped with the other OS package categories under
929
+ * `cdx:osquery:category` = `apk_packages`. Beyond what an osquery package row
930
+ * gives, an apk record knows its licence and home page, which become
931
+ * `licenses` and a `website` external reference.
932
+ *
933
+ * @param {Object[]} packages Packages from readApkInstalledDb()
934
+ * @returns {Object[]} Components
935
+ */
936
+ export function convertApkPackages(packages) {
937
+ const components = [];
938
+ for (const pkg of packages || []) {
939
+ const row = { name: pkg.name, version: pkg.version };
940
+ for (const column of ["arch", "origin", "maintainer"]) {
941
+ if (pkg[column]) {
942
+ row[column] = pkg[column];
943
+ }
944
+ }
945
+ const [component] = convertOSQueryResults(
946
+ "apk_packages",
947
+ { purlType: "apk" },
948
+ [row],
949
+ );
950
+ if (!component) {
951
+ continue;
952
+ }
953
+ // The description is set here and not in the row. The GTFOBins matcher reads
954
+ // a row's description for command names, which a deb or rpm row has no
955
+ // column for, and the free text of an apk description matches by chance:
956
+ // "Alpine base dir structure" is no use of the alpine mail client.
957
+ if (pkg.description) {
958
+ component.description = pkg.description;
959
+ }
960
+ const licenses = osPackageLicenses(pkg.license, /\s+/u);
961
+ if (licenses.length) {
962
+ component.licenses = licenses;
963
+ }
964
+ const website = osPackageWebsite(pkg.url);
965
+ if (website) {
966
+ component.externalReferences = [{ type: "website", url: website }];
967
+ }
968
+ components.push(component);
969
+ }
970
+ return components;
971
+ }
972
+
973
+ // The frontmatter and osquery columns of a skill row that become properties,
974
+ // named as the ai-skill project scan names them where it has a name.
975
+ const AGENT_SKILL_PROPERTY_NAMES = [
976
+ ["name", "cdx:skill:name"],
977
+ ["description", "cdx:skill:description"],
978
+ ["license", "cdx:skill:license"],
979
+ ["compatibility", "cdx:skill:compatibility"],
980
+ ["agent", "cdx:skill:agent"],
981
+ ["scope", "cdx:skill:scope"],
982
+ ["allowed_tools", "cdx:skill:allowedTools"],
983
+ ["resource_count", "cdx:skill:resourceCount"],
984
+ ["script_count", "cdx:skill:scriptCount"],
985
+ ["username", "username"],
986
+ ];
987
+ const SHA256_HEX = /^[0-9a-f]{64}$/u;
988
+
989
+ // A skill's licence is free text ("Complete terms in LICENSE.txt"), so it
990
+ // becomes `licenses` only when it is an SPDX id or expression.
991
+ function agentSkillLicenses(text) {
992
+ const license = `${text ?? ""}`.trim();
993
+ if (!license) {
994
+ return [];
995
+ }
996
+ if (isExplicitSpdxExpression(license)) {
997
+ return [{ expression: license }];
998
+ }
999
+ const id = findLicenseId(license);
1000
+ return spdxLicenses.includes(id) ? [{ license: { id } }] : [];
1001
+ }
1002
+
1003
+ /**
1004
+ * Convert the rows of the `ai_agent_skills` query into components.
1005
+ *
1006
+ * A skill installed on the host is what the `ai-skill` project scan finds in a
1007
+ * repository, so it is a `file` component with the same `cdx:file:kind` and
1008
+ * `cdx:skill:*` properties, and no purl: a skill has no package identity. Its
1009
+ * bom-ref is the SHA-256 of its SKILL.md rather than the path, which is under a
1010
+ * home directory, and where it was found is an occurrence. The same skill
1011
+ * found under several roots, such as a plugin marketplace checkout and the
1012
+ * plugin cache, is one component with one occurrence for each.
1013
+ *
1014
+ * @param {Object[]} results Rows of `ai_agent_skills` joined with `hash`
1015
+ * @returns {Object[]} Components
1016
+ */
1017
+ export function convertAgentSkillRows(results) {
1018
+ const components = [];
1019
+ for (const row of results || []) {
1020
+ const skillFile = `${row.path ?? ""}`;
1021
+ const directory = `${row.directory ?? ""}` || dirname(skillFile);
1022
+ // A SKILL.md whose frontmatter osquery cannot read has no name; the
1023
+ // project scan names such a skill after its directory too.
1024
+ const name = sanitizeOsQueryIdentity(row.name || basename(directory));
1025
+ if (!skillFile || !name) {
1026
+ continue;
1027
+ }
1028
+ const version = row.version || "";
1029
+ const sha256 = SHA256_HEX.test(`${row.sha256 ?? ""}`)
1030
+ ? row.sha256
1031
+ : undefined;
1032
+ const properties = [
1033
+ { name: "cdx:osquery:category", value: "ai_agent_skills" },
1034
+ { name: "cdx:file:kind", value: "skill-file" },
1035
+ { name: "cdx:agent:inventorySource", value: "osquery" },
1036
+ ];
1037
+ for (const [column, propertyName] of AGENT_SKILL_PROPERTY_NAMES) {
1038
+ const value = `${row[column] ?? ""}`.trim();
1039
+ if (value && value !== "null") {
1040
+ properties.push({ name: propertyName, value });
1041
+ }
1042
+ }
1043
+ if (Number(row.script_count) > 0) {
1044
+ properties.push({ name: "cdx:skill:hasScripts", value: "true" });
1045
+ }
1046
+ properties.push({
1047
+ name: "cdx:skill:specValid",
1048
+ value: isAgentSkillSpecValid(row) ? "true" : "false",
1049
+ });
1050
+ const component = {
1051
+ name,
1052
+ version,
1053
+ description: row.description || "",
1054
+ type: "file",
1055
+ "bom-ref": createOsQueryFallbackBomRef(
1056
+ "ai_agent_skills",
1057
+ "file",
1058
+ name,
1059
+ version,
1060
+ sha256 ? "sha256" : "scope",
1061
+ sha256 || `${row.agent || ""}/${row.scope || ""}`,
1062
+ ),
1063
+ properties,
1064
+ };
1065
+ if (sha256) {
1066
+ component.hashes = [{ alg: "SHA-256", content: sha256 }];
1067
+ }
1068
+ const licenses = agentSkillLicenses(row.license);
1069
+ if (licenses.length) {
1070
+ component.licenses = licenses;
1071
+ }
1072
+ const occurrence = createOccurrenceEvidence(skillFile);
1073
+ if (occurrence) {
1074
+ component.evidence = { occurrences: [occurrence] };
1075
+ }
1076
+ components.push(component);
1077
+ }
1078
+ return mergeLocatedPackages(components);
666
1079
  }
667
1080
  function createOsQueryCryptoProperties(queryCategory, res, version) {
668
1081
  switch (queryCategory) {
@@ -285,8 +285,9 @@ export function addFormulationSection(filePath, options, context = {}) {
285
285
  }
286
286
  }
287
287
 
288
- // Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby)
289
- const infoComponents = collectEnvInfo(projectPath);
288
+ // Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby,
289
+ // PHP, Composer)
290
+ const infoComponents = collectEnvInfo(projectPath, options);
290
291
  if (infoComponents?.length) {
291
292
  components = components.concat(infoComponents);
292
293
  }
@@ -0,0 +1,140 @@
1
+ // Bounded Go buildinfo reader for executable file components: recovers the
2
+ // toolchain version a binary was built with, so a BOM can answer "which Go
3
+ // programs predate the CVE-2026-111943 distrust list". Only the header and
4
+ // the inline version string are read; pointer-based (pre-1.18) layouts are
5
+ // reported as such and never followed.
6
+ //
7
+ // Layout: go/src/debug/buildinfo/buildinfo.go at tag go1.27.2. The 14-byte
8
+ // magic `\xff Go buildinf:` is 16-byte aligned; the next two bytes are the
9
+ // pointer size and flags. When flags&2 is set the version string follows as a
10
+ // uvarint length plus bytes. The magic can appear anywhere in the file's
11
+ // initial segment, so the search scans aligned offsets up to a cap.
12
+
13
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
14
+
15
+ const BUILD_INFO_MAGIC = Buffer.from([
16
+ 0xff, 0x20, 0x47, 0x6f, 0x20, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x66,
17
+ 0x3a,
18
+ ]);
19
+ // The buildinfo section follows text and rodata, often late in the file
20
+ // (37 MB into a 40 MB gh binary), so the cap must cover large binaries.
21
+ const MAX_SEARCH_BYTES = 512 * 1024 * 1024;
22
+ const READ_CHUNK_BYTES = 64 * 1024;
23
+ const MAX_VERSION_BYTES = 128;
24
+ export const PRE_1_18_VERSION = "unknown-pre-1.18";
25
+
26
+ function readUvarint(buffer, offset) {
27
+ let value = 0;
28
+ let shift = 0;
29
+ for (let i = 0; i < 10 && offset + i < buffer.length; i++) {
30
+ const byte = buffer[offset + i];
31
+ value += (byte & 0x7f) * 2 ** shift;
32
+ if (!(byte & 0x80)) {
33
+ return { value, next: offset + i + 1 };
34
+ }
35
+ shift += 7;
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ function parseHeaderAt(buffer, offset, limit) {
41
+ if (offset + 32 > limit) {
42
+ return undefined;
43
+ }
44
+ for (let i = 0; i < BUILD_INFO_MAGIC.length; i++) {
45
+ if (buffer[offset + i] !== BUILD_INFO_MAGIC[i]) {
46
+ return undefined;
47
+ }
48
+ }
49
+ const pointerSize = buffer[offset + 14];
50
+ const flags = buffer[offset + 15];
51
+ if (pointerSize !== 4 && pointerSize !== 8) {
52
+ return undefined;
53
+ }
54
+ if (!(flags & 0x02)) {
55
+ // Version lives behind pointers (pre-1.18 layout): not followed.
56
+ return PRE_1_18_VERSION;
57
+ }
58
+ // The inline strings start 16 bytes past the magic header (magic 14 +
59
+ // pointerSize 1 + flags 1, then a reserved 16-byte slot), as a uvarint
60
+ // length followed by the bytes.
61
+ const varint = readUvarint(buffer, offset + 32);
62
+ if (
63
+ !varint ||
64
+ varint.value === 0 ||
65
+ varint.value > MAX_VERSION_BYTES ||
66
+ varint.next + varint.value > limit
67
+ ) {
68
+ return undefined;
69
+ }
70
+ const version = buffer.toString(
71
+ "latin1",
72
+ varint.next,
73
+ varint.next + varint.value,
74
+ );
75
+ if (!/^go[0-9]/.test(version)) {
76
+ return undefined;
77
+ }
78
+ return version;
79
+ }
80
+
81
+ /**
82
+ * Read the Go toolchain version from an executable's buildinfo header.
83
+ *
84
+ * @param {string} filePath absolute path to a possible Go binary
85
+ * @returns {string|undefined} `go1.x.y`, `unknown-pre-1.18` for old layouts,
86
+ * or undefined when the file carries no Go buildinfo
87
+ */
88
+ export function readGoBuildVersion(filePath) {
89
+ if (typeof filePath !== "string" || !filePath) {
90
+ return undefined;
91
+ }
92
+ let fd;
93
+ try {
94
+ fd = openSync(filePath, "r");
95
+ const size = fstatSync(fd).size;
96
+ if (size < 32) {
97
+ return undefined;
98
+ }
99
+ const searchLimit = Math.min(size, MAX_SEARCH_BYTES);
100
+ // Each chunk examines the aligned offsets that start inside its 64 KiB
101
+ // window, reading a fixed overlap past it so a header near the window
102
+ // end is parsed whole; the next chunk starts at the next window.
103
+ const OVERLAP_BYTES = 256;
104
+ const chunk = Buffer.alloc(READ_CHUNK_BYTES + OVERLAP_BYTES);
105
+ for (
106
+ let position = 0;
107
+ position < searchLimit;
108
+ position += READ_CHUNK_BYTES
109
+ ) {
110
+ const want = Math.min(READ_CHUNK_BYTES + OVERLAP_BYTES, size - position);
111
+ const read = readSync(fd, chunk, 0, want, position);
112
+ if (read < 32) {
113
+ return undefined;
114
+ }
115
+ // position is a multiple of 16, so chunk offsets stay on the file's
116
+ // 16-byte grid.
117
+ for (
118
+ let offset = 0;
119
+ offset < READ_CHUNK_BYTES && offset + 32 <= read;
120
+ offset += 16
121
+ ) {
122
+ const version = parseHeaderAt(chunk, offset, read);
123
+ if (version) {
124
+ return version;
125
+ }
126
+ }
127
+ }
128
+ return undefined;
129
+ } catch {
130
+ return undefined;
131
+ } finally {
132
+ if (fd !== undefined) {
133
+ try {
134
+ closeSync(fd);
135
+ } catch {
136
+ // already closed
137
+ }
138
+ }
139
+ }
140
+ }