@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,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
+ });