@cdxgen/cdxgen 13.4.0 → 13.6.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Whether the frontmatter of a SKILL.md satisfies the open Agent Skills
3
+ * constraints (agentskills.io): a name of 1-64 lowercase letters, digits and
4
+ * hyphens, and a description of 1-1024 characters.
5
+ *
6
+ * @param {{name?: unknown, description?: unknown}|undefined} metadata Frontmatter values
7
+ * @returns {boolean} true when both constraints hold
8
+ */
9
+ export function isAgentSkillSpecValid(metadata) {
10
+ return (
11
+ typeof metadata?.name === "string" &&
12
+ /^[a-z0-9-]{1,64}$/u.test(metadata.name) &&
13
+ typeof metadata?.description === "string" &&
14
+ metadata.description.length >= 1 &&
15
+ metadata.description.length <= 1024
16
+ );
17
+ }
@@ -0,0 +1,243 @@
1
+ // Bounded, read-only reader of the apk installed database.
2
+ //
3
+ // osquery has no apk table, so an Alpine, Wolfi or Chainguard host would have no
4
+ // OS package inventory in an OBOM, and the PHP host probe needs the same
5
+ // database to tell which PHP files a package owns. Both read it through here, so
6
+ // what the OBOM lists and what the probe treats as owned follow one parser: the
7
+ // same file, location order, caps and record rules.
8
+ //
9
+ // The database is `/lib/apk/db/installed`. A usr-merged system keeps it at
10
+ // `/usr/lib/apk/db/installed` (and `/lib` is a link to `/usr/lib`, so the two
11
+ // names are one file), and Alpaquita keeps it under `/var/lib/apk/db`. The first
12
+ // of those that exists is the database. One that exists but cannot be read
13
+ // leaves the packages unknown: it is reported as unreadable, never read in part
14
+ // and never skipped for the next name.
15
+ //
16
+ // A record is a run of `<letter>:<value>` lines ended by a blank line. P: names
17
+ // the package, V: its version, A: its architecture, o: its origin (the source
18
+ // package), m: its maintainer, L: its licence, U: its home page and T: its
19
+ // description. F: names a directory and each R: after it a file in that
20
+ // directory. A record may start with C: (Alpine) or P: (Wolfi).
21
+
22
+ import { statSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import process from "node:process";
25
+
26
+ import { isDryRun, recordActivity } from "../core/activity.js";
27
+ import { safeExistsSync, safeReadFileSync } from "../core/fs.js";
28
+ import { thoughtLog } from "../core/logger.js";
29
+
30
+ /** The database, or the packages it lists, past these is unreadable rather than read in part. */
31
+ export const MAX_APK_DATABASE_BYTES = 64 * 1024 * 1024;
32
+ export const MAX_APK_PACKAGES = 100000;
33
+
34
+ /** Where apk keeps the installed database, relative to the root, in the order they are tried. */
35
+ export const APK_INSTALLED_DB_PATHS = [
36
+ "lib/apk/db/installed",
37
+ "usr/lib/apk/db/installed",
38
+ "var/lib/apk/db/installed",
39
+ ];
40
+
41
+ // No field of a package record is longer than this once read.
42
+ const MAX_FIELD_LENGTH = 4096;
43
+
44
+ const FIELD_NAMES = {
45
+ P: "name",
46
+ V: "version",
47
+ A: "arch",
48
+ o: "origin",
49
+ m: "maintainer",
50
+ L: "license",
51
+ U: "url",
52
+ T: "description",
53
+ };
54
+
55
+ function readBounded(file, maxBytes) {
56
+ try {
57
+ const stats = statSync(file);
58
+ if (!stats.isFile() || stats.size > maxBytes) {
59
+ return undefined;
60
+ }
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ const text = safeReadFileSync(file, "utf-8");
65
+ // A file that grew between the stat and the read is over the cap too.
66
+ return typeof text === "string" && text.length <= maxBytes ? text : undefined;
67
+ }
68
+
69
+ function newRecord() {
70
+ return { fields: {}, fileCount: 0, files: [], touched: false };
71
+ }
72
+
73
+ /**
74
+ * Parse the text of an installed database.
75
+ *
76
+ * @param {string} text Database text
77
+ * @param {Set<string>|undefined} wantedFiles Absolute paths whose owner is wanted
78
+ * @param {number} maxPackages Most packages to accept
79
+ * @returns {{packages: Object[], malformed: number, overflow: boolean}}
80
+ */
81
+ function parseDatabase(text, wantedFiles, maxPackages) {
82
+ const packages = [];
83
+ let malformed = 0;
84
+ let overflow = false;
85
+ let record = newRecord();
86
+ let directory = "";
87
+ const finishRecord = () => {
88
+ const { fields, fileCount, files, touched } = record;
89
+ record = newRecord();
90
+ directory = "";
91
+ if (!touched) {
92
+ return;
93
+ }
94
+ // A package with no name or no version cannot be named by a purl.
95
+ if (!fields.name || !fields.version) {
96
+ malformed++;
97
+ return;
98
+ }
99
+ packages.push({ ...fields, fileCount, files });
100
+ if (packages.length > maxPackages) {
101
+ overflow = true;
102
+ }
103
+ };
104
+ let position = 0;
105
+ while (position <= text.length && !overflow) {
106
+ let end = text.indexOf("\n", position);
107
+ if (end === -1) {
108
+ end = text.length;
109
+ }
110
+ let line = text.slice(position, end);
111
+ position = end + 1;
112
+ if (line.endsWith("\r")) {
113
+ line = line.slice(0, -1);
114
+ }
115
+ if (line === "") {
116
+ finishRecord();
117
+ continue;
118
+ }
119
+ if (line.length < 2 || line[1] !== ":") {
120
+ continue;
121
+ }
122
+ const letter = line[0];
123
+ const value = line.slice(2);
124
+ if (Object.hasOwn(FIELD_NAMES, letter)) {
125
+ record.touched = true;
126
+ if (value) {
127
+ record.fields[FIELD_NAMES[letter]] =
128
+ value.length > MAX_FIELD_LENGTH
129
+ ? value.slice(0, MAX_FIELD_LENGTH)
130
+ : value;
131
+ }
132
+ } else if (letter === "F") {
133
+ record.touched = true;
134
+ directory = value;
135
+ } else if (letter === "R") {
136
+ record.touched = true;
137
+ record.fileCount++;
138
+ if (wantedFiles) {
139
+ const file = `/${directory ? `${directory}/` : ""}${value}`;
140
+ if (wantedFiles.has(file)) {
141
+ record.files.push(file);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ if (!overflow) {
147
+ finishRecord();
148
+ }
149
+ return { packages, malformed, overflow };
150
+ }
151
+
152
+ /**
153
+ * Read the packages of the apk installed database.
154
+ *
155
+ * @param {Object} [options] Read options
156
+ * @param {string} [options.rootPath="/"] Directory the database paths hang off;
157
+ * tests point it at a built tree
158
+ * @param {string} [options.databaseFile] Read this file instead of looking
159
+ * under `rootPath`
160
+ * @param {Set<string>} [options.wantedFiles] Absolute paths whose owner is
161
+ * wanted. A package comes back with the ones it owns in `files`; the other
162
+ * files of a package are only counted, so a large database stays small.
163
+ * @param {number} [options.maxBytes] Largest database to read, 64 MiB by default
164
+ * @param {number} [options.maxPackages] Most packages to accept, 100000 by default
165
+ * @param {string} [options.platform] `linux` by default for the process; an apk
166
+ * database exists on Linux only
167
+ * @returns {{state: string, path?: string, packages: Object[], malformed: number}}
168
+ * `state` is `ok`, `absent` (no database, or not Linux), `unreadable` (a
169
+ * database that could not be read in full) or `skipped` (a dry run).
170
+ * `malformed` counts the records left out for having no name or version.
171
+ * Each package has `name`, `version` and `fileCount`, `files`, and, when the
172
+ * record has them, `arch`, `origin`, `maintainer`, `license`, `url` and
173
+ * `description`.
174
+ */
175
+ export function readApkInstalledDb(options = {}) {
176
+ const result = (state, extra = {}) => ({
177
+ state,
178
+ packages: [],
179
+ malformed: 0,
180
+ ...extra,
181
+ });
182
+ const platform = options.platform || process.platform;
183
+ if (platform !== "linux") {
184
+ return result("absent");
185
+ }
186
+ const rootPath = options.rootPath || "/";
187
+ if (isDryRun) {
188
+ recordActivity({
189
+ capability: "apk-installed-db",
190
+ kind: "read",
191
+ reason:
192
+ "Dry run mode blocks reading the apk installed database, which lives outside the project.",
193
+ status: "blocked",
194
+ target: options.databaseFile || rootPath,
195
+ });
196
+ return result("skipped");
197
+ }
198
+ const candidates = options.databaseFile
199
+ ? [options.databaseFile]
200
+ : APK_INSTALLED_DB_PATHS.map((location) => join(rootPath, location));
201
+ const file = candidates.find((candidate) => safeExistsSync(candidate));
202
+ if (!file) {
203
+ return result("absent");
204
+ }
205
+ const unreadable = (reason) => {
206
+ thoughtLog(`The apk installed database ${file} was not read: ${reason}.`);
207
+ recordActivity({
208
+ capability: "apk-installed-db",
209
+ kind: "read",
210
+ reason: `The apk installed database ${file} could not be read: ${reason}.`,
211
+ status: "failed",
212
+ target: file,
213
+ });
214
+ return result("unreadable", { path: file });
215
+ };
216
+ const maxBytes = options.maxBytes ?? MAX_APK_DATABASE_BYTES;
217
+ const text = readBounded(file, maxBytes);
218
+ if (text === undefined) {
219
+ return unreadable(`it is not a file of at most ${maxBytes} bytes`);
220
+ }
221
+ const maxPackages = options.maxPackages ?? MAX_APK_PACKAGES;
222
+ const { packages, malformed, overflow } = parseDatabase(
223
+ text,
224
+ options.wantedFiles,
225
+ maxPackages,
226
+ );
227
+ if (overflow) {
228
+ return unreadable(`it lists more than ${maxPackages} packages`);
229
+ }
230
+ if (malformed) {
231
+ thoughtLog(
232
+ `${malformed} record(s) of the apk installed database ${file} have no name or no version and were left out.`,
233
+ );
234
+ }
235
+ recordActivity({
236
+ capability: "apk-installed-db",
237
+ kind: "read",
238
+ reason: `Read ${packages.length} package(s) from the apk installed database ${file}.`,
239
+ status: "completed",
240
+ target: file,
241
+ });
242
+ return result("ok", { path: file, packages, malformed });
243
+ }
@@ -30,6 +30,7 @@ import {
30
30
  import { TRACE_MODE } from "../core/logger.js";
31
31
  import { dirNameStr, isWin } from "../core/paths.js";
32
32
  import { formatDuration, reapProcessTree } from "../core/processTree.js";
33
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
33
34
  import { readJsonFile } from "../parsers/largeJson.js";
34
35
  import { cmakeBuildDirCandidates } from "./cmakeBuildDirs.js";
35
36
 
@@ -182,6 +183,7 @@ export function findCompileCommands(src, options = {}) {
182
183
  }
183
184
 
184
185
  const atomFrontendArgKeysCache = new Map();
186
+ const atomOptionsCache = new Map();
185
187
 
186
188
  /**
187
189
  * Split an atom command that may carry a leading argument (`node
@@ -270,6 +272,43 @@ export function atomFrontendArgKeys(language) {
270
272
  return keys;
271
273
  }
272
274
 
275
+ /**
276
+ * The options the installed atom accepts, as `atom --help` lists them. Asked
277
+ * once per atom command. An atom that fails to start accepts none.
278
+ *
279
+ * @returns {Set<string>} Supported options, such as `--bom-file`
280
+ */
281
+ export function atomOptions() {
282
+ const command = getAtomCommand();
283
+ if (atomOptionsCache.has(command)) {
284
+ return atomOptionsCache.get(command);
285
+ }
286
+ const [bin, leadingArg] = splitAtomCommand(command);
287
+ const args = ["--help"];
288
+ if (leadingArg !== undefined) {
289
+ args.unshift(leadingArg);
290
+ }
291
+ const env = { ...process.env };
292
+ if (readEnvironmentVariable("ATOM_JAVA_HOME")) {
293
+ env.JAVA_HOME = readEnvironmentVariable("ATOM_JAVA_HOME");
294
+ }
295
+ const options = new Set();
296
+ const result = safeSpawnSync(bin, args, {
297
+ shell: isWin,
298
+ env,
299
+ timeout: 120000,
300
+ });
301
+ if (result?.status === 0 && result.stdout) {
302
+ for (const match of `${result.stdout}`.matchAll(
303
+ /(?:^|\s)(--[a-z][a-z0-9-]*)/g,
304
+ )) {
305
+ options.add(match[1]);
306
+ }
307
+ }
308
+ atomOptionsCache.set(command, options);
309
+ return options;
310
+ }
311
+
273
312
  /**
274
313
  * The `--frontend-args` atom takes for a C/C++ language: the project's
275
314
  * compilation database, when there is one and the installed atom reads it
@@ -314,6 +353,77 @@ export function atomCompileCommandsArgs(src, language, options = {}) {
314
353
  return ["--frontend-args", `compile-commands=${database}`];
315
354
  }
316
355
 
356
+ /**
357
+ * The `--bom-file` a reachables slice takes package purls from: the BOM evinse
358
+ * annotates. atom would otherwise only find a BOM saved as `bom.json` inside
359
+ * the scanned directory, so a BOM written anywhere else gave every reachable
360
+ * flow no purl. An atom without `--bom-file` is given the arguments it has
361
+ * always been given, and still reads a `bom.json` in the scanned directory.
362
+ *
363
+ * @param {string} sliceType atom slice command
364
+ * @param {Object} options CLI options, for `input`
365
+ * @returns {string[]} Arguments to add to the atom command
366
+ */
367
+ export function atomBomFileArgs(sliceType, options = {}) {
368
+ if (
369
+ sliceType !== "reachables" ||
370
+ !options?.input ||
371
+ !safeExistsSync(options.input)
372
+ ) {
373
+ return [];
374
+ }
375
+ const bomFile = resolve(options.input);
376
+ // The argument reaches a Windows shell command line through `shell: isWin`
377
+ // in executeAtom, so a path a shell would interpret is never passed on.
378
+ if (UNSAFE_SHELL_CHARS_RE.test(bomFile)) {
379
+ console.warn(
380
+ `The BOM path ${bomFile} contains characters a shell would interpret. Reachable flows will carry purls only if a bom.json is in the scanned directory.`,
381
+ );
382
+ return [];
383
+ }
384
+ if (!atomOptions().has("--bom-file")) {
385
+ if (DEBUG_MODE) {
386
+ console.log(
387
+ "The installed atom does not take --bom-file. Reachable flows will carry purls only if a bom.json is in the scanned directory.",
388
+ );
389
+ }
390
+ return [];
391
+ }
392
+ return ["--bom-file", bomFile];
393
+ }
394
+
395
+ /**
396
+ * The arguments that stop atom's Java frontend from running the scanned
397
+ * project's build. In its default delombok mode, atom runs
398
+ * `mvn dependency:build-classpath` inside a Maven project, which loads the
399
+ * project's `.mvn` configuration and build extensions, so delombok and
400
+ * dependency fetching are turned off whenever the project's own tooling may
401
+ * not run.
402
+ *
403
+ * @param {string} language atom `-l` value
404
+ * @param {Object} options CLI options
405
+ * @returns {string[]|undefined} Arguments to add to the atom command, or
406
+ * undefined when the installed atom cannot be told to leave the build alone
407
+ */
408
+ export function atomBuildToolArgs(language, options = {}) {
409
+ if (
410
+ `${language}`.toLowerCase() !== "java" ||
411
+ isProjectToolingAllowed(options)
412
+ ) {
413
+ return [];
414
+ }
415
+ const keys = atomFrontendArgKeys(language);
416
+ if (!keys.has("delombok-mode")) {
417
+ return undefined;
418
+ }
419
+ return [
420
+ "--frontend-args",
421
+ keys.has("fetch-dependencies")
422
+ ? "delombok-mode=no-delombok,fetch-dependencies=false"
423
+ : "delombok-mode=no-delombok",
424
+ ];
425
+ }
426
+
317
427
  // Absolute ceiling on atom's heap. Past this point a larger heap buys a
318
428
  // collector that runs less often rather than an analysis that succeeds where it
319
429
  // otherwise would not, while the reservation itself is what pushes a build
@@ -1,4 +1,4 @@
1
- import { shouldRunPredictiveBomAudit } from "../core/env.js";
1
+ import { getCommandName, shouldRunPredictiveBomAudit } from "../core/env.js";
2
2
 
3
3
  /** Frozen list of hardware-bill-of-materials (HBOM) audit categories. */
4
4
  export const HBOM_AUDIT_CATEGORIES = Object.freeze([
@@ -186,11 +186,11 @@ export function validateBomAuditCategories(categories, rules) {
186
186
  * @returns {string | undefined} Default category string, if any
187
187
  */
188
188
  export function getDefaultBomAuditCategories(options, commandPath) {
189
- const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
189
+ const commandName = getCommandName(commandPath);
190
190
  const defaultHbomCategories = options?.includeRuntime
191
191
  ? `${DEFAULT_HBOM_AUDIT_CATEGORIES},host-topology`
192
192
  : DEFAULT_HBOM_AUDIT_CATEGORIES;
193
- if (normalizedCommandPath.includes("hbom")) {
193
+ if (commandName.startsWith("hbom")) {
194
194
  return defaultHbomCategories;
195
195
  }
196
196
  const projectTypes = Array.isArray(options?.projectType)
@@ -0,0 +1,142 @@
1
+ // BOM components for the Brotli decoders compiled into native files. The
2
+ // detection itself, and why its markers can be trusted, is in
3
+ // lib/core/brotliProbe.js.
4
+
5
+ import {
6
+ MAX_BROTLI_PROBE_BYTES,
7
+ MAX_BROTLI_PROBES,
8
+ readBrotliDecoder,
9
+ } from "../core/brotliProbe.js";
10
+ import { applyPurl, tryBuildPurl } from "./purl.js";
11
+
12
+ export { MAX_BROTLI_PROBE_BYTES, MAX_BROTLI_PROBES, readBrotliDecoder };
13
+
14
+ /**
15
+ * The properties a file component carries once it has been probed: the fix
16
+ * state of each decoder found in it, or why it could not be read.
17
+ *
18
+ * @param {Object} decoder result of readBrotliDecoder
19
+ * @returns {{name: string, value: string}[]} properties to append
20
+ */
21
+ export function brotliDecoderProperties(decoder) {
22
+ const properties = [];
23
+ if (decoder?.skipped) {
24
+ properties.push({ name: "cdx:brotli:probe", value: decoder.skipped });
25
+ }
26
+ if (decoder?.c) {
27
+ properties.push(
28
+ { name: "cdx:brotli:c:blockSwitchFix", value: decoder.c.blockSwitchFix },
29
+ { name: "cdx:brotli:c:evidence", value: decoder.c.evidence },
30
+ );
31
+ }
32
+ if (decoder?.go) {
33
+ properties.push({
34
+ name: "cdx:brotli:go:blockSwitchFix",
35
+ value: decoder.go.blockSwitchFix,
36
+ });
37
+ }
38
+ return properties;
39
+ }
40
+
41
+ /**
42
+ * The library component every embedded decoder links to. It has no version:
43
+ * no build records one, and only the per-file cdx:brotli:*:blockSwitchFix
44
+ * properties say whether a copy is vulnerable. The `vcs_url` qualifier names
45
+ * the project and keeps the identity apart from an unpackaged `brotli`
46
+ * executable, which is `pkg:generic/brotli`. The purl has no namespace: a
47
+ * `pkg:github` purl is typed as an application and `pkg:generic/google` as a
48
+ * framework when the BOM is assembled, and this is neither.
49
+ *
50
+ * @returns {Object} component with `purl` and `bom-ref`
51
+ */
52
+ export function createBrotliComponent() {
53
+ return applyPurl(
54
+ {
55
+ type: "library",
56
+ name: "brotli",
57
+ description:
58
+ "Brotli decoder compiled into native files; see the cdx:brotli properties on each file",
59
+ properties: [],
60
+ },
61
+ tryBuildPurl({
62
+ type: "generic",
63
+ name: "brotli",
64
+ qualifiers: { vcs_url: "git+https://github.com/google/brotli" },
65
+ }),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Start collecting the decoder copies of one scan.
71
+ *
72
+ * @returns {{component: Object|undefined, dependencies: Object[], copies: number, unpatched: number, unknown: number}}
73
+ */
74
+ export function createBrotliInventory() {
75
+ return {
76
+ component: undefined,
77
+ dependencies: [],
78
+ copies: 0,
79
+ unpatched: 0,
80
+ unknown: 0,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Tag a probed file component and, when it holds a decoder, link it to the
86
+ * shared brotli component, creating that component on first use. Mutates the
87
+ * file component in place.
88
+ *
89
+ * @param {Object} inventory accumulator from createBrotliInventory
90
+ * @param {Object} fileComponent file component with a `properties` array
91
+ * @param {Object} decoder result of readBrotliDecoder
92
+ */
93
+ export function attachBrotliDecoder(inventory, fileComponent, decoder) {
94
+ const properties = brotliDecoderProperties(decoder);
95
+ if (!properties.length) {
96
+ return;
97
+ }
98
+ fileComponent.properties.push(...properties);
99
+ if (!decoder.c && !decoder.go) {
100
+ return;
101
+ }
102
+ if (!inventory.component) {
103
+ inventory.component = createBrotliComponent();
104
+ }
105
+ const states = [decoder.c?.blockSwitchFix, decoder.go?.blockSwitchFix];
106
+ inventory.copies += 1;
107
+ if (states.includes("false")) {
108
+ inventory.unpatched += 1;
109
+ } else if (states.includes("unknown")) {
110
+ inventory.unknown += 1;
111
+ }
112
+ if (fileComponent["bom-ref"] && inventory.component["bom-ref"]) {
113
+ inventory.dependencies.push({
114
+ ref: fileComponent["bom-ref"],
115
+ dependsOn: [inventory.component["bom-ref"]],
116
+ });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Finish an inventory: record the copy counts on the brotli component.
122
+ *
123
+ * @param {Object} inventory accumulator from createBrotliInventory
124
+ * @returns {{components: Object[], dependencies: Object[]}} what to add to the BOM
125
+ */
126
+ export function finishBrotliInventory(inventory) {
127
+ if (!inventory?.component) {
128
+ return { components: [], dependencies: [] };
129
+ }
130
+ inventory.component.properties = [
131
+ { name: "cdx:brotli:embeddedCopies", value: String(inventory.copies) },
132
+ { name: "cdx:brotli:unpatchedCopies", value: String(inventory.unpatched) },
133
+ { name: "cdx:brotli:unknownCopies", value: String(inventory.unknown) },
134
+ ];
135
+ return {
136
+ components: [inventory.component],
137
+ dependencies: [
138
+ { ref: inventory.component["bom-ref"], dependsOn: [] },
139
+ ...inventory.dependencies,
140
+ ],
141
+ };
142
+ }
@@ -129,7 +129,7 @@ export function noteBuildToolRateLimit(tool, output, context = {}) {
129
129
  ? `, so cdxgen runs no further ${tool} resolution in this scan and`
130
130
  : ", so";
131
131
  console.warn(
132
- `The repository ${tool} downloads from is rate limiting this machine (HTTP 429)${stopped} its dependency tree is incomplete. ${MIRROR_SETTINGS[tool] || MIRROR_SETTINGS[TOOL_MATCHER[tool]]} cdxgen's own lookups can be moved with MAVEN_CENTRAL_URL.`,
132
+ `The repository ${tool} downloads from is rate limiting this machine (HTTP 429)${stopped} its dependency tree is incomplete. ${MIRROR_SETTINGS[tool] || MIRROR_SETTINGS[TOOL_MATCHER[tool]]} Set MAVEN_CENTRAL_MIRROR=true to point every build tool at Google's Maven Central mirror, or MAVEN_CENTRAL_URL for cdxgen's own lookups.`,
133
133
  );
134
134
  recordDegradation("build.rate-limited", {
135
135
  ecosystem: "java",
@@ -28,13 +28,21 @@ import { isMac, isWin } from "../core/paths.js";
28
28
  * rather than reading `isWin`/`isMac` here, or the Windows and Linux branches
29
29
  * become untestable on a macOS developer machine.
30
30
  *
31
+ * Every environment-derived root must be absolute by the *target* platform's
32
+ * rules, never the host's (`path.isAbsolute` would accept `\cache` on a
33
+ * Windows host): a relative value is ignored, so it cannot point inside the
34
+ * working directory. XDG requires this of `XDG_CACHE_HOME`; the home and
35
+ * `LOCALAPPDATA` roots get the same treatment for the same reason.
36
+ *
31
37
  * @param {Object} inputs Resolution inputs.
32
38
  * @param {"linux"|"macos"|"windows"} inputs.platform Which convention to apply.
33
- * @param {string} [inputs.cdxgenCacheDir] `CDXGEN_CACHE_DIR`.
39
+ * @param {string} [inputs.cdxgenCacheDir] `CDXGEN_CACHE_DIR`, an explicit
40
+ * override taken as given.
34
41
  * @param {string} [inputs.xdgCacheHome] `XDG_CACHE_HOME`. Ignored unless
35
- * absolute, per the XDG spec, so it cannot point inside the working directory.
36
- * @param {string} [inputs.home] Home directory.
37
- * @param {string} [inputs.localAppData] `LOCALAPPDATA`.
42
+ * `/`-rooted.
43
+ * @param {string} [inputs.home] Home directory. Ignored unless absolute.
44
+ * @param {string} [inputs.localAppData] `LOCALAPPDATA`. Ignored unless drive-
45
+ * or UNC-rooted; the home profile is the fallback.
38
46
  * @returns {string|null} Absolute path, or null when nothing resolves.
39
47
  */
40
48
  export function resolveCacheDirFor({
@@ -48,20 +56,55 @@ export function resolveCacheDirFor({
48
56
  return path.resolve(cdxgenCacheDir);
49
57
  }
50
58
  if (platform === "windows") {
51
- if (localAppData?.trim()) {
59
+ if (isWindowsAbsolute(localAppData)) {
52
60
  return path.join(localAppData, "cdxgen", "cache");
53
61
  }
54
- return home?.trim()
62
+ return isWindowsAbsolute(home)
55
63
  ? path.join(home, "AppData", "Local", "cdxgen", "cache")
56
64
  : null;
57
65
  }
58
66
  if (platform === "macos") {
59
- return home?.trim() ? path.join(home, "Library", "Caches", "cdxgen") : null;
67
+ return isPosixAbsolute(home)
68
+ ? path.join(home, "Library", "Caches", "cdxgen")
69
+ : null;
60
70
  }
61
- if (xdgCacheHome?.trim() && path.isAbsolute(xdgCacheHome)) {
71
+ if (isPosixAbsolute(xdgCacheHome)) {
62
72
  return path.join(xdgCacheHome, "cdxgen");
63
73
  }
64
- return home?.trim() ? path.join(home, ".cache", "cdxgen") : null;
74
+ return isPosixAbsolute(home) ? path.join(home, ".cache", "cdxgen") : null;
75
+ }
76
+
77
+ /**
78
+ * Absolute by POSIX rules: `/`-rooted.
79
+ *
80
+ * @param {string} [value] Candidate path
81
+ * @returns {boolean} Whether the path is absolute
82
+ */
83
+ function isPosixAbsolute(value) {
84
+ return typeof value === "string" && value.startsWith("/");
85
+ }
86
+
87
+ /**
88
+ * Absolute by Windows rules: a drive letter followed by a separator (`C:\` or
89
+ * `C:/`), or a UNC path (`\\server\share`). `C:cache` (relative to the drive's
90
+ * current directory) and `\cache` (relative to the current drive) are not.
91
+ * Mirrors `is_windows_absolute` in cdxrs.
92
+ *
93
+ * @param {string} [value] Candidate path
94
+ * @returns {boolean} Whether the path is absolute
95
+ */
96
+ function isWindowsAbsolute(value) {
97
+ if (typeof value !== "string" || value.length < 3) {
98
+ return false;
99
+ }
100
+ const isSeparator = (char) => char === "\\" || char === "/";
101
+ const driveRooted = /^[a-zA-Z]$/.test(value[0]) && value[1] === ":";
102
+ if (driveRooted) {
103
+ return isSeparator(value[2]);
104
+ }
105
+ return (
106
+ isSeparator(value[0]) && isSeparator(value[1]) && !isSeparator(value[2])
107
+ );
65
108
  }
66
109
 
67
110
  /**