@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,97 @@
|
|
|
1
|
+
import { PackageURL } from "packageurl-js";
|
|
2
|
+
|
|
3
|
+
import { isSpdxJsonLd } from "./bomUtils.js";
|
|
4
|
+
|
|
5
|
+
const toArray = (value) => {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (value !== undefined && value !== null) {
|
|
10
|
+
return [value];
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const toCycloneDxLikeComponent = (spdxElement) => {
|
|
16
|
+
const purl = spdxElement?.software_packageUrl;
|
|
17
|
+
let group = "";
|
|
18
|
+
let name = spdxElement?.name || spdxElement?.spdxId || "unnamed-component";
|
|
19
|
+
let version = spdxElement?.software_packageVersion || "";
|
|
20
|
+
if (purl) {
|
|
21
|
+
try {
|
|
22
|
+
const purlObj = PackageURL.fromString(purl);
|
|
23
|
+
group = purlObj.namespace || "";
|
|
24
|
+
name = purlObj.name || name;
|
|
25
|
+
version = purlObj.version || version;
|
|
26
|
+
} catch (_err) {
|
|
27
|
+
// Keep SPDX element values when purl parsing fails.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
type: spdxElement?.type === "software_File" ? "file" : "library",
|
|
32
|
+
group,
|
|
33
|
+
name,
|
|
34
|
+
version,
|
|
35
|
+
purl,
|
|
36
|
+
"bom-ref": purl || spdxElement?.spdxId || name,
|
|
37
|
+
description: spdxElement?.description,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const toCycloneDxLikeBom = (bomJson) => {
|
|
42
|
+
if (!isSpdxJsonLd(bomJson)) {
|
|
43
|
+
return bomJson;
|
|
44
|
+
}
|
|
45
|
+
const graphElements = toArray(bomJson?.["@graph"]);
|
|
46
|
+
const packageElements = graphElements.filter((element) =>
|
|
47
|
+
["software_Package", "software_File"].includes(element?.type),
|
|
48
|
+
);
|
|
49
|
+
const components = packageElements.map(toCycloneDxLikeComponent);
|
|
50
|
+
const spdxIdToRef = new Map();
|
|
51
|
+
for (let index = 0; index < packageElements.length; index++) {
|
|
52
|
+
const spdxId = packageElements[index]?.spdxId;
|
|
53
|
+
if (spdxId) {
|
|
54
|
+
spdxIdToRef.set(spdxId, components[index]["bom-ref"]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const dependencyMap = new Map();
|
|
58
|
+
for (const component of components) {
|
|
59
|
+
dependencyMap.set(component["bom-ref"], new Set());
|
|
60
|
+
}
|
|
61
|
+
for (const element of graphElements) {
|
|
62
|
+
if (
|
|
63
|
+
element?.type !== "Relationship" ||
|
|
64
|
+
element?.relationshipType !== "dependsOn"
|
|
65
|
+
) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!element?.from || typeof element.from !== "string") {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const fromRef = spdxIdToRef.get(element.from) || element.from;
|
|
72
|
+
const toRefs = toArray(element?.to).map(
|
|
73
|
+
(toRef) => spdxIdToRef.get(toRef) || toRef,
|
|
74
|
+
);
|
|
75
|
+
if (!dependencyMap.has(fromRef)) {
|
|
76
|
+
dependencyMap.set(fromRef, new Set());
|
|
77
|
+
}
|
|
78
|
+
const dependsOnSet = dependencyMap.get(fromRef);
|
|
79
|
+
for (const toRef of toRefs) {
|
|
80
|
+
if (toRef) {
|
|
81
|
+
dependsOnSet.add(toRef);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const dependencies = [];
|
|
86
|
+
for (const [ref, dependsOnSet] of dependencyMap.entries()) {
|
|
87
|
+
dependencies.push({
|
|
88
|
+
ref,
|
|
89
|
+
dependsOn: Array.from(dependsOnSet).sort(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...bomJson,
|
|
94
|
+
components,
|
|
95
|
+
dependencies,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { toCycloneDxLikeBom } from "./spdxUtils.js";
|
|
4
|
+
|
|
5
|
+
const sampleSpdx = {
|
|
6
|
+
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
|
7
|
+
"@graph": [
|
|
8
|
+
{ type: "CreationInfo", spdxId: "urn:demo#CreationInfo-main" },
|
|
9
|
+
{ type: "SpdxDocument", spdxId: "urn:demo#SPDXRef-DOCUMENT" },
|
|
10
|
+
{
|
|
11
|
+
type: "software_Package",
|
|
12
|
+
spdxId: "urn:demo#SPDXRef-app",
|
|
13
|
+
name: "app",
|
|
14
|
+
software_packageUrl: "pkg:npm/@acme/app@1.2.3",
|
|
15
|
+
software_packageVersion: "1.2.3",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: "software_Package",
|
|
19
|
+
spdxId: "urn:demo#SPDXRef-lib",
|
|
20
|
+
name: "lib",
|
|
21
|
+
software_packageUrl: "pkg:npm/lodash@4.17.21",
|
|
22
|
+
software_packageVersion: "4.17.21",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "Relationship",
|
|
26
|
+
spdxId: "urn:demo#Relationship-1",
|
|
27
|
+
relationshipType: "dependsOn",
|
|
28
|
+
from: "urn:demo#SPDXRef-app",
|
|
29
|
+
to: ["urn:demo#SPDXRef-lib"],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("spdxUtils", () => {
|
|
35
|
+
it("returns non-SPDX BOMs unchanged", () => {
|
|
36
|
+
const cyclonedxBom = { bomFormat: "CycloneDX", components: [] };
|
|
37
|
+
assert.strictEqual(toCycloneDxLikeBom(cyclonedxBom), cyclonedxBom);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("converts SPDX package and relationship graph into CycloneDX-like components/dependencies", () => {
|
|
41
|
+
const converted = toCycloneDxLikeBom(sampleSpdx);
|
|
42
|
+
assert.strictEqual(Array.isArray(converted.components), true);
|
|
43
|
+
assert.strictEqual(converted.components.length, 2);
|
|
44
|
+
assert.strictEqual(converted.components[0].name, "app");
|
|
45
|
+
assert.strictEqual(converted.components[0].version, "1.2.3");
|
|
46
|
+
assert.strictEqual(Array.isArray(converted.dependencies), true);
|
|
47
|
+
const appDependency = converted.dependencies.find(
|
|
48
|
+
(dep) => dep.ref === "pkg:npm/@acme/app@1.2.3",
|
|
49
|
+
);
|
|
50
|
+
assert.ok(appDependency);
|
|
51
|
+
assert.deepStrictEqual(appDependency.dependsOn, ["pkg:npm/lodash@4.17.21"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("ignores invalid SPDX relationships where 'from' is not a string", () => {
|
|
55
|
+
const malformedSpdx = structuredClone(sampleSpdx);
|
|
56
|
+
malformedSpdx["@graph"].push({
|
|
57
|
+
type: "Relationship",
|
|
58
|
+
spdxId: "urn:demo#Relationship-2",
|
|
59
|
+
relationshipType: "dependsOn",
|
|
60
|
+
from: ["urn:demo#SPDXRef-app"],
|
|
61
|
+
to: ["urn:demo#SPDXRef-lib"],
|
|
62
|
+
});
|
|
63
|
+
const converted = toCycloneDxLikeBom(malformedSpdx);
|
|
64
|
+
const appDependency = converted.dependencies.find(
|
|
65
|
+
(dep) => dep.ref === "pkg:npm/@acme/app@1.2.3",
|
|
66
|
+
);
|
|
67
|
+
assert.ok(appDependency);
|
|
68
|
+
assert.deepStrictEqual(appDependency.dependsOn, ["pkg:npm/lodash@4.17.21"]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const BIDI_CHARS = /[\u202A-\u202E\u2066-\u2069]/gu;
|
|
2
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Hidden Unicode scanning must detect raw control ranges.
|
|
3
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu;
|
|
4
|
+
const ZERO_WIDTH_CHARS = /[\u200B-\u200D\uFEFF]/gu;
|
|
5
|
+
|
|
6
|
+
function formatCodePoint(char) {
|
|
7
|
+
return `U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findMatchesByPattern(text, pattern, kind) {
|
|
11
|
+
const matches = [];
|
|
12
|
+
for (const match of text.matchAll(pattern)) {
|
|
13
|
+
matches.push({
|
|
14
|
+
char: match[0],
|
|
15
|
+
codePoint: formatCodePoint(match[0]),
|
|
16
|
+
index: match.index,
|
|
17
|
+
kind,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return matches;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lineNumberForIndex(text, index) {
|
|
24
|
+
return text.slice(0, index).split(/\r?\n/u).length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function commentLineNumbers(text, syntax) {
|
|
28
|
+
const commentLines = new Set();
|
|
29
|
+
if (!text) {
|
|
30
|
+
return commentLines;
|
|
31
|
+
}
|
|
32
|
+
const lines = text.split(/\r?\n/u);
|
|
33
|
+
if (syntax === "yaml") {
|
|
34
|
+
lines.forEach((line, lineIndex) => {
|
|
35
|
+
if (line.trim().startsWith("#")) {
|
|
36
|
+
commentLines.add(lineIndex + 1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return commentLines;
|
|
40
|
+
}
|
|
41
|
+
if (syntax === "markdown") {
|
|
42
|
+
let inHtmlComment = false;
|
|
43
|
+
lines.forEach((line, lineIndex) => {
|
|
44
|
+
const hasCommentStart = line.includes("<!--");
|
|
45
|
+
const hasCommentEnd = line.includes("-->");
|
|
46
|
+
if (inHtmlComment || hasCommentStart) {
|
|
47
|
+
commentLines.add(lineIndex + 1);
|
|
48
|
+
}
|
|
49
|
+
if (hasCommentStart && !hasCommentEnd) {
|
|
50
|
+
inHtmlComment = true;
|
|
51
|
+
} else if (hasCommentEnd) {
|
|
52
|
+
inHtmlComment = false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return commentLines;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find dangerous Unicode characters and return their details.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} text string to inspect
|
|
63
|
+
* @returns {{ char: string, codePoint: string, index: number, kind: string }[]} matches
|
|
64
|
+
*/
|
|
65
|
+
export function findDangerousUnicodeMatches(text) {
|
|
66
|
+
if (!text || typeof text !== "string") {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const matches = [
|
|
70
|
+
...findMatchesByPattern(text, BIDI_CHARS, "bidirectional-control"),
|
|
71
|
+
...findMatchesByPattern(text, ZERO_WIDTH_CHARS, "zero-width"),
|
|
72
|
+
...findMatchesByPattern(text, CONTROL_CHARS, "control"),
|
|
73
|
+
];
|
|
74
|
+
matches.sort((left, right) => left.index - right.index);
|
|
75
|
+
return matches;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Scan a text blob for dangerous Unicode characters and summarize where they appear.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} text text to inspect
|
|
82
|
+
* @param {{ syntax?: "markdown" | "text" | "yaml" }} [options] scan options
|
|
83
|
+
* @returns {{
|
|
84
|
+
* codePoints: string[],
|
|
85
|
+
* commentCodePoints: string[],
|
|
86
|
+
* contexts: string[],
|
|
87
|
+
* hasHiddenUnicode: boolean,
|
|
88
|
+
* inComments: boolean,
|
|
89
|
+
* lineNumbers: number[],
|
|
90
|
+
* matches: { char: string, codePoint: string, index: number, kind: string, lineNumber: number, inComment: boolean }[],
|
|
91
|
+
* }} scan result
|
|
92
|
+
*/
|
|
93
|
+
export function scanTextForHiddenUnicode(text, options = {}) {
|
|
94
|
+
const matches = findDangerousUnicodeMatches(text);
|
|
95
|
+
if (!matches.length) {
|
|
96
|
+
return {
|
|
97
|
+
codePoints: [],
|
|
98
|
+
commentCodePoints: [],
|
|
99
|
+
contexts: [],
|
|
100
|
+
hasHiddenUnicode: false,
|
|
101
|
+
inComments: false,
|
|
102
|
+
lineNumbers: [],
|
|
103
|
+
matches: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const commentLines = commentLineNumbers(text, options.syntax || "text");
|
|
107
|
+
const enrichedMatches = matches.map((match) => {
|
|
108
|
+
const lineNumber = lineNumberForIndex(text, match.index);
|
|
109
|
+
return {
|
|
110
|
+
...match,
|
|
111
|
+
inComment: commentLines.has(lineNumber),
|
|
112
|
+
lineNumber,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const commentCodePoints = [
|
|
116
|
+
...new Set(
|
|
117
|
+
enrichedMatches
|
|
118
|
+
.filter((match) => match.inComment)
|
|
119
|
+
.map((match) => match.codePoint),
|
|
120
|
+
),
|
|
121
|
+
];
|
|
122
|
+
const contentCodePoints = [
|
|
123
|
+
...new Set(
|
|
124
|
+
enrichedMatches
|
|
125
|
+
.filter((match) => !match.inComment)
|
|
126
|
+
.map((match) => match.codePoint),
|
|
127
|
+
),
|
|
128
|
+
];
|
|
129
|
+
const contexts = [];
|
|
130
|
+
if (commentCodePoints.length) {
|
|
131
|
+
contexts.push("comment");
|
|
132
|
+
}
|
|
133
|
+
if (contentCodePoints.length) {
|
|
134
|
+
contexts.push("content");
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
codePoints: [...new Set(enrichedMatches.map((match) => match.codePoint))],
|
|
138
|
+
commentCodePoints,
|
|
139
|
+
contexts,
|
|
140
|
+
hasHiddenUnicode: true,
|
|
141
|
+
inComments: commentCodePoints.length > 0,
|
|
142
|
+
lineNumbers: [
|
|
143
|
+
...new Set(enrichedMatches.map((match) => match.lineNumber)),
|
|
144
|
+
].sort((left, right) => left - right),
|
|
145
|
+
matches: enrichedMatches,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findDangerousUnicodeMatches,
|
|
5
|
+
scanTextForHiddenUnicode,
|
|
6
|
+
} from "./unicodeScan.js";
|
|
7
|
+
|
|
8
|
+
describe("findDangerousUnicodeMatches()", () => {
|
|
9
|
+
it("finds bidirectional and zero-width characters with code points", () => {
|
|
10
|
+
const matches = findDangerousUnicodeMatches("safe\u202Evalue\u200Bhidden");
|
|
11
|
+
|
|
12
|
+
assert.strictEqual(matches.length, 2);
|
|
13
|
+
assert.deepStrictEqual(
|
|
14
|
+
matches.map((match) => match.codePoint),
|
|
15
|
+
["U+202E", "U+200B"],
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("scanTextForHiddenUnicode()", () => {
|
|
21
|
+
it("tracks markdown comment context for hidden Unicode", () => {
|
|
22
|
+
const scan = scanTextForHiddenUnicode(
|
|
23
|
+
"Visible line\n<!-- sneaky \u200B marker -->\nTrailing line",
|
|
24
|
+
{ syntax: "markdown" },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(scan.hasHiddenUnicode, true);
|
|
28
|
+
assert.strictEqual(scan.inComments, true);
|
|
29
|
+
assert.deepStrictEqual(scan.commentCodePoints, ["U+200B"]);
|
|
30
|
+
assert.deepStrictEqual(scan.lineNumbers, [2]);
|
|
31
|
+
assert.deepStrictEqual(scan.contexts, ["comment"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("tracks yaml comment context for hidden Unicode", () => {
|
|
35
|
+
const scan = scanTextForHiddenUnicode(
|
|
36
|
+
"name: build\n# hidden \u202E comment\njobs:\n test:\n runs-on: ubuntu-latest",
|
|
37
|
+
{ syntax: "yaml" },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
assert.strictEqual(scan.hasHiddenUnicode, true);
|
|
41
|
+
assert.strictEqual(scan.inComments, true);
|
|
42
|
+
assert.deepStrictEqual(scan.commentCodePoints, ["U+202E"]);
|
|
43
|
+
assert.deepStrictEqual(scan.lineNumbers, [2]);
|
|
44
|
+
});
|
|
45
|
+
});
|