@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,186 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getGtfoBinsMetadata } from "./gtfobins.js";
|
|
5
|
+
import { dirNameStr, safeExistsSync } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
const CONTAINER_RISK_INDEX_FILE = join(
|
|
8
|
+
dirNameStr,
|
|
9
|
+
"data",
|
|
10
|
+
"container-knowledge-index.json",
|
|
11
|
+
);
|
|
12
|
+
const DEFAULT_CONTAINER_RISK_INDEX = { entries: {}, sources: {} };
|
|
13
|
+
const CONTAINER_RISK_INDEX = loadContainerRiskIndex();
|
|
14
|
+
|
|
15
|
+
function loadContainerRiskIndex() {
|
|
16
|
+
if (!safeExistsSync(CONTAINER_RISK_INDEX_FILE)) {
|
|
17
|
+
return DEFAULT_CONTAINER_RISK_INDEX;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(CONTAINER_RISK_INDEX_FILE, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return DEFAULT_CONTAINER_RISK_INDEX;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeCandidate(candidate) {
|
|
27
|
+
if (!candidate || typeof candidate !== "string") {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = basename(candidate.trim()).toLowerCase();
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uniqueSortedStrings(values) {
|
|
38
|
+
return Array.from(
|
|
39
|
+
new Set(
|
|
40
|
+
values.filter(
|
|
41
|
+
(value) => typeof value === "string" && value.trim().length,
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
).sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveContainerEntry(name, linkedName, gtfoMetadata) {
|
|
48
|
+
const directCandidate = normalizeCandidate(name);
|
|
49
|
+
if (directCandidate && CONTAINER_RISK_INDEX.entries?.[directCandidate]) {
|
|
50
|
+
return {
|
|
51
|
+
canonicalName: directCandidate,
|
|
52
|
+
entry: CONTAINER_RISK_INDEX.entries[directCandidate],
|
|
53
|
+
matchSource: "basename",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const linkedCandidate = normalizeCandidate(linkedName);
|
|
57
|
+
if (linkedCandidate && CONTAINER_RISK_INDEX.entries?.[linkedCandidate]) {
|
|
58
|
+
return {
|
|
59
|
+
canonicalName: linkedCandidate,
|
|
60
|
+
entry: CONTAINER_RISK_INDEX.entries[linkedCandidate],
|
|
61
|
+
matchSource: "symlink",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const gtfoCandidate = normalizeCandidate(gtfoMetadata?.canonicalName);
|
|
65
|
+
if (gtfoCandidate && CONTAINER_RISK_INDEX.entries?.[gtfoCandidate]) {
|
|
66
|
+
return {
|
|
67
|
+
canonicalName: gtfoCandidate,
|
|
68
|
+
entry: CONTAINER_RISK_INDEX.entries[gtfoCandidate],
|
|
69
|
+
matchSource: "gtfobins",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveKnowledgeSourceRefs(sourceKeys) {
|
|
76
|
+
const refs = [];
|
|
77
|
+
for (const sourceKey of sourceKeys || []) {
|
|
78
|
+
const ref = CONTAINER_RISK_INDEX.sources?.[sourceKey];
|
|
79
|
+
if (ref) {
|
|
80
|
+
refs.push(ref);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return uniqueSortedStrings(refs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getContainerRiskMetadata(name, linkedName) {
|
|
87
|
+
const gtfoMetadata = getGtfoBinsMetadata(name, linkedName);
|
|
88
|
+
const resolvedEntry = resolveContainerEntry(name, linkedName, gtfoMetadata);
|
|
89
|
+
if (!resolvedEntry) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const attackTactics = uniqueSortedStrings(
|
|
93
|
+
resolvedEntry.entry.attackTactics || [],
|
|
94
|
+
);
|
|
95
|
+
const attackTechniques = uniqueSortedStrings([
|
|
96
|
+
...(resolvedEntry.entry.attackTechniques || []),
|
|
97
|
+
...(gtfoMetadata?.mitreTechniques || []),
|
|
98
|
+
]);
|
|
99
|
+
const knowledgeSources = uniqueSortedStrings(
|
|
100
|
+
resolvedEntry.entry.sourceKeys || [],
|
|
101
|
+
);
|
|
102
|
+
const knowledgeSourceRefs = resolveKnowledgeSourceRefs(knowledgeSources);
|
|
103
|
+
const offenseTools = uniqueSortedStrings(
|
|
104
|
+
resolvedEntry.entry.offenseTools || [],
|
|
105
|
+
);
|
|
106
|
+
const riskTags = uniqueSortedStrings([
|
|
107
|
+
...(resolvedEntry.entry.riskTags || []),
|
|
108
|
+
...(gtfoMetadata?.riskTags || []),
|
|
109
|
+
]);
|
|
110
|
+
const seccompBlockedSyscalls = uniqueSortedStrings(
|
|
111
|
+
resolvedEntry.entry.seccompBlockedSyscalls || [],
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
attackTactics,
|
|
115
|
+
attackTechniques,
|
|
116
|
+
canonicalName: resolvedEntry.canonicalName,
|
|
117
|
+
knowledgeSourceRefs,
|
|
118
|
+
knowledgeSources,
|
|
119
|
+
matchSource: resolvedEntry.matchSource,
|
|
120
|
+
offenseTools,
|
|
121
|
+
riskTags,
|
|
122
|
+
seccompBlockedSyscalls,
|
|
123
|
+
seccompProfile: resolvedEntry.entry.seccompProfile || "",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createContainerRiskProperties(name, linkedName) {
|
|
128
|
+
const metadata = getContainerRiskMetadata(name, linkedName);
|
|
129
|
+
if (!metadata) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const properties = [
|
|
133
|
+
{ name: "cdx:container:matched", value: "true" },
|
|
134
|
+
{ name: "cdx:container:name", value: metadata.canonicalName },
|
|
135
|
+
{ name: "cdx:container:matchSource", value: metadata.matchSource },
|
|
136
|
+
];
|
|
137
|
+
if (metadata.attackTactics.length) {
|
|
138
|
+
properties.push({
|
|
139
|
+
name: "cdx:container:attackTactics",
|
|
140
|
+
value: metadata.attackTactics.join(","),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (metadata.attackTechniques.length) {
|
|
144
|
+
properties.push({
|
|
145
|
+
name: "cdx:container:attackTechniques",
|
|
146
|
+
value: metadata.attackTechniques.join(","),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (metadata.knowledgeSources.length) {
|
|
150
|
+
properties.push({
|
|
151
|
+
name: "cdx:container:knowledgeSources",
|
|
152
|
+
value: metadata.knowledgeSources.join(","),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (metadata.knowledgeSourceRefs.length) {
|
|
156
|
+
properties.push({
|
|
157
|
+
name: "cdx:container:knowledgeSourceRefs",
|
|
158
|
+
value: metadata.knowledgeSourceRefs.join(","),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (metadata.offenseTools.length) {
|
|
162
|
+
properties.push({
|
|
163
|
+
name: "cdx:container:offenseTools",
|
|
164
|
+
value: metadata.offenseTools.join(","),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (metadata.riskTags.length) {
|
|
168
|
+
properties.push({
|
|
169
|
+
name: "cdx:container:riskTags",
|
|
170
|
+
value: metadata.riskTags.join(","),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (metadata.seccompBlockedSyscalls.length) {
|
|
174
|
+
properties.push({
|
|
175
|
+
name: "cdx:container:seccompBlockedSyscalls",
|
|
176
|
+
value: metadata.seccompBlockedSyscalls.join(","),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (metadata.seccompProfile) {
|
|
180
|
+
properties.push({
|
|
181
|
+
name: "cdx:container:seccompProfile",
|
|
182
|
+
value: metadata.seccompProfile,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return properties;
|
|
186
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
|
|
3
|
+
import { describe, it } from "poku";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createContainerRiskProperties,
|
|
7
|
+
getContainerRiskMetadata,
|
|
8
|
+
} from "./containerRisk.js";
|
|
9
|
+
|
|
10
|
+
describe("container risk helpers", () => {
|
|
11
|
+
it("returns offensive toolkit metadata for direct container tool matches", () => {
|
|
12
|
+
const metadata = getContainerRiskMetadata("cdk");
|
|
13
|
+
assert.ok(metadata);
|
|
14
|
+
assert.strictEqual(metadata.canonicalName, "cdk");
|
|
15
|
+
assert.ok(metadata.offenseTools.includes("cdk"));
|
|
16
|
+
assert.ok(metadata.riskTags.includes("offensive-toolkit"));
|
|
17
|
+
assert.ok(metadata.attackTechniques.includes("T1611"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("maps kubernetes control-plane helpers to ATT&CK and offensive playbooks", () => {
|
|
21
|
+
const metadata = getContainerRiskMetadata("kubectl");
|
|
22
|
+
assert.ok(metadata);
|
|
23
|
+
assert.ok(metadata.offenseTools.includes("peirates"));
|
|
24
|
+
assert.ok(metadata.offenseTools.includes("cdk"));
|
|
25
|
+
assert.ok(metadata.attackTechniques.includes("T1613"));
|
|
26
|
+
assert.ok(metadata.riskTags.includes("k8s-cluster-pivot"));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("tracks seccomp-sensitive namespace escape helpers", () => {
|
|
30
|
+
const metadata = getContainerRiskMetadata("nsenter");
|
|
31
|
+
assert.ok(metadata);
|
|
32
|
+
assert.strictEqual(metadata.seccompProfile, "docker-default");
|
|
33
|
+
assert.ok(metadata.seccompBlockedSyscalls.includes("setns"));
|
|
34
|
+
assert.ok(metadata.seccompBlockedSyscalls.includes("unshare"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("emits stable CycloneDX properties for enriched container binaries", () => {
|
|
38
|
+
const properties = createContainerRiskProperties("docker");
|
|
39
|
+
const propertyMap = Object.fromEntries(
|
|
40
|
+
properties.map((property) => [property.name, property.value]),
|
|
41
|
+
);
|
|
42
|
+
assert.strictEqual(propertyMap["cdx:container:matched"], "true");
|
|
43
|
+
assert.strictEqual(propertyMap["cdx:container:name"], "docker");
|
|
44
|
+
assert.ok(propertyMap["cdx:container:attackTechniques"].includes("T1611"));
|
|
45
|
+
assert.ok(propertyMap["cdx:container:offenseTools"].includes("deepce"));
|
|
46
|
+
assert.ok(
|
|
47
|
+
propertyMap["cdx:container:knowledgeSources"].includes(
|
|
48
|
+
"attack-containers",
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/lib/helpers/display.js
CHANGED
|
@@ -2,6 +2,10 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
hasComponentRegistryProvenance,
|
|
7
|
+
REGISTRY_PROVENANCE_ICON,
|
|
8
|
+
} from "./provenanceUtils.js";
|
|
5
9
|
import { createStream, table } from "./table.js";
|
|
6
10
|
import { isSecureMode, safeExistsSync, toCamel } from "./utils.js";
|
|
7
11
|
|
|
@@ -15,12 +19,48 @@ const SYMBOLS_ANSI = {
|
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
const MAX_TREE_DEPTH = 6;
|
|
22
|
+
const CYCLE_NODE_ICON = "↺";
|
|
23
|
+
const REPEATED_NODE_ICON = "⤴";
|
|
18
24
|
const highlightStr = (s, highlight) => {
|
|
19
25
|
if (highlight && s?.includes(highlight)) {
|
|
20
26
|
s = s.replaceAll(highlight, `\x1b[1;33m${highlight}\x1b[0m`);
|
|
21
27
|
}
|
|
22
28
|
return s;
|
|
23
29
|
};
|
|
30
|
+
|
|
31
|
+
const formatComponentName = (component, highlight) => {
|
|
32
|
+
const displayName = highlightStr(component?.name || "", highlight);
|
|
33
|
+
if (hasComponentRegistryProvenance(component)) {
|
|
34
|
+
return `${REGISTRY_PROVENANCE_ICON} ${displayName}`;
|
|
35
|
+
}
|
|
36
|
+
return displayName;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const printProvenanceLegend = () => {
|
|
40
|
+
console.log(
|
|
41
|
+
`Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds legend lines for dependency tree marker icons.
|
|
47
|
+
*
|
|
48
|
+
* @param {string[]} treeGraphics Dependency tree lines
|
|
49
|
+
* @returns {string[]} Legend lines to print after the tree output
|
|
50
|
+
*/
|
|
51
|
+
export const buildDependencyTreeLegendLines = (treeGraphics) => {
|
|
52
|
+
const legendLines = [];
|
|
53
|
+
if (treeGraphics.some((line) => line.includes(`${REPEATED_NODE_ICON} `))) {
|
|
54
|
+
legendLines.push(`${REPEATED_NODE_ICON} = already shown`);
|
|
55
|
+
}
|
|
56
|
+
if (treeGraphics.some((line) => line.includes(`${CYCLE_NODE_ICON} `))) {
|
|
57
|
+
legendLines.push(`${CYCLE_NODE_ICON} = cycle`);
|
|
58
|
+
}
|
|
59
|
+
if (!legendLines.length) {
|
|
60
|
+
return legendLines;
|
|
61
|
+
}
|
|
62
|
+
return [`Legend: ${legendLines.join("; ")}`];
|
|
63
|
+
};
|
|
24
64
|
/**
|
|
25
65
|
* Prints the BOM components as a streaming table to the console.
|
|
26
66
|
* Delegates to {@link printOSTable} automatically when the BOM metadata indicates
|
|
@@ -29,12 +69,14 @@ const highlightStr = (s, highlight) => {
|
|
|
29
69
|
* @param {Object} bomJson CycloneDX BOM JSON object
|
|
30
70
|
* @param {string[]} [filterTypes] Optional list of component types to include; all types shown when omitted
|
|
31
71
|
* @param {string} [highlight] Optional string to highlight in the output
|
|
72
|
+
* @param {string} [summaryText] Optional summary message to print after the table
|
|
32
73
|
* @returns {void}
|
|
33
74
|
*/
|
|
34
75
|
export function printTable(
|
|
35
76
|
bomJson,
|
|
36
77
|
filterTypes = undefined,
|
|
37
78
|
highlight = undefined,
|
|
79
|
+
summaryText = undefined,
|
|
38
80
|
) {
|
|
39
81
|
if (!bomJson?.components) {
|
|
40
82
|
return;
|
|
@@ -59,6 +101,7 @@ export function printTable(
|
|
|
59
101
|
],
|
|
60
102
|
};
|
|
61
103
|
const stream = createStream(config);
|
|
104
|
+
let displayedProvenanceCount = 0;
|
|
62
105
|
stream.write([
|
|
63
106
|
filterTypes?.includes("cryptographic-asset")
|
|
64
107
|
? "Asset Type / Group"
|
|
@@ -81,9 +124,12 @@ export function printTable(
|
|
|
81
124
|
(comp.tags || []).join(", "),
|
|
82
125
|
]);
|
|
83
126
|
} else {
|
|
127
|
+
if (hasComponentRegistryProvenance(comp)) {
|
|
128
|
+
displayedProvenanceCount += 1;
|
|
129
|
+
}
|
|
84
130
|
stream.write([
|
|
85
131
|
highlightStr(comp.group || "", highlight),
|
|
86
|
-
|
|
132
|
+
formatComponentName(comp, highlight),
|
|
87
133
|
`\x1b[1;35m${comp.version || ""}\x1b[0m`,
|
|
88
134
|
comp.scope || "",
|
|
89
135
|
(comp.tags || []).join(", "),
|
|
@@ -92,7 +138,9 @@ export function printTable(
|
|
|
92
138
|
}
|
|
93
139
|
stream.end();
|
|
94
140
|
console.log();
|
|
95
|
-
if (
|
|
141
|
+
if (summaryText) {
|
|
142
|
+
console.log(summaryText);
|
|
143
|
+
} else if (!filterTypes) {
|
|
96
144
|
console.log(
|
|
97
145
|
"BOM includes",
|
|
98
146
|
bomJson?.components?.length || 0,
|
|
@@ -103,6 +151,12 @@ export function printTable(
|
|
|
103
151
|
} else {
|
|
104
152
|
console.log(`Components filtered based on type: ${filterTypes.join(", ")}`);
|
|
105
153
|
}
|
|
154
|
+
if (displayedProvenanceCount > 0) {
|
|
155
|
+
printProvenanceLegend();
|
|
156
|
+
console.log(
|
|
157
|
+
`${REGISTRY_PROVENANCE_ICON} ${displayedProvenanceCount} component(s) include registry provenance or trusted publishing metadata.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
106
160
|
}
|
|
107
161
|
const formatProps = (props) => {
|
|
108
162
|
const retList = [];
|
|
@@ -326,19 +380,8 @@ export function printDependencyTree(
|
|
|
326
380
|
if (!dependencies.length) {
|
|
327
381
|
return;
|
|
328
382
|
}
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
for (const d of dependencies) {
|
|
332
|
-
if (d[mode]?.length) {
|
|
333
|
-
depMap[d.ref] = d[mode].sort();
|
|
334
|
-
} else {
|
|
335
|
-
if (mode === "provides") {
|
|
336
|
-
shownList.push(d.ref);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
const treeGraphics = [];
|
|
341
|
-
recursePrint(depMap, dependencies, 0, shownList, treeGraphics);
|
|
383
|
+
const treeGraphics = buildDependencyTreeLines(dependencies, mode);
|
|
384
|
+
const legendLines = buildDependencyTreeLegendLines(treeGraphics);
|
|
342
385
|
// table library is too slow for display large lists.
|
|
343
386
|
// Fixes #491
|
|
344
387
|
if (treeGraphics.length && treeGraphics.length < 100) {
|
|
@@ -359,64 +402,203 @@ export function printDependencyTree(
|
|
|
359
402
|
} else {
|
|
360
403
|
console.log(highlightStr(treeGraphics.slice(0, 500).join("\n"), highlight));
|
|
361
404
|
}
|
|
405
|
+
if (legendLines.length) {
|
|
406
|
+
console.log(legendLines.join("\n"));
|
|
407
|
+
}
|
|
362
408
|
}
|
|
363
409
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
410
|
+
const dependencyTreePrefix = (ancestorContinuations, isLast) => {
|
|
411
|
+
let prefix = "";
|
|
412
|
+
for (const hasNextSibling of ancestorContinuations) {
|
|
413
|
+
prefix = `${prefix}${hasNextSibling ? "│ " : " "}`;
|
|
367
414
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
415
|
+
return `${prefix}${isLast ? SYMBOLS_ANSI.LAST_BRANCH : SYMBOLS_ANSI.BRANCH}`;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const dependencyTreeRefKey = (ref) => ref.toLowerCase();
|
|
419
|
+
|
|
420
|
+
const compareDependencyTreeNodes = (a, b) => {
|
|
421
|
+
if (a.order !== b.order) {
|
|
422
|
+
return a.order - b.order;
|
|
375
423
|
}
|
|
376
|
-
return
|
|
424
|
+
return a.ref.localeCompare(b.ref);
|
|
377
425
|
};
|
|
378
426
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
427
|
+
const createDependencyTreeGraph = (dependencies, mode) => {
|
|
428
|
+
const nodes = new Map();
|
|
429
|
+
let nextOrder = 0;
|
|
430
|
+
|
|
431
|
+
const ensureNode = (ref) => {
|
|
432
|
+
if (!ref) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
const refKey = dependencyTreeRefKey(ref);
|
|
436
|
+
if (!nodes.has(refKey)) {
|
|
437
|
+
nodes.set(refKey, {
|
|
438
|
+
childKeys: new Set(),
|
|
439
|
+
children: [],
|
|
440
|
+
order: nextOrder,
|
|
441
|
+
parents: new Set(),
|
|
442
|
+
ref,
|
|
443
|
+
});
|
|
444
|
+
nextOrder += 1;
|
|
445
|
+
}
|
|
446
|
+
return nodes.get(refKey);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
for (const dependency of dependencies) {
|
|
450
|
+
const rawChildren = Array.isArray(dependency?.[mode])
|
|
451
|
+
? dependency[mode].filter(Boolean)
|
|
452
|
+
: [];
|
|
453
|
+
const childRefs = Array.from(new Set(rawChildren)).sort((a, b) =>
|
|
454
|
+
a.localeCompare(b),
|
|
455
|
+
);
|
|
456
|
+
let parentNode;
|
|
457
|
+
if (mode !== "provides" || childRefs.length) {
|
|
458
|
+
parentNode = ensureNode(dependency.ref);
|
|
459
|
+
}
|
|
460
|
+
if (!childRefs.length) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
parentNode = parentNode || ensureNode(dependency.ref);
|
|
464
|
+
for (const childRef of childRefs) {
|
|
465
|
+
const childNode = ensureNode(childRef);
|
|
466
|
+
if (!parentNode || !childNode) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
parentNode.childKeys.add(dependencyTreeRefKey(childRef));
|
|
470
|
+
childNode.parents.add(dependencyTreeRefKey(parentNode.ref));
|
|
387
471
|
}
|
|
388
472
|
}
|
|
389
|
-
|
|
473
|
+
|
|
474
|
+
for (const node of nodes.values()) {
|
|
475
|
+
node.children = Array.from(node.childKeys).sort((a, b) =>
|
|
476
|
+
compareDependencyTreeNodes(nodes.get(a), nodes.get(b)),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return nodes;
|
|
390
481
|
};
|
|
391
482
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
483
|
+
const renderDependencyTreeNode = (
|
|
484
|
+
nodes,
|
|
485
|
+
nodeKey,
|
|
486
|
+
depth,
|
|
487
|
+
ancestorContinuations,
|
|
488
|
+
isLast,
|
|
489
|
+
renderedNodes,
|
|
490
|
+
treeGraphics,
|
|
491
|
+
visitingNodes = new Set(),
|
|
492
|
+
) => {
|
|
493
|
+
const node = nodes.get(nodeKey);
|
|
494
|
+
if (!node || renderedNodes.has(nodeKey)) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const prefix =
|
|
498
|
+
depth === 0
|
|
499
|
+
? SYMBOLS_ANSI.EMPTY
|
|
500
|
+
: dependencyTreePrefix(ancestorContinuations, isLast);
|
|
501
|
+
treeGraphics.push(`${prefix}${node.ref}`);
|
|
502
|
+
renderedNodes.add(nodeKey);
|
|
503
|
+
if (depth >= MAX_TREE_DEPTH) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const nextVisitingNodes = new Set(visitingNodes);
|
|
507
|
+
nextVisitingNodes.add(nodeKey);
|
|
508
|
+
const nextAncestorContinuations =
|
|
509
|
+
depth === 0 ? ancestorContinuations : [...ancestorContinuations, !isLast];
|
|
510
|
+
const childEntries = [];
|
|
511
|
+
for (const childKey of node.children) {
|
|
512
|
+
if (nextVisitingNodes.has(childKey)) {
|
|
513
|
+
childEntries.push({ childKey, isCycle: true });
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (renderedNodes.has(childKey)) {
|
|
517
|
+
childEntries.push({ childKey, isRepeated: true });
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
childEntries.push({ childKey, isCycle: false });
|
|
521
|
+
}
|
|
522
|
+
for (let i = 0; i < childEntries.length; i++) {
|
|
523
|
+
const childEntry = childEntries[i];
|
|
524
|
+
const childNode = nodes.get(childEntry.childKey);
|
|
525
|
+
const childIsLast = i === childEntries.length - 1;
|
|
526
|
+
if (!childNode) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (childEntry.isCycle) {
|
|
403
530
|
treeGraphics.push(
|
|
404
|
-
`${
|
|
531
|
+
`${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${CYCLE_NODE_ICON} ${childNode.ref}`,
|
|
405
532
|
);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
shownList,
|
|
414
|
-
treeGraphics,
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (childEntry.isRepeated) {
|
|
536
|
+
treeGraphics.push(
|
|
537
|
+
`${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${REPEATED_NODE_ICON} ${childNode.ref}`,
|
|
538
|
+
);
|
|
539
|
+
continue;
|
|
418
540
|
}
|
|
541
|
+
renderDependencyTreeNode(
|
|
542
|
+
nodes,
|
|
543
|
+
childEntry.childKey,
|
|
544
|
+
depth + 1,
|
|
545
|
+
nextAncestorContinuations,
|
|
546
|
+
childIsLast,
|
|
547
|
+
renderedNodes,
|
|
548
|
+
treeGraphics,
|
|
549
|
+
nextVisitingNodes,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Builds printable dependency tree lines from a BOM dependency graph.
|
|
556
|
+
* Produces a spanning forest so shared children are rendered once, while
|
|
557
|
+
* disconnected or cyclic subgraphs are still emitted as dangling trees.
|
|
558
|
+
*
|
|
559
|
+
* @param {Object[]} dependencies CycloneDX dependency objects
|
|
560
|
+
* @param {string} [mode="dependsOn"] Dependency relation to traverse
|
|
561
|
+
* @returns {string[]} Dependency tree lines ready for console rendering
|
|
562
|
+
*/
|
|
563
|
+
export const buildDependencyTreeLines = (dependencies, mode = "dependsOn") => {
|
|
564
|
+
const nodes = createDependencyTreeGraph(dependencies, mode);
|
|
565
|
+
if (!nodes.size) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
const nodeEntries = Array.from(nodes.entries()).sort(([, a], [, b]) =>
|
|
569
|
+
compareDependencyTreeNodes(a, b),
|
|
570
|
+
);
|
|
571
|
+
const rootKeys = nodeEntries
|
|
572
|
+
.filter(([, node]) => !node.parents.size)
|
|
573
|
+
.map(([nodeKey]) => nodeKey);
|
|
574
|
+
const renderedNodes = new Set();
|
|
575
|
+
const treeGraphics = [];
|
|
576
|
+
for (let i = 0; i < rootKeys.length; i++) {
|
|
577
|
+
renderDependencyTreeNode(
|
|
578
|
+
nodes,
|
|
579
|
+
rootKeys[i],
|
|
580
|
+
0,
|
|
581
|
+
[],
|
|
582
|
+
i === rootKeys.length - 1,
|
|
583
|
+
renderedNodes,
|
|
584
|
+
treeGraphics,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const danglingNodeKeys = nodeEntries
|
|
588
|
+
.map(([nodeKey]) => nodeKey)
|
|
589
|
+
.filter((nodeKey) => !renderedNodes.has(nodeKey));
|
|
590
|
+
for (let i = 0; i < danglingNodeKeys.length; i++) {
|
|
591
|
+
renderDependencyTreeNode(
|
|
592
|
+
nodes,
|
|
593
|
+
danglingNodeKeys[i],
|
|
594
|
+
0,
|
|
595
|
+
[],
|
|
596
|
+
i === danglingNodeKeys.length - 1,
|
|
597
|
+
renderedNodes,
|
|
598
|
+
treeGraphics,
|
|
599
|
+
);
|
|
419
600
|
}
|
|
601
|
+
return treeGraphics;
|
|
420
602
|
};
|
|
421
603
|
|
|
422
604
|
/**
|