@cyclonedx/cdxgen 12.3.3 → 12.4.1

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 (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -1,35 +1,40 @@
1
- import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
2
- import { arch as _arch, platform as _platform, homedir } from "node:os";
3
1
  import {
4
- basename,
5
- delimiter,
6
- dirname,
7
- join,
8
- relative,
9
- resolve,
10
- } from "node:path";
2
+ lstatSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ realpathSync,
6
+ statSync,
7
+ } from "node:fs";
8
+ import { platform as _platform, homedir } from "node:os";
9
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
11
10
  import process from "node:process";
12
11
 
13
12
  import { PackageURL } from "packageurl-js";
14
13
 
15
14
  import { createContainerRiskProperties } from "../helpers/containerRisk.js";
16
15
  import { createGtfoBinsProperties } from "../helpers/gtfobins.js";
16
+ import {
17
+ resolveCdxgenPlugins,
18
+ resolvePluginBinary,
19
+ setPluginsPathEnv,
20
+ } from "../helpers/plugins.js";
17
21
  import {
18
22
  adjustLicenseInformation,
19
23
  attachIdentityTools,
20
24
  collectExecutables,
21
25
  collectSharedLibs,
22
26
  DEBUG_MODE,
23
- dirNameStr,
24
27
  extractPathEnv,
25
28
  extractToolRefs,
26
29
  findLicenseId,
27
30
  getTmpDir,
31
+ hasDangerousUnicode,
28
32
  isDryRun,
29
33
  isSpdxLicenseExpression,
34
+ isValidDriveRoot,
30
35
  multiChecksumFile,
31
36
  recordActivity,
32
- retrieveCdxgenPluginVersion,
37
+ recordSymlinkResolution,
33
38
  safeExistsSync,
34
39
  safeMkdirSync,
35
40
  safeMkdtempSync,
@@ -38,241 +43,398 @@ import {
38
43
  } from "../helpers/utils.js";
39
44
  import { getDirs } from "./containerutils.js";
40
45
 
41
- const dirName = dirNameStr;
42
46
  const isWin = _platform() === "win32";
43
47
  const OS_PURL_TYPES = ["deb", "apk", "rpm", "alpm", "qpkg"];
48
+ const pluginRuntime = setPluginsPathEnv(resolveCdxgenPlugins());
49
+ const platform = pluginRuntime.platform;
50
+ const CDXGEN_PLUGINS_DIR = pluginRuntime.pluginsDir;
51
+ const PLUGIN_MANIFEST_FILE = pluginRuntime.pluginManifestFile;
52
+ let pluginManifest;
44
53
 
45
- function isMusl() {
46
- const result = safeSpawnSync("ldd", ["--version"]);
47
- return result?.stdout?.includes("musl") || result?.stderr?.includes("musl");
48
- }
49
-
50
- let platform = _platform();
51
- let extn = "";
52
- let pluginsBinSuffix = "";
53
- if (platform === "win32") {
54
- platform = "windows";
55
- extn = ".exe";
56
- } else if (platform === "linux" && isMusl()) {
57
- platform = "linuxmusl";
58
- }
59
-
60
- let arch = _arch();
61
- switch (arch) {
62
- case "x32":
63
- arch = "386";
64
- break;
65
- case "x64":
66
- arch = "amd64";
67
- pluginsBinSuffix = `-${platform}-amd64`;
68
- break;
69
- case "arm64":
70
- pluginsBinSuffix = `-${platform}-arm64`;
71
- break;
72
- case "ppc64":
73
- arch = "ppc64le";
74
- pluginsBinSuffix = "-ppc64";
75
- break;
76
- }
77
-
78
- // cdxgen plugins version
79
- const CDXGEN_PLUGINS_VERSION = retrieveCdxgenPluginVersion();
80
-
81
- // Retrieve the cdxgen plugins directory
82
- let CDXGEN_PLUGINS_DIR = process.env.CDXGEN_PLUGINS_DIR;
83
- let extraNMBinPath;
84
- // Is there a non-empty local plugins directory
85
- if (
86
- !CDXGEN_PLUGINS_DIR &&
87
- safeExistsSync(join(dirName, "plugins")) &&
88
- safeExistsSync(join(dirName, "plugins", "trivy"))
89
- ) {
90
- CDXGEN_PLUGINS_DIR = join(dirName, "plugins");
91
- }
92
-
93
- // Is there a non-empty local node_modules directory
94
- if (
95
- !CDXGEN_PLUGINS_DIR &&
96
- safeExistsSync(
97
- join(
98
- dirName,
99
- "node_modules",
100
- "@cdxgen",
101
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
102
- "plugins",
103
- ),
104
- ) &&
105
- safeExistsSync(
106
- join(
107
- dirName,
108
- "node_modules",
109
- "@cdxgen",
110
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
111
- "plugins",
112
- "trivy",
113
- ),
114
- )
115
- ) {
116
- CDXGEN_PLUGINS_DIR = join(
117
- dirName,
118
- "node_modules",
119
- "@cdxgen",
120
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
121
- "plugins",
54
+ const MAX_PLUGIN_MANIFEST_BYTES = 1024 * 1024;
55
+ const MAX_PLUGIN_MANIFEST_PLUGINS = 32;
56
+ const MAX_PLUGIN_COMPONENT_HASHES = 16;
57
+ const MAX_PLUGIN_COMPONENT_REFERENCES = 32;
58
+ const MAX_PLUGIN_COMPONENT_PROPERTIES = 128;
59
+ const MAX_PLUGIN_COMPONENT_LICENSES = 8;
60
+ const MAX_PLUGIN_STRING_LENGTH = 4096;
61
+ const MAX_PLUGIN_LONG_STRING_LENGTH = 16384;
62
+
63
+ function sanitizeManifestString(value, maxLength = MAX_PLUGIN_STRING_LENGTH) {
64
+ if (typeof value !== "string") {
65
+ return undefined;
66
+ }
67
+ const trimmedValue = value.trim();
68
+ if (!trimmedValue || trimmedValue.length > maxLength) {
69
+ return undefined;
70
+ }
71
+ return trimmedValue;
72
+ }
73
+
74
+ function sanitizeManifestObjectList(values, limit, mapper) {
75
+ if (!Array.isArray(values) || !values.length) {
76
+ return undefined;
77
+ }
78
+ const mappedValues = values
79
+ .slice(0, limit)
80
+ .map((value) => mapper(value))
81
+ .filter(Boolean);
82
+ return mappedValues.length ? mappedValues : undefined;
83
+ }
84
+
85
+ function sanitizeManifestHash(hash) {
86
+ const alg = sanitizeManifestString(hash?.alg, 64);
87
+ const content = sanitizeManifestString(hash?.content, 512);
88
+ if (!alg || !content) {
89
+ return undefined;
90
+ }
91
+ return { alg, content };
92
+ }
93
+
94
+ function sanitizeManifestProperty(property) {
95
+ const name = sanitizeManifestString(property?.name, 256);
96
+ const value = sanitizeManifestString(
97
+ property?.value,
98
+ MAX_PLUGIN_LONG_STRING_LENGTH,
122
99
  );
123
- if (safeExistsSync(join(dirName, "node_modules", ".bin"))) {
124
- extraNMBinPath = join(dirName, "node_modules", ".bin");
100
+ if (!name || !value) {
101
+ return undefined;
125
102
  }
103
+ return { name, value };
126
104
  }
127
- if (!CDXGEN_PLUGINS_DIR) {
128
- let globalNodePath = process.env.GLOBAL_NODE_MODULES_PATH || undefined;
129
- if (!globalNodePath) {
130
- if (DEBUG_MODE) {
131
- console.log(
132
- `Trying to find the global node_modules path with "pnpm root -g" command.`,
105
+
106
+ function sanitizeManifestExternalReference(reference) {
107
+ const type = sanitizeManifestString(reference?.type, 64);
108
+ const url = sanitizeManifestString(
109
+ reference?.url,
110
+ MAX_PLUGIN_LONG_STRING_LENGTH,
111
+ );
112
+ if (!type || !url) {
113
+ return undefined;
114
+ }
115
+ const sanitizedReference = { type, url };
116
+ const comment = sanitizeManifestString(reference?.comment, 512);
117
+ if (comment) {
118
+ sanitizedReference.comment = comment;
119
+ }
120
+ return sanitizedReference;
121
+ }
122
+
123
+ function sanitizeManifestLicense(licenseEntry) {
124
+ const licenseId = sanitizeManifestString(licenseEntry?.license?.id, 128);
125
+ const licenseName = sanitizeManifestString(licenseEntry?.license?.name, 256);
126
+ const licenseUrl = sanitizeManifestString(
127
+ licenseEntry?.license?.url,
128
+ MAX_PLUGIN_LONG_STRING_LENGTH,
129
+ );
130
+ if (!licenseId && !licenseName) {
131
+ return undefined;
132
+ }
133
+ const license = {};
134
+ if (licenseId) {
135
+ license.id = licenseId;
136
+ }
137
+ if (licenseName) {
138
+ license.name = licenseName;
139
+ }
140
+ if (licenseUrl) {
141
+ license.url = licenseUrl;
142
+ }
143
+ return { license };
144
+ }
145
+
146
+ function sanitizeManifestComponent(component, fallbackName) {
147
+ if (!component || typeof component !== "object") {
148
+ return undefined;
149
+ }
150
+ const sanitizedComponent = {};
151
+ const stringFields = {
152
+ group: 256,
153
+ name: 256,
154
+ version: 256,
155
+ description: MAX_PLUGIN_LONG_STRING_LENGTH,
156
+ publisher: 256,
157
+ purl: MAX_PLUGIN_LONG_STRING_LENGTH,
158
+ "bom-ref": MAX_PLUGIN_LONG_STRING_LENGTH,
159
+ type: 64,
160
+ };
161
+ for (const [field, maxLength] of Object.entries(stringFields)) {
162
+ const sanitizedValue = sanitizeManifestString(component[field], maxLength);
163
+ if (sanitizedValue) {
164
+ sanitizedComponent[field] = sanitizedValue;
165
+ }
166
+ }
167
+ if (!sanitizedComponent.name && fallbackName) {
168
+ sanitizedComponent.name = fallbackName;
169
+ }
170
+ if (!sanitizedComponent.name || !sanitizedComponent["bom-ref"]) {
171
+ return undefined;
172
+ }
173
+ const hashes = sanitizeManifestObjectList(
174
+ component.hashes,
175
+ MAX_PLUGIN_COMPONENT_HASHES,
176
+ sanitizeManifestHash,
177
+ );
178
+ const externalReferences = sanitizeManifestObjectList(
179
+ component.externalReferences,
180
+ MAX_PLUGIN_COMPONENT_REFERENCES,
181
+ sanitizeManifestExternalReference,
182
+ );
183
+ const properties = sanitizeManifestObjectList(
184
+ component.properties,
185
+ MAX_PLUGIN_COMPONENT_PROPERTIES,
186
+ sanitizeManifestProperty,
187
+ );
188
+ const licenses = sanitizeManifestObjectList(
189
+ component.licenses,
190
+ MAX_PLUGIN_COMPONENT_LICENSES,
191
+ sanitizeManifestLicense,
192
+ );
193
+ if (hashes) {
194
+ sanitizedComponent.hashes = hashes;
195
+ }
196
+ if (externalReferences) {
197
+ sanitizedComponent.externalReferences = externalReferences;
198
+ }
199
+ if (properties) {
200
+ sanitizedComponent.properties = properties;
201
+ }
202
+ if (licenses) {
203
+ sanitizedComponent.licenses = licenses;
204
+ }
205
+ return sanitizedComponent;
206
+ }
207
+
208
+ function sanitizePluginManifest(manifest) {
209
+ if (!manifest || typeof manifest !== "object") {
210
+ return null;
211
+ }
212
+ const sanitizedManifest = {
213
+ plugins: [],
214
+ };
215
+ const generatedAt = sanitizeManifestString(manifest.generatedAt, 128);
216
+ if (generatedAt) {
217
+ sanitizedManifest.generatedAt = generatedAt;
218
+ }
219
+ if (manifest.package && typeof manifest.package === "object") {
220
+ const sanitizedPackage = {};
221
+ for (const [field, maxLength] of Object.entries({
222
+ name: 256,
223
+ version: 256,
224
+ repository: MAX_PLUGIN_LONG_STRING_LENGTH,
225
+ homepage: MAX_PLUGIN_LONG_STRING_LENGTH,
226
+ })) {
227
+ const sanitizedValue = sanitizeManifestString(
228
+ manifest.package[field],
229
+ maxLength,
133
230
  );
231
+ if (sanitizedValue) {
232
+ sanitizedPackage[field] = sanitizedValue;
233
+ }
134
234
  }
135
- const result = safeSpawnSync(isWin ? "pnpm.cmd" : "pnpm", ["root", "-g"]);
136
- if (result) {
137
- const stdout = result.stdout;
138
- if (stdout) {
139
- globalNodePath = `${stdout.trim()}/`;
235
+ if (Object.keys(sanitizedPackage).length) {
236
+ sanitizedManifest.package = sanitizedPackage;
237
+ }
238
+ }
239
+ for (const pluginEntry of Array.isArray(manifest.plugins)
240
+ ? manifest.plugins.slice(0, MAX_PLUGIN_MANIFEST_PLUGINS)
241
+ : []) {
242
+ const name = sanitizeManifestString(pluginEntry?.name, 128);
243
+ const component = sanitizeManifestComponent(pluginEntry?.component, name);
244
+ if (!name || !component) {
245
+ continue;
246
+ }
247
+ const sanitizedPlugin = { name, component };
248
+ for (const [field, maxLength] of Object.entries({
249
+ version: 256,
250
+ binaryPath: MAX_PLUGIN_LONG_STRING_LENGTH,
251
+ sbomFile: MAX_PLUGIN_LONG_STRING_LENGTH,
252
+ sha256: 256,
253
+ })) {
254
+ const sanitizedValue = sanitizeManifestString(
255
+ pluginEntry?.[field],
256
+ maxLength,
257
+ );
258
+ if (sanitizedValue) {
259
+ sanitizedPlugin[field] = sanitizedValue;
140
260
  }
141
261
  }
262
+ sanitizedManifest.plugins.push(sanitizedPlugin);
142
263
  }
143
- let globalPlugins;
144
- if (globalNodePath) {
145
- globalPlugins = join(
146
- globalNodePath,
147
- "@cdxgen",
148
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
149
- "plugins",
150
- );
151
- extraNMBinPath = join(
152
- globalNodePath,
153
- "..",
154
- ".pnpm",
155
- "node_modules",
156
- ".bin",
157
- );
264
+ return sanitizedManifest.plugins.length ? sanitizedManifest : null;
265
+ }
266
+
267
+ function loadPluginManifest() {
268
+ if (pluginManifest !== undefined) {
269
+ return pluginManifest;
158
270
  }
159
- // pnpm add -g
160
- let altGlobalPlugins;
161
- if (dirName.includes(join("node_modules", ".pnpm", "@cyclonedx+cdxgen"))) {
162
- const tmpA = dirName.split(join("node_modules", ".pnpm"));
163
- altGlobalPlugins = join(
164
- tmpA[0],
165
- "node_modules",
166
- ".pnpm",
167
- `@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
168
- "node_modules",
169
- "@cdxgen",
170
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
171
- "plugins",
172
- );
173
- if (safeExistsSync(join(tmpA[0], "node_modules", ".bin"))) {
174
- extraNMBinPath = join(tmpA[0], "node_modules", ".bin");
175
- }
176
- } else if (dirName.includes(join(".pnpm", "@cyclonedx+cdxgen"))) {
177
- // pnpm dlx
178
- const tmpA = dirName.split(".pnpm");
179
- altGlobalPlugins = join(
180
- tmpA[0],
181
- ".pnpm",
182
- `@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
183
- "node_modules",
184
- "@cdxgen",
185
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
186
- "plugins",
187
- );
188
- if (safeExistsSync(join(tmpA[0], ".bin"))) {
189
- extraNMBinPath = join(tmpA[0], ".bin");
190
- }
191
- } else if (dirName.includes(join("caxa", "applications"))) {
192
- // sae binaries
193
- altGlobalPlugins = join(
194
- dirName,
195
- "node_modules",
196
- "pnpm",
197
- `@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
198
- "node_modules",
199
- "@cdxgen",
200
- `cdxgen-plugins-bin${pluginsBinSuffix}`,
201
- "plugins",
202
- );
203
- extraNMBinPath = join(dirName, "node_modules", ".bin");
271
+ if (!PLUGIN_MANIFEST_FILE) {
272
+ pluginManifest = null;
273
+ return pluginManifest;
204
274
  }
205
- // Set the plugins directory
206
- if (globalPlugins && safeExistsSync(globalPlugins)) {
207
- CDXGEN_PLUGINS_DIR = globalPlugins;
208
- if (DEBUG_MODE) {
209
- console.log("Found global plugins", CDXGEN_PLUGINS_DIR);
210
- }
211
- } else if (altGlobalPlugins && safeExistsSync(altGlobalPlugins)) {
212
- CDXGEN_PLUGINS_DIR = altGlobalPlugins;
213
- // To help detect bin commands such as atom, astgen, etc, we need to set this to the PATH variable.
214
- if (DEBUG_MODE) {
215
- console.log("Found global plugins", CDXGEN_PLUGINS_DIR);
275
+ try {
276
+ const manifestRealPath = realpathSync(PLUGIN_MANIFEST_FILE);
277
+ const manifestDirectory = dirname(manifestRealPath);
278
+ const expectedManifestDirectory = realpathSync(CDXGEN_PLUGINS_DIR);
279
+ const manifestStats = statSync(manifestRealPath);
280
+ if (
281
+ basename(manifestRealPath) !== "plugins-manifest.json" ||
282
+ manifestDirectory !== expectedManifestDirectory ||
283
+ !manifestStats.isFile() ||
284
+ manifestStats.size > MAX_PLUGIN_MANIFEST_BYTES
285
+ ) {
286
+ pluginManifest = null;
287
+ return pluginManifest;
216
288
  }
289
+ pluginManifest = sanitizePluginManifest(
290
+ JSON.parse(readFileSync(manifestRealPath, { encoding: "utf-8" })),
291
+ );
292
+ } catch (_err) {
293
+ pluginManifest = null;
217
294
  }
295
+ return pluginManifest;
218
296
  }
219
- // Set bin path for commands such as atom, astgen etc.
220
- if (extraNMBinPath && !process.env?.PATH?.includes(extraNMBinPath)) {
221
- process.env.PATH = `${extraNMBinPath}${delimiter}${process.env.PATH}`;
297
+
298
+ function cloneSerializable(value) {
299
+ if (!value || typeof value !== "object") {
300
+ return value;
301
+ }
302
+ return JSON.parse(JSON.stringify(value));
222
303
  }
223
- if (!CDXGEN_PLUGINS_DIR) {
224
- if (DEBUG_MODE) {
225
- console.warn(
226
- "The optional cdxgen plugin was not found. Please install cdxgen without excluding optional dependencies if needed.",
227
- );
304
+
305
+ function getPluginManifestEntry(toolName) {
306
+ const manifest = loadPluginManifest();
307
+ if (!manifest?.plugins?.length) {
308
+ return undefined;
228
309
  }
229
- CDXGEN_PLUGINS_DIR = "";
310
+ return manifest.plugins.find((entry) => entry?.name === toolName);
230
311
  }
231
- let TRIVY_BIN = process.env.TRIVY_CMD;
232
- if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "trivy"))) {
233
- TRIVY_BIN = join(
234
- CDXGEN_PLUGINS_DIR,
235
- "trivy",
236
- `trivy-cdxgen-${platform}-${arch}${extn}`,
312
+
313
+ function mergeToolComponent(manifestComponent, existingComponent) {
314
+ if (!manifestComponent) {
315
+ return cloneSerializable(existingComponent);
316
+ }
317
+ if (!existingComponent) {
318
+ return cloneSerializable(manifestComponent);
319
+ }
320
+ const merged = {
321
+ ...cloneSerializable(manifestComponent),
322
+ ...cloneSerializable(existingComponent),
323
+ };
324
+ for (const key of [
325
+ "group",
326
+ "name",
327
+ "version",
328
+ "description",
329
+ "publisher",
330
+ "purl",
331
+ "bom-ref",
332
+ "type",
333
+ ]) {
334
+ if (manifestComponent?.[key]) {
335
+ merged[key] = manifestComponent[key];
336
+ }
337
+ }
338
+ merged.hashes = manifestComponent?.hashes?.length
339
+ ? manifestComponent.hashes
340
+ : existingComponent?.hashes;
341
+ merged.externalReferences = uniqueExternalReferences(
342
+ (manifestComponent?.externalReferences || []).concat(
343
+ existingComponent?.externalReferences || [],
344
+ ),
237
345
  );
238
- }
239
- let CARGO_AUDITABLE_BIN = process.env.CARGO_AUDITABLE_CMD;
240
- if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "cargo-auditable"))) {
241
- CARGO_AUDITABLE_BIN = join(
242
- CDXGEN_PLUGINS_DIR,
243
- "cargo-auditable",
244
- `cargo-auditable-cdxgen-${platform}-${arch}${extn}`,
346
+ merged.properties = uniqueProperties(
347
+ (manifestComponent?.properties || []).concat(
348
+ existingComponent?.properties || [],
349
+ ),
245
350
  );
351
+ merged.licenses =
352
+ manifestComponent?.licenses?.length || !existingComponent?.licenses?.length
353
+ ? manifestComponent?.licenses
354
+ : existingComponent.licenses;
355
+ merged.evidence = manifestComponent?.evidence || existingComponent?.evidence;
356
+ return merged;
246
357
  }
247
- let OSQUERY_BIN = process.env.OSQUERY_CMD;
248
- if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "osquery"))) {
249
- OSQUERY_BIN = join(
250
- CDXGEN_PLUGINS_DIR,
251
- "osquery",
252
- `osqueryi-${platform}-${arch}${extn}`,
253
- );
254
- // osqueryi-darwin-amd64.app/Contents/MacOS/osqueryd
255
- if (platform === "darwin") {
256
- OSQUERY_BIN = `${OSQUERY_BIN}.app/Contents/MacOS/osqueryd`;
358
+
359
+ function uniqueExternalReferences(references) {
360
+ const seen = new Set();
361
+ const uniqueValues = [];
362
+ for (const reference of references || []) {
363
+ if (!reference?.url || !reference?.type) {
364
+ continue;
365
+ }
366
+ const key = `${reference.type}\u0000${reference.url}`;
367
+ if (seen.has(key)) {
368
+ continue;
369
+ }
370
+ seen.add(key);
371
+ uniqueValues.push(reference);
257
372
  }
373
+ return uniqueValues;
258
374
  }
259
- let DOSAI_BIN = process.env.DOSAI_CMD;
260
- if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "dosai"))) {
261
- DOSAI_BIN = join(
262
- CDXGEN_PLUGINS_DIR,
263
- "dosai",
264
- `dosai-${platform}-${arch}${extn}`,
265
- );
375
+
376
+ export function getPluginToolComponents(toolNames = []) {
377
+ const components = [];
378
+ const seenRefs = new Set();
379
+ for (const toolName of uniqueSortedStrings(toolNames)) {
380
+ const component = cloneSerializable(
381
+ getPluginManifestEntry(toolName)?.component,
382
+ );
383
+ if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
384
+ continue;
385
+ }
386
+ seenRefs.add(component["bom-ref"]);
387
+ components.push(component);
388
+ }
389
+ return components;
390
+ }
391
+
392
+ function enrichToolComponents(existingTools = [], toolNames = []) {
393
+ const manifestTools = getPluginToolComponents(toolNames);
394
+ if (!existingTools?.length) {
395
+ return manifestTools;
396
+ }
397
+ const toolMap = new Map();
398
+ for (const tool of existingTools) {
399
+ if (!tool) {
400
+ continue;
401
+ }
402
+ toolMap.set(tool["bom-ref"] || tool.name || JSON.stringify(tool), tool);
403
+ }
404
+ for (const manifestTool of manifestTools) {
405
+ const matchKey = Array.from(toolMap.keys()).find((key) => {
406
+ const existing = toolMap.get(key);
407
+ return (
408
+ existing?.["bom-ref"] === manifestTool["bom-ref"] ||
409
+ existing?.name === manifestTool.name
410
+ );
411
+ });
412
+ if (matchKey) {
413
+ toolMap.set(
414
+ matchKey,
415
+ mergeToolComponent(manifestTool, toolMap.get(matchKey)),
416
+ );
417
+ continue;
418
+ }
419
+ toolMap.set(manifestTool["bom-ref"], manifestTool);
420
+ }
421
+ return Array.from(toolMap.values());
266
422
  }
267
423
 
424
+ const TRIVY_BIN = resolvePluginBinary("trivy", pluginRuntime);
425
+ const CARGO_AUDITABLE_BIN = resolvePluginBinary(
426
+ "cargo-auditable",
427
+ pluginRuntime,
428
+ );
429
+ const OSQUERY_BIN = resolvePluginBinary("osquery", pluginRuntime);
430
+ const DOSAI_BIN = resolvePluginBinary("dosai", pluginRuntime);
431
+ const TRUSTINSPECTOR_BIN = resolvePluginBinary("trustinspector", pluginRuntime);
432
+
268
433
  // Blint bin
269
434
  const BLINT_BIN = process.env.BLINT_CMD || "blint";
270
435
 
271
436
  // sourcekitten
272
- let SOURCEKITTEN_BIN = process.env.SOURCEKITTEN_CMD;
273
- if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "sourcekitten"))) {
274
- SOURCEKITTEN_BIN = join(CDXGEN_PLUGINS_DIR, "sourcekitten", "sourcekitten");
275
- }
437
+ const SOURCEKITTEN_BIN = resolvePluginBinary("sourcekitten", pluginRuntime);
276
438
 
277
439
  // Keep this list updated every year
278
440
  const OS_DISTRO_ALIAS = {
@@ -466,15 +628,20 @@ export async function getOSPackages(src, imageConfig) {
466
628
  dependenciesList: [],
467
629
  executables: [],
468
630
  osPackages: [],
631
+ osPackageFiles: [],
469
632
  sharedLibs: [],
633
+ services: [],
470
634
  tools: [],
471
635
  };
472
636
  }
473
637
  const pkgList = [];
638
+ const osPackageEntries = [];
474
639
  const dependenciesList = [];
475
640
  const allTypes = new Set();
476
641
  const bundledSdks = new Set();
477
642
  const bundledRuntimes = new Set();
643
+ let osPackageFiles = [];
644
+ let services = [];
478
645
  let tools = [];
479
646
  let binPaths = extractPathEnv(imageConfig?.Env);
480
647
  if (!binPaths?.length) {
@@ -515,25 +682,13 @@ export async function getOSPackages(src, imageConfig) {
515
682
  const bomJsonFile = join(tempDir, "trivy-bom.json");
516
683
  const args = [
517
684
  imageType,
518
- "--skip-db-update",
519
- "--skip-java-db-update",
520
- "--offline-scan",
521
- "--disable-telemetry",
522
- "--skip-version-check",
523
- "--skip-files",
524
- "**/*.jar,**/*.war,**/*.par,**/*.ear",
525
- "--no-progress",
526
- "--exit-code",
527
- "0",
528
- "--format",
529
- "cyclonedx",
530
685
  "--cache-dir",
531
686
  trivyCacheDir,
532
687
  "--output",
533
688
  bomJsonFile,
534
689
  ];
535
- if (!DEBUG_MODE) {
536
- args.push("-q");
690
+ if (DEBUG_MODE) {
691
+ args.push("--debug");
537
692
  }
538
693
  args.push(src);
539
694
  if (DEBUG_MODE) {
@@ -637,15 +792,17 @@ export async function getOSPackages(src, imageConfig) {
637
792
  }
638
793
  }
639
794
  const tmpDependencies = {};
795
+ tools = enrichToolComponents(
796
+ (Array.isArray(tmpBom?.metadata?.tools)
797
+ ? tmpBom.metadata.tools
798
+ : tmpBom?.metadata?.tools?.components || []
799
+ ).filter((tool) => tool?.["bom-ref"] && tool?.name !== "cdxgen"),
800
+ ["trivy"].concat(imageType === "rootfs" ? ["trustinspector"] : []),
801
+ );
640
802
  const toolRefs = extractToolRefs(
641
- tmpBom?.metadata?.tools,
803
+ { components: tools },
642
804
  (tool) => tool?.name !== "cdxgen",
643
805
  );
644
- tools = (
645
- Array.isArray(tmpBom?.metadata?.tools)
646
- ? tmpBom.metadata.tools
647
- : tmpBom?.metadata?.tools?.components || []
648
- ).filter((tool) => tool?.["bom-ref"] && tool?.name !== "cdxgen");
649
806
  (tmpBom.dependencies || []).forEach((d) => {
650
807
  tmpDependencies[d.ref] = d.dependsOn;
651
808
  });
@@ -838,30 +995,12 @@ export async function getOSPackages(src, imageConfig) {
838
995
  }
839
996
  }
840
997
  const compProperties = comp.properties;
841
- let srcName;
842
- let srcVersion;
843
- let srcRelease;
844
- let epoch;
845
- if (compProperties && Array.isArray(compProperties)) {
846
- for (const aprop of compProperties) {
847
- // Property name: aquasecurity:trivy:SrcName
848
- if (aprop.name.endsWith("SrcName")) {
849
- srcName = aprop.value;
850
- }
851
- // Property name: aquasecurity:trivy:SrcVersion
852
- if (aprop.name.endsWith("SrcVersion")) {
853
- srcVersion = aprop.value;
854
- }
855
- // Property name: aquasecurity:trivy:SrcRelease
856
- if (aprop.name.endsWith("SrcRelease")) {
857
- srcRelease = aprop.value;
858
- }
859
- // Property name: aquasecurity:trivy:SrcEpoch
860
- if (aprop.name.endsWith("SrcEpoch")) {
861
- epoch = aprop.value;
862
- }
863
- }
864
- }
998
+ const trivyMetadata = extractTrivyOsPackageMetadata(compProperties);
999
+ const fallbackIdentityProperties = promoteTrivyOsPackageIdentity(
1000
+ comp,
1001
+ trivyMetadata,
1002
+ );
1003
+ let { srcName, srcVersion, srcRelease, epoch } = trivyMetadata;
865
1004
  // See issue #2067
866
1005
  if (srcVersion && srcRelease) {
867
1006
  srcVersion = `${srcVersion}-${srcRelease}`;
@@ -869,7 +1008,18 @@ export async function getOSPackages(src, imageConfig) {
869
1008
  if (epoch) {
870
1009
  srcVersion = `${epoch}:${srcVersion}`;
871
1010
  }
872
- delete comp.properties;
1011
+ if (
1012
+ trivyMetadata.retainedProperties.length ||
1013
+ fallbackIdentityProperties.length
1014
+ ) {
1015
+ comp.properties = uniqueProperties(
1016
+ trivyMetadata.retainedProperties.concat(
1017
+ fallbackIdentityProperties,
1018
+ ),
1019
+ );
1020
+ } else {
1021
+ delete comp.properties;
1022
+ }
873
1023
  // Bug fix: We can get bom-ref like this: pkg:rpm/sles/libstdc%2B%2B6@14.2.0+git10526-150000.1.6.1?arch=x86_64&distro=sles-15.5
874
1024
  if (
875
1025
  comp["bom-ref"] &&
@@ -879,6 +1029,17 @@ export async function getOSPackages(src, imageConfig) {
879
1029
  comp["bom-ref"] = decodeURIComponent(comp.purl);
880
1030
  }
881
1031
  pkgList.push(comp);
1032
+ if (trivyMetadata.installedFiles.length) {
1033
+ osPackageEntries.push({
1034
+ capabilities: trivyMetadata.capabilities,
1035
+ commandPaths: trivyMetadata.installedCommandPaths,
1036
+ commands: trivyMetadata.installedCommands,
1037
+ files: trivyMetadata.installedFiles,
1038
+ packageName: comp.name,
1039
+ packageRef: comp["bom-ref"],
1040
+ packageVersion: comp.version,
1041
+ });
1042
+ }
882
1043
  detectSdksRuntimes(comp, bundledSdks, bundledRuntimes);
883
1044
  const compDeps = retrieveDependencies(
884
1045
  tmpDependencies,
@@ -930,6 +1091,7 @@ export async function getOSPackages(src, imageConfig) {
930
1091
  ).toString();
931
1092
  }
932
1093
  newComp["bom-ref"] = decodeURIComponent(newComp.purl);
1094
+ delete newComp.properties;
933
1095
  attachIdentityTools(newComp, toolRefs);
934
1096
  pkgList.push(newComp);
935
1097
  detectSdksRuntimes(newComp, bundledSdks, bundledRuntimes);
@@ -939,11 +1101,33 @@ export async function getOSPackages(src, imageConfig) {
939
1101
  }
940
1102
  }
941
1103
  }
1104
+ const rootfsRepositoryInventory = await collectRootfsRepositoryInventory(src);
1105
+ if (rootfsRepositoryInventory.components.length) {
1106
+ pkgList.push(...rootfsRepositoryInventory.components);
1107
+ }
1108
+ if (rootfsRepositoryInventory.dependenciesList.length) {
1109
+ dependenciesList.push(...rootfsRepositoryInventory.dependenciesList);
1110
+ }
1111
+ const {
1112
+ components: ownedFileComponents,
1113
+ dependenciesList: ownedFileDependencies,
1114
+ ownedFilePaths,
1115
+ services: ownedServices,
1116
+ } = await createOSPackageFileComponents(src, osPackageEntries);
1117
+ if (ownedFileComponents.length) {
1118
+ osPackageFiles = ownedFileComponents;
1119
+ }
1120
+ if (ownedFileDependencies.length) {
1121
+ dependenciesList.push(...ownedFileDependencies);
1122
+ }
1123
+ if (ownedServices.length) {
1124
+ services = ownedServices;
1125
+ }
942
1126
  let executables = [];
943
1127
  if (binPaths?.length) {
944
1128
  executables = await fileComponents(
945
1129
  src,
946
- collectExecutables(src, binPaths),
1130
+ collectExecutables(src, binPaths, ownedFilePaths),
947
1131
  "executable",
948
1132
  );
949
1133
  }
@@ -971,11 +1155,13 @@ export async function getOSPackages(src, imageConfig) {
971
1155
  defaultLibPaths,
972
1156
  "/etc/ld.so.conf",
973
1157
  "/etc/ld.so.conf.d/*.conf",
1158
+ ownedFilePaths,
974
1159
  ),
975
1160
  "shared_library",
976
1161
  );
977
1162
  return {
978
1163
  osPackages: pkgList,
1164
+ osPackageFiles,
979
1165
  dependenciesList,
980
1166
  allTypes: Array.from(allTypes).sort(),
981
1167
  bundledSdks: Array.from(bundledSdks).sort(),
@@ -983,110 +1169,1854 @@ export async function getOSPackages(src, imageConfig) {
983
1169
  binPaths,
984
1170
  executables,
985
1171
  sharedLibs,
1172
+ services,
986
1173
  tools,
987
1174
  };
988
1175
  }
989
1176
 
990
- // Detect common sdks and runtimes from the name
991
- function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
992
- if (!comp?.name) {
993
- return;
1177
+ function extractTrivyOsPackageMetadata(compProperties) {
1178
+ const metadata = {
1179
+ capabilities: [],
1180
+ installedCommandPaths: [],
1181
+ installedCommands: [],
1182
+ installedFiles: [],
1183
+ packageMaintainer: undefined,
1184
+ packageVendor: undefined,
1185
+ retainedProperties: [],
1186
+ srcName: undefined,
1187
+ srcRelease: undefined,
1188
+ srcVersion: undefined,
1189
+ epoch: undefined,
1190
+ };
1191
+ if (!Array.isArray(compProperties) || !compProperties.length) {
1192
+ return metadata;
994
1193
  }
995
- if (/dotnet[6-9]?-sdk/.test(comp.name)) {
996
- bundledSdks.add(comp.name);
1194
+ for (const aprop of compProperties) {
1195
+ if (!aprop?.name) {
1196
+ continue;
1197
+ }
1198
+ if (aprop.name.endsWith("SrcName")) {
1199
+ metadata.srcName = aprop.value;
1200
+ continue;
1201
+ }
1202
+ if (aprop.name.endsWith("SrcVersion")) {
1203
+ metadata.srcVersion = aprop.value;
1204
+ continue;
1205
+ }
1206
+ if (aprop.name.endsWith("SrcRelease")) {
1207
+ metadata.srcRelease = aprop.value;
1208
+ continue;
1209
+ }
1210
+ if (aprop.name.endsWith("SrcEpoch")) {
1211
+ metadata.epoch = aprop.value;
1212
+ continue;
1213
+ }
1214
+ if (aprop.name.endsWith("PackageMaintainer")) {
1215
+ metadata.packageMaintainer = aprop.value;
1216
+ continue;
1217
+ }
1218
+ if (aprop.name.endsWith("PackageVendor")) {
1219
+ metadata.packageVendor = aprop.value;
1220
+ continue;
1221
+ }
1222
+ if (aprop.name.endsWith("InstalledFile")) {
1223
+ metadata.installedFiles.push(aprop.value);
1224
+ continue;
1225
+ }
1226
+ if (aprop.name.endsWith("InstalledCommandPath")) {
1227
+ metadata.installedCommandPaths.push(aprop.value);
1228
+ metadata.retainedProperties.push(aprop);
1229
+ continue;
1230
+ }
1231
+ if (aprop.name.endsWith("InstalledCommand")) {
1232
+ metadata.installedCommands.push(aprop.value);
1233
+ metadata.retainedProperties.push(aprop);
1234
+ continue;
1235
+ }
1236
+ if (aprop.name.endsWith("Capability")) {
1237
+ metadata.capabilities.push(aprop.value);
1238
+ metadata.retainedProperties.push(aprop);
1239
+ continue;
1240
+ }
1241
+ if (
1242
+ aprop.name.endsWith("CapabilityCount") ||
1243
+ aprop.name.endsWith("InstalledFileCount") ||
1244
+ aprop.name.endsWith("InstalledCommandCount")
1245
+ ) {
1246
+ metadata.retainedProperties.push(aprop);
1247
+ continue;
1248
+ }
1249
+ metadata.retainedProperties.push(aprop);
997
1250
  }
998
- if (
999
- /dotnet[6-9]?-runtime/.test(comp.name) ||
1000
- comp.name.includes("aspnet-runtime") ||
1001
- /aspnetcore[6-9]?-runtime/.test(comp.name)
1002
- ) {
1003
- bundledRuntimes.add(comp.name);
1251
+ metadata.capabilities = uniqueSortedStrings(metadata.capabilities);
1252
+ metadata.installedCommandPaths = uniqueSortedStrings(
1253
+ metadata.installedCommandPaths,
1254
+ );
1255
+ metadata.installedCommands = uniqueSortedStrings(metadata.installedCommands);
1256
+ metadata.installedFiles = uniqueSortedStrings(metadata.installedFiles);
1257
+ metadata.retainedProperties = uniqueProperties(metadata.retainedProperties);
1258
+ return metadata;
1259
+ }
1260
+
1261
+ function getOrganizationalEntityName(entity) {
1262
+ if (!entity) {
1263
+ return undefined;
1004
1264
  }
1005
- // TODO: Need to test this for a range of base images
1006
- if (COMMON_RUNTIMES.includes(comp.name)) {
1007
- bundledRuntimes.add(comp.name);
1265
+ if (typeof entity === "string") {
1266
+ return entity.trim() || undefined;
1008
1267
  }
1268
+ if (typeof entity === "object" && typeof entity.name === "string") {
1269
+ return entity.name.trim() || undefined;
1270
+ }
1271
+ return undefined;
1009
1272
  }
1010
1273
 
1011
- const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
1012
- try {
1013
- const tmpDependsOn =
1014
- tmpDependencies[origBomRef] || tmpDependencies[comp["bom-ref"]] || [];
1015
- const dependsOn = new Set();
1016
- tmpDependsOn.forEach((d) => {
1017
- try {
1018
- const compPurl = PackageURL.fromString(comp.purl);
1019
- const tmpPurl = PackageURL.fromString(d.replace("none", compPurl.type));
1020
- tmpPurl.type = compPurl.type;
1021
- // FIXME: Check if this hack is still needed with the latest trivy
1022
- if (OS_PURL_TYPES.includes(compPurl.type)) {
1023
- tmpPurl.namespace = compPurl.namespace;
1024
- tmpPurl.qualifiers = tmpPurl.qualifiers || {};
1025
- if (compPurl.qualifiers) {
1026
- if (compPurl.qualifiers.distro_name) {
1027
- tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
1028
- }
1029
- if (compPurl.qualifiers.distro) {
1030
- tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
1031
- }
1032
- }
1033
- }
1034
- if (tmpPurl.qualifiers) {
1035
- if (
1036
- tmpPurl.qualifiers.epoch &&
1037
- !tmpPurl.version.startsWith(`${tmpPurl.qualifiers.epoch}:`)
1038
- ) {
1039
- tmpPurl.version = `${tmpPurl.qualifiers.epoch}:${tmpPurl.version}`;
1040
- }
1041
- }
1042
- // Prevents purls ending with ?
1043
- if (!Object.keys(tmpPurl.qualifiers).length) {
1044
- tmpPurl.qualifiers = undefined;
1045
- }
1046
- dependsOn.add(decodeURIComponent(tmpPurl.toString()));
1047
- } catch (_e) {
1048
- // ignore
1049
- }
1050
- });
1051
- return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
1052
- } catch (_e) {
1053
- // ignore
1274
+ function sameOrganizationalEntity(entity, expectedName) {
1275
+ const currentName = getOrganizationalEntityName(entity);
1276
+ return Boolean(
1277
+ currentName &&
1278
+ expectedName &&
1279
+ currentName.localeCompare(expectedName, undefined, {
1280
+ sensitivity: "accent",
1281
+ }) === 0,
1282
+ );
1283
+ }
1284
+
1285
+ function mergeOrganizationalEntityField(component, fieldName, entityName) {
1286
+ const normalizedName = `${entityName || ""}`.trim();
1287
+ if (!normalizedName) {
1288
+ return { applied: false, represented: false };
1054
1289
  }
1055
- return undefined;
1056
- };
1290
+ if (!component?.[fieldName]) {
1291
+ component[fieldName] = { name: normalizedName };
1292
+ return { applied: true, represented: true };
1293
+ }
1294
+ if (sameOrganizationalEntity(component[fieldName], normalizedName)) {
1295
+ return { applied: false, represented: true };
1296
+ }
1297
+ return { applied: false, represented: false };
1298
+ }
1057
1299
 
1058
- export function executeOsQuery(query) {
1059
- if (isDryRun) {
1060
- recordActivity({
1061
- kind: "osquery",
1062
- reason:
1063
- "Dry run mode blocks osquery execution and reports the query instead.",
1064
- status: "blocked",
1065
- target: query,
1066
- });
1300
+ function parseOrganizationalContact(value) {
1301
+ const normalizedValue = `${value || ""}`.trim();
1302
+ if (!normalizedValue) {
1067
1303
  return undefined;
1068
1304
  }
1069
- if (OSQUERY_BIN) {
1070
- if (!query.endsWith(";")) {
1071
- query = `${query};`;
1072
- }
1073
- const args = ["--json", query];
1074
- // On darwin, we need to disable the safety check and run cdxgen with sudo
1075
- // https://github.com/osquery/osquery/issues/1382
1076
- if (platform === "darwin") {
1077
- args.push("--allow_unsafe");
1078
- args.push("--disable_logging");
1079
- args.push("--disable_events");
1080
- }
1081
- if (DEBUG_MODE) {
1082
- console.log("Executing", OSQUERY_BIN, args.join(" "));
1083
- }
1084
- const result = safeSpawnSync(OSQUERY_BIN, args);
1085
- if (result.status !== 0 || result.error) {
1086
- if (
1087
- DEBUG_MODE &&
1088
- result.stderr &&
1089
- !result.stderr.includes("no such table")
1305
+ const match = normalizedValue.match(/^([^<>]+?)\s*<([^<>\s]+@[^<>\s]+)>$/);
1306
+ if (match) {
1307
+ return {
1308
+ name: match[1].trim(),
1309
+ email: match[2].trim(),
1310
+ };
1311
+ }
1312
+ return { name: normalizedValue };
1313
+ }
1314
+
1315
+ function sameOrganizationalContact(left, right) {
1316
+ if (!left || !right) {
1317
+ return false;
1318
+ }
1319
+ const leftName = `${left.name || ""}`.trim();
1320
+ const rightName = `${right.name || ""}`.trim();
1321
+ const leftEmail = `${left.email || ""}`.trim().toLowerCase();
1322
+ const rightEmail = `${right.email || ""}`.trim().toLowerCase();
1323
+ return leftName === rightName && leftEmail === rightEmail;
1324
+ }
1325
+
1326
+ function mergeAuthorsFromMaintainer(component, maintainerValue) {
1327
+ const authorContact = parseOrganizationalContact(maintainerValue);
1328
+ if (!authorContact?.name) {
1329
+ return { applied: false, represented: false };
1330
+ }
1331
+ if (!Array.isArray(component?.authors) || !component.authors.length) {
1332
+ component.authors = [authorContact];
1333
+ return { applied: true, represented: true };
1334
+ }
1335
+ if (
1336
+ component.authors.some((author) =>
1337
+ sameOrganizationalContact(author, authorContact),
1338
+ )
1339
+ ) {
1340
+ return { applied: false, represented: true };
1341
+ }
1342
+ return { applied: false, represented: false };
1343
+ }
1344
+
1345
+ function promoteTrivyOsPackageIdentity(component, trivyMetadata) {
1346
+ const fallbackProperties = [];
1347
+ const vendorValue = `${trivyMetadata?.packageVendor || ""}`.trim();
1348
+ const supplierName = getOrganizationalEntityName(component?.supplier);
1349
+ const maintainerValue = `${
1350
+ trivyMetadata?.packageMaintainer || supplierName || ""
1351
+ }`.trim();
1352
+
1353
+ const maintainerAuthorResult = mergeAuthorsFromMaintainer(
1354
+ component,
1355
+ maintainerValue,
1356
+ );
1357
+ const maintainerSupplierResult = mergeOrganizationalEntityField(
1358
+ component,
1359
+ "supplier",
1360
+ maintainerValue,
1361
+ );
1362
+ if (
1363
+ trivyMetadata?.packageMaintainer &&
1364
+ !maintainerAuthorResult.represented &&
1365
+ !maintainerSupplierResult.represented
1366
+ ) {
1367
+ fallbackProperties.push({
1368
+ name: "PackageMaintainer",
1369
+ value: trivyMetadata.packageMaintainer,
1370
+ });
1371
+ }
1372
+
1373
+ const vendorSupplierResult = mergeOrganizationalEntityField(
1374
+ component,
1375
+ "supplier",
1376
+ vendorValue,
1377
+ );
1378
+ const vendorManufacturerResult = mergeOrganizationalEntityField(
1379
+ component,
1380
+ "manufacturer",
1381
+ vendorValue,
1382
+ );
1383
+ if (
1384
+ vendorValue &&
1385
+ !vendorSupplierResult.represented &&
1386
+ !vendorManufacturerResult.represented
1387
+ ) {
1388
+ fallbackProperties.push({
1389
+ name: "PackageVendor",
1390
+ value: vendorValue,
1391
+ });
1392
+ }
1393
+ return fallbackProperties;
1394
+ }
1395
+
1396
+ async function collectRootfsRepositoryInventory(basePath) {
1397
+ let { components: trustedKeyComponents, refsByPath } =
1398
+ await collectTrustedKeyComponents(basePath);
1399
+ trustedKeyComponents = applyTrustMaterialEnhancements(
1400
+ trustedKeyComponents,
1401
+ collectTrustInspectorRootfsInventory(basePath),
1402
+ );
1403
+ refsByPath = new Map(
1404
+ trustedKeyComponents
1405
+ .map((component) => {
1406
+ const srcFile = (component.properties || []).find(
1407
+ (property) => property.name === "SrcFile",
1408
+ )?.value;
1409
+ return srcFile
1410
+ ? [normalizeContainerPath(srcFile), component["bom-ref"]]
1411
+ : undefined;
1412
+ })
1413
+ .filter(Boolean),
1414
+ );
1415
+ const repositoryEntries = uniqueRepositoryEntries(
1416
+ parseAptRepositorySources(basePath).concat(
1417
+ parseYumRepositorySources(basePath),
1418
+ ),
1419
+ );
1420
+ const components = [...trustedKeyComponents];
1421
+ const dependenciesList = [];
1422
+ const seenComponentRefs = new Set(components.map((comp) => comp["bom-ref"]));
1423
+ for (const entry of repositoryEntries) {
1424
+ const component = createRepositorySourceComponent(entry);
1425
+ if (seenComponentRefs.has(component["bom-ref"])) {
1426
+ continue;
1427
+ }
1428
+ seenComponentRefs.add(component["bom-ref"]);
1429
+ components.push(component);
1430
+ const dependsOn = uniqueSortedStrings(
1431
+ (entry.keyReferences || [])
1432
+ .map((keyRef) => normalizeLocalRepositoryReference(keyRef))
1433
+ .map((keyRef) => refsByPath.get(keyRef))
1434
+ .filter(Boolean),
1435
+ );
1436
+ if (dependsOn.length) {
1437
+ dependenciesList.push({
1438
+ ref: component["bom-ref"],
1439
+ dependsOn,
1440
+ });
1441
+ }
1442
+ }
1443
+ return { components, dependenciesList };
1444
+ }
1445
+
1446
+ async function collectTrustedKeyComponents(basePath) {
1447
+ const refsByPath = new Map();
1448
+ const components = [];
1449
+ for (const normalizedPath of collectTrustedKeyPaths(basePath)) {
1450
+ const component = await createTrustedKeyComponent(basePath, normalizedPath);
1451
+ if (!component?.["bom-ref"]) {
1452
+ continue;
1453
+ }
1454
+ refsByPath.set(normalizedPath, component["bom-ref"]);
1455
+ components.push(component);
1456
+ }
1457
+ return { components, refsByPath };
1458
+ }
1459
+
1460
+ function collectTrustedKeyPaths(basePath) {
1461
+ const results = new Set();
1462
+ for (const candidate of [
1463
+ "/etc/apt/trusted.gpg",
1464
+ "/etc/apt/trusted.gpg.d",
1465
+ "/usr/share/keyrings",
1466
+ "/etc/pki/rpm-gpg",
1467
+ "/usr/share/distribution-gpg-keys",
1468
+ "/etc/apk/keys",
1469
+ ]) {
1470
+ const normalizedCandidate = normalizeContainerPath(candidate);
1471
+ const absoluteCandidate = join(
1472
+ basePath,
1473
+ normalizedCandidate.replace(/^\/+/, ""),
1474
+ );
1475
+ if (!safeExistsSync(absoluteCandidate)) {
1476
+ continue;
1477
+ }
1478
+ const stats = statSync(absoluteCandidate, { throwIfNoEntry: false });
1479
+ if (!stats) {
1480
+ continue;
1481
+ }
1482
+ if (stats.isDirectory()) {
1483
+ for (const filePath of walkRootfsFiles(basePath, normalizedCandidate)) {
1484
+ if (isTrustedKeyPath(filePath)) {
1485
+ results.add(filePath);
1486
+ }
1487
+ }
1488
+ continue;
1489
+ }
1490
+ if (isTrustedKeyPath(normalizedCandidate)) {
1491
+ results.add(normalizedCandidate);
1492
+ }
1493
+ }
1494
+ return Array.from(results).sort();
1495
+ }
1496
+
1497
+ function walkRootfsFiles(basePath, normalizedDir) {
1498
+ const results = [];
1499
+ const absoluteDir = join(basePath, normalizedDir.replace(/^\/+/, ""));
1500
+ if (!safeExistsSync(absoluteDir)) {
1501
+ return results;
1502
+ }
1503
+ for (const entry of readdirSync(absoluteDir, { withFileTypes: true })) {
1504
+ const normalizedPath = normalizeContainerPath(
1505
+ `${normalizedDir.replace(/\/+$/, "")}/${entry.name}`,
1506
+ );
1507
+ if (entry.isDirectory()) {
1508
+ results.push(...walkRootfsFiles(basePath, normalizedPath));
1509
+ continue;
1510
+ }
1511
+ if (entry.isFile()) {
1512
+ results.push(normalizedPath);
1513
+ }
1514
+ }
1515
+ return results;
1516
+ }
1517
+
1518
+ function isTrustedKeyPath(normalizedPath) {
1519
+ const lowerPath = normalizeContainerPath(normalizedPath)?.toLowerCase();
1520
+ if (!lowerPath) {
1521
+ return false;
1522
+ }
1523
+ return (
1524
+ lowerPath === "/etc/apt/trusted.gpg" ||
1525
+ lowerPath.includes("/trusted.gpg.d/") ||
1526
+ lowerPath.includes("/keyrings/") ||
1527
+ lowerPath.includes("/rpm-gpg/") ||
1528
+ lowerPath.includes("/distribution-gpg-keys/") ||
1529
+ lowerPath.includes("/apk/keys/")
1530
+ );
1531
+ }
1532
+
1533
+ async function createTrustedKeyComponent(basePath, normalizedPath) {
1534
+ const absolutePath = join(basePath, normalizedPath.replace(/^\/+/, ""));
1535
+ const stats = statSync(absolutePath, { throwIfNoEntry: false });
1536
+ if (!stats || stats.isDirectory()) {
1537
+ return undefined;
1538
+ }
1539
+ let hashValues = {};
1540
+ try {
1541
+ hashValues = await multiChecksumFile(["sha1", "sha256"], absolutePath);
1542
+ } catch (_err) {
1543
+ // ignore
1544
+ }
1545
+ const version = hashValues.sha256 || hashValues.sha1 || `${stats.mtimeMs}`;
1546
+ const hashes = [];
1547
+ if (hashValues.sha1) {
1548
+ hashes.push({ alg: "SHA-1", content: hashValues.sha1 });
1549
+ }
1550
+ if (hashValues.sha256) {
1551
+ hashes.push({ alg: "SHA-256", content: hashValues.sha256 });
1552
+ }
1553
+ return {
1554
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${hashValues.sha256 ? `sha256:${hashValues.sha256}` : version}`,
1555
+ name: basename(normalizedPath),
1556
+ type: "cryptographic-asset",
1557
+ version,
1558
+ hashes,
1559
+ cryptoProperties: {
1560
+ assetType: "related-crypto-material",
1561
+ relatedCryptoMaterialProperties: {
1562
+ type: "public-key",
1563
+ id: hashValues.sha256 || hashValues.sha1 || normalizedPath,
1564
+ state: "active",
1565
+ },
1566
+ },
1567
+ properties: uniqueProperties([
1568
+ { name: "SrcFile", value: normalizedPath },
1569
+ {
1570
+ name: "cdx:crypto:trustDomain",
1571
+ value: deriveTrustedKeyDomain(normalizedPath),
1572
+ },
1573
+ { name: "cdx:crypto:keyPath", value: normalizedPath },
1574
+ {
1575
+ name: "cdx:crypto:fileExtension",
1576
+ value: extname(normalizedPath).replace(/^\./, "") || "gpg",
1577
+ },
1578
+ ]),
1579
+ };
1580
+ }
1581
+
1582
+ function deriveTrustedKeyDomain(normalizedPath) {
1583
+ const lowerPath = normalizeContainerPath(normalizedPath)?.toLowerCase() || "";
1584
+ if (lowerPath.includes("/apt/") || lowerPath.includes("/keyrings/")) {
1585
+ return "apt";
1586
+ }
1587
+ if (
1588
+ lowerPath.includes("/rpm-gpg/") ||
1589
+ lowerPath.includes("/distribution-gpg-keys/")
1590
+ ) {
1591
+ return "rpm";
1592
+ }
1593
+ if (lowerPath.includes("/apk/keys/")) {
1594
+ return "apk";
1595
+ }
1596
+ return "generic";
1597
+ }
1598
+
1599
+ function trustInspectorToolRefs() {
1600
+ return extractToolRefs({
1601
+ components: getPluginToolComponents(["trustinspector"]),
1602
+ });
1603
+ }
1604
+
1605
+ function executeTrustInspector(args, activity) {
1606
+ if (isDryRun) {
1607
+ recordActivity({
1608
+ kind: "trustinspector",
1609
+ reason:
1610
+ "Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
1611
+ status: "blocked",
1612
+ ...activity,
1613
+ });
1614
+ return undefined;
1615
+ }
1616
+ if (!TRUSTINSPECTOR_BIN) {
1617
+ return undefined;
1618
+ }
1619
+ if (DEBUG_MODE) {
1620
+ console.log("Executing", TRUSTINSPECTOR_BIN, args.join(" "));
1621
+ }
1622
+ const result = safeSpawnSync(TRUSTINSPECTOR_BIN, args);
1623
+ if (result?.status !== 0 || result?.error) {
1624
+ if (DEBUG_MODE && (result?.stdout || result?.stderr)) {
1625
+ console.error(result.stdout, result.stderr);
1626
+ }
1627
+ return undefined;
1628
+ }
1629
+ if (!result?.stdout) {
1630
+ return undefined;
1631
+ }
1632
+ try {
1633
+ return JSON.parse(result.stdout);
1634
+ } catch (_err) {
1635
+ return undefined;
1636
+ }
1637
+ }
1638
+
1639
+ function normalizeTrustInspectorTargetPath(basePath) {
1640
+ if (typeof basePath !== "string") {
1641
+ return undefined;
1642
+ }
1643
+ const trimmedPath = basePath.trim();
1644
+ if (
1645
+ !trimmedPath ||
1646
+ hasDangerousUnicode(trimmedPath) ||
1647
+ /[\r\n]/.test(trimmedPath)
1648
+ ) {
1649
+ return undefined;
1650
+ }
1651
+ const resolvedPath = resolve(trimmedPath);
1652
+ if (
1653
+ !resolvedPath ||
1654
+ hasDangerousUnicode(resolvedPath) ||
1655
+ /[\r\n]/.test(resolvedPath)
1656
+ ) {
1657
+ return undefined;
1658
+ }
1659
+ if (
1660
+ (isWin &&
1661
+ !(
1662
+ resolvedPath.startsWith("\\\\") ||
1663
+ isValidDriveRoot(resolvedPath.slice(0, 3))
1664
+ )) ||
1665
+ (!isWin && !resolvedPath.startsWith("/")) ||
1666
+ !safeExistsSync(resolvedPath)
1667
+ ) {
1668
+ return undefined;
1669
+ }
1670
+ const targetStats = statSync(resolvedPath, { throwIfNoEntry: false });
1671
+ if (!targetStats?.isDirectory()) {
1672
+ return undefined;
1673
+ }
1674
+ let canonicalPath;
1675
+ try {
1676
+ canonicalPath = realpathSync(resolvedPath);
1677
+ } catch (_err) {
1678
+ return undefined;
1679
+ }
1680
+ if (
1681
+ !canonicalPath ||
1682
+ hasDangerousUnicode(canonicalPath) ||
1683
+ /[\r\n]/.test(canonicalPath)
1684
+ ) {
1685
+ return undefined;
1686
+ }
1687
+ return resolvedPath;
1688
+ }
1689
+
1690
+ function trustMaterialHashes(material) {
1691
+ const hashes = [];
1692
+ if (material?.sha1) {
1693
+ hashes.push({ alg: "SHA-1", content: material.sha1 });
1694
+ }
1695
+ if (material?.sha256) {
1696
+ hashes.push({ alg: "SHA-256", content: material.sha256 });
1697
+ }
1698
+ return hashes;
1699
+ }
1700
+
1701
+ function trustMaterialState(material) {
1702
+ const expiresAt = material?.expiresAt
1703
+ ? Date.parse(material.expiresAt)
1704
+ : Number.NaN;
1705
+ if (!Number.isNaN(expiresAt) && expiresAt < Date.now()) {
1706
+ return "expired";
1707
+ }
1708
+ return "active";
1709
+ }
1710
+
1711
+ function createTrustMaterialComponent(material) {
1712
+ const normalizedPath = normalizeContainerPath(material?.path);
1713
+ if (!normalizedPath || !material?.kind) {
1714
+ return undefined;
1715
+ }
1716
+ const hashes = trustMaterialHashes(material);
1717
+ const sharedProperties = uniqueProperties([
1718
+ { name: "SrcFile", value: normalizedPath },
1719
+ ...(material?.properties || []),
1720
+ ...(material?.fingerprint
1721
+ ? [{ name: "cdx:crypto:fingerprint", value: material.fingerprint }]
1722
+ : []),
1723
+ ...(material?.algorithm
1724
+ ? [{ name: "cdx:crypto:algorithm", value: material.algorithm }]
1725
+ : []),
1726
+ ...(material?.keyStrength
1727
+ ? [{ name: "cdx:crypto:keyStrength", value: `${material.keyStrength}` }]
1728
+ : []),
1729
+ ...(material?.createdAt
1730
+ ? [{ name: "cdx:crypto:createdAt", value: material.createdAt }]
1731
+ : []),
1732
+ ...(material?.expiresAt
1733
+ ? [{ name: "cdx:crypto:expiresAt", value: material.expiresAt }]
1734
+ : []),
1735
+ ]);
1736
+ let component;
1737
+ if (material.kind === "certificate") {
1738
+ component = {
1739
+ "bom-ref": `crypto/certificate/${encodeURIComponent(material.name || normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.serial || material.expiresAt || "unknown"}`,
1740
+ name: material.name || basename(normalizedPath),
1741
+ type: "cryptographic-asset",
1742
+ version:
1743
+ material.sha256 ||
1744
+ material.serial ||
1745
+ material.expiresAt ||
1746
+ "configured",
1747
+ hashes,
1748
+ description: material.subject || normalizedPath,
1749
+ cryptoProperties: {
1750
+ assetType: "certificate",
1751
+ algorithmProperties: {
1752
+ executionEnvironment: "unknown",
1753
+ implementationPlatform: "unknown",
1754
+ },
1755
+ certificateProperties: {
1756
+ serialNumber: material.serial || undefined,
1757
+ subjectName: material.subject || undefined,
1758
+ issuerName: material.issuer || undefined,
1759
+ notValidBefore: material.createdAt || undefined,
1760
+ notValidAfter: material.expiresAt || undefined,
1761
+ certificateFormat: material.format || "X.509",
1762
+ certificateFileExtension: material.fileExtension || undefined,
1763
+ fingerprint: material.fingerprint
1764
+ ? { alg: "SHA-256", content: material.fingerprint }
1765
+ : undefined,
1766
+ },
1767
+ },
1768
+ properties: uniqueProperties(
1769
+ sharedProperties.concat(
1770
+ material.trustDomain
1771
+ ? [{ name: "cdx:crypto:trustDomain", value: material.trustDomain }]
1772
+ : [],
1773
+ ),
1774
+ ),
1775
+ };
1776
+ } else {
1777
+ component = {
1778
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.keyId || "unknown"}`,
1779
+ name: material.name || basename(normalizedPath),
1780
+ type: "cryptographic-asset",
1781
+ version: material.sha256 || material.keyId || normalizedPath,
1782
+ hashes,
1783
+ cryptoProperties: {
1784
+ assetType: "related-crypto-material",
1785
+ relatedCryptoMaterialProperties: {
1786
+ type: "public-key",
1787
+ id:
1788
+ material.keyId ||
1789
+ material.fingerprint ||
1790
+ material.sha256 ||
1791
+ normalizedPath,
1792
+ state: trustMaterialState(material),
1793
+ },
1794
+ },
1795
+ properties: uniqueProperties(
1796
+ sharedProperties.concat([
1797
+ {
1798
+ name: "cdx:crypto:trustDomain",
1799
+ value:
1800
+ material.trustDomain || deriveTrustedKeyDomain(normalizedPath),
1801
+ },
1802
+ { name: "cdx:crypto:keyPath", value: normalizedPath },
1803
+ {
1804
+ name: "cdx:crypto:fileExtension",
1805
+ value:
1806
+ material.fileExtension ||
1807
+ extname(normalizedPath).replace(/^\./, "") ||
1808
+ "gpg",
1809
+ },
1810
+ ...(material?.keyId
1811
+ ? [{ name: "cdx:crypto:keyId", value: material.keyId }]
1812
+ : []),
1813
+ ...(material?.userIds || []).map((value) => ({
1814
+ name: "cdx:crypto:userId",
1815
+ value,
1816
+ })),
1817
+ ]),
1818
+ ),
1819
+ };
1820
+ }
1821
+ attachIdentityTools(component, trustInspectorToolRefs());
1822
+ return component;
1823
+ }
1824
+
1825
+ function enhanceComponentFromTrustMaterial(component, material) {
1826
+ if (!component || !material) {
1827
+ return component;
1828
+ }
1829
+ component.hashes = component.hashes?.length
1830
+ ? component.hashes
1831
+ : trustMaterialHashes(material);
1832
+ component.properties = uniqueProperties(
1833
+ (component.properties || []).concat(
1834
+ createTrustMaterialComponent(material)?.properties || [],
1835
+ ),
1836
+ );
1837
+ if (!component.cryptoProperties) {
1838
+ component.cryptoProperties =
1839
+ createTrustMaterialComponent(material)?.cryptoProperties;
1840
+ }
1841
+ attachIdentityTools(component, trustInspectorToolRefs());
1842
+ return component;
1843
+ }
1844
+
1845
+ function applyTrustMaterialEnhancements(components, materials) {
1846
+ if (!materials?.length) {
1847
+ return components || [];
1848
+ }
1849
+ const componentList = [...(components || [])];
1850
+ const bySrcFile = new Map();
1851
+ const seenRefs = new Set();
1852
+ for (const component of componentList) {
1853
+ if (component?.["bom-ref"]) {
1854
+ seenRefs.add(component["bom-ref"]);
1855
+ }
1856
+ const srcFile = (component?.properties || []).find(
1857
+ (property) => property.name === "SrcFile",
1858
+ )?.value;
1859
+ if (srcFile) {
1860
+ bySrcFile.set(normalizeContainerPath(srcFile), component);
1861
+ }
1862
+ }
1863
+ for (const material of materials) {
1864
+ const srcFile = normalizeContainerPath(material?.path);
1865
+ const existing = bySrcFile.get(srcFile);
1866
+ if (
1867
+ existing &&
1868
+ existing.type === "cryptographic-asset" &&
1869
+ material?.kind === "public-key"
1870
+ ) {
1871
+ enhanceComponentFromTrustMaterial(existing, material);
1872
+ continue;
1873
+ }
1874
+ const component = createTrustMaterialComponent(material);
1875
+ if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
1876
+ continue;
1877
+ }
1878
+ seenRefs.add(component["bom-ref"]);
1879
+ componentList.push(component);
1880
+ if (srcFile) {
1881
+ bySrcFile.set(srcFile, component);
1882
+ }
1883
+ }
1884
+ return componentList;
1885
+ }
1886
+
1887
+ function collectTrustInspectorRootfsInventory(basePath) {
1888
+ const targetPath = normalizeTrustInspectorTargetPath(basePath);
1889
+ if (!targetPath) {
1890
+ return [];
1891
+ }
1892
+ const trustData = executeTrustInspector(["rootfs", targetPath], {
1893
+ target: targetPath,
1894
+ });
1895
+ return trustData?.materials || [];
1896
+ }
1897
+
1898
+ function parseAptRepositorySources(basePath) {
1899
+ const sourceFiles = [
1900
+ "/etc/apt/sources.list",
1901
+ ...walkRootfsFiles(basePath, "/etc/apt/sources.list.d").filter(
1902
+ (filePath) => filePath.endsWith(".list") || filePath.endsWith(".sources"),
1903
+ ),
1904
+ ].filter((filePath, index, values) => values.indexOf(filePath) === index);
1905
+ const entries = [];
1906
+ for (const sourceFile of sourceFiles) {
1907
+ const data = readRootfsTextFile(basePath, sourceFile);
1908
+ if (!data) {
1909
+ continue;
1910
+ }
1911
+ if (sourceFile.endsWith(".sources")) {
1912
+ entries.push(...parseDeb822AptRepositorySources(data, sourceFile));
1913
+ continue;
1914
+ }
1915
+ entries.push(...parseLegacyAptRepositorySources(data, sourceFile));
1916
+ }
1917
+ return entries;
1918
+ }
1919
+
1920
+ function parseLegacyAptRepositorySources(data, sourceFile) {
1921
+ const entries = [];
1922
+ for (const rawLine of data.split(/\r?\n/)) {
1923
+ const line = rawLine.split("#")[0].trim();
1924
+ if (!line || (!line.startsWith("deb ") && !line.startsWith("deb-src "))) {
1925
+ continue;
1926
+ }
1927
+ const match = line.match(
1928
+ /^(deb(?:-src)?)\s+(?:\[(?<options>[^\]]+)\]\s+)?(?<uri>\S+)\s+(?<suite>\S+)(?:\s+(?<components>.+))?$/,
1929
+ );
1930
+ if (!match?.groups?.uri) {
1931
+ continue;
1932
+ }
1933
+ const options = parseRepositoryOptionString(match.groups.options);
1934
+ entries.push({
1935
+ name: deriveRepositoryDisplayName(match.groups.uri, sourceFile),
1936
+ path: sourceFile,
1937
+ repoType: isPpaRepository(match.groups.uri) ? "ppa-source" : "apt-source",
1938
+ release: match.groups.suite,
1939
+ url: match.groups.uri,
1940
+ description: line,
1941
+ enabled: true,
1942
+ keyReferences: extractRepositoryKeyReferences(options["signed-by"]),
1943
+ properties: uniqueProperties([
1944
+ { name: "SrcFile", value: sourceFile },
1945
+ { name: "cdx:os:repo:kind", value: match[1] },
1946
+ { name: "cdx:os:repo:url", value: match.groups.uri },
1947
+ { name: "cdx:os:repo:release", value: match.groups.suite },
1948
+ ...(match.groups.components
1949
+ ? [
1950
+ {
1951
+ name: "cdx:os:repo:components",
1952
+ value: match.groups.components,
1953
+ },
1954
+ ]
1955
+ : []),
1956
+ ...(options.arch
1957
+ ? [{ name: "cdx:os:repo:architectures", value: options.arch }]
1958
+ : []),
1959
+ ...(options["signed-by"]
1960
+ ? [{ name: "cdx:os:repo:signedBy", value: options["signed-by"] }]
1961
+ : []),
1962
+ ]),
1963
+ });
1964
+ }
1965
+ return entries;
1966
+ }
1967
+
1968
+ function parseDeb822AptRepositorySources(data, sourceFile) {
1969
+ const entries = [];
1970
+ for (const stanza of data.split(/\n\s*\n/)) {
1971
+ const fields = parseDeb822Fields(stanza);
1972
+ const uris = splitRepositoryField(fields.uris);
1973
+ const suites = splitRepositoryField(fields.suites || fields.suite);
1974
+ if (!uris.length) {
1975
+ continue;
1976
+ }
1977
+ for (const uri of uris) {
1978
+ entries.push({
1979
+ name: deriveRepositoryDisplayName(uri, sourceFile),
1980
+ path: sourceFile,
1981
+ repoType: isPpaRepository(uri) ? "ppa-source" : "apt-source",
1982
+ release: suites.join(",") || "configured",
1983
+ url: uri,
1984
+ description: stanza.trim(),
1985
+ enabled: !["no", "false", "0"].includes(
1986
+ `${fields.enabled || "yes"}`.toLowerCase(),
1987
+ ),
1988
+ keyReferences: extractRepositoryKeyReferences(fields["signed-by"]),
1989
+ properties: uniqueProperties([
1990
+ { name: "SrcFile", value: sourceFile },
1991
+ { name: "cdx:os:repo:kind", value: fields.types || "deb" },
1992
+ { name: "cdx:os:repo:url", value: uri },
1993
+ ...(suites.length
1994
+ ? [{ name: "cdx:os:repo:release", value: suites.join(",") }]
1995
+ : []),
1996
+ ...(fields.components
1997
+ ? [{ name: "cdx:os:repo:components", value: fields.components }]
1998
+ : []),
1999
+ ...(fields.architectures
2000
+ ? [
2001
+ {
2002
+ name: "cdx:os:repo:architectures",
2003
+ value: fields.architectures,
2004
+ },
2005
+ ]
2006
+ : []),
2007
+ ...(fields["signed-by"]
2008
+ ? [
2009
+ {
2010
+ name: "cdx:os:repo:signedBy",
2011
+ value: fields["signed-by"],
2012
+ },
2013
+ ]
2014
+ : []),
2015
+ ]),
2016
+ });
2017
+ }
2018
+ }
2019
+ return entries;
2020
+ }
2021
+
2022
+ function parseYumRepositorySources(basePath) {
2023
+ const entries = [];
2024
+ for (const repoFile of walkRootfsFiles(basePath, "/etc/yum.repos.d").filter(
2025
+ (f) => f.endsWith(".repo"),
2026
+ )) {
2027
+ const data = readRootfsTextFile(basePath, repoFile);
2028
+ if (!data) {
2029
+ continue;
2030
+ }
2031
+ let currentSection;
2032
+ let currentConfig = {};
2033
+ const flushCurrentSection = () => {
2034
+ if (!currentSection) {
2035
+ return;
2036
+ }
2037
+ const url =
2038
+ currentConfig.baseurl ||
2039
+ currentConfig.mirrorlist ||
2040
+ currentConfig.metalink;
2041
+ if (!url) {
2042
+ return;
2043
+ }
2044
+ entries.push({
2045
+ name: currentSection,
2046
+ path: repoFile,
2047
+ repoType: "yum-source",
2048
+ release:
2049
+ `${currentConfig.enabled || "1"}` === "1" ? "enabled" : "disabled",
2050
+ url,
2051
+ description: `${repoFile}#${currentSection}`,
2052
+ enabled: `${currentConfig.enabled || "1"}` === "1",
2053
+ keyReferences: extractRepositoryKeyReferences(currentConfig.gpgkey),
2054
+ properties: uniqueProperties([
2055
+ { name: "SrcFile", value: repoFile },
2056
+ { name: "cdx:os:repo:url", value: url },
2057
+ ...(currentConfig.baseurl
2058
+ ? [{ name: "cdx:os:repo:baseurl", value: currentConfig.baseurl }]
2059
+ : []),
2060
+ ...(currentConfig.mirrorlist
2061
+ ? [
2062
+ {
2063
+ name: "cdx:os:repo:mirrorlist",
2064
+ value: currentConfig.mirrorlist,
2065
+ },
2066
+ ]
2067
+ : []),
2068
+ ...(currentConfig.metalink
2069
+ ? [{ name: "cdx:os:repo:metalink", value: currentConfig.metalink }]
2070
+ : []),
2071
+ {
2072
+ name: "cdx:os:repo:enabled",
2073
+ value: `${currentConfig.enabled || "1"}`,
2074
+ },
2075
+ ...(currentConfig.gpgcheck
2076
+ ? [{ name: "cdx:os:repo:gpgcheck", value: currentConfig.gpgcheck }]
2077
+ : []),
2078
+ ...(currentConfig.gpgkey
2079
+ ? [{ name: "cdx:os:repo:gpgkey", value: currentConfig.gpgkey }]
2080
+ : []),
2081
+ ]),
2082
+ });
2083
+ };
2084
+ for (const rawLine of data.split(/\r?\n/)) {
2085
+ const line = rawLine.trim();
2086
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
2087
+ continue;
2088
+ }
2089
+ if (line.startsWith("[") && line.endsWith("]")) {
2090
+ flushCurrentSection();
2091
+ currentSection = line.slice(1, -1).trim();
2092
+ currentConfig = {};
2093
+ continue;
2094
+ }
2095
+ const equalsIndex = line.indexOf("=");
2096
+ if (equalsIndex === -1) {
2097
+ continue;
2098
+ }
2099
+ currentConfig[line.slice(0, equalsIndex).trim().toLowerCase()] = line
2100
+ .slice(equalsIndex + 1)
2101
+ .trim();
2102
+ }
2103
+ flushCurrentSection();
2104
+ }
2105
+ return entries;
2106
+ }
2107
+
2108
+ function createRepositorySourceComponent(entry) {
2109
+ const version = entry.release || "configured";
2110
+ const purl = new PackageURL(
2111
+ "generic",
2112
+ "os-repository",
2113
+ entry.name,
2114
+ version,
2115
+ {
2116
+ path: entry.path,
2117
+ repo_type: entry.repoType,
2118
+ },
2119
+ undefined,
2120
+ ).toString();
2121
+ return {
2122
+ "bom-ref": decodeURIComponent(purl),
2123
+ purl,
2124
+ name: entry.name,
2125
+ type: "data",
2126
+ version,
2127
+ description: entry.description || entry.url,
2128
+ properties: uniqueProperties(
2129
+ [
2130
+ { name: "SrcFile", value: entry.path },
2131
+ { name: "cdx:os:repo:type", value: entry.repoType },
2132
+ { name: "cdx:os:repo:url", value: entry.url },
2133
+ {
2134
+ name: "cdx:os:repo:enabled",
2135
+ value: entry.enabled === false ? "false" : "true",
2136
+ },
2137
+ ].concat(entry.properties || []),
2138
+ ),
2139
+ };
2140
+ }
2141
+
2142
+ function uniqueRepositoryEntries(entries) {
2143
+ const seen = new Set();
2144
+ const results = [];
2145
+ for (const entry of entries || []) {
2146
+ if (!entry?.name || !entry?.path || !entry?.url) {
2147
+ continue;
2148
+ }
2149
+ const key = `${entry.repoType}\u0000${entry.path}\u0000${entry.url}\u0000${entry.release || ""}`;
2150
+ if (seen.has(key)) {
2151
+ continue;
2152
+ }
2153
+ seen.add(key);
2154
+ results.push(entry);
2155
+ }
2156
+ return results;
2157
+ }
2158
+
2159
+ function deriveRepositoryDisplayName(url, sourceFile) {
2160
+ try {
2161
+ const parsedUrl = new URL(url);
2162
+ const repoPath = parsedUrl.pathname.replace(/\/+$/, "") || "/";
2163
+ return `${parsedUrl.hostname}${repoPath}`;
2164
+ } catch (_err) {
2165
+ return basename(sourceFile);
2166
+ }
2167
+ }
2168
+
2169
+ function isPpaRepository(url) {
2170
+ try {
2171
+ const hostname = new URL(`${url || ""}`).hostname.toLowerCase();
2172
+ return (
2173
+ hostname === "ppa.launchpadcontent.net" ||
2174
+ hostname === "ppa.launchpad.net"
2175
+ );
2176
+ } catch (_err) {
2177
+ return false;
2178
+ }
2179
+ }
2180
+
2181
+ function parseRepositoryOptionString(optionString) {
2182
+ const options = {};
2183
+ for (const token of `${optionString || ""}`.split(/\s+/).filter(Boolean)) {
2184
+ const [key, ...valueParts] = token.split("=");
2185
+ if (!key || !valueParts.length) {
2186
+ continue;
2187
+ }
2188
+ options[key.toLowerCase()] = valueParts.join("=");
2189
+ }
2190
+ return options;
2191
+ }
2192
+
2193
+ function parseDeb822Fields(stanza) {
2194
+ const fields = {};
2195
+ let currentKey;
2196
+ for (const rawLine of stanza.split(/\r?\n/)) {
2197
+ if (!rawLine.trim()) {
2198
+ continue;
2199
+ }
2200
+ if (/^[ \t]/.test(rawLine) && currentKey) {
2201
+ fields[currentKey] = `${fields[currentKey]} ${rawLine.trim()}`.trim();
2202
+ continue;
2203
+ }
2204
+ const separatorIndex = rawLine.indexOf(":");
2205
+ if (separatorIndex === -1) {
2206
+ continue;
2207
+ }
2208
+ currentKey = rawLine.slice(0, separatorIndex).trim().toLowerCase();
2209
+ fields[currentKey] = rawLine.slice(separatorIndex + 1).trim();
2210
+ }
2211
+ return fields;
2212
+ }
2213
+
2214
+ function splitRepositoryField(value) {
2215
+ return `${value || ""}`.split(/\s+/).filter(Boolean);
2216
+ }
2217
+
2218
+ function extractRepositoryKeyReferences(value) {
2219
+ return uniqueSortedStrings(
2220
+ `${value || ""}`
2221
+ .split(/[\s,]+/)
2222
+ .map((part) => normalizeLocalRepositoryReference(part))
2223
+ .filter(Boolean),
2224
+ );
2225
+ }
2226
+
2227
+ function normalizeLocalRepositoryReference(value) {
2228
+ if (!value) {
2229
+ return undefined;
2230
+ }
2231
+ const trimmedValue = `${value}`.trim();
2232
+ if (!trimmedValue || trimmedValue.includes("BEGIN PGP PUBLIC KEY BLOCK")) {
2233
+ return undefined;
2234
+ }
2235
+ if (trimmedValue.startsWith("file://")) {
2236
+ return normalizeContainerPath(trimmedValue.slice("file://".length));
2237
+ }
2238
+ if (trimmedValue.startsWith("/")) {
2239
+ return normalizeContainerPath(trimmedValue);
2240
+ }
2241
+ return undefined;
2242
+ }
2243
+
2244
+ function readRootfsTextFile(basePath, normalizedPath) {
2245
+ const absolutePath = join(basePath, normalizedPath.replace(/^\/+/, ""));
2246
+ if (!safeExistsSync(absolutePath)) {
2247
+ return undefined;
2248
+ }
2249
+ try {
2250
+ return readFileSync(absolutePath, "utf-8");
2251
+ } catch (_err) {
2252
+ return undefined;
2253
+ }
2254
+ }
2255
+
2256
+ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2257
+ const components = [];
2258
+ const dependenciesList = [];
2259
+ const ownedFilePaths = new Set();
2260
+ const services = [];
2261
+ const componentByPath = new Map();
2262
+ for (const packageEntry of osPackageEntries) {
2263
+ const commandPathSet = new Set(packageEntry.commandPaths || []);
2264
+ const providedRefs = new Set();
2265
+ for (const filePath of packageEntry.files || []) {
2266
+ if (!filePath) {
2267
+ continue;
2268
+ }
2269
+ ownedFilePaths.add(filePath);
2270
+ let fileComponent = componentByPath.get(filePath);
2271
+ if (!fileComponent) {
2272
+ fileComponent = await createOSPackageFileComponent(
2273
+ basePath,
2274
+ filePath,
2275
+ commandPathSet,
2276
+ );
2277
+ if (!fileComponent) {
2278
+ continue;
2279
+ }
2280
+ componentByPath.set(filePath, fileComponent);
2281
+ components.push(fileComponent);
2282
+ }
2283
+ providedRefs.add(fileComponent["bom-ref"]);
2284
+ }
2285
+ const serviceResults = await createOSPackageServices(
2286
+ basePath,
2287
+ packageEntry,
2288
+ componentByPath,
2289
+ );
2290
+ for (const service of serviceResults.services) {
2291
+ services.push(service);
2292
+ providedRefs.add(service["bom-ref"]);
2293
+ }
2294
+ if (serviceResults.dependenciesList.length) {
2295
+ dependenciesList.push(...serviceResults.dependenciesList);
2296
+ }
2297
+ if (providedRefs.size) {
2298
+ dependenciesList.push({
2299
+ ref: packageEntry.packageRef,
2300
+ provides: Array.from(providedRefs).sort(),
2301
+ });
2302
+ }
2303
+ }
2304
+ return {
2305
+ components,
2306
+ dependenciesList,
2307
+ ownedFilePaths: Array.from(ownedFilePaths).sort(),
2308
+ services: dedupeServices(services),
2309
+ };
2310
+ }
2311
+
2312
+ async function createOSPackageFileComponent(
2313
+ basePath,
2314
+ filePath,
2315
+ commandPathSet,
2316
+ ) {
2317
+ const normalizedFilePath = normalizeContainerPath(filePath);
2318
+ if (!normalizedFilePath) {
2319
+ return undefined;
2320
+ }
2321
+ let hashes;
2322
+ try {
2323
+ const hashValues = await multiChecksumFile(
2324
+ ["md5", "sha1"],
2325
+ join(basePath, normalizedFilePath.replace(/^\/+/, "")),
2326
+ );
2327
+ hashes = [
2328
+ { alg: "MD5", content: hashValues.md5 },
2329
+ { alg: "SHA-1", content: hashValues.sha1 },
2330
+ ];
2331
+ } catch (_e) {
2332
+ // ignore
2333
+ }
2334
+ const fileName = basename(normalizedFilePath);
2335
+ const stats = statSync(
2336
+ join(basePath, normalizedFilePath.replace(/^\/+/, "")),
2337
+ {
2338
+ throwIfNoEntry: false,
2339
+ },
2340
+ );
2341
+ if (!stats || stats.isDirectory()) {
2342
+ return undefined;
2343
+ }
2344
+ let linkedName;
2345
+ try {
2346
+ const resolvedPath = realpathSync(
2347
+ join(basePath, normalizedFilePath.replace(/^\/+/, "")),
2348
+ );
2349
+ const linkStats = lstatSync(
2350
+ join(basePath, normalizedFilePath.replace(/^\/+/, "")),
2351
+ );
2352
+ if (linkStats?.isSymbolicLink()) {
2353
+ linkedName = basename(resolvedPath);
2354
+ recordSymlinkResolution(
2355
+ join(basePath, normalizedFilePath.replace(/^\/+/, "")),
2356
+ resolvedPath,
2357
+ {
2358
+ basePath,
2359
+ metadata: {
2360
+ resolutionKind: "container-os-package-file",
2361
+ },
2362
+ },
2363
+ );
2364
+ }
2365
+ } catch (_e) {
2366
+ // ignore
2367
+ }
2368
+ const fileType = determineOwnedFileType(
2369
+ normalizedFilePath,
2370
+ stats,
2371
+ commandPathSet,
2372
+ );
2373
+ const properties = [{ name: "SrcFile", value: normalizedFilePath }];
2374
+ if (fileType === "executable") {
2375
+ properties.push({ name: "internal:is_executable", value: "true" });
2376
+ } else if (fileType === "shared_library") {
2377
+ properties.push({ name: "internal:is_shared_library", value: "true" });
2378
+ } else {
2379
+ properties.push({ name: "internal:is_file", value: "true" });
2380
+ }
2381
+ properties.push(...createContainerRiskProperties(fileName, linkedName));
2382
+ properties.push(...createGtfoBinsProperties(fileName, linkedName));
2383
+ const purl = `pkg:generic/${encodeURIComponent(fileName)}?path=${encodeURIComponent(normalizedFilePath)}`;
2384
+ return {
2385
+ name: fileName,
2386
+ type: "file",
2387
+ purl,
2388
+ "bom-ref": purl,
2389
+ hashes,
2390
+ properties,
2391
+ evidence: {
2392
+ identity: [
2393
+ {
2394
+ field: "purl",
2395
+ confidence: 0,
2396
+ methods: [
2397
+ {
2398
+ technique: "filename",
2399
+ confidence: 0,
2400
+ value: normalizedFilePath,
2401
+ },
2402
+ ],
2403
+ concludedValue: normalizedFilePath,
2404
+ },
2405
+ ],
2406
+ },
2407
+ };
2408
+ }
2409
+
2410
+ async function createOSPackageServices(
2411
+ basePath,
2412
+ packageEntry,
2413
+ componentByPath,
2414
+ ) {
2415
+ const services = [];
2416
+ const dependenciesList = [];
2417
+ for (const filePath of packageEntry.files || []) {
2418
+ const serviceDescriptor = parseOwnedServiceFile(basePath, filePath);
2419
+ if (!serviceDescriptor) {
2420
+ continue;
2421
+ }
2422
+ const service = buildOwnedServiceComponent(packageEntry, serviceDescriptor);
2423
+ services.push(service);
2424
+ const dependsOn = new Set([packageEntry.packageRef]);
2425
+ const fileComponent = componentByPath.get(filePath);
2426
+ if (fileComponent?.["bom-ref"]) {
2427
+ dependsOn.add(fileComponent["bom-ref"]);
2428
+ }
2429
+ for (const execPath of serviceDescriptor.execPaths) {
2430
+ const execComponent = componentByPath.get(execPath);
2431
+ if (execComponent?.["bom-ref"]) {
2432
+ dependsOn.add(execComponent["bom-ref"]);
2433
+ }
2434
+ }
2435
+ dependenciesList.push({
2436
+ ref: service["bom-ref"],
2437
+ dependsOn: Array.from(dependsOn).sort(),
2438
+ });
2439
+ }
2440
+ return { services, dependenciesList };
2441
+ }
2442
+
2443
+ function parseOwnedServiceFile(basePath, filePath) {
2444
+ const normalizedFilePath = normalizeContainerPath(filePath);
2445
+ if (!normalizedFilePath) {
2446
+ return undefined;
2447
+ }
2448
+ const fileContentPath = join(
2449
+ basePath,
2450
+ normalizedFilePath.replace(/^\/+/, ""),
2451
+ );
2452
+ if (!safeExistsSync(fileContentPath)) {
2453
+ return undefined;
2454
+ }
2455
+ if (isSystemdServiceFile(normalizedFilePath)) {
2456
+ const unitData = readFileSync(fileContentPath, "utf-8");
2457
+ const unitMetadata = parseSystemdUnitFile(unitData);
2458
+ return {
2459
+ description: unitMetadata.description,
2460
+ execPaths: unitMetadata.execPaths,
2461
+ filePath: normalizedFilePath,
2462
+ manager: "systemd",
2463
+ name: basename(normalizedFilePath).replace(/\.[^.]+$/, ""),
2464
+ properties: [
2465
+ { name: "cdx:service:manager", value: "systemd" },
2466
+ {
2467
+ name: "cdx:service:unitType",
2468
+ value: basename(normalizedFilePath).split(".").pop() || "service",
2469
+ },
2470
+ ...unitMetadata.properties,
2471
+ ],
2472
+ };
2473
+ }
2474
+ if (isInitServiceFile(normalizedFilePath)) {
2475
+ const initData = readFileSync(fileContentPath, "utf-8");
2476
+ const initMetadata = parseInitScriptMetadata(initData);
2477
+ return {
2478
+ description: initMetadata.description,
2479
+ execPaths: initMetadata.execPaths,
2480
+ filePath: normalizedFilePath,
2481
+ manager: "sysvinit",
2482
+ name: initMetadata.name || basename(normalizedFilePath),
2483
+ properties: [
2484
+ { name: "cdx:service:manager", value: "sysvinit" },
2485
+ ...initMetadata.properties,
2486
+ ],
2487
+ };
2488
+ }
2489
+ return undefined;
2490
+ }
2491
+
2492
+ function buildOwnedServiceComponent(packageEntry, serviceDescriptor) {
2493
+ const serviceName = sanitizeServiceName(serviceDescriptor.name);
2494
+ const manager = serviceDescriptor.manager;
2495
+ return {
2496
+ "bom-ref": `urn:service:${manager}:${sanitizeServiceRefToken(packageEntry.packageRef)}:${sanitizeServiceRefToken(serviceName)}`,
2497
+ name: serviceName,
2498
+ version: packageEntry.packageVersion || "latest",
2499
+ group: packageEntry.packageName || manager,
2500
+ description: serviceDescriptor.description,
2501
+ properties: uniqueProperties(
2502
+ [
2503
+ { name: "SrcFile", value: serviceDescriptor.filePath },
2504
+ { name: "cdx:service:packageRef", value: packageEntry.packageRef },
2505
+ { name: "cdx:service:packageName", value: packageEntry.packageName },
2506
+ ].concat(serviceDescriptor.properties || []),
2507
+ ),
2508
+ };
2509
+ }
2510
+
2511
+ function parseSystemdUnitFile(unitData) {
2512
+ const properties = [];
2513
+ const execPaths = new Set();
2514
+ let description;
2515
+ let currentSection = "";
2516
+ for (const rawLine of unitData.split(/\r?\n/)) {
2517
+ const line = rawLine.trim();
2518
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
2519
+ continue;
2520
+ }
2521
+ if (line.startsWith("[") && line.endsWith("]")) {
2522
+ currentSection = line.slice(1, -1);
2523
+ continue;
2524
+ }
2525
+ const equalsIndex = line.indexOf("=");
2526
+ if (equalsIndex === -1) {
2527
+ continue;
2528
+ }
2529
+ const key = line.slice(0, equalsIndex).trim();
2530
+ const value = line.slice(equalsIndex + 1).trim();
2531
+ if (!value) {
2532
+ continue;
2533
+ }
2534
+ if (currentSection === "Unit" && key === "Description") {
2535
+ description = value;
2536
+ }
2537
+ if (currentSection === "Service") {
2538
+ if (key.startsWith("Exec")) {
2539
+ properties.push({ name: `cdx:service:${key}`, value });
2540
+ const execPath = extractExecPath(value);
2541
+ if (execPath) {
2542
+ execPaths.add(execPath);
2543
+ }
2544
+ } else if (
2545
+ ["Type", "User", "Group", "WorkingDirectory", "Restart"].includes(key)
2546
+ ) {
2547
+ properties.push({ name: `cdx:service:${key}`, value });
2548
+ }
2549
+ }
2550
+ if (
2551
+ currentSection === "Install" &&
2552
+ ["WantedBy", "RequiredBy", "Alias"].includes(key)
2553
+ ) {
2554
+ properties.push({ name: `cdx:service:${key}`, value });
2555
+ }
2556
+ if (
2557
+ currentSection === "Unit" &&
2558
+ ["After", "Requires", "Wants"].includes(key)
2559
+ ) {
2560
+ properties.push({ name: `cdx:service:${key}`, value });
2561
+ }
2562
+ }
2563
+ return {
2564
+ description,
2565
+ execPaths: Array.from(execPaths).sort(),
2566
+ properties: uniqueProperties(properties),
2567
+ };
2568
+ }
2569
+
2570
+ function parseInitScriptMetadata(initData) {
2571
+ const properties = [];
2572
+ const execPaths = new Set();
2573
+ let description;
2574
+ let name;
2575
+ for (const rawLine of initData.split(/\r?\n/)) {
2576
+ const line = rawLine.trim();
2577
+ if (!line.startsWith("#")) {
2578
+ const execPath = extractExecPath(line);
2579
+ if (execPath) {
2580
+ execPaths.add(execPath);
2581
+ }
2582
+ continue;
2583
+ }
2584
+ const normalized = line.replace(/^#+\s*/, "");
2585
+ if (normalized.startsWith("Provides:")) {
2586
+ const providedName = normalized
2587
+ .replace(/^Provides:\s*/, "")
2588
+ .split(/\s+/)[0];
2589
+ if (providedName) {
2590
+ name = providedName;
2591
+ properties.push({ name: "cdx:service:Provides", value: providedName });
2592
+ }
2593
+ continue;
2594
+ }
2595
+ if (normalized.startsWith("Short-Description:")) {
2596
+ description = normalized.replace(/^Short-Description:\s*/, "");
2597
+ continue;
2598
+ }
2599
+ if (normalized.startsWith("Description:")) {
2600
+ description = normalized.replace(/^Description:\s*/, "");
2601
+ continue;
2602
+ }
2603
+ if (normalized.startsWith("Required-Start:")) {
2604
+ properties.push({
2605
+ name: "cdx:service:RequiredStart",
2606
+ value: normalized.replace(/^Required-Start:\s*/, ""),
2607
+ });
2608
+ continue;
2609
+ }
2610
+ if (normalized.startsWith("Default-Start:")) {
2611
+ properties.push({
2612
+ name: "cdx:service:DefaultStart",
2613
+ value: normalized.replace(/^Default-Start:\s*/, ""),
2614
+ });
2615
+ }
2616
+ }
2617
+ return {
2618
+ description,
2619
+ execPaths: Array.from(execPaths).sort(),
2620
+ name,
2621
+ properties: uniqueProperties(properties),
2622
+ };
2623
+ }
2624
+
2625
+ function extractExecPath(commandLine) {
2626
+ if (!commandLine) {
2627
+ return undefined;
2628
+ }
2629
+ const sanitized = commandLine.replace(/^[\-@:+!|]+/, "").trim();
2630
+ const match = sanitized.match(/^("[^"]+"|'[^']+'|\S+)/);
2631
+ if (!match?.[1]) {
2632
+ return undefined;
2633
+ }
2634
+ const candidate = match[1].replace(/^['"]|['"]$/g, "");
2635
+ return candidate.startsWith("/")
2636
+ ? normalizeContainerPath(candidate)
2637
+ : undefined;
2638
+ }
2639
+
2640
+ function determineOwnedFileType(filePath, stats, commandPathSet) {
2641
+ if (!stats) {
2642
+ return "file";
2643
+ }
2644
+ if (isSharedLibraryPath(filePath)) {
2645
+ return "shared_library";
2646
+ }
2647
+ if (commandPathSet?.has(filePath)) {
2648
+ return "executable";
2649
+ }
2650
+ if (stats.mode & 0o111) {
2651
+ return "executable";
2652
+ }
2653
+ return "file";
2654
+ }
2655
+
2656
+ function isSharedLibraryPath(filePath) {
2657
+ return /(?:^|\/)[^/]+\.(?:so(?:\.[^/]+)?|a|lib|dll)$/i.test(filePath);
2658
+ }
2659
+
2660
+ function isSystemdServiceFile(filePath) {
2661
+ return /\/(?:etc|lib|usr\/lib)\/systemd\/system\/.+\.(?:service|socket|timer|mount|path|target|slice|automount)$/i.test(
2662
+ filePath,
2663
+ );
2664
+ }
2665
+
2666
+ function isInitServiceFile(filePath) {
2667
+ return /\/(?:etc\/init\.d|etc\/rc\.d\/init\.d)\/.+/i.test(filePath);
2668
+ }
2669
+
2670
+ function sanitizeServiceName(value) {
2671
+ return String(value || "service").trim() || "service";
2672
+ }
2673
+
2674
+ function sanitizeServiceRefToken(value) {
2675
+ return (
2676
+ String(value || "service")
2677
+ .toLowerCase()
2678
+ .replace(/[^a-z0-9._:-]+/g, "-")
2679
+ .replace(/^-+|-+$/g, "") || "service"
2680
+ );
2681
+ }
2682
+
2683
+ function uniqueProperties(properties) {
2684
+ const seen = new Set();
2685
+ const uniqueValues = [];
2686
+ for (const property of properties || []) {
2687
+ if (!property?.name || !property?.value) {
2688
+ continue;
2689
+ }
2690
+ const key = `${property.name}\u0000${property.value}`;
2691
+ if (seen.has(key)) {
2692
+ continue;
2693
+ }
2694
+ seen.add(key);
2695
+ uniqueValues.push(property);
2696
+ }
2697
+ return uniqueValues;
2698
+ }
2699
+
2700
+ function uniqueSortedStrings(values) {
2701
+ return Array.from(new Set((values || []).filter(Boolean))).sort();
2702
+ }
2703
+
2704
+ function dedupeServices(services) {
2705
+ const serviceMap = new Map();
2706
+ for (const service of services || []) {
2707
+ if (!service?.["bom-ref"]) {
2708
+ continue;
2709
+ }
2710
+ if (!serviceMap.has(service["bom-ref"])) {
2711
+ serviceMap.set(service["bom-ref"], {
2712
+ ...service,
2713
+ properties: uniqueProperties(service.properties),
2714
+ });
2715
+ continue;
2716
+ }
2717
+ const existing = serviceMap.get(service["bom-ref"]);
2718
+ existing.properties = uniqueProperties(
2719
+ (existing.properties || []).concat(service.properties || []),
2720
+ );
2721
+ }
2722
+ return Array.from(serviceMap.values());
2723
+ }
2724
+
2725
+ function normalizeContainerPath(filePath) {
2726
+ if (!filePath) {
2727
+ return undefined;
2728
+ }
2729
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\/+/, "/");
2730
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
2731
+ }
2732
+
2733
+ // Detect common sdks and runtimes from the name
2734
+ function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
2735
+ if (!comp?.name) {
2736
+ return;
2737
+ }
2738
+ if (/dotnet[6-9]?-sdk/.test(comp.name)) {
2739
+ bundledSdks.add(comp.name);
2740
+ }
2741
+ if (
2742
+ /dotnet[6-9]?-runtime/.test(comp.name) ||
2743
+ comp.name.includes("aspnet-runtime") ||
2744
+ /aspnetcore[6-9]?-runtime/.test(comp.name)
2745
+ ) {
2746
+ bundledRuntimes.add(comp.name);
2747
+ }
2748
+ // TODO: Need to test this for a range of base images
2749
+ if (COMMON_RUNTIMES.includes(comp.name)) {
2750
+ bundledRuntimes.add(comp.name);
2751
+ }
2752
+ }
2753
+
2754
+ const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
2755
+ try {
2756
+ const tmpDependsOn =
2757
+ tmpDependencies[origBomRef] || tmpDependencies[comp["bom-ref"]] || [];
2758
+ const dependsOn = new Set();
2759
+ tmpDependsOn.forEach((d) => {
2760
+ try {
2761
+ const compPurl = PackageURL.fromString(comp.purl);
2762
+ const tmpPurl = PackageURL.fromString(d.replace("none", compPurl.type));
2763
+ tmpPurl.type = compPurl.type;
2764
+ // FIXME: Check if this hack is still needed with the latest trivy
2765
+ if (OS_PURL_TYPES.includes(compPurl.type)) {
2766
+ tmpPurl.namespace = compPurl.namespace;
2767
+ tmpPurl.qualifiers = tmpPurl.qualifiers || {};
2768
+ if (compPurl.qualifiers) {
2769
+ if (compPurl.qualifiers.distro_name) {
2770
+ tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
2771
+ }
2772
+ if (compPurl.qualifiers.distro) {
2773
+ tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
2774
+ }
2775
+ }
2776
+ }
2777
+ if (tmpPurl.qualifiers) {
2778
+ if (
2779
+ tmpPurl.qualifiers.epoch &&
2780
+ !tmpPurl.version.startsWith(`${tmpPurl.qualifiers.epoch}:`)
2781
+ ) {
2782
+ tmpPurl.version = `${tmpPurl.qualifiers.epoch}:${tmpPurl.version}`;
2783
+ }
2784
+ }
2785
+ // Prevents purls ending with ?
2786
+ if (!Object.keys(tmpPurl.qualifiers).length) {
2787
+ tmpPurl.qualifiers = undefined;
2788
+ }
2789
+ dependsOn.add(decodeURIComponent(tmpPurl.toString()));
2790
+ } catch (_e) {
2791
+ // ignore
2792
+ }
2793
+ });
2794
+ return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
2795
+ } catch (_e) {
2796
+ // ignore
2797
+ }
2798
+ return undefined;
2799
+ };
2800
+
2801
+ function isHostInspectionPath(value) {
2802
+ if (!value) {
2803
+ return false;
2804
+ }
2805
+ if (platform === "darwin") {
2806
+ return value.startsWith("/");
2807
+ }
2808
+ if (platform === "windows") {
2809
+ return /^[a-z]:\\/i.test(value) || /^\\\\/.test(value);
2810
+ }
2811
+ return false;
2812
+ }
2813
+
2814
+ function isDarwinSystemHostPath(value) {
2815
+ if (typeof value !== "string") {
2816
+ return false;
2817
+ }
2818
+ return (
2819
+ value.startsWith("/bin/") ||
2820
+ value.startsWith("/sbin/") ||
2821
+ value.startsWith("/System/") ||
2822
+ value.startsWith("/usr/bin/") ||
2823
+ value.startsWith("/usr/libexec/") ||
2824
+ value.startsWith("/usr/sbin/") ||
2825
+ value.startsWith("/Library/Apple/System/") ||
2826
+ value.startsWith("/System/Volumes/Preboot/Cryptexes/")
2827
+ );
2828
+ }
2829
+
2830
+ function shouldInspectComponentHostPath(value) {
2831
+ if (!isHostInspectionPath(value)) {
2832
+ return false;
2833
+ }
2834
+ if (platform === "darwin") {
2835
+ return !(
2836
+ value.toLowerCase().endsWith(".plist") || isDarwinSystemHostPath(value)
2837
+ );
2838
+ }
2839
+ return true;
2840
+ }
2841
+
2842
+ function shouldInspectComponentTrust(component) {
2843
+ if (platform !== "darwin") {
2844
+ return true;
2845
+ }
2846
+ const queryCategory = `${
2847
+ (component?.properties || []).find(
2848
+ (property) => property?.name === "cdx:osquery:category",
2849
+ )?.value || ""
2850
+ }`.trim();
2851
+ if (!queryCategory) {
2852
+ return true;
2853
+ }
2854
+ return new Set(["launchd_services", "startup_items", "running_apps"]).has(
2855
+ queryCategory,
2856
+ );
2857
+ }
2858
+
2859
+ function extractComponentHostPaths(component) {
2860
+ if (!shouldInspectComponentTrust(component)) {
2861
+ return [];
2862
+ }
2863
+ const pathPropertyNames = new Set([
2864
+ "path",
2865
+ "bundle_path",
2866
+ "bundle_executable",
2867
+ "executable",
2868
+ "program",
2869
+ "image_path",
2870
+ "binary_path",
2871
+ "action_path",
2872
+ ]);
2873
+ const paths = [];
2874
+ for (const property of component?.properties || []) {
2875
+ const propertyName = `${property?.name || ""}`.toLowerCase();
2876
+ if (!pathPropertyNames.has(propertyName)) {
2877
+ continue;
2878
+ }
2879
+ const normalizedValue = `${property?.value || ""}`.trim();
2880
+ if (shouldInspectComponentHostPath(normalizedValue)) {
2881
+ paths.push(normalizedValue);
2882
+ }
2883
+ }
2884
+ return uniqueSortedStrings(paths);
2885
+ }
2886
+
2887
+ function createHostTrustFindingComponent(finding) {
2888
+ if (!finding?.kind || !finding?.name) {
2889
+ return undefined;
2890
+ }
2891
+ const purl = new PackageURL(
2892
+ "generic",
2893
+ "host-trust",
2894
+ finding.name,
2895
+ finding.version || "observed",
2896
+ {
2897
+ kind: finding.kind,
2898
+ ...(finding.path ? { path: finding.path } : {}),
2899
+ },
2900
+ undefined,
2901
+ ).toString();
2902
+ const component = {
2903
+ "bom-ref": decodeURIComponent(purl),
2904
+ name: finding.name,
2905
+ version: finding.version || "observed",
2906
+ description: finding.description,
2907
+ purl,
2908
+ type: "data",
2909
+ properties: uniqueProperties(
2910
+ (finding.properties || []).concat([
2911
+ { name: "cdx:trustinspector:kind", value: finding.kind },
2912
+ ...(finding.path ? [{ name: "SrcFile", value: finding.path }] : []),
2913
+ ]),
2914
+ ),
2915
+ };
2916
+ if (finding.sha256) {
2917
+ component.hashes = [{ alg: "SHA-256", content: finding.sha256 }];
2918
+ }
2919
+ attachIdentityTools(component, trustInspectorToolRefs());
2920
+ return component;
2921
+ }
2922
+
2923
+ export function enrichOSComponentsWithTrustData(components = []) {
2924
+ if (!["darwin", "windows"].includes(platform) || !TRUSTINSPECTOR_BIN) {
2925
+ return { components, tools: [] };
2926
+ }
2927
+ const mergedComponents = [...components];
2928
+ const pathInspectionCandidates = uniqueSortedStrings(
2929
+ components.flatMap((component) => extractComponentHostPaths(component)),
2930
+ );
2931
+ if (pathInspectionCandidates.length) {
2932
+ const inspectionMap = new Map();
2933
+ const batchSize = 200;
2934
+ for (
2935
+ let offset = 0;
2936
+ offset < pathInspectionCandidates.length;
2937
+ offset += batchSize
2938
+ ) {
2939
+ const inspectionBatch = pathInspectionCandidates.slice(
2940
+ offset,
2941
+ offset + batchSize,
2942
+ );
2943
+ const inspectionData = executeTrustInspector(
2944
+ ["paths", ...inspectionBatch],
2945
+ {
2946
+ target: `${inspectionBatch.length} path(s)`,
2947
+ },
2948
+ );
2949
+ for (const inspection of inspectionData?.inspections || []) {
2950
+ inspectionMap.set(
2951
+ inspection.path,
2952
+ uniqueProperties(inspection.properties || []),
2953
+ );
2954
+ }
2955
+ }
2956
+ if (inspectionMap.size) {
2957
+ for (const component of mergedComponents) {
2958
+ const matchingProperties = extractComponentHostPaths(component)
2959
+ .map((candidatePath) => inspectionMap.get(candidatePath))
2960
+ .filter(Boolean)
2961
+ .flat();
2962
+ if (!matchingProperties.length) {
2963
+ continue;
2964
+ }
2965
+ component.properties = uniqueProperties(
2966
+ (component.properties || []).concat(matchingProperties),
2967
+ );
2968
+ attachIdentityTools(component, trustInspectorToolRefs());
2969
+ }
2970
+ }
2971
+ }
2972
+ const hostData = executeTrustInspector(["host"], { target: platform });
2973
+ const hostComponents = (hostData?.hostFindings || [])
2974
+ .map((finding) => createHostTrustFindingComponent(finding))
2975
+ .filter(Boolean);
2976
+ if (hostComponents.length) {
2977
+ mergedComponents.push(...hostComponents);
2978
+ }
2979
+ return {
2980
+ components: mergedComponents,
2981
+ tools:
2982
+ pathInspectionCandidates.length || hostComponents.length
2983
+ ? getPluginToolComponents(["trustinspector"])
2984
+ : [],
2985
+ };
2986
+ }
2987
+
2988
+ export function executeOsQuery(query) {
2989
+ if (isDryRun) {
2990
+ recordActivity({
2991
+ kind: "osquery",
2992
+ reason:
2993
+ "Dry run mode blocks osquery execution and reports the query instead.",
2994
+ status: "blocked",
2995
+ target: query,
2996
+ });
2997
+ return undefined;
2998
+ }
2999
+ if (OSQUERY_BIN) {
3000
+ if (!query.endsWith(";")) {
3001
+ query = `${query};`;
3002
+ }
3003
+ const args = ["--S", "--disable_database", "--json", query];
3004
+ // On darwin, we need to disable the safety check and run cdxgen with sudo
3005
+ // https://github.com/osquery/osquery/issues/1382
3006
+ if (platform === "darwin") {
3007
+ args.push("--allow_unsafe");
3008
+ args.push("--disable_logging");
3009
+ args.push("--disable_events");
3010
+ }
3011
+ if (DEBUG_MODE) {
3012
+ console.log("Executing", OSQUERY_BIN, args.join(" "));
3013
+ }
3014
+ const result = safeSpawnSync(OSQUERY_BIN, args);
3015
+ if (result.status !== 0 || result.error) {
3016
+ if (
3017
+ DEBUG_MODE &&
3018
+ result.stderr &&
3019
+ !result.stderr.includes("no such table")
1090
3020
  ) {
1091
3021
  console.error(result.stdout, result.stderr);
1092
3022
  }
@@ -1231,6 +3161,16 @@ async function fileComponents(basePath, fileList, fileType) {
1231
3161
  const linkStats = lstatSync(join(basePath, f.replace(/^\/+/, "")));
1232
3162
  if (linkStats?.isSymbolicLink()) {
1233
3163
  linkedName = basename(resolvedPath);
3164
+ recordSymlinkResolution(
3165
+ join(basePath, f.replace(/^\/+/, "")),
3166
+ resolvedPath,
3167
+ {
3168
+ basePath,
3169
+ metadata: {
3170
+ resolutionKind: "container-binary",
3171
+ },
3172
+ },
3173
+ );
1234
3174
  }
1235
3175
  } catch (_e) {
1236
3176
  // ignore
@@ -1241,7 +3181,7 @@ async function fileComponents(basePath, fileList, fileType) {
1241
3181
  let isSetgid;
1242
3182
  let isSticky;
1243
3183
  try {
1244
- const stats = statSync(f);
3184
+ const stats = statSync(join(basePath, f.replace(/^\/+/, "")));
1245
3185
  const mode = stats.mode;
1246
3186
  isExecutable = !!(mode & 0o111);
1247
3187
  isSetuid = !!(mode & 0o4000);