ecdsa-scan 0.1.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.
@@ -0,0 +1,199 @@
1
+ // Comment masking.
2
+ //
3
+ // Rules match against a copy of the source in which every comment character is
4
+ // replaced by a space. Offsets and line numbers stay identical to the original
5
+ // file, so a match index in the masked text always points at the same place in
6
+ // the real file — but documentation, disabled code and prose examples never
7
+ // produce findings. String literals are left intact: many rules legitimately
8
+ // look inside them (PEM blocks, `createSign("sha1")`, curve names).
9
+
10
+ const QUOTES = new Set(['"', "'", "`"]);
11
+
12
+ /** Index of the closing quote of the string starting at `i` (or end of text). */
13
+ function skipString(text, i, { rawBacktick = false } = {}) {
14
+ const quote = text[i];
15
+ const raw = rawBacktick && quote === "`";
16
+ for (let j = i + 1; j < text.length; j++) {
17
+ const ch = text[j];
18
+ if (!raw && ch === "\\") {
19
+ j++;
20
+ continue;
21
+ }
22
+ if (ch === quote) return j;
23
+ // Unterminated single-quoted strings are a lexing dead end; stop at the
24
+ // newline so one stray apostrophe cannot swallow the rest of the file.
25
+ if (!raw && ch === "\n" && quote !== "`") return j - 1;
26
+ }
27
+ return text.length - 1;
28
+ }
29
+
30
+ // A `/` starts a regular expression (rather than division) when the previous
31
+ // significant token cannot end an expression. Standard heuristic — good enough
32
+ // to keep `/https:\/\//` from being mistaken for a line comment.
33
+ const REGEX_PRECEDERS = new Set("(,=:[!&|?{};+-*%^~<>".split(""));
34
+ const REGEX_KEYWORDS = /\b(?:return|typeof|instanceof|in|of|new|delete|void|case|do|else|yield|await)$/;
35
+
36
+ function regexLiteralAllowed(text, slashIndex) {
37
+ let k = slashIndex - 1;
38
+ while (k >= 0 && /\s/.test(text[k])) k--;
39
+ if (k < 0) return true;
40
+ const prev = text[k];
41
+ if (REGEX_PRECEDERS.has(prev)) return true;
42
+ return REGEX_KEYWORDS.test(text.slice(Math.max(0, k - 12), k + 1));
43
+ }
44
+
45
+ /** Index of the closing `/` of the regex literal starting at `i`. */
46
+ function skipRegex(text, i) {
47
+ let inClass = false;
48
+ for (let j = i + 1; j < text.length; j++) {
49
+ const ch = text[j];
50
+ if (ch === "\\") {
51
+ j++;
52
+ continue;
53
+ }
54
+ if (ch === "\n") return j - 1; // not a regex after all; bail out safely
55
+ if (ch === "[") inClass = true;
56
+ else if (ch === "]") inClass = false;
57
+ else if (ch === "/" && !inClass) return j;
58
+ }
59
+ return text.length - 1;
60
+ }
61
+
62
+ function blank(out, from, to) {
63
+ for (let i = from; i <= to && i < out.length; i++) {
64
+ if (out[i] !== "\n") out[i] = " ";
65
+ }
66
+ }
67
+
68
+ function maskCLike(text, { regexLiterals, rawBacktick }) {
69
+ const out = text.split("");
70
+ for (let i = 0; i < text.length; i++) {
71
+ const ch = text[i];
72
+ if (QUOTES.has(ch)) {
73
+ i = skipString(text, i, { rawBacktick });
74
+ continue;
75
+ }
76
+ if (ch === "/" && text[i + 1] === "/") {
77
+ let end = text.indexOf("\n", i);
78
+ if (end === -1) end = text.length;
79
+ blank(out, i, end - 1);
80
+ i = end;
81
+ continue;
82
+ }
83
+ if (ch === "/" && text[i + 1] === "*") {
84
+ let end = text.indexOf("*/", i + 2);
85
+ end = end === -1 ? text.length - 1 : end + 1;
86
+ blank(out, i, end);
87
+ i = end;
88
+ continue;
89
+ }
90
+ if (ch === "/" && regexLiterals && regexLiteralAllowed(text, i)) {
91
+ i = skipRegex(text, i);
92
+ continue;
93
+ }
94
+ }
95
+ return out.join("");
96
+ }
97
+
98
+ function maskPython(text) {
99
+ const out = text.split("");
100
+ for (let i = 0; i < text.length; i++) {
101
+ const ch = text[i];
102
+ if ((ch === '"' || ch === "'") && text.slice(i, i + 3) === ch.repeat(3)) {
103
+ const close = text.indexOf(ch.repeat(3), i + 3);
104
+ i = close === -1 ? text.length : close + 2;
105
+ continue;
106
+ }
107
+ if (ch === '"' || ch === "'") {
108
+ i = skipString(text, i);
109
+ continue;
110
+ }
111
+ if (ch === "#") {
112
+ let end = text.indexOf("\n", i);
113
+ if (end === -1) end = text.length;
114
+ blank(out, i, end - 1);
115
+ i = end;
116
+ continue;
117
+ }
118
+ }
119
+ return out.join("");
120
+ }
121
+
122
+ /**
123
+ * Blank the *contents* of string, template and regex literals, keeping the
124
+ * delimiters and every offset. Rules that look for code structure (a call, a
125
+ * comparison, an assignment) use this view so that documentation strings, code
126
+ * samples and a scanner's own patterns never look like real code.
127
+ *
128
+ * Expects text whose comments are already masked.
129
+ */
130
+ export function maskLiterals(code, lang) {
131
+ const cLike = lang === "js" || lang === "ts" || lang === "go";
132
+ if (!cLike && lang !== "python") return code;
133
+ const out = code.split("");
134
+ for (let i = 0; i < code.length; i++) {
135
+ const ch = code[i];
136
+ if (lang === "python" && (ch === '"' || ch === "'") && code.slice(i, i + 3) === ch.repeat(3)) {
137
+ const close = code.indexOf(ch.repeat(3), i + 3);
138
+ const end = close === -1 ? code.length - 1 : close - 1;
139
+ blank(out, i + 3, end);
140
+ i = close === -1 ? code.length : close + 2;
141
+ continue;
142
+ }
143
+ if (QUOTES.has(ch)) {
144
+ const end = skipString(code, i, { rawBacktick: lang === "go" });
145
+ blank(out, i + 1, end - 1);
146
+ i = end;
147
+ continue;
148
+ }
149
+ if (cLike && lang !== "go" && ch === "/" && regexLiteralAllowed(code, i)) {
150
+ const end = skipRegex(code, i);
151
+ blank(out, i + 1, end - 1);
152
+ i = end;
153
+ continue;
154
+ }
155
+ }
156
+ const masked = out.join("");
157
+ return lang === "js" || lang === "ts" ? maskJsxText(masked) : masked;
158
+ }
159
+
160
+ // Prose written as JSX children — "…the curve is P-256 or secp256k1…" — is text,
161
+ // not code, but it carries no quotes to give it away. A run between a closing
162
+ // `>` and the next opening `<` is treated as prose when it reads like a
163
+ // sentence and contains nothing that could be an expression.
164
+ const NOT_PROSE = /[=;{}]|&&|\|\||==|!=|<=|>=/;
165
+
166
+ function maskJsxText(code) {
167
+ if (!code.includes("</")) return code;
168
+ const out = code.split("");
169
+ let i = code.indexOf(">");
170
+ while (i !== -1) {
171
+ const next = code.indexOf("<", i + 1);
172
+ if (next === -1) break;
173
+ const run = code.slice(i + 1, next);
174
+ const spaces = (run.match(/ /g) ?? []).length;
175
+ if (run.length >= 12 && spaces >= 4 && !NOT_PROSE.test(run)) {
176
+ blank(out, i + 1, next - 1);
177
+ }
178
+ i = code.indexOf(">", next + 1);
179
+ }
180
+ return out.join("");
181
+ }
182
+
183
+ /**
184
+ * Return `text` with comments blanked out. Length, newlines and every byte
185
+ * offset are preserved.
186
+ */
187
+ export function maskComments(text, lang) {
188
+ switch (lang) {
189
+ case "js":
190
+ case "ts":
191
+ return maskCLike(text, { regexLiterals: true, rawBacktick: false });
192
+ case "go":
193
+ return maskCLike(text, { regexLiterals: false, rawBacktick: true });
194
+ case "python":
195
+ return maskPython(text);
196
+ default:
197
+ return text;
198
+ }
199
+ }
@@ -0,0 +1,86 @@
1
+ // Small text helpers shared by rules: regex iteration with offsets, balanced
2
+ // bracket scanning (so a rule can look at the whole argument list of a call,
3
+ // even across newlines) and line windows for context checks.
4
+
5
+ const CLOSERS = { "(": ")", "[": "]", "{": "}" };
6
+
7
+ function skipStringFrom(text, i) {
8
+ const quote = text[i];
9
+ for (let j = i + 1; j < text.length; j++) {
10
+ const ch = text[j];
11
+ if (ch === "\\") {
12
+ j++;
13
+ continue;
14
+ }
15
+ if (ch === quote) return j;
16
+ if (ch === "\n" && quote !== "`") return j - 1;
17
+ }
18
+ return text.length - 1;
19
+ }
20
+
21
+ /**
22
+ * Scan from an opening bracket to its match, ignoring brackets inside string
23
+ * literals. Returns `{ start, end, inner, truncated }`.
24
+ */
25
+ export function balancedSpan(text, openIndex, limit = 20000) {
26
+ const open = text[openIndex];
27
+ const close = CLOSERS[open];
28
+ if (!close) return { start: openIndex, end: openIndex, inner: "", truncated: true };
29
+ const hardEnd = Math.min(text.length, openIndex + limit);
30
+ let depth = 0;
31
+ for (let i = openIndex; i < hardEnd; i++) {
32
+ const ch = text[i];
33
+ if (ch === '"' || ch === "'" || ch === "`") {
34
+ i = skipStringFrom(text, i);
35
+ continue;
36
+ }
37
+ if (ch === open) depth++;
38
+ else if (ch === close) {
39
+ depth--;
40
+ if (depth === 0) {
41
+ return { start: openIndex, end: i, inner: text.slice(openIndex + 1, i), truncated: false };
42
+ }
43
+ }
44
+ }
45
+ return { start: openIndex, end: hardEnd - 1, inner: text.slice(openIndex + 1, hardEnd), truncated: true };
46
+ }
47
+
48
+ /** Iterate regex matches, always in global mode, yielding the match objects. */
49
+ export function* matchAll(text, re) {
50
+ const rx = re.global ? new RegExp(re.source, re.flags) : new RegExp(re.source, re.flags + "g");
51
+ let m;
52
+ while ((m = rx.exec(text)) !== null) {
53
+ if (m[0] === "") rx.lastIndex++;
54
+ else yield m;
55
+ }
56
+ }
57
+
58
+ /** `function foo(`, `def foo(`, `func Foo(` — a definition, not a call site. */
59
+ const DECLARATION_BEFORE = /\b(?:function|def|func|class|interface|type)\s+$/;
60
+
61
+ /**
62
+ * Find calls whose callee matches `calleeRe`. Yields
63
+ * `{ index, callee, args, end }` where `args` is the raw argument text.
64
+ * Function declarations are skipped: defining `decodeJwt(...)` is not calling it.
65
+ */
66
+ export function* findCalls(text, calleeRe) {
67
+ for (const m of matchAll(text, calleeRe)) {
68
+ let i = m.index + m[0].length;
69
+ while (i < text.length && /\s/.test(text[i])) i++;
70
+ if (text[i] !== "(") continue;
71
+ if (DECLARATION_BEFORE.test(text.slice(Math.max(0, m.index - 12), m.index))) continue;
72
+ const span = balancedSpan(text, i);
73
+ yield { index: m.index, callee: m[0], match: m, args: span.inner, end: span.end };
74
+ }
75
+ }
76
+
77
+ /** True when `re` matches anywhere in `text`. */
78
+ export function has(text, re) {
79
+ return typeof re === "string" ? text.includes(re) : re.test(text);
80
+ }
81
+
82
+ /** Longest common trimmed line, used for readable snippets. */
83
+ export function snippetAt(lines, lineNumber, maxLen = 160) {
84
+ const raw = (lines[lineNumber - 1] ?? "").trim();
85
+ return raw.length > maxLen ? `${raw.slice(0, maxLen - 1)}…` : raw;
86
+ }
package/src/report.js ADDED
@@ -0,0 +1,298 @@
1
+ // Output formats: human-readable terminal text, JSON, and SARIF 2.1.0 for
2
+ // GitHub Code Scanning. No dependencies — ANSI codes are written directly.
3
+
4
+ import { createHash } from "node:crypto";
5
+ import path from "node:path";
6
+ import { rules as allRules } from "./rules/index.js";
7
+
8
+ export const TOOL_NAME = "ecdsa-scan";
9
+ export const TOOL_VERSION = "0.1.0";
10
+ export const TOOL_URI = "https://ecdsa.com/scanner";
11
+
12
+ const ANSI = {
13
+ reset: "\u001b[0m",
14
+ bold: "\u001b[1m",
15
+ dim: "\u001b[2m",
16
+ red: "\u001b[31m",
17
+ yellow: "\u001b[33m",
18
+ blue: "\u001b[34m",
19
+ cyan: "\u001b[36m",
20
+ green: "\u001b[32m",
21
+ gray: "\u001b[90m",
22
+ };
23
+
24
+ export function makeColors(enabled) {
25
+ const wrap = (code) => (text) => (enabled ? `${code}${text}${ANSI.reset}` : String(text));
26
+ return {
27
+ bold: wrap(ANSI.bold),
28
+ dim: wrap(ANSI.dim),
29
+ red: wrap(ANSI.red),
30
+ yellow: wrap(ANSI.yellow),
31
+ blue: wrap(ANSI.blue),
32
+ cyan: wrap(ANSI.cyan),
33
+ green: wrap(ANSI.green),
34
+ gray: wrap(ANSI.gray),
35
+ };
36
+ }
37
+
38
+ const CONFIDENCE_STYLE = {
39
+ confirmed: (c) => c.red("confirmed"),
40
+ suspected: (c) => c.yellow("suspected"),
41
+ advisory: (c) => c.cyan("advisory "),
42
+ };
43
+
44
+ /** Wrap `text` to `width`, indenting every line with `indent`. */
45
+ export function wrapText(text, width, indent = "") {
46
+ const words = String(text).split(/\s+/).filter(Boolean);
47
+ const lines = [];
48
+ let current = "";
49
+ for (const word of words) {
50
+ if (current && current.length + 1 + word.length > width) {
51
+ lines.push(current);
52
+ current = word;
53
+ } else {
54
+ current = current ? `${current} ${word}` : word;
55
+ }
56
+ }
57
+ if (current) lines.push(current);
58
+ return lines.map((line) => indent + line).join("\n");
59
+ }
60
+
61
+ export function countBy(items, key) {
62
+ const out = {};
63
+ for (const item of items) out[item[key]] = (out[item[key]] ?? 0) + 1;
64
+ return out;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Terminal
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Human-readable report. The full explanation of a rule (why / fix / docs) is
73
+ * printed once, with the first finding it produced; later findings show only
74
+ * the location and the message so long scans stay readable.
75
+ */
76
+ export function formatText(result, options = {}) {
77
+ const c = makeColors(options.color ?? false);
78
+ const width = Math.min(Math.max(options.width ?? 100, 60), 120);
79
+ const findings = result.findings;
80
+ const out = [];
81
+
82
+ out.push(`${c.bold("ecdsa scan")} ${c.dim("·")} ${result.root}`);
83
+
84
+ if (findings.length === 0) {
85
+ out.push("");
86
+ out.push(c.green("No findings at the requested confidence level."));
87
+ }
88
+
89
+ const explained = new Set();
90
+ let currentFile = null;
91
+ for (const f of findings) {
92
+ if (f.relPath !== currentFile) {
93
+ currentFile = f.relPath;
94
+ out.push("");
95
+ out.push(c.bold(currentFile));
96
+ }
97
+ const badge = (CONFIDENCE_STYLE[f.confidence] ?? ((x) => x.dim(f.confidence)))(c);
98
+ out.push(` ${c.dim(`${f.line}:${f.column}`)} ${badge} ${c.blue(f.ruleId)} ${f.title}`);
99
+ if (f.snippet) out.push(c.gray(` │ ${f.snippet}`));
100
+ out.push(wrapText(f.message, width - 6, " "));
101
+ if (!explained.has(f.ruleId)) {
102
+ explained.add(f.ruleId);
103
+ if (f.why) {
104
+ out.push(c.dim(wrapText(`Why it matters: ${f.why}`, width - 6, " ")));
105
+ }
106
+ if (f.fix) {
107
+ out.push(c.dim(" Fix:"));
108
+ for (const line of String(f.fix).split("\n")) out.push(c.dim(` ${line}`));
109
+ }
110
+ if (f.docs) out.push(c.dim(` Reference: ${f.docs}`));
111
+ }
112
+ out.push("");
113
+ }
114
+
115
+ // Inventory
116
+ if (options.inventory !== false && result.inventory.length > 0) {
117
+ out.push(c.bold("Inventory"));
118
+ const groups = new Map();
119
+ for (const item of result.inventory) {
120
+ if (!groups.has(item.kind)) groups.set(item.kind, []);
121
+ groups.get(item.kind).push(item);
122
+ }
123
+ for (const [kind, items] of groups) {
124
+ const names = items.map((i) => `${i.name} ${c.dim(`(${i.files.length})`)}`).join(", ");
125
+ out.push(wrapText(`${kind}: ${names}`, width - 2, " "));
126
+ }
127
+ out.push("");
128
+ }
129
+
130
+ const byConfidence = countBy(findings, "confidence");
131
+ const summary = ["confirmed", "suspected", "advisory"]
132
+ .filter((level) => byConfidence[level])
133
+ .map((level) => `${byConfidence[level]} ${level}`)
134
+ .join(", ");
135
+ out.push(
136
+ `${c.bold("Summary")} ${result.stats.scannedFiles} file${result.stats.scannedFiles === 1 ? "" : "s"} scanned, ${findings.length} finding${findings.length === 1 ? "" : "s"}${summary ? ` (${summary})` : ""}`
137
+ );
138
+ if (byConfidence.confirmed) {
139
+ out.push(c.red(`Exit code 1: ${byConfidence.confirmed} confirmed finding${byConfidence.confirmed === 1 ? "" : "s"}.`));
140
+ }
141
+ if (result.errors.length > 0) {
142
+ out.push(c.dim(`${result.errors.length} path(s) could not be read; run with --json to see them.`));
143
+ }
144
+ out.push(
145
+ c.dim("Static pattern analysis: it can miss defects and can flag correct code. Treat advisory findings as questions, not verdicts.")
146
+ );
147
+ return out.join("\n");
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // JSON
152
+ // ---------------------------------------------------------------------------
153
+
154
+ export function formatJson(result, options = {}) {
155
+ const ruleMeta = {};
156
+ for (const f of result.findings) {
157
+ if (ruleMeta[f.ruleId]) continue;
158
+ const rule = allRules.find((r) => r.id === f.ruleId);
159
+ ruleMeta[f.ruleId] = {
160
+ title: rule?.title ?? f.title,
161
+ severity: rule?.severity ?? f.severity,
162
+ defaultConfidence: rule?.confidence ?? f.confidence,
163
+ why: rule?.why,
164
+ fix: rule?.fix,
165
+ docs: rule?.docs,
166
+ };
167
+ }
168
+
169
+ const INVENTORY_SECTION = {
170
+ library: "libraries",
171
+ algorithm: "algorithms",
172
+ curve: "curves",
173
+ operation: "operations",
174
+ };
175
+ const grouped = { libraries: [], algorithms: [], curves: [], operations: [] };
176
+ for (const item of result.inventory) {
177
+ const key = INVENTORY_SECTION[item.kind] ?? `${item.kind}s`;
178
+ (grouped[key] ??= []).push({ name: item.name, detail: item.detail, files: item.files });
179
+ }
180
+
181
+ return JSON.stringify(
182
+ {
183
+ tool: { name: TOOL_NAME, version: TOOL_VERSION, informationUri: TOOL_URI },
184
+ scannedAt: options.now ?? new Date().toISOString(),
185
+ root: result.root,
186
+ summary: {
187
+ filesScanned: result.stats.scannedFiles,
188
+ filesSkipped: result.stats.skippedFiles,
189
+ findings: result.findings.length,
190
+ byConfidence: countBy(result.findings, "confidence"),
191
+ bySeverity: countBy(result.findings, "severity"),
192
+ byRule: countBy(result.findings, "ruleId"),
193
+ },
194
+ rules: ruleMeta,
195
+ findings: result.findings.map((f) => ({
196
+ ruleId: f.ruleId,
197
+ title: f.title,
198
+ severity: f.severity,
199
+ confidence: f.confidence,
200
+ message: f.message,
201
+ path: f.relPath,
202
+ absolutePath: f.file,
203
+ line: f.line,
204
+ column: f.column,
205
+ snippet: f.snippet,
206
+ docs: f.docs,
207
+ })),
208
+ inventory: grouped,
209
+ errors: result.errors,
210
+ },
211
+ null,
212
+ 2
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // SARIF 2.1.0
218
+ // ---------------------------------------------------------------------------
219
+
220
+ const SARIF_LEVEL = { confirmed: "error", suspected: "warning", advisory: "note" };
221
+ const SECURITY_SEVERITY = { high: "7.5", medium: "5.0", low: "3.0" };
222
+
223
+ export function formatSarif(result) {
224
+ const usedRuleIds = [...new Set(result.findings.map((f) => f.ruleId))];
225
+ const sarifRules = usedRuleIds.map((id) => {
226
+ const rule = allRules.find((r) => r.id === id) ?? {};
227
+ return {
228
+ id,
229
+ name: id
230
+ .split("-")
231
+ .map((part) => part[0].toUpperCase() + part.slice(1))
232
+ .join(""),
233
+ shortDescription: { text: rule.title ?? id },
234
+ fullDescription: { text: rule.why ?? rule.title ?? id },
235
+ help: {
236
+ text: [rule.why, rule.fix ? `Fix:\n${rule.fix}` : null, rule.docs].filter(Boolean).join("\n\n"),
237
+ markdown: [rule.why, rule.fix ? `**Fix**\n\n\`\`\`\n${rule.fix}\n\`\`\`` : null, rule.docs ? `[Reference](${rule.docs})` : null]
238
+ .filter(Boolean)
239
+ .join("\n\n"),
240
+ },
241
+ helpUri: rule.docs ?? TOOL_URI,
242
+ defaultConfiguration: { level: SARIF_LEVEL[rule.confidence] ?? "warning" },
243
+ properties: {
244
+ tags: ["security", "cryptography", "signatures"],
245
+ precision: rule.confidence === "confirmed" ? "high" : rule.confidence === "suspected" ? "medium" : "low",
246
+ "security-severity": SECURITY_SEVERITY[rule.severity] ?? "5.0",
247
+ },
248
+ };
249
+ });
250
+
251
+ const results = result.findings.map((f) => ({
252
+ ruleId: f.ruleId,
253
+ ruleIndex: usedRuleIds.indexOf(f.ruleId),
254
+ level: SARIF_LEVEL[f.confidence] ?? "warning",
255
+ message: { text: `${f.title}: ${f.message}` },
256
+ locations: [
257
+ {
258
+ physicalLocation: {
259
+ artifactLocation: { uri: f.relPath, uriBaseId: "%SRCROOT%" },
260
+ region: {
261
+ startLine: f.line,
262
+ startColumn: f.column,
263
+ snippet: { text: f.snippet ?? "" },
264
+ },
265
+ },
266
+ },
267
+ ],
268
+ partialFingerprints: {
269
+ primaryLocationLineHash: createHash("sha256").update(`${f.ruleId}|${f.relPath}|${f.snippet}`).digest("hex").slice(0, 32),
270
+ },
271
+ properties: { confidence: f.confidence, severity: f.severity },
272
+ }));
273
+
274
+ const rootUri = `file://${path.resolve(result.root).split(path.sep).join("/")}/`;
275
+ return JSON.stringify(
276
+ {
277
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
278
+ version: "2.1.0",
279
+ runs: [
280
+ {
281
+ tool: {
282
+ driver: {
283
+ name: "ecdsa scan",
284
+ version: TOOL_VERSION,
285
+ informationUri: TOOL_URI,
286
+ rules: sarifRules,
287
+ },
288
+ },
289
+ originalUriBaseIds: { "%SRCROOT%": { uri: rootUri } },
290
+ invocations: [{ executionSuccessful: true }],
291
+ results,
292
+ },
293
+ ],
294
+ },
295
+ null,
296
+ 2
297
+ );
298
+ }