@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
@@ -0,0 +1,487 @@
1
+ // Bounded PHP build-version reader for unpackaged PHP binaries and
2
+ // libraries. Recovers the version a source-built PHP carries, so a BOM can
3
+ // answer "does this image ship a PHP affected by CVE-2026-109618/109619".
4
+ //
5
+ // The primary marker comes from php-src: SAPI_PHP_VERSION_HEADER is
6
+ // "X-Powered-By: PHP/" PHP_VERSION and is compiled into every SAPI binary and
7
+ // libphp, e.g. the literal `X-Powered-By: PHP/8.4.26`:
8
+ // https://github.com/php/php-src/blob/php-8.4.27/main/SAPI.h#L309
9
+ // The same definition is present in 7.4 through 8.5 (main/SAPI.h at each tag).
10
+ // If a stripped or unusual build lacks it, the installed php_version.h is read
11
+ // from the same prefix. configure.ac generates its PHP_VERSION line:
12
+ // https://github.com/php/php-src/blob/php-8.4.27/configure.ac#L83-L90
13
+ // and install-headers copies it under $(phpincludedir)/main:
14
+ // https://github.com/php/php-src/blob/php-8.4.27/build/Makefile.global#L52
15
+ //
16
+ // Every file in an image is hostile: only regular files inside the extracted
17
+ // root are opened, reads are capped, and a missing/odd file yields no result
18
+ // rather than throwing.
19
+ //
20
+ // The runtime component helpers at the end are the one place a PHP version
21
+ // becomes a component, for the container scan, the OS scan and formulation.
22
+
23
+ import {
24
+ closeSync,
25
+ fstatSync,
26
+ lstatSync,
27
+ openSync,
28
+ readSync,
29
+ realpathSync,
30
+ } from "node:fs";
31
+ import { basename, join, resolve, sep } from "node:path";
32
+
33
+ import { applyPurl, tryBuildPurl } from "./purl.js";
34
+
35
+ // The version grammar the contract accepts: M.m.p, optionally with -dev or an
36
+ // alpha/beta/RC number.
37
+ const VERSION_BODY = "\\d+\\.\\d+\\.\\d+(?:-dev|(?:alpha|beta|RC)\\d+)?";
38
+ const MARKER = Buffer.from("X-Powered-By: PHP/");
39
+ const MAX_VERSION_BYTES = 32;
40
+ // Overlap is at least the marker length plus 32 bytes, per the contract, so a
41
+ // marker split across two reads is seen whole by the second one.
42
+ const OVERLAP_BYTES = MARKER.length + 32;
43
+ const READ_CHUNK_BYTES = 1024 * 1024;
44
+ const MAX_BINARY_BYTES = 256 * 1024 * 1024;
45
+ const MAX_HEADER_BYTES = 64 * 1024;
46
+
47
+ /**
48
+ * The most PHP-named files one scan opens in search of a build version.
49
+ *
50
+ * @type {number}
51
+ */
52
+ export const MAX_PHP_PROBES = 64;
53
+
54
+ // The trailing lookahead rejects 8.4.26.1 and 8.4.26foo while accepting
55
+ // 8.4.26, 8.4.26-dev and 8.6.0RC3 followed by a non-version byte.
56
+ const VERSION_PREFIX_RE = new RegExp(`^(${VERSION_BODY})(?![0-9A-Za-z.-])`);
57
+ const HEADER_VERSION_RE = new RegExp(
58
+ `^#define\\s+PHP_VERSION\\s+"(${VERSION_BODY})"\\s*$`,
59
+ "m",
60
+ );
61
+
62
+ /**
63
+ * Read the version a text starts with, in the grammar the build-version reader
64
+ * accepts: `8.4.27`, `8.5.0-dev`, `8.6.0RC3`. What follows the version must not
65
+ * continue it, so `8.4.26.1` and `8.4.26foo` parse to nothing. A distro
66
+ * revision such as Ubuntu's `8.1.2-1ubuntu2.14` is not an upstream version
67
+ * either, and parses to nothing rather than to a guess.
68
+ *
69
+ * @param {string} text text that begins with the version
70
+ * @returns {string|undefined} the version, or undefined when the text does not
71
+ * begin with one
72
+ */
73
+ export function parsePhpVersion(text) {
74
+ if (typeof text !== "string") {
75
+ return undefined;
76
+ }
77
+ // One character past the maximum lets the lookahead see a too-long version
78
+ // instead of treating the end of the slice as a boundary.
79
+ const match = VERSION_PREFIX_RE.exec(text.slice(0, MAX_VERSION_BYTES + 1));
80
+ if (!match || match[1].length > MAX_VERSION_BYTES) {
81
+ return undefined;
82
+ }
83
+ return match[1];
84
+ }
85
+
86
+ // A distro package can append its revision to PHP_VERSION, as Ubuntu 22.04
87
+ // does. The digit after the hyphen tells it from PHP's own `-dev`.
88
+ const DISTRO_VERSION_RE =
89
+ /^\d+\.\d+\.\d+-\d[0-9A-Za-z.+~-]*(?![0-9A-Za-z.+~-])/;
90
+ const MAX_REPORTED_VERSION_LENGTH = 64;
91
+
92
+ /**
93
+ * Read the version a PHP reports about itself, as `php -v` prints it. An
94
+ * upstream version parses as parsePhpVersion does. A version with a distro
95
+ * revision, such as Ubuntu's `8.1.2-1ubuntu2.14`, is kept whole and marked: it
96
+ * is what that PHP reports, but the distro backports fixes without changing the
97
+ * upstream part, so it must not be matched against upstream advisories.
98
+ *
99
+ * @param {string} text text that begins with the version
100
+ * @returns {{version: string, distroRevision: boolean}|undefined} the version,
101
+ * or undefined when the text does not begin with one
102
+ */
103
+ export function parsePhpReportedVersion(text) {
104
+ const version = parsePhpVersion(text);
105
+ if (version) {
106
+ return { version, distroRevision: false };
107
+ }
108
+ if (typeof text !== "string") {
109
+ return undefined;
110
+ }
111
+ const match = DISTRO_VERSION_RE.exec(
112
+ text.slice(0, MAX_REPORTED_VERSION_LENGTH + 1),
113
+ );
114
+ if (!match || match[0].length > MAX_REPORTED_VERSION_LENGTH) {
115
+ return undefined;
116
+ }
117
+ return { version: match[0], distroRevision: true };
118
+ }
119
+
120
+ /**
121
+ * The package manager whose store a path lies in. Homebrew (`Cellar/<formula>/
122
+ * <version>/`), Nix (`nix/store/`) and Guix (`gnu/store/`) own everything under
123
+ * their stores, whatever links lead there, and a PHP they own is identified by
124
+ * their package, never as an unpackaged build.
125
+ *
126
+ * @param {string} filePath absolute or root-relative path, either separator
127
+ * @returns {string|undefined} `homebrew`, `nix` or `guix`, or undefined
128
+ */
129
+ export function packageStoreOf(filePath) {
130
+ if (typeof filePath !== "string" || !filePath) {
131
+ return undefined;
132
+ }
133
+ const parts = filePath.split(/[\\/]+/);
134
+ const cellar = parts.indexOf("Cellar");
135
+ if (cellar !== -1 && parts.length - cellar >= 4) {
136
+ return "homebrew";
137
+ }
138
+ for (let index = 0; index + 2 < parts.length; index++) {
139
+ if (parts[index + 1] !== "store") {
140
+ continue;
141
+ }
142
+ if (parts[index] === "nix") {
143
+ return "nix";
144
+ }
145
+ if (parts[index] === "gnu") {
146
+ return "guix";
147
+ }
148
+ }
149
+ return undefined;
150
+ }
151
+
152
+ // Installed names come from the per-SAPI Makefile.frag files at the 8.4.27
153
+ // tag: bin/php, bin/php-cgi, sbin/php-fpm, bin/phpdbg and libs/libphp.so,
154
+ // each with the optional $(program_prefix)/$(program_suffix):
155
+ // https://github.com/php/php-src/blob/php-8.4.27/sapi/cli/Makefile.frag#L9
156
+ // https://github.com/php/php-src/blob/php-8.4.27/sapi/cgi/Makefile.frag#L9
157
+ // https://github.com/php/php-src/blob/php-8.4.27/sapi/fpm/Makefile.frag#L11
158
+ // https://github.com/php/php-src/blob/php-8.4.27/sapi/phpdbg/Makefile.frag#L28
159
+ // The Apache module is libphp.so (libs/libphp.so in build/Makefile.global).
160
+ // FrankenPHP embeds PHP in its own binary.
161
+ const PHP_SAPI_BY_NAME = [
162
+ [/^php(\d+(\.\d+)?)?$/, "cli"],
163
+ [/^php-fpm(\d+(\.\d+)?)?$/, "fpm"],
164
+ [/^php-cgi(\d+(\.\d+)?)?$/, "cgi"],
165
+ [/^phpdbg(\d+(\.\d+)?)?$/, "phpdbg"],
166
+ [/^libphp(\d+(\.\d+)?)?\.so$/, "libphp"],
167
+ [/^frankenphp$/, "frankenphp"],
168
+ ];
169
+
170
+ /**
171
+ * Classify a file by its basename into a PHP SAPI, or undefined when the name
172
+ * is not one php-src installs.
173
+ *
174
+ * @param {string} fileName basename or path of the candidate
175
+ * @returns {string|undefined} cli, fpm, cgi, phpdbg, libphp or frankenphp
176
+ */
177
+ export function classifyPhpSapi(fileName) {
178
+ if (typeof fileName !== "string" || !fileName) {
179
+ return undefined;
180
+ }
181
+ const name = basename(fileName);
182
+ for (const [pattern, sapi] of PHP_SAPI_BY_NAME) {
183
+ if (pattern.test(name)) {
184
+ return sapi;
185
+ }
186
+ }
187
+ return undefined;
188
+ }
189
+
190
+ /**
191
+ * Open a path only when it is a regular file that does not resolve outside
192
+ * rootDir. lstat runs first so a FIFO is rejected without opening it (an
193
+ * open on a FIFO blocks until a writer appears) and a symlink is never
194
+ * followed.
195
+ *
196
+ * @param {string} absPath absolute path
197
+ * @param {string|undefined} rootDir extracted root the path must stay under
198
+ * @returns {{fd: number, size: number}|undefined}
199
+ */
200
+ function openRegularFile(absPath, rootDir) {
201
+ let fd;
202
+ try {
203
+ if (typeof absPath !== "string" || !absPath) {
204
+ return undefined;
205
+ }
206
+ const lst = lstatSync(absPath);
207
+ if (!lst.isFile()) {
208
+ return undefined;
209
+ }
210
+ if (rootDir) {
211
+ const root = realpathSync(resolve(rootDir));
212
+ const real = realpathSync(absPath);
213
+ const rootPrefix = root.endsWith(sep) ? root : root + sep;
214
+ if (real !== root && !real.startsWith(rootPrefix)) {
215
+ return undefined;
216
+ }
217
+ }
218
+ fd = openSync(absPath, "r");
219
+ const stats = fstatSync(fd);
220
+ if (!stats.isFile()) {
221
+ closeSync(fd);
222
+ return undefined;
223
+ }
224
+ return { fd, size: stats.size };
225
+ } catch {
226
+ if (fd !== undefined) {
227
+ try {
228
+ closeSync(fd);
229
+ } catch {
230
+ // already closed
231
+ }
232
+ }
233
+ return undefined;
234
+ }
235
+ }
236
+
237
+ function matchVersionAt(buffer, start, readLength) {
238
+ if (start >= readLength) {
239
+ return undefined;
240
+ }
241
+ // One byte past the maximum lets the lookahead see a too-long version
242
+ // instead of treating the end of the slice as a boundary.
243
+ const end = Math.min(readLength, start + MAX_VERSION_BYTES + 1);
244
+ return parsePhpVersion(buffer.toString("latin1", start, end));
245
+ }
246
+
247
+ /**
248
+ * Scan a bounded prefix of a binary for the compiled-in marker. Files larger
249
+ * than the cap are skipped entirely: the contract treats them as hostile and
250
+ * emits no property.
251
+ *
252
+ * @param {number} fd open file descriptor
253
+ * @param {number} size file size from fstat
254
+ * @returns {string|undefined}
255
+ */
256
+ function scanBinaryVersion(fd, size) {
257
+ if (size <= 0 || size > MAX_BINARY_BYTES) {
258
+ return undefined;
259
+ }
260
+ const bufferSize = Math.min(size, READ_CHUNK_BYTES) + OVERLAP_BYTES;
261
+ const buffer = Buffer.allocUnsafe(bufferSize);
262
+ for (let position = 0; position < size; position += READ_CHUNK_BYTES) {
263
+ const want = Math.min(bufferSize, size - position);
264
+ const read = readSync(fd, buffer, 0, want, position);
265
+ if (read <= 0) {
266
+ return undefined;
267
+ }
268
+ // Only matches starting inside this window are new; the overlap exists so
269
+ // a version split at the window edge is readable.
270
+ const searchLimit = Math.min(READ_CHUNK_BYTES, read);
271
+ let offset = buffer.indexOf(MARKER);
272
+ while (offset !== -1 && offset < searchLimit) {
273
+ const version = matchVersionAt(buffer, offset + MARKER.length, read);
274
+ if (version) {
275
+ return version;
276
+ }
277
+ offset = buffer.indexOf(MARKER, offset + 1);
278
+ }
279
+ if (read < bufferSize) {
280
+ return undefined;
281
+ }
282
+ }
283
+ return undefined;
284
+ }
285
+
286
+ /**
287
+ * Derive the install prefix from a path installed under bin, sbin or lib:
288
+ * /usr/local/bin/php -> /usr/local, /usr/lib/apache2/modules/libphp.so -> /usr.
289
+ *
290
+ * @param {string} absPath absolute path
291
+ * @returns {string|undefined}
292
+ */
293
+ function installPrefix(absPath) {
294
+ const parts = resolve(absPath).split(sep);
295
+ for (let i = parts.length - 2; i >= 0; i--) {
296
+ if (parts[i] === "bin" || parts[i] === "sbin" || parts[i] === "lib") {
297
+ return parts.slice(0, i).join(sep) || sep;
298
+ }
299
+ }
300
+ return undefined;
301
+ }
302
+
303
+ /**
304
+ * Read PHP_VERSION from <prefix>/include/php/main/php_version.h. Only called
305
+ * for files installed under bin, sbin or lib, and only when the binary marker
306
+ * was absent.
307
+ *
308
+ * @param {string} absPath path of the PHP binary/library
309
+ * @param {string|undefined} rootDir extracted root
310
+ * @returns {string|undefined}
311
+ */
312
+ function readHeaderVersion(absPath, rootDir) {
313
+ const prefix = installPrefix(absPath);
314
+ if (!prefix) {
315
+ return undefined;
316
+ }
317
+ const headerPath = join(prefix, "include", "php", "main", "php_version.h");
318
+ const opened = openRegularFile(headerPath, rootDir);
319
+ if (!opened) {
320
+ return undefined;
321
+ }
322
+ try {
323
+ const length = Math.min(opened.size, MAX_HEADER_BYTES);
324
+ if (length <= 0) {
325
+ return undefined;
326
+ }
327
+ const buffer = Buffer.allocUnsafe(length);
328
+ const read = readSync(opened.fd, buffer, 0, length, 0);
329
+ if (read <= 0) {
330
+ return undefined;
331
+ }
332
+ const match = HEADER_VERSION_RE.exec(buffer.toString("latin1", 0, read));
333
+ return match ? match[1] : undefined;
334
+ } catch {
335
+ return undefined;
336
+ } finally {
337
+ try {
338
+ closeSync(opened.fd);
339
+ } catch {
340
+ // already closed
341
+ }
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Read the PHP build version of a binary or shared library.
347
+ *
348
+ * @param {string} absPath absolute path
349
+ * @param {string|undefined} rootDir extracted root, when the file came from one
350
+ * @returns {{version: string, source: string}|undefined} source is `binary`
351
+ * or `php_version.h`
352
+ */
353
+ export function readPhpBuildVersion(absPath, rootDir) {
354
+ const opened = openRegularFile(absPath, rootDir);
355
+ if (!opened) {
356
+ return undefined;
357
+ }
358
+ try {
359
+ const fromBinary = scanBinaryVersion(opened.fd, opened.size);
360
+ if (fromBinary) {
361
+ return { version: fromBinary, source: "binary" };
362
+ }
363
+ } finally {
364
+ try {
365
+ closeSync(opened.fd);
366
+ } catch {
367
+ // already closed
368
+ }
369
+ }
370
+ const fromHeader = readHeaderVersion(absPath, rootDir);
371
+ return fromHeader
372
+ ? { version: fromHeader, source: "php_version.h" }
373
+ : undefined;
374
+ }
375
+
376
+ /**
377
+ * The CPE that lets a downstream scanner match a PHP version. NVD puts a
378
+ * pre-release suffix in the update field, e.g.
379
+ * cpe:2.3:a:php:php:8.5.0:rc1:*:*:*:*:*:* (NVD CPE dictionary).
380
+ *
381
+ * @param {string} version PHP version in the build-version grammar
382
+ * @returns {string} CPE 2.3 string
383
+ */
384
+ export function phpCpe(version) {
385
+ const match = /^(\d+\.\d+\.\d+)(?:-(dev)|(alpha|beta|RC)(\d+))?$/.exec(
386
+ version,
387
+ );
388
+ if (!match) {
389
+ return `cpe:2.3:a:php:php:${version}:*:*:*:*:*:*:*`;
390
+ }
391
+ let update = "*";
392
+ if (match[2]) {
393
+ update = "dev";
394
+ } else if (match[3]) {
395
+ update = `${match[3].toLowerCase()}${match[4]}`;
396
+ }
397
+ return `cpe:2.3:a:php:php:${match[1]}:${update}:*:*:*:*:*:*`;
398
+ }
399
+
400
+ /**
401
+ * A PHP runtime is one `platform` component per distinct version, like the Go
402
+ * stdlib component. Its identity is `pkg:generic/php`: it is only used where no
403
+ * package manager owns PHP, or where a PATH probe cannot tell which one does.
404
+ *
405
+ * @param {string} version PHP version in the build-version grammar, or as a
406
+ * distro build reports it
407
+ * @param {Object} [options] options
408
+ * @param {boolean} [options.withCpe=true] false for a version with a distro
409
+ * revision, which no upstream CPE describes
410
+ * @returns {Object} component with `purl`, `bom-ref` and, unless left out, `cpe`
411
+ */
412
+ export function createPhpRuntimeComponent(version, { withCpe = true } = {}) {
413
+ const component = { type: "platform", name: "php", version };
414
+ if (withCpe) {
415
+ component.cpe = phpCpe(version);
416
+ }
417
+ return applyPurl(
418
+ component,
419
+ tryBuildPurl({ type: "generic", name: "php", version }),
420
+ );
421
+ }
422
+
423
+ /**
424
+ * The bom-ref of the runtime component for a version.
425
+ *
426
+ * @param {string} version PHP version in the build-version grammar
427
+ * @returns {string} bom-ref, which is the runtime's purl
428
+ */
429
+ export function phpRuntimeRef(version) {
430
+ return createPhpRuntimeComponent(version)["bom-ref"];
431
+ }
432
+
433
+ /**
434
+ * The properties a PHP file component carries once its build version is known.
435
+ * They belong on unpackaged `file` components only: a package-owned PHP is
436
+ * identified by its package, and distro builds carry backported fixes under
437
+ * old version numbers, so the PHP-RCE audit rules that match these properties
438
+ * would raise false positives there.
439
+ *
440
+ * @param {{version: string, source: string}} build result of readPhpBuildVersion
441
+ * @param {string} sapi result of classifyPhpSapi
442
+ * @returns {{name: string, value: string}[]} properties to append
443
+ */
444
+ export function phpBuildProperties(build, sapi) {
445
+ return [
446
+ { name: "cdx:php:buildVersion", value: build.version },
447
+ { name: "cdx:php:buildVersionSource", value: build.source },
448
+ { name: "cdx:php:sapi", value: sapi },
449
+ ];
450
+ }
451
+
452
+ /**
453
+ * Start collecting the runtime components and edges of one scan.
454
+ *
455
+ * @returns {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}}
456
+ */
457
+ export function createPhpRuntimeInventory() {
458
+ return { runtimeComponents: [], dependencies: [], runtimeRefs: new Set() };
459
+ }
460
+
461
+ /**
462
+ * Tag a PHP file component with its build facts and link it to the runtime
463
+ * component of its version, creating that component on first use. Mutates the
464
+ * component in place, as the Go collector does.
465
+ *
466
+ * @param {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}} inventory
467
+ * accumulator from createPhpRuntimeInventory
468
+ * @param {Object} component file component, with a `properties` array
469
+ * @param {{version: string, source: string}} build result of readPhpBuildVersion
470
+ * @param {string} sapi result of classifyPhpSapi
471
+ */
472
+ export function attachPhpBuild(inventory, component, build, sapi) {
473
+ component.properties.push(...phpBuildProperties(build, sapi));
474
+ const runtime = createPhpRuntimeComponent(build.version);
475
+ const runtimeRef = runtime["bom-ref"];
476
+ if (!inventory.runtimeRefs.has(runtimeRef)) {
477
+ inventory.runtimeRefs.add(runtimeRef);
478
+ inventory.runtimeComponents.push(runtime);
479
+ inventory.dependencies.push({ ref: runtimeRef, dependsOn: [] });
480
+ }
481
+ if (component["bom-ref"]) {
482
+ inventory.dependencies.push({
483
+ ref: component["bom-ref"],
484
+ dependsOn: [runtimeRef],
485
+ });
486
+ }
487
+ }