@trofimyukdev/repo-truth 0.0.0-stage → 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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -2
  3. package/dist/action.d.ts +52 -0
  4. package/dist/action.js +182 -0
  5. package/dist/action.js.map +1 -0
  6. package/dist/checks/action-pins.d.ts +52 -0
  7. package/dist/checks/action-pins.js +196 -0
  8. package/dist/checks/action-pins.js.map +1 -0
  9. package/dist/checks/autosquash.d.ts +23 -0
  10. package/dist/checks/autosquash.js +85 -0
  11. package/dist/checks/autosquash.js.map +1 -0
  12. package/dist/checks/bidi-controls.d.ts +58 -0
  13. package/dist/checks/bidi-controls.js +172 -0
  14. package/dist/checks/bidi-controls.js.map +1 -0
  15. package/dist/checks/binary-files.d.ts +53 -0
  16. package/dist/checks/binary-files.js +157 -0
  17. package/dist/checks/binary-files.js.map +1 -0
  18. package/dist/checks/config-changes.d.ts +57 -0
  19. package/dist/checks/config-changes.js +175 -0
  20. package/dist/checks/config-changes.js.map +1 -0
  21. package/dist/checks/credentials.d.ts +35 -0
  22. package/dist/checks/credentials.js +138 -0
  23. package/dist/checks/credentials.js.map +1 -0
  24. package/dist/checks/install-scripts.d.ts +39 -0
  25. package/dist/checks/install-scripts.js +173 -0
  26. package/dist/checks/install-scripts.js.map +1 -0
  27. package/dist/checks/lint-rules.d.ts +49 -0
  28. package/dist/checks/lint-rules.js +184 -0
  29. package/dist/checks/lint-rules.js.map +1 -0
  30. package/dist/checks/path-policy.d.ts +40 -0
  31. package/dist/checks/path-policy.js +195 -0
  32. package/dist/checks/path-policy.js.map +1 -0
  33. package/dist/checks/raised-timeouts.d.ts +66 -0
  34. package/dist/checks/raised-timeouts.js +248 -0
  35. package/dist/checks/raised-timeouts.js.map +1 -0
  36. package/dist/checks/script-changes.d.ts +51 -0
  37. package/dist/checks/script-changes.js +199 -0
  38. package/dist/checks/script-changes.js.map +1 -0
  39. package/dist/checks/secret-files.d.ts +58 -0
  40. package/dist/checks/secret-files.js +165 -0
  41. package/dist/checks/secret-files.js.map +1 -0
  42. package/dist/checks/strictness.d.ts +59 -0
  43. package/dist/checks/strictness.js +260 -0
  44. package/dist/checks/strictness.js.map +1 -0
  45. package/dist/checks/submodules.d.ts +51 -0
  46. package/dist/checks/submodules.js +172 -0
  47. package/dist/checks/submodules.js.map +1 -0
  48. package/dist/checks/suppressions.d.ts +53 -0
  49. package/dist/checks/suppressions.js +203 -0
  50. package/dist/checks/suppressions.js.map +1 -0
  51. package/dist/checks/workflow-permissions.d.ts +56 -0
  52. package/dist/checks/workflow-permissions.js +237 -0
  53. package/dist/checks/workflow-permissions.js.map +1 -0
  54. package/dist/checks/workflow-weakening.d.ts +38 -0
  55. package/dist/checks/workflow-weakening.js +172 -0
  56. package/dist/checks/workflow-weakening.js.map +1 -0
  57. package/dist/cli.d.ts +31 -0
  58. package/dist/cli.js +300 -0
  59. package/dist/cli.js.map +1 -0
  60. package/dist/commit-range.d.ts +28 -0
  61. package/dist/commit-range.js +152 -0
  62. package/dist/commit-range.js.map +1 -0
  63. package/dist/config.d.ts +35 -0
  64. package/dist/config.js +235 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/conflict-markers.d.ts +40 -0
  67. package/dist/conflict-markers.js +234 -0
  68. package/dist/conflict-markers.js.map +1 -0
  69. package/dist/dependency-sources.d.ts +32 -0
  70. package/dist/dependency-sources.js +260 -0
  71. package/dist/dependency-sources.js.map +1 -0
  72. package/dist/figure-claims.d.ts +35 -0
  73. package/dist/figure-claims.js +139 -0
  74. package/dist/figure-claims.js.map +1 -0
  75. package/dist/figure-text.d.ts +22 -0
  76. package/dist/figure-text.js +119 -0
  77. package/dist/figure-text.js.map +1 -0
  78. package/dist/index.d.ts +46 -0
  79. package/dist/index.js +38 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/large-blobs.d.ts +35 -0
  82. package/dist/large-blobs.js +163 -0
  83. package/dist/large-blobs.js.map +1 -0
  84. package/dist/lockfile-drift.d.ts +36 -0
  85. package/dist/lockfile-drift.js +206 -0
  86. package/dist/lockfile-drift.js.map +1 -0
  87. package/dist/lowered-thresholds.d.ts +70 -0
  88. package/dist/lowered-thresholds.js +281 -0
  89. package/dist/lowered-thresholds.js.map +1 -0
  90. package/dist/measurement-rule.d.ts +39 -0
  91. package/dist/measurement-rule.js +213 -0
  92. package/dist/measurement-rule.js.map +1 -0
  93. package/dist/portable-paths.d.ts +37 -0
  94. package/dist/portable-paths.js +215 -0
  95. package/dist/portable-paths.js.map +1 -0
  96. package/dist/registry.d.ts +43 -0
  97. package/dist/registry.js +144 -0
  98. package/dist/registry.js.map +1 -0
  99. package/dist/replay-plan.d.ts +36 -0
  100. package/dist/replay-plan.js +196 -0
  101. package/dist/replay-plan.js.map +1 -0
  102. package/dist/replayed-figures.d.ts +36 -0
  103. package/dist/replayed-figures.js +159 -0
  104. package/dist/replayed-figures.js.map +1 -0
  105. package/dist/snapshot-refreshes.d.ts +36 -0
  106. package/dist/snapshot-refreshes.js +202 -0
  107. package/dist/snapshot-refreshes.js.map +1 -0
  108. package/dist/stray-files.d.ts +52 -0
  109. package/dist/stray-files.js +234 -0
  110. package/dist/stray-files.js.map +1 -0
  111. package/dist/symlinks.d.ts +28 -0
  112. package/dist/symlinks.js +193 -0
  113. package/dist/symlinks.js.map +1 -0
  114. package/dist/trailers.d.ts +36 -0
  115. package/dist/trailers.js +110 -0
  116. package/dist/trailers.js.map +1 -0
  117. package/dist/weakened-tests.d.ts +94 -0
  118. package/dist/weakened-tests.js +458 -0
  119. package/dist/weakened-tests.js.map +1 -0
  120. package/package.json +31 -4
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Unpinned actions.
3
+ *
4
+ * A `uses: owner/repo@v4` runs whatever commit the tag names on the day the job
5
+ * runs, with the job's token and secrets. This check reads, whole and through
6
+ * git at the merge base of `base` and `candidate` and at `candidate` (never the
7
+ * working tree), every workflow file the range adds, changes or renames onto -
8
+ * `.github/workflows/<name>.yml` or `.yaml` directly in that directory - and
9
+ * every file named `action.yml` or `action.yaml` at any depth.
10
+ *
11
+ * The YAML is not parsed. A `uses:` line is a line whose text, white space at
12
+ * both ends removed, opens `uses:` or `- uses:`; its value is the rest of the
13
+ * line with a trailing comment (a `#` after white space) removed, then white
14
+ * space at both ends, then one pair of matching surrounding quotes. A line is
15
+ * added when the candidate's file holds its text more often than its source
16
+ * file (the file at the merge base, the one a rename came from, or none) did;
17
+ * a line moved or re-indented is not, and a text added more than once is one
18
+ * finding.
19
+ *
20
+ * Pinned: a local reference (opening `./`), a `docker://` image by digest
21
+ * (`@sha256:` and sixty-four hexadecimal digits), and a repository reference
22
+ * `<owner>/<repo>[/<path>]@<commit>` whose commit is exactly forty hexadecimal
23
+ * digits. Every other value - a tag, a branch, no `@`, a short commit id, a
24
+ * `docker://` image by tag - is unpinned and is a finding `unpinned-action`.
25
+ *
26
+ * The option `allowedOwners` (strings): a repository reference whose owner, the
27
+ * text before its first `/`, is one of them, compared exactly, yields no
28
+ * finding. `docker://` and local references are judged as above regardless.
29
+ *
30
+ * A finding's id is `unpinned-action:<file>`; it is sanctioned by a
31
+ * `Sanctioned-Weakening: <id> <reason>` line in a commit message of the range
32
+ * or by the caller's `sanctions`.
33
+ */
34
+ import { execFile } from "node:child_process";
35
+ import { SANCTION_KEY } from "../weakened-tests.js";
36
+ export const name = "RT-88";
37
+ export const optionKinds = { allowedOwners: "strings" };
38
+ function runGit(cwd, args) {
39
+ return new Promise((resolve, reject) => {
40
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
41
+ if (error)
42
+ reject(new Error(stderr.toString("utf8").trim() || error.message));
43
+ else
44
+ resolve(stdout.toString("utf8"));
45
+ });
46
+ });
47
+ }
48
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
49
+ function escapeText(text) {
50
+ let escaped = "";
51
+ for (const char of text) {
52
+ const codePoint = char.codePointAt(0) ?? 0;
53
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
54
+ }
55
+ return escaped;
56
+ }
57
+ async function resolveCommit(cwd, revision) {
58
+ const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
59
+ // A revision that begins with a dash would be read as an option.
60
+ if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
61
+ throw fail("");
62
+ let hash = "";
63
+ try {
64
+ hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
65
+ }
66
+ catch (error) {
67
+ throw fail(`: ${error.message}`);
68
+ }
69
+ if (hash.length === 0)
70
+ throw fail("");
71
+ return hash;
72
+ }
73
+ const isReadPath = (path) => /^\.github\/workflows\/[^/]+\.ya?ml$/.test(path) || /(?:^|\/)action\.ya?ml$/.test(path);
74
+ const isNull = (sha) => /^0+$/.test(sha);
75
+ /** A blob read whole through git; an absent blob is an empty file. */
76
+ const readBlob = async (cwd, sha) => isNull(sha) ? "" : runGit(cwd, ["cat-file", "blob", sha]);
77
+ /** How many times each line's text occurs, lines being split at line feeds, one CR dropped, white space trimmed. */
78
+ function countLines(content) {
79
+ const counts = new Map();
80
+ for (const raw of content.split("\n")) {
81
+ const text = (raw.endsWith("\r") ? raw.slice(0, -1) : raw).trim();
82
+ if (text.length > 0)
83
+ counts.set(text, (counts.get(text) ?? 0) + 1);
84
+ }
85
+ return counts;
86
+ }
87
+ /** The value of a `uses:` line's text, or null when the text is not such a line. */
88
+ function usesValue(text) {
89
+ const match = /^(?:-\s+)?uses:(.*)$/.exec(text);
90
+ if (match === null)
91
+ return null;
92
+ let value = (match[1] ?? "").replace(/(?:^|\s)#.*$/, "").trim();
93
+ const quote = value.charAt(0);
94
+ if (value.length >= 2 && (quote === '"' || quote === "'") && value.endsWith(quote))
95
+ value = value.slice(1, -1);
96
+ return value;
97
+ }
98
+ /** Whether a value is unpinned and, if it is a repository reference, not by an allowed owner. */
99
+ function isFinding(value, allowed) {
100
+ if (value.length === 0 || value.startsWith("./"))
101
+ return false;
102
+ if (value.startsWith("docker://"))
103
+ return !/@sha256:[0-9a-fA-F]{64}$/.test(value);
104
+ const at = value.lastIndexOf("@");
105
+ const pinned = at > 0 && /^[0-9a-fA-F]{40}$/.test(value.slice(at + 1)) && value.slice(0, at).includes("/");
106
+ if (pinned)
107
+ return false;
108
+ const slash = value.indexOf("/");
109
+ return !(slash > 0 && allowed.includes(value.slice(0, slash)));
110
+ }
111
+ async function compare(cwd, from, to, allowed) {
112
+ const out = await runGit(cwd, ["diff", "--raw", "-z", "-M", "--no-abbrev", "--no-ext-diff", "--no-relative", from, to, "--"]);
113
+ const fields = out.split("\0");
114
+ const drafts = [];
115
+ for (let i = 0; i < fields.length; i++) {
116
+ const head = fields[i] ?? "";
117
+ if (!head.startsWith(":"))
118
+ continue;
119
+ const [, , oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
120
+ const moved = status.startsWith("R") || status.startsWith("C");
121
+ const oldPath = fields[++i] ?? "";
122
+ const newPath = moved ? (fields[++i] ?? "") : oldPath;
123
+ const letter = status.charAt(0);
124
+ if (!["A", "M", "R", "C", "T"].includes(letter) || !isReadPath(newPath))
125
+ continue;
126
+ const fromRead = letter === "M" || letter === "T" || letter === "R";
127
+ const before = countLines(fromRead ? await readBlob(cwd, oldSha) : "");
128
+ const after = countLines(await readBlob(cwd, newSha));
129
+ for (const [text, count] of after) {
130
+ if (count <= (before.get(text) ?? 0))
131
+ continue;
132
+ const value = usesValue(text);
133
+ if (value !== null && isFinding(value, allowed))
134
+ drafts.push({ file: newPath, value });
135
+ }
136
+ }
137
+ return drafts;
138
+ }
139
+ /** The text after the sanction key on every such line of the range's commit messages. */
140
+ async function readSanctionLines(cwd, from, to) {
141
+ const out = await runGit(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"]);
142
+ const prefix = `${SANCTION_KEY}:`;
143
+ return out
144
+ .split(/[\n\0]/)
145
+ .map((line) => line.trim())
146
+ .filter((line) => line.startsWith(prefix))
147
+ .map((line) => line.slice(prefix.length).trim());
148
+ }
149
+ function sanctionFor(id, lines, given) {
150
+ const reason = Object.hasOwn(given, id) ? given[id] : undefined;
151
+ if (typeof reason === "string" && reason.trim().length > 0)
152
+ return reason.trim();
153
+ for (const line of lines) {
154
+ const rest = line.slice(id.length);
155
+ if (line.startsWith(id) && /^\s+\S/.test(rest))
156
+ return rest.trim();
157
+ }
158
+ return null;
159
+ }
160
+ /**
161
+ * Report each unpinned `uses:` reference `base..candidate` adds. Fails while any
162
+ * finding is unsanctioned, and fails closed: a revision that names no commit,
163
+ * or any git invocation that fails, is a `fail` record naming the cause.
164
+ */
165
+ export async function check(options) {
166
+ try {
167
+ const { cwd, base, candidate } = options;
168
+ const allowed = Array.isArray(options.allowedOwners) ? options.allowedOwners.filter((o) => typeof o === "string") : [];
169
+ const baseCommit = await resolveCommit(cwd, base);
170
+ const candidateCommit = await resolveCommit(cwd, candidate);
171
+ let ancestor;
172
+ try {
173
+ ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
174
+ }
175
+ catch (error) {
176
+ throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
177
+ }
178
+ const drafts = await compare(cwd, ancestor, candidateCommit, allowed);
179
+ if (drafts.length === 0)
180
+ return { name, status: "pass", evidence: ["no unpinned action reference added"] };
181
+ const lines = await readSanctionLines(cwd, baseCommit, candidateCommit);
182
+ let open = false;
183
+ const evidence = drafts.map((draft) => {
184
+ const reason = sanctionFor(`unpinned-action:${draft.file}`, lines, options.sanctions ?? {});
185
+ if (reason === null)
186
+ open = true;
187
+ const tail = reason === null ? "" : ` - sanctioned: ${escapeText(reason)}`;
188
+ return `${escapeText(draft.file)}: unpinned-action ${escapeText(draft.value)}${tail}`;
189
+ });
190
+ return { name, status: open ? "fail" : "pass", evidence };
191
+ }
192
+ catch (error) {
193
+ return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))] };
194
+ }
195
+ }
196
+ //# sourceMappingURL=action-pins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-pins.js","sourceRoot":"","sources":["../../src/checks/action-pins.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Autosquash leftovers.
3
+ *
4
+ * `git commit --fixup`, `--squash` and `--fixup=amend:` write a commit whose
5
+ * subject begins with `fixup! `, `squash! ` or `amend! `, a note that
6
+ * `git rebase --autosquash` is to fold it into an earlier commit. This check
7
+ * reads the subject of every commit of `base..candidate` as `git log --format=%s`
8
+ * prints it (the first paragraph, its lines joined by spaces) and reports each
9
+ * commit whose subject begins with one of `AUTOSQUASH_PREFIXES`.
10
+ *
11
+ * The list is read each time the check runs: a consumer appends its own entry.
12
+ * There is no sanction; the fix is to squash the commits before the range lands.
13
+ */
14
+ import type { CheckRecord } from "../index.js";
15
+ export declare const name = "RT-29";
16
+ /** The prefixes git's autosquash reads, compared character for character. */
17
+ export declare const AUTOSQUASH_PREFIXES: string[];
18
+ export interface AutosquashOptions {
19
+ readonly cwd: string;
20
+ readonly base: string;
21
+ readonly candidate: string;
22
+ }
23
+ export declare function check(options: AutosquashOptions): Promise<CheckRecord>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Autosquash leftovers.
3
+ *
4
+ * `git commit --fixup`, `--squash` and `--fixup=amend:` write a commit whose
5
+ * subject begins with `fixup! `, `squash! ` or `amend! `, a note that
6
+ * `git rebase --autosquash` is to fold it into an earlier commit. This check
7
+ * reads the subject of every commit of `base..candidate` as `git log --format=%s`
8
+ * prints it (the first paragraph, its lines joined by spaces) and reports each
9
+ * commit whose subject begins with one of `AUTOSQUASH_PREFIXES`.
10
+ *
11
+ * The list is read each time the check runs: a consumer appends its own entry.
12
+ * There is no sanction; the fix is to squash the commits before the range lands.
13
+ */
14
+ import { execFile } from "node:child_process";
15
+ export const name = "RT-29";
16
+ /** The prefixes git's autosquash reads, compared character for character. */
17
+ export const AUTOSQUASH_PREFIXES = ["fixup! ", "squash! ", "amend! "];
18
+ const ID_LENGTH = 12;
19
+ function runGit(cwd, args) {
20
+ return new Promise((resolve, reject) => {
21
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
22
+ if (error)
23
+ reject(new Error(stderr.trim() || error.message));
24
+ else
25
+ resolve(stdout);
26
+ });
27
+ });
28
+ }
29
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
30
+ function escapeText(text) {
31
+ let escaped = "";
32
+ for (const char of text) {
33
+ const codePoint = char.codePointAt(0) ?? 0;
34
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
35
+ }
36
+ return escaped;
37
+ }
38
+ /** The commit a revision names; throws a message that names the revision when it names none. */
39
+ async function resolveCommit(cwd, revision) {
40
+ try {
41
+ const out = await runGit(cwd, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${revision}^{commit}`]);
42
+ const id = out.trim();
43
+ if (id !== "")
44
+ return id;
45
+ }
46
+ catch {
47
+ // a revision that does not resolve is reported by name below
48
+ }
49
+ throw new Error(`revision ${revision} names no commit`);
50
+ }
51
+ export async function check(options) {
52
+ try {
53
+ const base = await resolveCommit(options.cwd, options.base);
54
+ const candidate = await resolveCommit(options.cwd, options.candidate);
55
+ const log = await runGit(options.cwd, [
56
+ "log",
57
+ "-z",
58
+ "--topo-order",
59
+ "--reverse",
60
+ "--encoding=UTF-8",
61
+ "--format=%H %s",
62
+ `${base}..${candidate}`,
63
+ "--",
64
+ ]);
65
+ const prefixes = AUTOSQUASH_PREFIXES.filter((prefix) => prefix !== "");
66
+ const findings = [];
67
+ for (const entry of log.split("\0")) {
68
+ if (entry === "")
69
+ continue;
70
+ const space = entry.indexOf(" ");
71
+ const id = entry.slice(0, space);
72
+ const subject = entry.slice(space + 1);
73
+ if (prefixes.some((prefix) => subject.startsWith(prefix))) {
74
+ findings.push(`${id.slice(0, ID_LENGTH)} ${escapeText(subject)}`);
75
+ }
76
+ }
77
+ if (findings.length > 0)
78
+ return { name, status: "fail", evidence: findings };
79
+ return { name, status: "pass", evidence: ["no autosquash subject in the range"] };
80
+ }
81
+ catch (error) {
82
+ return { name, status: "fail", evidence: [`git failed: ${escapeText(error instanceof Error ? error.message : String(error))}`] };
83
+ }
84
+ }
85
+ //# sourceMappingURL=autosquash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autosquash.js","sourceRoot":"","sources":["../../src/checks/autosquash.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Bidirectional controls.
3
+ *
4
+ * Nine Unicode characters - U+202A, U+202B, U+202C, U+202D, U+202E (the
5
+ * embeddings and overrides) and U+2066, U+2067, U+2068, U+2069 (the isolates) -
6
+ * reorder the text around them when it is shown and do nothing when a compiler
7
+ * reads it, so a line holding them reads one way to a reviewer and runs another
8
+ * (CVE-2021-42574, "Trojan Source"). This check reads every path
9
+ * `base..candidate` adds, changes or renames onto whose entry at the candidate
10
+ * is a regular file (mode 100644 or 100755), whole through git at the merge
11
+ * base of `base` and `candidate` and at the candidate, renames detected, never
12
+ * from the working tree. A symbolic link or a gitlink is never read, and a file
13
+ * holding a NUL byte is not text: such a candidate yields nothing and such a
14
+ * source counts as none.
15
+ *
16
+ * Text is read as UTF-8 and split at line feeds, one carriage return at a
17
+ * line's end dropped. A line is added when the candidate's file holds its text,
18
+ * white space at both ends removed, more often than the source file held it -
19
+ * the file at the merge base, the one a rename came from, or none for a file
20
+ * the range adds. A moved or re-indented line is not added, and a text added
21
+ * more than once counts once.
22
+ *
23
+ * A file whose added lines hold at least one of the nine is one finding
24
+ * `bidi-control`, its evidence line `<file>: bidi-control <characters>`: each
25
+ * distinct character, `U+` and four upper-case hexadecimal digits, ascending,
26
+ * joined by a comma. The finding's id is `bidi-control:<file>`, sanctioned by a
27
+ * `Sanctioned-Weakening: <id> <reason>` line in a commit message of the range
28
+ * or by the caller's `sanctions`; the reason ends the evidence line.
29
+ */
30
+ import type { CheckRecord } from "../index.js";
31
+ export declare const name = "RT-93";
32
+ export interface BidiControlsOptions {
33
+ readonly cwd: string;
34
+ readonly base: string;
35
+ readonly candidate: string;
36
+ /** Ids (or `bidi-control:<file>`) mapped to reasons. */
37
+ readonly sanctions?: Readonly<Record<string, string>> | undefined;
38
+ }
39
+ /** One file whose added lines hold the nine. */
40
+ export interface BidiControlFinding {
41
+ readonly id: string;
42
+ readonly category: "bidi-control";
43
+ readonly file: string;
44
+ /** Each distinct character, as `U+XXXX`, ascending. */
45
+ readonly characters: readonly string[];
46
+ /** The sanction's reason, or `null` while unsanctioned. */
47
+ readonly sanction: string | null;
48
+ }
49
+ export interface BidiControlsRecord extends CheckRecord {
50
+ readonly findings: readonly BidiControlFinding[];
51
+ }
52
+ /**
53
+ * Report each file `base..candidate` adds a line holding one of the nine to.
54
+ * Fails while any finding is unsanctioned, and fails closed: a revision that
55
+ * names no commit, or any git invocation that fails, is a `fail` record naming
56
+ * the cause - never a `pass`, never a throw.
57
+ */
58
+ export declare function check(options: BidiControlsOptions): Promise<BidiControlsRecord>;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Bidirectional controls.
3
+ *
4
+ * Nine Unicode characters - U+202A, U+202B, U+202C, U+202D, U+202E (the
5
+ * embeddings and overrides) and U+2066, U+2067, U+2068, U+2069 (the isolates) -
6
+ * reorder the text around them when it is shown and do nothing when a compiler
7
+ * reads it, so a line holding them reads one way to a reviewer and runs another
8
+ * (CVE-2021-42574, "Trojan Source"). This check reads every path
9
+ * `base..candidate` adds, changes or renames onto whose entry at the candidate
10
+ * is a regular file (mode 100644 or 100755), whole through git at the merge
11
+ * base of `base` and `candidate` and at the candidate, renames detected, never
12
+ * from the working tree. A symbolic link or a gitlink is never read, and a file
13
+ * holding a NUL byte is not text: such a candidate yields nothing and such a
14
+ * source counts as none.
15
+ *
16
+ * Text is read as UTF-8 and split at line feeds, one carriage return at a
17
+ * line's end dropped. A line is added when the candidate's file holds its text,
18
+ * white space at both ends removed, more often than the source file held it -
19
+ * the file at the merge base, the one a rename came from, or none for a file
20
+ * the range adds. A moved or re-indented line is not added, and a text added
21
+ * more than once counts once.
22
+ *
23
+ * A file whose added lines hold at least one of the nine is one finding
24
+ * `bidi-control`, its evidence line `<file>: bidi-control <characters>`: each
25
+ * distinct character, `U+` and four upper-case hexadecimal digits, ascending,
26
+ * joined by a comma. The finding's id is `bidi-control:<file>`, sanctioned by a
27
+ * `Sanctioned-Weakening: <id> <reason>` line in a commit message of the range
28
+ * or by the caller's `sanctions`; the reason ends the evidence line.
29
+ */
30
+ import { execFile } from "node:child_process";
31
+ import { SANCTION_KEY } from "../weakened-tests.js";
32
+ export const name = "RT-93";
33
+ const CONTROLS = /[\u202a-\u202e\u2066-\u2069]/g;
34
+ function runGit(cwd, args) {
35
+ return new Promise((resolve, reject) => {
36
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
37
+ if (error)
38
+ reject(new Error(stderr.toString("utf8").trim() || error.message));
39
+ else
40
+ resolve(stdout);
41
+ });
42
+ });
43
+ }
44
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
45
+ function escapeText(text) {
46
+ let escaped = "";
47
+ for (const char of text) {
48
+ const codePoint = char.codePointAt(0) ?? 0;
49
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
50
+ }
51
+ return escaped;
52
+ }
53
+ async function resolveCommit(cwd, revision) {
54
+ const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
55
+ // A revision that begins with a dash would be read as an option.
56
+ if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
57
+ throw fail("");
58
+ let hash = "";
59
+ try {
60
+ hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).toString("utf8").trim();
61
+ }
62
+ catch (error) {
63
+ throw fail(`: ${error.message}`);
64
+ }
65
+ if (hash.length === 0)
66
+ throw fail("");
67
+ return hash;
68
+ }
69
+ const REGULAR = new Set(["100644", "100755"]);
70
+ /** A blob's lines, trimmed, counted; `null` for no entry, or a file holding a NUL byte. */
71
+ async function readTexts(cwd, mode, sha) {
72
+ if (!REGULAR.has(mode))
73
+ return null;
74
+ const bytes = await runGit(cwd, ["cat-file", "blob", sha]);
75
+ if (bytes.includes(0))
76
+ return null;
77
+ const counts = new Map();
78
+ for (const raw of bytes.toString("utf8").split("\n")) {
79
+ const text = raw.replace(/\r$/, "").trim();
80
+ counts.set(text, (counts.get(text) ?? 0) + 1);
81
+ }
82
+ return counts;
83
+ }
84
+ async function readDrafts(cwd, from, to) {
85
+ const out = (await runGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--no-relative", from, to, "--"])).toString("utf8");
86
+ const fields = out.split("\0");
87
+ const drafts = [];
88
+ for (let i = 0; i < fields.length; i++) {
89
+ const head = fields[i] ?? "";
90
+ if (!head.startsWith(":"))
91
+ continue;
92
+ const [oldMode = "", newMode = "", oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
93
+ const first = fields[++i] ?? "";
94
+ const renamed = status.startsWith("R") || status.startsWith("C");
95
+ const file = renamed ? (fields[++i] ?? "") : first;
96
+ if (status.startsWith("D"))
97
+ continue;
98
+ const now = await readTexts(cwd, newMode, newSha);
99
+ if (now === null)
100
+ continue;
101
+ const before = status.startsWith("A") ? null : await readTexts(cwd, oldMode, oldSha);
102
+ const found = new Set();
103
+ for (const [text, count] of now) {
104
+ if (count <= (before?.get(text) ?? 0))
105
+ continue;
106
+ for (const char of text.match(CONTROLS) ?? [])
107
+ found.add(`U+${(char.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")}`);
108
+ }
109
+ if (found.size > 0)
110
+ drafts.push({ file, characters: [...found].sort() });
111
+ }
112
+ return drafts.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
113
+ }
114
+ /** The text after the sanction key on every such line of the range's commit messages. */
115
+ async function readSanctionLines(cwd, from, to) {
116
+ const out = (await runGit(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"])).toString("utf8");
117
+ const prefix = `${SANCTION_KEY}:`;
118
+ return out
119
+ .split(/[\n\0]/)
120
+ .map((line) => line.trim())
121
+ .filter((line) => line.startsWith(prefix))
122
+ .map((line) => line.slice(prefix.length).trim());
123
+ }
124
+ function sanctionFor(id, lines, given) {
125
+ const reason = Object.hasOwn(given, id) ? given[id] : undefined;
126
+ if (typeof reason === "string" && reason.trim().length > 0)
127
+ return reason.trim();
128
+ for (const line of lines) {
129
+ const rest = line.slice(id.length);
130
+ if (line.startsWith(id) && /^\s+\S/.test(rest))
131
+ return rest.trim();
132
+ }
133
+ return null;
134
+ }
135
+ function describeFinding(finding) {
136
+ const tail = finding.sanction === null ? "" : ` - sanctioned: ${escapeText(finding.sanction)}`;
137
+ return `${escapeText(finding.file)}: bidi-control ${finding.characters.join(",")}${tail}`;
138
+ }
139
+ /**
140
+ * Report each file `base..candidate` adds a line holding one of the nine to.
141
+ * Fails while any finding is unsanctioned, and fails closed: a revision that
142
+ * names no commit, or any git invocation that fails, is a `fail` record naming
143
+ * the cause - never a `pass`, never a throw.
144
+ */
145
+ export async function check(options) {
146
+ try {
147
+ const { cwd, base, candidate } = options;
148
+ const baseCommit = await resolveCommit(cwd, base);
149
+ const candidateCommit = await resolveCommit(cwd, candidate);
150
+ let ancestor;
151
+ try {
152
+ ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).toString("utf8").trim();
153
+ }
154
+ catch (error) {
155
+ throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
156
+ }
157
+ const drafts = await readDrafts(cwd, ancestor, candidateCommit);
158
+ const lines = drafts.length === 0 ? [] : await readSanctionLines(cwd, baseCommit, candidateCommit);
159
+ const findings = drafts.map((draft) => {
160
+ const id = `bidi-control:${draft.file}`;
161
+ return { id, category: "bidi-control", ...draft, sanction: sanctionFor(id, lines, options.sanctions ?? {}) };
162
+ });
163
+ if (findings.length === 0)
164
+ return { name, status: "pass", evidence: ["no bidirectional control character added"], findings };
165
+ const open = findings.some((finding) => finding.sanction === null);
166
+ return { name, status: open ? "fail" : "pass", evidence: findings.map(describeFinding), findings };
167
+ }
168
+ catch (error) {
169
+ return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))], findings: [] };
170
+ }
171
+ }
172
+ //# sourceMappingURL=bidi-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bidi-controls.js","sourceRoot":"","sources":["../../src/checks/bidi-controls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG9C,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC;AAE5B,MAAM,QAAQ,GAAG,+BAA+B,CAAC;AAyBjD,SAAS,MAAM,CAAC,GAAW,EAAE,IAAuB;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CAAC,KAAK,EAAE,IAAgB,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACtH,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;;gBACzE,OAAO,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,+FAA+F;AAC/F,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC3C,OAAO,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC;IAC/G,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,QAAgB;IACxD,MAAM,IAAI,GAAG,CAAC,KAAa,EAAS,EAAE,CAAC,IAAI,KAAK,CAAC,aAAa,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,2BAA2B,KAAK,EAAE,CAAC,CAAC;IAC9H,iEAAiE;IACjE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;IACtG,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IACnH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,CAAC,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;IACtC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE9C,2FAA2F;AAC3F,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,IAAY,EAAE,GAAW;IAC7D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAOD,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,IAAY,EAAE,EAAU;IAC7D,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5I,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACpC,MAAM,CAAC,OAAO,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrG,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACnD,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACrC,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAClD,IAAI,GAAG,KAAK,IAAI;YAAE,SAAS;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACrF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;QAChC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC;YAChC,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAAE,SAAS;YAChD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE;gBAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1I,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,yFAAyF;AACzF,KAAK,UAAU,iBAAiB,CAAC,GAAW,EAAE,IAAY,EAAE,EAAU;IACpE,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,IAAI,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrG,MAAM,MAAM,GAAG,GAAG,YAAY,GAAG,CAAC;IAClC,OAAO,GAAG;SACP,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;SACzC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,WAAW,CAAC,EAAU,EAAE,KAAwB,EAAE,KAAuC;IAChG,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACjF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,OAA2B;IAClD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC/F,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;AAC5F,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,OAA4B;IACtD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;QACzC,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClD,MAAM,eAAe,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC5D,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACtG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,qBAAqB,UAAU,CAAC,IAAI,CAAC,UAAU,UAAU,CAAC,SAAS,CAAC,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QACxH,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;QAChE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,iBAAiB,CAAC,GAAG,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;QACnG,MAAM,QAAQ,GAAyB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YAC1D,MAAM,EAAE,GAAG,gBAAgB,KAAK,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/G,CAAC,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,0CAA0C,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC7H,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,UAAU,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAChI,CAAC;AACH,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Binary files.
3
+ *
4
+ * A review reads a range line by line and a binary file has no lines: what the
5
+ * range adds of it enters the history whole and unreviewed. This check reads
6
+ * the files the range adds, changes or renames, through git at the candidate,
7
+ * and reports each whose new content is binary by git's own test - a NUL byte
8
+ * among its first `BINARY_PROBE_BYTES` bytes - and was not already that same
9
+ * content at the merge base. The file's name and `.gitattributes` decide
10
+ * nothing, and neither does the working tree.
11
+ *
12
+ * A finding is `binary-changed` when the file's source blob at the merge base
13
+ * is binary too, and `binary-added` otherwise. It is sanctioned by a
14
+ * `Sanctioned-Weakening: <category>:<path> <reason>` line in a commit message
15
+ * of the range or by the caller's `sanctions`. A path ending with one of the
16
+ * caller's `allowedSuffixes` yields no finding.
17
+ */
18
+ import type { CheckRecord } from "../index.js";
19
+ export declare const name = "RT-55";
20
+ /** The number of leading bytes git looks at for a NUL byte. */
21
+ export declare const BINARY_PROBE_BYTES = 8000;
22
+ /** The option a configuration file may set for this check. */
23
+ export declare const optionKinds: {
24
+ allowedSuffixes: string;
25
+ };
26
+ export type BinaryCategory = "binary-added" | "binary-changed";
27
+ export interface BinaryFilesOptions {
28
+ readonly cwd: string;
29
+ readonly base: string;
30
+ readonly candidate: string;
31
+ /** Ids (`<category>:<path>`) mapped to reasons. */
32
+ readonly sanctions?: Readonly<Record<string, string>> | undefined;
33
+ /** Paths ending with one of these strings yield no finding. */
34
+ readonly allowedSuffixes?: readonly string[] | undefined;
35
+ }
36
+ export interface BinaryFinding {
37
+ readonly id: string;
38
+ readonly category: BinaryCategory;
39
+ readonly path: string;
40
+ readonly size: number;
41
+ /** The sanction's reason, or `null` while unsanctioned. */
42
+ readonly sanction: string | null;
43
+ }
44
+ export interface BinaryFilesRecord extends CheckRecord {
45
+ readonly findings: readonly BinaryFinding[];
46
+ }
47
+ /**
48
+ * Report each file `base..candidate` adds or changes into binary content.
49
+ * Fails while any finding is unsanctioned, and fails closed: a revision that
50
+ * names no commit, or any git invocation that fails, is a `fail` record naming
51
+ * the cause - never a `pass`, never a throw.
52
+ */
53
+ export declare function check(options: BinaryFilesOptions): Promise<BinaryFilesRecord>;