@cplieger/deadset-ts 3.10.0 → 3.12.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 (57) hide show
  1. package/README.md +13 -13
  2. package/dist/src/analysis.js +56 -16
  3. package/dist/src/config.js +6 -0
  4. package/dist/src/conformance-record.js +2 -12
  5. package/dist/src/consumers.js +49 -0
  6. package/dist/src/discover.js +59 -23
  7. package/dist/src/findings/completion.js +49 -17
  8. package/dist/src/findings/non-code-artifacts.js +9 -2
  9. package/dist/src/findings/reads-and-writes.js +3 -1
  10. package/dist/src/findings-pass.js +3 -5
  11. package/dist/src/graph.js +8 -5
  12. package/dist/src/matrix.js +14 -3
  13. package/dist/src/references.js +21 -9
  14. package/dist/src/report.js +4 -1
  15. package/dist/src/reporters/annotations.js +36 -0
  16. package/dist/src/reporters/fingerprint.js +61 -0
  17. package/dist/src/reporters/reporter.js +7 -1
  18. package/dist/src/reporters/reporters.js +18 -0
  19. package/dist/src/reporters/rules.js +178 -0
  20. package/dist/src/reporters/sarif.js +235 -0
  21. package/dist/src/reporters/sha256.js +101 -0
  22. package/dist/src/reporters/template.js +877 -0
  23. package/dist/src/run.js +4 -10
  24. package/dist/src/session.js +15 -0
  25. package/dist/src/sweep.js +11 -0
  26. package/dist/src/verbs/analyze.js +48 -16
  27. package/dist/src/verbs/explain.js +317 -0
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +123 -29
  31. package/src/config.ts +8 -0
  32. package/src/conformance-record.ts +2 -14
  33. package/src/consumers.ts +86 -0
  34. package/src/discover.ts +67 -23
  35. package/src/findings/completion.ts +56 -20
  36. package/src/findings/non-code-artifacts.ts +9 -2
  37. package/src/findings/reads-and-writes.ts +3 -1
  38. package/src/findings-pass.ts +5 -7
  39. package/src/graph.ts +14 -5
  40. package/src/matrix.ts +14 -5
  41. package/src/references.ts +48 -9
  42. package/src/report.ts +17 -2
  43. package/src/reporters/annotations.ts +67 -0
  44. package/src/reporters/fingerprint.ts +69 -0
  45. package/src/reporters/json.ts +2 -2
  46. package/src/reporters/reporter.ts +21 -2
  47. package/src/reporters/reporters.ts +18 -1
  48. package/src/reporters/rules.ts +195 -0
  49. package/src/reporters/sarif.ts +287 -0
  50. package/src/reporters/sha256.ts +120 -0
  51. package/src/reporters/template.ts +989 -0
  52. package/src/reporters/text.ts +2 -3
  53. package/src/run.ts +5 -11
  54. package/src/session.ts +19 -0
  55. package/src/sweep.ts +13 -1
  56. package/src/verbs/analyze.ts +60 -19
  57. package/src/verbs/explain.ts +380 -0
@@ -0,0 +1,235 @@
1
+ import { KINDS } from "../kinds.js";
2
+ import { lineHashes, symbolFingerprint } from "./fingerprint.js";
3
+ import { RenderError } from "./reporter.js";
4
+ import { RULE_TEXTS } from "./rules.js";
5
+ import { utf8Bytes } from "./sha256.js";
6
+ const SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
7
+ const VERSION = "2.1.0";
8
+ /** The unit a finding's column counts. */
9
+ const COLUMN_KIND = "utf16CodeUnits";
10
+ const URI_BASE_ID = "%SRCROOT%";
11
+ const ROOT_DESCRIPTION = "The target root, the directory the analyzer was run on.";
12
+ /** The most related locations one result carries; the report stays complete beyond them. */
13
+ const MAX_RELATED = 100;
14
+ const INDENT = 2;
15
+ const LEVEL = { deny: "error", warn: "warning", allow: "note" };
16
+ const PROBLEM = {
17
+ deny: "error",
18
+ warn: "warning",
19
+ allow: "recommendation",
20
+ };
21
+ const PRECISION = {
22
+ certain: "very-high",
23
+ probable: "high",
24
+ possible: "medium",
25
+ };
26
+ /** The text up to and including the first full stop a space follows or that ends the text. */
27
+ function firstSentence(text) {
28
+ for (let i = 0; i < text.length; i += 1) {
29
+ if (text[i] === "." && (i + 1 === text.length || text[i + 1] === " ")) {
30
+ return text.slice(0, i + 1);
31
+ }
32
+ }
33
+ return text;
34
+ }
35
+ /**
36
+ * One rule per live kind of this analyzer's language, in code order, whether or not a result
37
+ * names it, so a rule's index is the same in every run and under every configuration.
38
+ */
39
+ function rules() {
40
+ return [...RULE_TEXTS].map(([code, text]) => {
41
+ const kind = KINDS.get(code);
42
+ if (kind === undefined) {
43
+ throw new RenderError(`the rule ${code} names no live kind`);
44
+ }
45
+ return {
46
+ id: code,
47
+ name: kind.name,
48
+ shortDescription: { text: firstSentence(text.rule) },
49
+ fullDescription: { text: text.rule },
50
+ help: {
51
+ text: text.precondition === undefined ? text.rule : `${text.rule}\n\n${text.precondition}`,
52
+ },
53
+ defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
54
+ properties: {
55
+ precision: PRECISION[kind.maxClass],
56
+ problem: { severity: PROBLEM[kind.defaultSeverity] },
57
+ },
58
+ };
59
+ });
60
+ }
61
+ /** The characters a path segment keeps unencoded beside letters and digits. */
62
+ const SEGMENT_KEPT = "-_.~$&+:=@";
63
+ function isAlphanumeric(byte) {
64
+ return ((byte >= 0x30 && byte <= 0x39) ||
65
+ (byte >= 0x41 && byte <= 0x5a) ||
66
+ (byte >= 0x61 && byte <= 0x7a));
67
+ }
68
+ /** One path segment percent-encoded as RFC 3986 requires of a path segment. */
69
+ function encodeSegment(segment) {
70
+ return utf8Bytes(segment)
71
+ .map((byte) => isAlphanumeric(byte) || SEGMENT_KEPT.includes(String.fromCharCode(byte))
72
+ ? String.fromCharCode(byte)
73
+ : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`)
74
+ .join("");
75
+ }
76
+ /**
77
+ * A target-relative path as a relative reference: each segment encoded, and a colon in the
78
+ * first segment encoded as well, where it would otherwise read as a scheme.
79
+ */
80
+ function relativeReference(path) {
81
+ const segments = path.split("/").map(encodeSegment);
82
+ segments[0] = (segments[0] ?? "").replaceAll(":", "%3A");
83
+ return segments.join("/");
84
+ }
85
+ function physicalLocation(at, endLine) {
86
+ return {
87
+ artifactLocation: { uri: relativeReference(at.path), uriBaseId: URI_BASE_ID },
88
+ region: { startLine: at.line, startColumn: at.column, endLine },
89
+ };
90
+ }
91
+ /** The line fingerprints of the files one rendering reads, each file read and hashed once. */
92
+ function lineHashCache(read) {
93
+ const held = new Map();
94
+ return (at) => {
95
+ let hashes = held.get(at.path);
96
+ if (hashes === undefined) {
97
+ let text;
98
+ try {
99
+ text = read(at.path);
100
+ }
101
+ catch (error) {
102
+ throw new RenderError(`read ${at.path} for its line fingerprint: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ hashes = lineHashes(text);
105
+ held.set(at.path, hashes);
106
+ }
107
+ const hash = hashes[at.line - 1];
108
+ if (hash === undefined || at.line < 1) {
109
+ throw new RenderError(`${at.path} holds ${String(hashes.length)} lines and a record names line ${String(at.line)}`);
110
+ }
111
+ return hash;
112
+ };
113
+ }
114
+ /** Every position a finding names beyond its own, in the mapping's order, the first hundred. */
115
+ function relatedOf(found) {
116
+ const related = [];
117
+ for (const one of found.details.implementations ?? []) {
118
+ related.push({ label: "implementation", at: one.position, endLine: one.position.end_line });
119
+ }
120
+ for (const at of found.details.write_positions ?? []) {
121
+ related.push({ label: "write", at, endLine: at.end_line });
122
+ }
123
+ return related.slice(0, MAX_RELATED);
124
+ }
125
+ /** The finding's message, then a link to each related location, since only a linked one shows. */
126
+ function messageWithLinks(message, related) {
127
+ if (related.length === 0) {
128
+ return message;
129
+ }
130
+ const links = related.map((one, i) => `[${one.label} ${one.at.path}:${String(one.at.line)}:${String(one.at.column)}](${String(i + 1)})`);
131
+ return `${message} (see ${links.join(", ")})`;
132
+ }
133
+ /** Every member of the finding the mapping places nowhere else, under its schema name. */
134
+ function propertiesOf(found) {
135
+ return {
136
+ language: found.language,
137
+ symbol: found.symbol,
138
+ reachability_class: found.reachability_class,
139
+ confidence: found.confidence,
140
+ ...(found.liveness_relation === undefined
141
+ ? {}
142
+ : { liveness_relation: found.liveness_relation }),
143
+ test_only: found.test_only,
144
+ generated: found.generated,
145
+ component: found.component,
146
+ retained_by: found.retained_by,
147
+ configurations: found.configurations,
148
+ consumers_loaded: found.consumers_loaded,
149
+ fixability: found.fixability,
150
+ details: found.details,
151
+ };
152
+ }
153
+ function ruleIndex(index, code) {
154
+ const at = index.get(code);
155
+ if (at === undefined) {
156
+ throw new RenderError(`the run has no rule ${code}, which one of its records names`);
157
+ }
158
+ return at;
159
+ }
160
+ function findingResult(found, index, hashOf) {
161
+ const related = relatedOf(found);
162
+ return {
163
+ ruleId: found.code,
164
+ ruleIndex: ruleIndex(index, found.code),
165
+ level: LEVEL[found.severity],
166
+ message: { text: messageWithLinks(found.message, related) },
167
+ locations: [{ physicalLocation: physicalLocation(found.position, found.position.end_line) }],
168
+ ...(related.length === 0
169
+ ? {}
170
+ : {
171
+ relatedLocations: related.map((one, i) => ({
172
+ id: i + 1,
173
+ physicalLocation: physicalLocation(one.at, one.endLine),
174
+ message: { text: one.label },
175
+ })),
176
+ }),
177
+ partialFingerprints: {
178
+ primaryLocationLineHash: hashOf(found.position),
179
+ "deadsetSymbolRef/v1": symbolFingerprint(found.code, found.symbol.ref),
180
+ },
181
+ properties: propertiesOf(found),
182
+ };
183
+ }
184
+ /** A stale suppression at the failing level, at its own site; it is no finding, so it carries no member bag. */
185
+ function staleResult(stale, index, hashOf) {
186
+ return {
187
+ ruleId: stale.code,
188
+ ruleIndex: ruleIndex(index, stale.code),
189
+ level: LEVEL.deny,
190
+ message: { text: stale.message },
191
+ locations: [{ physicalLocation: physicalLocation(stale.position, stale.position.line) }],
192
+ partialFingerprints: {
193
+ primaryLocationLineHash: hashOf(stale.position),
194
+ "deadsetSymbolRef/v1": symbolFingerprint(stale.code, stale.symbol),
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * The report as one SARIF 2.1.0 log of one run, mapped as the Contract states. A suppressed
200
+ * finding has no record in the report and so no result, and the totals the run carries
201
+ * count it. A file the rendering cannot read, or a line it does not hold, fails the
202
+ * rendering, because a line fingerprint computed from other bytes opens a second alert.
203
+ */
204
+ export function sarif(report, options) {
205
+ const ruleList = rules();
206
+ const index = new Map([...RULE_TEXTS.keys()].map((code, at) => [code, at]));
207
+ const hashOf = lineHashCache(options.readSource);
208
+ const results = [
209
+ ...report.findings.map((found) => findingResult(found, index, hashOf)),
210
+ ...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
211
+ ];
212
+ const languages = [...report.analyzer.languages].sort().join("+");
213
+ const log = {
214
+ $schema: SCHEMA,
215
+ version: VERSION,
216
+ runs: [
217
+ {
218
+ tool: {
219
+ driver: {
220
+ name: report.analyzer.name,
221
+ version: report.analyzer.version,
222
+ semanticVersion: report.analyzer.version,
223
+ rules: ruleList,
224
+ },
225
+ },
226
+ automationDetails: { id: `deadset/${languages}/` },
227
+ columnKind: COLUMN_KIND,
228
+ originalUriBaseIds: { [URI_BASE_ID]: { description: { text: ROOT_DESCRIPTION } } },
229
+ results,
230
+ properties: { totals: report.totals },
231
+ },
232
+ ],
233
+ };
234
+ return `${JSON.stringify(log, null, INDENT)}\n`;
235
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * SHA-256 over the UTF-8 encoding of a string, computed synchronously in the module itself:
3
+ * a rendering is a pure function of the report, and the platform's own digests are either
4
+ * asynchronous or reached through a platform module the analysis does not import.
5
+ */
6
+ /** The first 32 bits of the fractional parts of the cube roots of the first 64 primes. */
7
+ const ROUND_CONSTANTS = new Uint32Array([
8
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
9
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
10
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
11
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
12
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
13
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
14
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
15
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
16
+ ]);
17
+ /** The first 32 bits of the fractional parts of the square roots of the first eight primes. */
18
+ const INITIAL_STATE = [
19
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
20
+ ];
21
+ /** The replacement character's encoding, written for a lone surrogate, which has no code point. */
22
+ const REPLACEMENT = [0xef, 0xbf, 0xbd];
23
+ /** The UTF-8 encoding of a string. */
24
+ export function utf8Bytes(text) {
25
+ const bytes = [];
26
+ for (const char of text) {
27
+ const point = char.codePointAt(0) ?? 0;
28
+ if (point < 0x80) {
29
+ bytes.push(point);
30
+ }
31
+ else if (point < 0x800) {
32
+ bytes.push(0xc0 | (point >> 6), 0x80 | (point & 0x3f));
33
+ }
34
+ else if (point >= 0xd800 && point <= 0xdfff) {
35
+ bytes.push(...REPLACEMENT);
36
+ }
37
+ else if (point < 0x10000) {
38
+ bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 0x3f), 0x80 | (point & 0x3f));
39
+ }
40
+ else {
41
+ bytes.push(0xf0 | (point >> 18), 0x80 | ((point >> 12) & 0x3f), 0x80 | ((point >> 6) & 0x3f), 0x80 | (point & 0x3f));
42
+ }
43
+ }
44
+ return bytes;
45
+ }
46
+ function rotate(value, by) {
47
+ return (value >>> by) | (value << (32 - by));
48
+ }
49
+ /** The message padded to a whole number of 64-byte blocks, its bit length in the last eight bytes. */
50
+ function padded(bytes) {
51
+ const length = Math.ceil((bytes.length + 9) / 64) * 64;
52
+ const block = new Uint8Array(length);
53
+ block.set(bytes);
54
+ block[bytes.length] = 0x80;
55
+ const bits = bytes.length * 8;
56
+ const view = new DataView(block.buffer);
57
+ view.setUint32(length - 8, Math.floor(bits / 0x100000000));
58
+ view.setUint32(length - 4, bits >>> 0);
59
+ return block;
60
+ }
61
+ /** The SHA-256 digest of a string's UTF-8 encoding, as 64 lowercase hexadecimal digits. */
62
+ export function sha256Hex(text) {
63
+ const message = padded(utf8Bytes(text));
64
+ const view = new DataView(message.buffer);
65
+ const state = [...INITIAL_STATE];
66
+ const words = new Uint32Array(64);
67
+ for (let offset = 0; offset < message.length; offset += 64) {
68
+ for (let i = 0; i < 16; i += 1) {
69
+ words[i] = view.getUint32(offset + i * 4);
70
+ }
71
+ for (let i = 16; i < 64; i += 1) {
72
+ const early = words[i - 15] ?? 0;
73
+ const late = words[i - 2] ?? 0;
74
+ const s0 = rotate(early, 7) ^ rotate(early, 18) ^ (early >>> 3);
75
+ const s1 = rotate(late, 17) ^ rotate(late, 19) ^ (late >>> 10);
76
+ words[i] = (words[i - 16] ?? 0) + s0 + (words[i - 7] ?? 0) + s1;
77
+ }
78
+ let [a, b, c, d, e, f, g, h] = state;
79
+ for (let i = 0; i < 64; i += 1) {
80
+ const t1 = (h +
81
+ (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) +
82
+ ((e & f) ^ (~e & g)) +
83
+ (ROUND_CONSTANTS[i] ?? 0) +
84
+ (words[i] ?? 0)) >>>
85
+ 0;
86
+ const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
87
+ h = g;
88
+ g = f;
89
+ f = e;
90
+ e = (d + t1) >>> 0;
91
+ d = c;
92
+ c = b;
93
+ b = a;
94
+ a = (t1 + t2) >>> 0;
95
+ }
96
+ [a, b, c, d, e, f, g, h].forEach((value, i) => {
97
+ state[i] = ((state[i] ?? 0) + value) >>> 0;
98
+ });
99
+ }
100
+ return state.map((word) => word.toString(16).padStart(8, "0")).join("");
101
+ }