@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,157 @@
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 { execFile } from "node:child_process";
19
+ import { SANCTION_KEY } from "../weakened-tests.js";
20
+ export const name = "RT-55";
21
+ /** The number of leading bytes git looks at for a NUL byte. */
22
+ export const BINARY_PROBE_BYTES = 8000;
23
+ /** The option a configuration file may set for this check. */
24
+ export const optionKinds = { allowedSuffixes: "strings" };
25
+ function runGit(cwd, args) {
26
+ return new Promise((resolve, reject) => {
27
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
28
+ if (error)
29
+ reject(new Error(stderr.toString("utf8").trim() || error.message));
30
+ else
31
+ resolve(stdout);
32
+ });
33
+ });
34
+ }
35
+ const runText = async (cwd, args) => (await runGit(cwd, args)).toString("utf8");
36
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
37
+ function escapeText(text) {
38
+ let escaped = "";
39
+ for (const char of text) {
40
+ const codePoint = char.codePointAt(0) ?? 0;
41
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
42
+ }
43
+ return escaped;
44
+ }
45
+ async function resolveCommit(cwd, revision) {
46
+ const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
47
+ // A revision that begins with a dash would be read as an option.
48
+ if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
49
+ throw fail("");
50
+ let hash = "";
51
+ try {
52
+ hash = (await runText(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
53
+ }
54
+ catch (error) {
55
+ throw fail(`: ${error.message}`);
56
+ }
57
+ if (hash.length === 0)
58
+ throw fail("");
59
+ return hash;
60
+ }
61
+ const isNull = (sha) => /^0+$/.test(sha);
62
+ /** A blob's content read through git, or `null` when there is no such blob. */
63
+ async function readBlob(cwd, sha) {
64
+ return isNull(sha) ? null : runGit(cwd, ["cat-file", "blob", sha]);
65
+ }
66
+ const isBinary = (content) => content.subarray(0, BINARY_PROBE_BYTES).includes(0);
67
+ async function compare(cwd, from, to, allowed) {
68
+ const out = await runText(cwd, ["diff", "--raw", "-z", "-M", "--no-abbrev", "--no-ext-diff", "--no-relative", from, to, "--"]);
69
+ const fields = out.split("\0");
70
+ const drafts = [];
71
+ for (let i = 0; i < fields.length; i++) {
72
+ const head = fields[i] ?? "";
73
+ if (!head.startsWith(":"))
74
+ continue;
75
+ const [, newMode = "", oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
76
+ i++;
77
+ const moved = status.startsWith("R") || status.startsWith("C");
78
+ const path = moved ? (fields[++i] ?? "") : (fields[i] ?? "");
79
+ const letter = status.charAt(0);
80
+ if (!["A", "M", "R", "C", "T"].includes(letter))
81
+ continue;
82
+ if (newMode !== "100644" && newMode !== "100755")
83
+ continue;
84
+ // A copy git does not list as a rename is an added file.
85
+ const sourceSha = letter === "A" || letter === "C" ? "" : oldSha;
86
+ if (sourceSha !== "" && sourceSha === newSha)
87
+ continue;
88
+ if (allowed.some((suffix) => suffix.length > 0 && path.endsWith(suffix)))
89
+ continue;
90
+ const content = await readBlob(cwd, newSha);
91
+ if (content === null || !isBinary(content))
92
+ continue;
93
+ const source = sourceSha === "" ? null : await readBlob(cwd, sourceSha);
94
+ drafts.push({ category: source !== null && isBinary(source) ? "binary-changed" : "binary-added", path, size: content.length });
95
+ }
96
+ const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
97
+ return drafts.sort((a, b) => order(a.path, b.path) || order(a.category, b.category));
98
+ }
99
+ /** The text after the sanction key on every such line of the range's commit messages. */
100
+ async function readSanctionLines(cwd, from, to) {
101
+ const out = await runText(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"]);
102
+ const prefix = `${SANCTION_KEY}:`;
103
+ return out
104
+ .split(/[\n\0]/)
105
+ .map((line) => line.trim())
106
+ .filter((line) => line.startsWith(prefix))
107
+ .map((line) => line.slice(prefix.length).trim());
108
+ }
109
+ function sanctionFor(id, lines, given) {
110
+ const reason = Object.hasOwn(given, id) ? given[id] : undefined;
111
+ if (typeof reason === "string" && reason.trim().length > 0)
112
+ return reason.trim();
113
+ for (const line of lines) {
114
+ const rest = line.slice(id.length);
115
+ if (line.startsWith(id) && /^\s+\S/.test(rest))
116
+ return rest.trim();
117
+ }
118
+ return null;
119
+ }
120
+ function describeFinding(finding) {
121
+ const tail = finding.sanction === null ? "" : ` - sanctioned: ${escapeText(finding.sanction)}`;
122
+ return `${escapeText(finding.path)}: ${finding.category}, ${finding.size} bytes${tail}`;
123
+ }
124
+ /**
125
+ * Report each file `base..candidate` adds or changes into binary content.
126
+ * Fails while any finding is unsanctioned, and fails closed: a revision that
127
+ * names no commit, or any git invocation that fails, is a `fail` record naming
128
+ * the cause - never a `pass`, never a throw.
129
+ */
130
+ export async function check(options) {
131
+ try {
132
+ const { cwd, base, candidate } = options;
133
+ const baseCommit = await resolveCommit(cwd, base);
134
+ const candidateCommit = await resolveCommit(cwd, candidate);
135
+ let ancestor;
136
+ try {
137
+ ancestor = (await runText(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
138
+ }
139
+ catch (error) {
140
+ throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
141
+ }
142
+ const drafts = await compare(cwd, ancestor, candidateCommit, options.allowedSuffixes ?? []);
143
+ const lines = drafts.length === 0 ? [] : await readSanctionLines(cwd, baseCommit, candidateCommit);
144
+ const findings = drafts.map((draft) => {
145
+ const id = `${draft.category}:${draft.path}`;
146
+ return { id, ...draft, sanction: sanctionFor(id, lines, options.sanctions ?? {}) };
147
+ });
148
+ if (findings.length === 0)
149
+ return { name, status: "pass", evidence: ["no binary file added or changed"], findings };
150
+ const open = findings.some((finding) => finding.sanction === null);
151
+ return { name, status: open ? "fail" : "pass", evidence: findings.map(describeFinding), findings };
152
+ }
153
+ catch (error) {
154
+ return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))], findings: [] };
155
+ }
156
+ }
157
+ //# sourceMappingURL=binary-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary-files.js","sourceRoot":"","sources":["../../src/checks/binary-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Configuration file changed.
3
+ *
4
+ * The command reads its settings from one file, so a range that loosens that
5
+ * file is judged by what it found, and every later range by the loosened
6
+ * settings. This check fails a range that adds, changes, deletes or renames the
7
+ * file the run's settings were read from.
8
+ *
9
+ * The file it watches: with no `config` option (no `--config`), repo-truth.json
10
+ * at the top directory of the repository, by that path, from any directory of
11
+ * the repository; with `config`, that path, read against `cwd` when relative, when
12
+ * its directory (symbolic links resolved) is the top directory or below it. A
13
+ * file outside the repository is one no range can change: a `skip` naming it.
14
+ * A revision that names no commit, a git that fails or a `config` path whose
15
+ * directory cannot be read is a `fail`.
16
+ *
17
+ * The range is read through git, from the merge base of `base` and `candidate`
18
+ * to `candidate`, renames detected; the working tree is never read. An entry
19
+ * that names the watched path, on either side of a rename, is a finding of
20
+ * category `config-added`, `config-changed`, `config-deleted` or
21
+ * `config-renamed`, in one evidence line `<file>: <category>`.
22
+ *
23
+ * A finding is sanctioned by a `Sanctioned-Weakening: <id> <reason>` line in a
24
+ * commit message of the range or by the caller's `sanctions`. Its id is
25
+ * `<category>:<file>`. The check declares no options.
26
+ */
27
+ import type { CheckRecord } from "../index.js";
28
+ export declare const name = "RT-66";
29
+ export type ConfigCategory = "config-added" | "config-changed" | "config-deleted" | "config-renamed";
30
+ export interface ConfigChangesOptions {
31
+ readonly cwd: string;
32
+ readonly base: string;
33
+ readonly candidate: string;
34
+ /** The path `--config` named; absent when the settings were read from repo-truth.json. */
35
+ readonly config?: string | undefined;
36
+ /** Ids mapped to reasons. */
37
+ readonly sanctions?: Readonly<Record<string, string>> | undefined;
38
+ }
39
+ export interface ConfigFinding {
40
+ readonly id: string;
41
+ readonly category: ConfigCategory;
42
+ readonly file: string;
43
+ /** The other path of a rename; `null` otherwise. */
44
+ readonly other: string | null;
45
+ /** The sanction's reason, or `null` while unsanctioned. */
46
+ readonly sanction: string | null;
47
+ }
48
+ export interface ConfigChangesRecord extends CheckRecord {
49
+ readonly findings: readonly ConfigFinding[];
50
+ }
51
+ /**
52
+ * Report a change `base..candidate` makes to the configuration file the run
53
+ * read. Fails while a finding is unsanctioned, and fails closed: a revision
54
+ * that names no commit, or any git invocation that fails, is a `fail` record
55
+ * naming the cause - never a `pass`, never a throw.
56
+ */
57
+ export declare function check(options: ConfigChangesOptions): Promise<ConfigChangesRecord>;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Configuration file changed.
3
+ *
4
+ * The command reads its settings from one file, so a range that loosens that
5
+ * file is judged by what it found, and every later range by the loosened
6
+ * settings. This check fails a range that adds, changes, deletes or renames the
7
+ * file the run's settings were read from.
8
+ *
9
+ * The file it watches: with no `config` option (no `--config`), repo-truth.json
10
+ * at the top directory of the repository, by that path, from any directory of
11
+ * the repository; with `config`, that path, read against `cwd` when relative, when
12
+ * its directory (symbolic links resolved) is the top directory or below it. A
13
+ * file outside the repository is one no range can change: a `skip` naming it.
14
+ * A revision that names no commit, a git that fails or a `config` path whose
15
+ * directory cannot be read is a `fail`.
16
+ *
17
+ * The range is read through git, from the merge base of `base` and `candidate`
18
+ * to `candidate`, renames detected; the working tree is never read. An entry
19
+ * that names the watched path, on either side of a rename, is a finding of
20
+ * category `config-added`, `config-changed`, `config-deleted` or
21
+ * `config-renamed`, in one evidence line `<file>: <category>`.
22
+ *
23
+ * A finding is sanctioned by a `Sanctioned-Weakening: <id> <reason>` line in a
24
+ * commit message of the range or by the caller's `sanctions`. Its id is
25
+ * `<category>:<file>`. The check declares no options.
26
+ */
27
+ import { execFile } from "node:child_process";
28
+ import { realpath } from "node:fs/promises";
29
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
30
+ import { SANCTION_KEY } from "../weakened-tests.js";
31
+ export const name = "RT-66";
32
+ function runGit(cwd, args) {
33
+ return new Promise((resolvePromise, reject) => {
34
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
35
+ if (error)
36
+ reject(new Error(stderr.trim() || error.message));
37
+ else
38
+ resolvePromise(stdout);
39
+ });
40
+ });
41
+ }
42
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
43
+ function escapeText(text) {
44
+ let escaped = "";
45
+ for (const char of text) {
46
+ const codePoint = char.codePointAt(0) ?? 0;
47
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
48
+ }
49
+ return escaped;
50
+ }
51
+ async function resolveCommit(cwd, revision) {
52
+ const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
53
+ // A revision that begins with a dash would be read as an option.
54
+ if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
55
+ throw fail("");
56
+ let hash = "";
57
+ try {
58
+ hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
59
+ }
60
+ catch (error) {
61
+ throw fail(`: ${error.message}`);
62
+ }
63
+ if (hash.length === 0)
64
+ throw fail("");
65
+ return hash;
66
+ }
67
+ /** The watched path from the top directory, or `null` when the file is outside the repository. */
68
+ async function watchedPath(cwd, config) {
69
+ if (config === undefined)
70
+ return "repo-truth.json";
71
+ const top = await realpath((await runGit(cwd, ["rev-parse", "--show-toplevel"])).replace(/\n$/, ""));
72
+ const absolute = isAbsolute(config) ? config : resolve(cwd, config);
73
+ let directory;
74
+ try {
75
+ directory = await realpath(dirname(absolute));
76
+ }
77
+ catch (error) {
78
+ throw new Error(`${escapeText(config)}: cannot read the directory of the configuration file (${error.message})`);
79
+ }
80
+ const path = relative(top, join(directory, basename(absolute)));
81
+ if (path === "" || path === ".." || path.startsWith("../") || isAbsolute(path))
82
+ return null;
83
+ return path.split("\\").join("/");
84
+ }
85
+ async function compare(cwd, from, to, file) {
86
+ const out = await runGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--no-relative", from, to, "--"]);
87
+ const fields = out.split("\0");
88
+ const drafts = [];
89
+ for (let i = 0; i < fields.length; i++) {
90
+ const head = fields[i] ?? "";
91
+ if (!head.startsWith(":"))
92
+ continue;
93
+ const status = head.slice(1).split(" ")[4] ?? "";
94
+ const first = fields[++i] ?? "";
95
+ const pair = status.startsWith("R") || status.startsWith("C");
96
+ const second = pair ? (fields[++i] ?? "") : first;
97
+ if (first !== file && second !== file)
98
+ continue;
99
+ if (status.startsWith("R"))
100
+ drafts.push({ category: "config-renamed", other: first === file ? second : first });
101
+ else if (status.startsWith("A") || status.startsWith("C"))
102
+ drafts.push({ category: "config-added", other: null });
103
+ else if (status.startsWith("D"))
104
+ drafts.push({ category: "config-deleted", other: null });
105
+ else
106
+ drafts.push({ category: "config-changed", other: null });
107
+ }
108
+ return drafts;
109
+ }
110
+ /** The text after the sanction key on every such line of the range's commit messages. */
111
+ async function readSanctionLines(cwd, from, to) {
112
+ const out = await runGit(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"]);
113
+ const prefix = `${SANCTION_KEY}:`;
114
+ return out
115
+ .split(/[\n\0]/)
116
+ .map((line) => line.trim())
117
+ .filter((line) => line.startsWith(prefix))
118
+ .map((line) => line.slice(prefix.length).trim());
119
+ }
120
+ function sanctionFor(id, lines, given) {
121
+ const reason = Object.hasOwn(given, id) ? given[id] : undefined;
122
+ if (typeof reason === "string" && reason.trim().length > 0)
123
+ return reason.trim();
124
+ for (const line of lines) {
125
+ const rest = line.slice(id.length);
126
+ if (line.startsWith(id) && /^\s+\S/.test(rest))
127
+ return rest.trim();
128
+ }
129
+ return null;
130
+ }
131
+ function describeFinding(finding) {
132
+ const other = finding.other === null ? "" : ` (other path: ${escapeText(finding.other)})`;
133
+ const tail = finding.sanction === null ? "" : ` - sanctioned: ${escapeText(finding.sanction)}`;
134
+ return `${escapeText(finding.file)}: ${finding.category}${other}${tail}`;
135
+ }
136
+ /**
137
+ * Report a change `base..candidate` makes to the configuration file the run
138
+ * read. Fails while a finding is unsanctioned, and fails closed: a revision
139
+ * that names no commit, or any git invocation that fails, is a `fail` record
140
+ * naming the cause - never a `pass`, never a throw.
141
+ */
142
+ export async function check(options) {
143
+ try {
144
+ const { cwd, base, candidate, config } = options;
145
+ const baseCommit = await resolveCommit(cwd, base);
146
+ const candidateCommit = await resolveCommit(cwd, candidate);
147
+ let ancestor;
148
+ try {
149
+ ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
150
+ }
151
+ catch (error) {
152
+ throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
153
+ }
154
+ const file = await watchedPath(cwd, config);
155
+ if (file === null) {
156
+ const named = escapeText(String(config));
157
+ return { name, status: "skip", evidence: [`${named}: outside the repository, no range can change it`], findings: [] };
158
+ }
159
+ const drafts = await compare(cwd, ancestor, candidateCommit, file);
160
+ if (drafts.length === 0) {
161
+ return { name, status: "pass", evidence: [`${escapeText(file)}: not added, changed, deleted or renamed`], findings: [] };
162
+ }
163
+ const lines = await readSanctionLines(cwd, ancestor, candidateCommit);
164
+ const findings = drafts.map((draft) => {
165
+ const id = `${draft.category}:${file}`;
166
+ return { id, category: draft.category, file, other: draft.other, sanction: sanctionFor(id, lines, options.sanctions ?? {}) };
167
+ });
168
+ const open = findings.some((finding) => finding.sanction === null);
169
+ return { name, status: open ? "fail" : "pass", evidence: findings.map(describeFinding), findings };
170
+ }
171
+ catch (error) {
172
+ return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))], findings: [] };
173
+ }
174
+ }
175
+ //# sourceMappingURL=config-changes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-changes.js","sourceRoot":"","sources":["../../src/checks/config-changes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Credentials.
3
+ *
4
+ * A credential committed once is published to every clone for good. This check
5
+ * reads the lines `base..candidate` adds, in every file git can read as text,
6
+ * and reports each line that holds a credential in a form only a real one has:
7
+ * the file, the line and the kind - never the text it matched, because a report
8
+ * that prints a credential has published it a second time. A credential the
9
+ * base already holds, a line the range removes and a binary file say nothing.
10
+ * There is no sanction: remove the credential and revoke it.
11
+ *
12
+ * The forms live in the exported `CREDENTIAL_SHAPES`, read each time the check
13
+ * runs: a consumer appends its own entry. They are regular expressions only.
14
+ */
15
+ import type { CheckRecord } from "../index.js";
16
+ export declare const name = "RT-26";
17
+ /** One form of credential. */
18
+ export interface CredentialShape {
19
+ /** The kind a finding names. */
20
+ readonly name: string;
21
+ /** Tested against each line the range adds. */
22
+ readonly pattern: RegExp;
23
+ }
24
+ export interface CredentialsOptions {
25
+ readonly cwd: string;
26
+ readonly base: string;
27
+ readonly candidate: string;
28
+ }
29
+ export declare const CREDENTIAL_SHAPES: CredentialShape[];
30
+ /**
31
+ * Report each credential-shaped line `base..candidate` adds. Fails closed: a
32
+ * revision that names no commit, or any git invocation that fails, is a `fail`
33
+ * record naming the cause - never a `pass`, and never a thrown error.
34
+ */
35
+ export declare function check(options: CredentialsOptions): Promise<CheckRecord>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Credentials.
3
+ *
4
+ * A credential committed once is published to every clone for good. This check
5
+ * reads the lines `base..candidate` adds, in every file git can read as text,
6
+ * and reports each line that holds a credential in a form only a real one has:
7
+ * the file, the line and the kind - never the text it matched, because a report
8
+ * that prints a credential has published it a second time. A credential the
9
+ * base already holds, a line the range removes and a binary file say nothing.
10
+ * There is no sanction: remove the credential and revoke it.
11
+ *
12
+ * The forms live in the exported `CREDENTIAL_SHAPES`, read each time the check
13
+ * runs: a consumer appends its own entry. They are regular expressions only.
14
+ */
15
+ import { execFile } from "node:child_process";
16
+ export const name = "RT-26";
17
+ export const CREDENTIAL_SHAPES = [
18
+ { name: "private key", pattern: /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----/ },
19
+ { name: "cloud access key id", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/ },
20
+ { name: "code host token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})/ },
21
+ { name: "chat token", pattern: /xox[abposr]-[A-Za-z0-9-]{10,}/ },
22
+ {
23
+ name: "assigned secret",
24
+ pattern: /(?:api_key|api-key|apikey|secret_key|access_token|auth_token|oauth_token|password)["']?\s*[:=]\s*(?:"[^"'\s]{20,}"|'[^"'\s]{20,}')/i,
25
+ },
26
+ ];
27
+ function runGit(cwd, args) {
28
+ return new Promise((resolve, reject) => {
29
+ execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
30
+ if (error)
31
+ reject(new Error(stderr.trim() || error.message));
32
+ else
33
+ resolve(stdout);
34
+ });
35
+ });
36
+ }
37
+ /** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
38
+ function escapeText(text) {
39
+ let escaped = "";
40
+ for (const char of text) {
41
+ const codePoint = char.codePointAt(0) ?? 0;
42
+ escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
43
+ }
44
+ return escaped;
45
+ }
46
+ async function resolveCommit(cwd, revision) {
47
+ const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
48
+ // A revision that begins with a dash would be read as an option.
49
+ if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
50
+ throw fail("");
51
+ let hash = "";
52
+ try {
53
+ hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
54
+ }
55
+ catch (error) {
56
+ throw fail(`: ${error.message}`);
57
+ }
58
+ if (hash.length === 0)
59
+ throw fail("");
60
+ return hash;
61
+ }
62
+ /** The lines a change adds to a file; a missing old side (`null`) means every line is added. */
63
+ async function addedLines(cwd, oldBlob, newBlob) {
64
+ const text = await runGit(cwd, ["cat-file", "blob", newBlob]);
65
+ if (text.includes("\0"))
66
+ return [];
67
+ const all = () => text.replace(/\n$/, "").split("\n").map((t, i) => ({ number: i + 1, text: t.replace(/\r$/, "") }));
68
+ if (oldBlob === null)
69
+ return all();
70
+ const out = await runGit(cwd, ["diff", "-U0", "--no-color", "--no-ext-diff", "--no-textconv", oldBlob, newBlob]);
71
+ const added = [];
72
+ let newLine = 0;
73
+ let inHunk = false;
74
+ for (const raw of out.split("\n")) {
75
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
76
+ if (hunk !== null) {
77
+ newLine = Number(hunk[1]);
78
+ inHunk = true;
79
+ }
80
+ else if (inHunk && raw.startsWith("+"))
81
+ added.push({ number: newLine++, text: raw.slice(1).replace(/\r$/, "") });
82
+ }
83
+ return added;
84
+ }
85
+ async function findings(cwd, from, to) {
86
+ const out = await runGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--no-relative", from, to, "--"]);
87
+ const fields = out.split("\0");
88
+ const shapes = CREDENTIAL_SHAPES.map((shape) => ({
89
+ name: shape.name,
90
+ pattern: new RegExp(shape.pattern.source, shape.pattern.flags.replace(/[gy]/g, "")),
91
+ }));
92
+ const lines = [];
93
+ for (let i = 0; i < fields.length; i++) {
94
+ const head = fields[i] ?? "";
95
+ if (!head.startsWith(":"))
96
+ continue;
97
+ const [oldMode = "", newMode = "", oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
98
+ const first = fields[++i] ?? "";
99
+ const path = status.startsWith("R") || status.startsWith("C") ? (fields[++i] ?? "") : first;
100
+ if (status.startsWith("D") || !newMode.startsWith("100"))
101
+ continue;
102
+ const oldBlob = status.startsWith("A") || !oldMode.startsWith("100") ? null : oldSha;
103
+ for (const line of await addedLines(cwd, oldBlob, newSha)) {
104
+ for (const shape of shapes) {
105
+ if (shape.pattern.test(line.text))
106
+ lines.push(`${escapeText(path)}:${line.number}: ${escapeText(shape.name)}`);
107
+ }
108
+ }
109
+ }
110
+ return lines;
111
+ }
112
+ /**
113
+ * Report each credential-shaped line `base..candidate` adds. Fails closed: a
114
+ * revision that names no commit, or any git invocation that fails, is a `fail`
115
+ * record naming the cause - never a `pass`, and never a thrown error.
116
+ */
117
+ export async function check(options) {
118
+ try {
119
+ const { cwd, base, candidate } = options;
120
+ const baseCommit = await resolveCommit(cwd, base);
121
+ const candidateCommit = await resolveCommit(cwd, candidate);
122
+ let ancestor;
123
+ try {
124
+ ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
125
+ }
126
+ catch (error) {
127
+ throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
128
+ }
129
+ const found = await findings(cwd, ancestor, candidateCommit);
130
+ if (found.length === 0)
131
+ return { name, status: "pass", evidence: ["no credential in the lines the range adds"] };
132
+ return { name, status: "fail", evidence: found };
133
+ }
134
+ catch (error) {
135
+ return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))] };
136
+ }
137
+ }
138
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/checks/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Install scripts added.
3
+ *
4
+ * `npm ci` and `npm install` run a package's `preinstall`, `install` and
5
+ * `postinstall` scripts on the machine that installs it, and npm's lockfile
6
+ * says which packages have one: the entry of such a package under `packages`
7
+ * holds `"hasInstallScript": true`. This check reads every file the range adds,
8
+ * changes or renames onto whose last path segment is exactly `package-lock.json`
9
+ * or `npm-shrinkwrap.json`, at any depth, whole through git at the merge base
10
+ * of `base` and `candidate` and at `candidate` (never the working tree, renames
11
+ * detected), and parses each with `JSON.parse`. The source is the file at the
12
+ * merge base, the one a rename came from, or none for a file the range adds.
13
+ *
14
+ * A finding is a key of the candidate's `packages` object whose entry holds
15
+ * `hasInstallScript` equal to `true` while the source's `packages` does not
16
+ * hold that key with such an entry (whatever its version). A lockfile of
17
+ * version 1 has no `packages` object and yields nothing. A lockfile, candidate
18
+ * or source, that `JSON.parse` refuses fails the record and names the file.
19
+ *
20
+ * A finding's id is `install-script:<file>`, which sanctions every finding of
21
+ * that file, by a `Sanctioned-Weakening: <id> <reason>` line in a commit
22
+ * message of the range or by the caller's `sanctions`.
23
+ */
24
+ import type { CheckRecord } from "../index.js";
25
+ export declare const name = "RT-94";
26
+ export interface InstallScriptsOptions {
27
+ readonly cwd: string;
28
+ readonly base: string;
29
+ readonly candidate: string;
30
+ /** Ids (`install-script:<file>`) mapped to reasons. */
31
+ readonly sanctions?: Readonly<Record<string, string>> | undefined;
32
+ }
33
+ /**
34
+ * Report each package `base..candidate` adds to a lockfile with an install
35
+ * script. Fails while any finding is unsanctioned or a lockfile cannot be read,
36
+ * and fails closed: a revision that names no commit, or any git invocation that
37
+ * fails, is a `fail` record naming the cause - never a `pass`, never a throw.
38
+ */
39
+ export declare function check(options: InstallScriptsOptions): Promise<CheckRecord>;