@titan-design/egress-scan 0.1.1 → 0.2.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.
- package/README.md +18 -4
- package/dist/bin.js +57 -17
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-FZDZ52KS.js → chunk-RK4WHDXR.js} +20 -2
- package/dist/chunk-RK4WHDXR.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FZDZ52KS.js.map +0 -1
package/README.md
CHANGED
|
@@ -23,8 +23,19 @@ notices and errors to stderr.
|
|
|
23
23
|
|
|
24
24
|
- **Ranges.** `pre-push` skips ref deletions, scans `remote..local` for an existing branch,
|
|
25
25
|
and for a new branch scans only the commits no ref of that remote has. `range` with an
|
|
26
|
-
all-zero base scans the head commit alone.
|
|
27
|
-
|
|
26
|
+
all-zero base scans the head commit alone. A merge commit is diffed against each parent
|
|
27
|
+
in turn (`--diff-merges=separate`), because git's combined diff ignores `--text`; a path
|
|
28
|
+
both diffs name is reported once. The message is read with `--encoding=UTF-8`, so
|
|
29
|
+
`i18n.logOutputEncoding` cannot re-encode it past the rules.
|
|
30
|
+
- **Binary files are scanned as text.** Every `git show` and `git diff` passes `--text`, so a
|
|
31
|
+
file git calls binary (one NUL byte is enough) still yields its lines, and the report's
|
|
32
|
+
`binary files skipped` stays 0; if git ever prints a file as binary anyway, the scan exits 2.
|
|
33
|
+
There is no opt-out, since an opt-out would be a bypass.
|
|
34
|
+
Matching is on the bytes decoded as UTF-8: a term written in UTF-16 or another encoding is
|
|
35
|
+
not matched.
|
|
36
|
+
- **Size limit.** A commit whose patch text is over 128 MiB (`MAX_PATCH_BYTES`) exits 2 with
|
|
37
|
+
one line naming its short sha and the limit. `tree` has the same limit on the whole tree's
|
|
38
|
+
patch text and exits 2 with one line naming `tree` and the limit. GitHub itself refuses a file over 100 MiB.
|
|
28
39
|
- **Arguments.** Shas on pre-push stdin must be full hex shas, a `range` base or head must be
|
|
29
40
|
a hex sha or a ref name, and a remote name must not start with a dash or hold whitespace.
|
|
30
41
|
A bad value exits 2 with its position, never its value. Every revision reaches git after
|
|
@@ -59,7 +70,7 @@ notices and errors to stderr.
|
|
|
59
70
|
```ts
|
|
60
71
|
import { formatReport, parseAllow, parseCommit, parseTerms, scan } from "@titan-design/egress-scan";
|
|
61
72
|
|
|
62
|
-
// Each commit's text from `git show
|
|
73
|
+
// Each commit's text from `git show --text -U0 --encoding=UTF-8 --format=%B%x00 --no-color --no-ext-diff <sha>`.
|
|
63
74
|
const sources = commits.map(({ sha, text }) => parseCommit(sha, text));
|
|
64
75
|
const result = scan(sources, { terms: parseTerms(termFileText), allow: parseAllow(allowFileText) });
|
|
65
76
|
for (const line of formatReport(result.findings, { ...result, termsLoaded: true })) console.log(line);
|
|
@@ -67,7 +78,10 @@ process.exitCode = result.findings.length > 0 ? 1 : 0;
|
|
|
67
78
|
```
|
|
68
79
|
|
|
69
80
|
`scan` reads added lines, the paths of new, renamed and copied files, and commit messages.
|
|
70
|
-
Removed lines are ignored
|
|
81
|
+
Removed lines are ignored. A patch made without `--text` shows a binary file as
|
|
82
|
+
`Binary files ... differ`, which `scan` counts and skips, so pass `--text` as the bin does. A merge's combined diff
|
|
83
|
+
ignores `--text`, so for a merge read the message with `--no-patch --format=%B` and the patch
|
|
84
|
+
with `--diff-merges=separate --format=`, and pass the patch to `parseDiff`. Scan each commit, not
|
|
71
85
|
only the range endpoints: a leak added and then removed is still in the pushed history.
|
|
72
86
|
|
|
73
87
|
## Rules
|
package/dist/bin.js
CHANGED
|
@@ -5,11 +5,10 @@ import {
|
|
|
5
5
|
TermFileError,
|
|
6
6
|
formatReport,
|
|
7
7
|
parseAllow,
|
|
8
|
-
parseCommit,
|
|
9
8
|
parseDiff,
|
|
10
9
|
parseTerms,
|
|
11
10
|
scan
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RK4WHDXR.js";
|
|
13
12
|
|
|
14
13
|
// src/bin.ts
|
|
15
14
|
import * as fs3 from "fs";
|
|
@@ -110,7 +109,19 @@ var GitError = class extends Error {
|
|
|
110
109
|
this.name = "GitError";
|
|
111
110
|
}
|
|
112
111
|
};
|
|
112
|
+
var MAX_PATCH_BYTES = 128 * 1024 * 1024;
|
|
113
|
+
var PatchTooLargeError = class extends Error {
|
|
114
|
+
constructor(sha, limit) {
|
|
115
|
+
super(`${sha === void 0 ? "tree" : `commit ${sha.slice(0, 7)}`}: patch text is over the scan limit of ${formatBytes(limit)}; refusing it`);
|
|
116
|
+
this.name = "PatchTooLargeError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function formatBytes(bytes) {
|
|
120
|
+
const mib = 1024 * 1024;
|
|
121
|
+
return bytes % mib === 0 ? `${bytes / mib} MiB` : `${bytes} bytes`;
|
|
122
|
+
}
|
|
113
123
|
var PATCH_FLAGS = [
|
|
124
|
+
"--text",
|
|
114
125
|
"-U0",
|
|
115
126
|
"--no-color",
|
|
116
127
|
"--no-ext-diff",
|
|
@@ -119,6 +130,8 @@ var PATCH_FLAGS = [
|
|
|
119
130
|
"--src-prefix=a/",
|
|
120
131
|
"--dst-prefix=b/"
|
|
121
132
|
];
|
|
133
|
+
var MESSAGE_FLAGS = ["--no-patch", "--encoding=UTF-8", "--format=%B"];
|
|
134
|
+
var COMMIT_PATCH_FLAGS = ["--diff-merges=separate", "--format=", ...PATCH_FLAGS];
|
|
122
135
|
var MAX_BUFFER = 1024 * 1024 * 1024;
|
|
123
136
|
var END_OF_OPTIONS = "--end-of-options";
|
|
124
137
|
var FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
@@ -136,12 +149,17 @@ function isRemoteName(value) {
|
|
|
136
149
|
function requireRevision(value, what) {
|
|
137
150
|
if (!isRevision(value)) throw new ConfigError(`${what} is not a sha or ref name`);
|
|
138
151
|
}
|
|
139
|
-
function
|
|
140
|
-
|
|
152
|
+
function runGit(cwd, args, maxBuffer, input) {
|
|
153
|
+
return spawnSync("git", args, { cwd, input, encoding: "utf-8", maxBuffer });
|
|
154
|
+
}
|
|
155
|
+
function stdoutOf(result, subcommand) {
|
|
141
156
|
if (result.error) throw result.error;
|
|
142
|
-
if (result.status !== 0) throw new GitError(
|
|
157
|
+
if (result.status !== 0) throw new GitError(subcommand, result.status);
|
|
143
158
|
return result.stdout;
|
|
144
159
|
}
|
|
160
|
+
function git(cwd, args, input) {
|
|
161
|
+
return stdoutOf(runGit(cwd, args, MAX_BUFFER, input), args[0] ?? "");
|
|
162
|
+
}
|
|
145
163
|
function lines(text) {
|
|
146
164
|
return text.split("\n").filter((line) => line !== "");
|
|
147
165
|
}
|
|
@@ -183,13 +201,30 @@ function commitsForRange(cwd, base, head) {
|
|
|
183
201
|
if (isZeroSha(base)) return lines(git(cwd, ["rev-list", "--no-walk", END_OF_OPTIONS, head]));
|
|
184
202
|
return lines(git(cwd, ["rev-list", "--reverse", END_OF_OPTIONS, `${base}..${head}`]));
|
|
185
203
|
}
|
|
186
|
-
function
|
|
204
|
+
function isOverBuffer(error) {
|
|
205
|
+
return error?.code === "ENOBUFS";
|
|
206
|
+
}
|
|
207
|
+
function requireAllText(source, what) {
|
|
208
|
+
if (source.binaryFiles > 0) throw new Error(`${what}: git printed a file as binary despite --text; refusing it`);
|
|
209
|
+
return source;
|
|
210
|
+
}
|
|
211
|
+
function showCommit(cwd, sha, flags, maxBytes) {
|
|
212
|
+
const result = runGit(cwd, ["show", ...flags, END_OF_OPTIONS, sha], maxBytes);
|
|
213
|
+
if (isOverBuffer(result.error)) throw new PatchTooLargeError(sha, maxBytes);
|
|
214
|
+
return stdoutOf(result, "show");
|
|
215
|
+
}
|
|
216
|
+
function readCommit(cwd, sha, maxPatchBytes = MAX_PATCH_BYTES) {
|
|
187
217
|
requireRevision(sha, "commit");
|
|
188
|
-
|
|
218
|
+
const message = showCommit(cwd, sha, MESSAGE_FLAGS, maxPatchBytes).replace(/\n+$/, "").split("\n");
|
|
219
|
+
const patch = parseDiff(showCommit(cwd, sha, COMMIT_PATCH_FLAGS, maxPatchBytes));
|
|
220
|
+
return requireAllText({ ...patch, sha, message }, `commit ${sha.slice(0, 7)}`);
|
|
189
221
|
}
|
|
190
|
-
function readTree(cwd) {
|
|
222
|
+
function readTree(cwd, maxPatchBytes = MAX_PATCH_BYTES) {
|
|
191
223
|
const emptyTree = git(cwd, ["hash-object", "-t", "tree", "--stdin"], "").trim();
|
|
192
|
-
|
|
224
|
+
const args = ["diff", ...PATCH_FLAGS, "--no-renames", END_OF_OPTIONS, emptyTree, "HEAD"];
|
|
225
|
+
const result = runGit(cwd, args, maxPatchBytes);
|
|
226
|
+
if (isOverBuffer(result.error)) throw new PatchTooLargeError(void 0, maxPatchBytes);
|
|
227
|
+
return requireAllText(parseDiff(stdoutOf(result, "diff")), "tree");
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
// src/install.ts
|
|
@@ -222,17 +257,19 @@ var USAGE = [
|
|
|
222
257
|
" range <base> <head> scan every commit in base..head (CI)",
|
|
223
258
|
" tree scan every tracked file at HEAD",
|
|
224
259
|
" install-hook install the pre-push hook into git's hooks directory",
|
|
260
|
+
"files git calls binary are scanned as text; a commit or tree over 128 MiB of patch text exits 2",
|
|
225
261
|
"exit: 0 clean, 1 findings, 2 usage or configuration error"
|
|
226
262
|
];
|
|
227
263
|
var PREFIX = "titan-egress-scan: ";
|
|
228
|
-
function readCommits(root, shas) {
|
|
229
|
-
return [...new Set(shas)].map((sha) => readCommit(root, sha));
|
|
264
|
+
function readCommits(root, shas, maxPatchBytes) {
|
|
265
|
+
return [...new Set(shas)].map((sha) => readCommit(root, sha, maxPatchBytes));
|
|
230
266
|
}
|
|
231
|
-
function prePushSources(root, remote,
|
|
232
|
-
const updates = parsePrePush(
|
|
267
|
+
function prePushSources(root, remote, io) {
|
|
268
|
+
const updates = parsePrePush(io.readStdin());
|
|
233
269
|
return readCommits(
|
|
234
270
|
root,
|
|
235
|
-
updates.flatMap((update) => commitsForUpdate(root, remote, update))
|
|
271
|
+
updates.flatMap((update) => commitsForUpdate(root, remote, update)),
|
|
272
|
+
io.maxPatchBytes
|
|
236
273
|
);
|
|
237
274
|
}
|
|
238
275
|
function runScan(io, collect) {
|
|
@@ -275,14 +312,17 @@ function dispatch(command, args, io) {
|
|
|
275
312
|
case "pre-push":
|
|
276
313
|
expectArgs(command, args, 1, 2);
|
|
277
314
|
expectValid(command, args.slice(0, 1), isRemoteName, "a remote name");
|
|
278
|
-
return runScan(io, (root) => prePushSources(root, args[0] ?? "", io
|
|
315
|
+
return runScan(io, (root) => prePushSources(root, args[0] ?? "", io));
|
|
279
316
|
case "range":
|
|
280
317
|
expectArgs(command, args, 2, 2);
|
|
281
318
|
expectValid(command, args, isRevision, "a sha or ref name");
|
|
282
|
-
return runScan(
|
|
319
|
+
return runScan(
|
|
320
|
+
io,
|
|
321
|
+
(root) => readCommits(root, commitsForRange(root, args[0] ?? "", args[1] ?? ""), io.maxPatchBytes)
|
|
322
|
+
);
|
|
283
323
|
case "tree":
|
|
284
324
|
expectArgs(command, args, 0, 0);
|
|
285
|
-
return runScan(io, (root) => [readTree(root)]);
|
|
325
|
+
return runScan(io, (root) => [readTree(root, io.maxPatchBytes)]);
|
|
286
326
|
case "install-hook":
|
|
287
327
|
expectArgs(command, args, 0, 0);
|
|
288
328
|
return runInstall(io);
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bin.ts","../src/cli.ts","../src/config.ts","../src/git.ts","../src/install.ts"],"sourcesContent":["#!/usr/bin/env node\nimport * as fs from \"node:fs\";\nimport { runCli } from \"./cli.js\";\n\nprocess.exitCode = runCli(process.argv.slice(2), {\n cwd: process.cwd(),\n env: process.env,\n readStdin: () => fs.readFileSync(0, \"utf-8\"),\n out: (line) => process.stdout.write(`${line}\\n`),\n err: (line) => process.stderr.write(`${line}\\n`),\n});\n","import { parseArgs } from \"node:util\";\nimport { ConfigError, loadAllow, loadTerms } from \"./config.js\";\nimport type { ScanSource } from \"./diff.js\";\nimport {\n commitsForRange,\n commitsForUpdate,\n isRemoteName,\n isRevision,\n parsePrePush,\n readCommit,\n readTree,\n repoRoot,\n} from \"./git.js\";\nimport { installHook, type InstallResult } from \"./install.js\";\nimport { formatReport } from \"./report.js\";\nimport { scan } from \"./scan.js\";\n\nexport interface CliIo {\n readonly cwd: string;\n readonly env: Readonly<Record<string, string | undefined>>;\n readStdin(): string;\n out(line: string): void;\n err(line: string): void;\n}\n\nconst USAGE = [\n \"usage: titan-egress-scan <command>\",\n \" pre-push <remote> scan the commits a push sends (reads git's pre-push stdin)\",\n \" range <base> <head> scan every commit in base..head (CI)\",\n \" tree scan every tracked file at HEAD\",\n \" install-hook install the pre-push hook into git's hooks directory\",\n \"exit: 0 clean, 1 findings, 2 usage or configuration error\",\n];\n\nconst PREFIX = \"titan-egress-scan: \";\n\nfunction readCommits(root: string, shas: readonly string[]): ScanSource[] {\n return [...new Set(shas)].map((sha) => readCommit(root, sha));\n}\n\nfunction prePushSources(root: string, remote: string, stdin: string): ScanSource[] {\n const updates = parsePrePush(stdin);\n return readCommits(\n root,\n updates.flatMap((update) => commitsForUpdate(root, remote, update)),\n );\n}\n\nfunction runScan(io: CliIo, collect: (root: string) => ScanSource[]): number {\n const root = repoRoot(io.cwd);\n const allow = loadAllow(root);\n const { terms, loaded, notices } = loadTerms(io.env);\n for (const notice of notices) io.err(PREFIX + notice);\n const result = scan(collect(root), { terms, allow });\n for (const line of formatReport(result.findings, { ...result, termsLoaded: loaded })) io.out(line);\n return result.findings.length > 0 ? 1 : 0;\n}\n\nconst INSTALL_MESSAGES: Record<InstallResult[\"outcome\"], string> = {\n installed: \"pre-push hook installed\",\n updated: \"pre-push hook updated\",\n unchanged: \"pre-push hook already installed\",\n \"skipped-in-ci\": \"CI detected; hook not installed\",\n \"foreign-hook\": \"an existing pre-push hook was not written by this tool; left it in place.\",\n};\n\nfunction runInstall(io: CliIo): number {\n const result = installHook(io.cwd, io.env);\n const where = result.hookPath === undefined ? \"\" : ` (${result.hookPath})`;\n if (result.outcome !== \"foreign-hook\") {\n io.out(PREFIX + INSTALL_MESSAGES[result.outcome] + where);\n return 0;\n }\n io.err(PREFIX + INSTALL_MESSAGES[result.outcome] + where);\n io.err(PREFIX + 'chain it: call `titan-egress-scan pre-push \"$1\"` from that hook with git\\'s stdin passed through');\n return 2;\n}\n\nfunction expectArgs(command: string, args: readonly string[], min: number, max: number): void {\n if (args.length < min || args.length > max) throw new ConfigError(`${command}: wrong number of arguments`);\n}\n\n/** Rejects a value by its position only, so the message never repeats what was passed. */\nfunction expectValid(command: string, args: readonly string[], valid: (value: string) => boolean, what: string): void {\n args.forEach((value, i) => {\n if (!valid(value)) throw new ConfigError(`${command}: argument ${i + 1} is not ${what}`);\n });\n}\n\nfunction dispatch(command: string | undefined, args: readonly string[], io: CliIo): number {\n switch (command) {\n case \"pre-push\":\n // git passes the remote name and its URL; only the name is used.\n expectArgs(command, args, 1, 2);\n expectValid(command, args.slice(0, 1), isRemoteName, \"a remote name\");\n return runScan(io, (root) => prePushSources(root, args[0] ?? \"\", io.readStdin()));\n case \"range\":\n expectArgs(command, args, 2, 2);\n expectValid(command, args, isRevision, \"a sha or ref name\");\n return runScan(io, (root) => readCommits(root, commitsForRange(root, args[0] ?? \"\", args[1] ?? \"\")));\n case \"tree\":\n expectArgs(command, args, 0, 0);\n return runScan(io, (root) => [readTree(root)]);\n case \"install-hook\":\n expectArgs(command, args, 0, 0);\n return runInstall(io);\n default:\n throw new ConfigError(command === undefined ? \"missing command\" : \"unknown command\");\n }\n}\n\n/** Help counts only before the command; after it, a help flag is a usage error, never a skipped scan. */\nfunction parseCommandLine(argv: readonly string[]): { positionals: string[]; help: boolean } {\n const { positionals, tokens } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n tokens: true,\n options: { help: { type: \"boolean\", short: \"h\" } },\n });\n const command = tokens.find((token) => token.kind === \"positional\");\n const helps = tokens.filter((token) => token.kind === \"option\" && token.name === \"help\");\n if (helps.some((token) => command !== undefined && token.index > command.index)) {\n throw new ConfigError(\"a help flag after the command is not allowed\");\n }\n return { positionals, help: helps.length > 0 };\n}\n\n/** Runs one command. Errors print as one line to stderr and exit 2; nothing echoes scanned text. */\nexport function runCli(argv: readonly string[], io: CliIo): number {\n try {\n const { positionals, help } = parseCommandLine(argv);\n if (help) {\n for (const line of USAGE) io.out(line);\n return 0;\n }\n return dispatch(positionals[0], positionals.slice(1), io);\n } catch (error) {\n io.err(PREFIX + (error instanceof Error ? error.message : \"unexpected error\"));\n if (error instanceof ConfigError) io.err(USAGE[0] ?? \"\");\n return 2;\n }\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { AllowFileError, EMPTY_ALLOW, parseAllow, type AllowList } from \"./allow.js\";\nimport type { TermRule } from \"./rules.js\";\nimport { parseTerms, TermFileError } from \"./terms.js\";\n\n/** A usage or configuration problem: exit 2. Messages never carry a term or scanned text. */\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\nexport interface LoadedTerms {\n readonly terms: readonly TermRule[];\n readonly loaded: boolean;\n readonly notices: readonly string[];\n}\n\ntype Env = Readonly<Record<string, string | undefined>>;\n\nconst NOT_LOADED: LoadedTerms = { terms: [], loaded: false, notices: [] };\n\nexport function isCi(env: Env): boolean {\n const value = env.CI?.toLowerCase();\n return value !== undefined && value !== \"\" && value !== \"false\" && value !== \"0\";\n}\n\nexport function termFilePath(env: Env): string | undefined {\n if (env.TITAN_EGRESS_TERMS) return env.TITAN_EGRESS_TERMS;\n const configHome = env.XDG_CONFIG_HOME || (env.HOME && path.join(env.HOME, \".config\"));\n return configHome ? path.join(configHome, \"titan-egress\", \"private-terms\") : undefined;\n}\n\nfunction readIfPresent(file: string, what: string): string | undefined {\n try {\n return fs.readFileSync(file, \"utf-8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw new ConfigError(`${what} could not be read`);\n }\n}\n\nconst ON_VALUES = new Set([\"1\", \"true\", \"yes\", \"on\"]);\nconst OFF_VALUES = new Set([\"\", \"0\", \"false\", \"no\", \"off\"]);\n\n/** An unrecognised value fails the scan: a typo must never read as off. The value is not echoed. */\nfunction requiresTerms(env: Env): boolean {\n const value = (env.TITAN_EGRESS_REQUIRE_TERMS ?? \"\").trim().toLowerCase();\n if (ON_VALUES.has(value)) return true;\n if (OFF_VALUES.has(value)) return false;\n throw new ConfigError(\"TITAN_EGRESS_REQUIRE_TERMS is not one of 1, true, yes, on, 0, false, no, off\");\n}\n\nfunction readTermFile(file: string, required: boolean): string | undefined {\n try {\n return readIfPresent(file, \"private term list\");\n } catch (error) {\n if (required && error instanceof ConfigError) {\n throw new ConfigError(\"private term list could not be read and TITAN_EGRESS_REQUIRE_TERMS=1\");\n }\n throw error;\n }\n}\n\nfunction missingTerms(required: boolean): LoadedTerms {\n if (required) {\n throw new ConfigError(\"private term list not found and TITAN_EGRESS_REQUIRE_TERMS=1\");\n }\n return { ...NOT_LOADED, notices: [\"private term list not found; generic rules only\"] };\n}\n\nfunction compileTerms(text: string): readonly TermRule[] {\n try {\n return parseTerms(text);\n } catch (error) {\n if (error instanceof TermFileError) throw new ConfigError(error.message);\n throw error;\n }\n}\n\n/**\n * Loads the private term list. In CI it is never looked up and an absent file is a notice, unless\n * TITAN_EGRESS_REQUIRE_TERMS=1, which always loads and fails on a missing, unreadable or empty list.\n */\nexport function loadTerms(env: Env): LoadedTerms {\n const required = requiresTerms(env);\n if (isCi(env) && !required) return NOT_LOADED;\n const file = termFilePath(env);\n const text = file === undefined ? undefined : readTermFile(file, required);\n if (file === undefined || text === undefined) return missingTerms(required);\n const openToOthers = process.platform !== \"win32\" && (fs.statSync(file).mode & 0o077) !== 0;\n const notices = openToOthers ? [\"private term list is readable by other users; chmod 600 it\"] : [];\n const terms = compileTerms(text);\n if (terms.length === 0 && required) {\n throw new ConfigError(\"private term list holds no terms and TITAN_EGRESS_REQUIRE_TERMS=1\");\n }\n return { terms, loaded: true, notices };\n}\n\n/** Loads `.egress-allow` from the repo root. A malformed file fails the scan, never reads as empty. */\nexport function loadAllow(root: string): AllowList {\n const text = readIfPresent(path.join(root, \".egress-allow\"), \".egress-allow\");\n if (text === undefined) return EMPTY_ALLOW;\n try {\n return parseAllow(text);\n } catch (error) {\n if (error instanceof AllowFileError) throw new ConfigError(error.message);\n throw error;\n }\n}\n","import { spawnSync } from \"node:child_process\";\nimport * as path from \"node:path\";\nimport { ConfigError } from \"./config.js\";\nimport { parseCommit, parseDiff, type ScanSource } from \"./diff.js\";\n\n/** A git command that failed. The message names the subcommand, never git's output. */\nexport class GitError extends Error {\n constructor(subcommand: string, status: number | null) {\n super(`git ${subcommand} failed (exit ${status ?? \"signal\"}); run it by hand to see git's error`);\n this.name = \"GitError\";\n }\n}\n\nexport interface PushUpdate {\n readonly localSha: string;\n readonly remoteSha: string;\n}\n\n// Pinned so user config (prefixes, textconv, relative, external diff) cannot reshape the patch text.\nconst PATCH_FLAGS = [\n \"-U0\",\n \"--no-color\",\n \"--no-ext-diff\",\n \"--no-textconv\",\n \"--no-relative\",\n \"--src-prefix=a/\",\n \"--dst-prefix=b/\",\n];\nconst MAX_BUFFER = 1024 * 1024 * 1024;\n// Every revision argument follows this, so a value that starts with a dash cannot become an option.\nconst END_OF_OPTIONS = \"--end-of-options\";\n\nconst FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;\nconst REVISION = /^(?:[0-9a-f]{7,64}|[^-\\s]\\S*)$/;\nconst REMOTE_NAME = /^[^-\\s]\\S*$/;\n\nexport function isFullSha(value: string): boolean {\n return FULL_SHA.test(value);\n}\n\n/** A hex sha of 7 to 64 characters, or a ref name that cannot be read as an option. */\nexport function isRevision(value: string): boolean {\n return REVISION.test(value);\n}\n\nexport function isRemoteName(value: string): boolean {\n return REMOTE_NAME.test(value);\n}\n\nfunction requireRevision(value: string, what: string): void {\n if (!isRevision(value)) throw new ConfigError(`${what} is not a sha or ref name`);\n}\n\nexport function git(cwd: string, args: readonly string[], input?: string): string {\n const result = spawnSync(\"git\", args, { cwd, input, encoding: \"utf-8\", maxBuffer: MAX_BUFFER });\n if (result.error) throw result.error;\n if (result.status !== 0) throw new GitError(args[0] ?? \"\", result.status);\n return result.stdout;\n}\n\nfunction lines(text: string): string[] {\n return text.split(\"\\n\").filter((line) => line !== \"\");\n}\n\nexport function isZeroSha(sha: string): boolean {\n return /^0+$/.test(sha);\n}\n\nexport function repoRoot(cwd: string): string {\n return git(cwd, [\"rev-parse\", \"--show-toplevel\"]).trim();\n}\n\n/** Resolves the hooks directory as git does: `core.hooksPath` if set, else the common git dir's hooks. */\nexport function hooksDir(cwd: string): string {\n return path.resolve(cwd, git(cwd, [\"rev-parse\", \"--git-path\", \"hooks\"]).trim());\n}\n\n/** Parses git's pre-push stdin: `<local-ref> <local-sha> <remote-ref> <remote-sha>` per line. */\nexport function parsePrePush(stdin: string): PushUpdate[] {\n return lines(stdin).map((line, i) => {\n const fields = line.trim().split(/\\s+/);\n if (fields.length !== 4) throw new ConfigError(`pre-push stdin line ${i + 1}: expected four fields`);\n for (const field of [1, 3]) {\n if (!isFullSha(fields[field] ?? \"\")) {\n throw new ConfigError(`pre-push stdin line ${i + 1}: field ${field + 1} is not a full sha`);\n }\n }\n return { localSha: fields[1] ?? \"\", remoteSha: fields[3] ?? \"\" };\n });\n}\n\nfunction hasCommit(cwd: string, sha: string): boolean {\n return spawnSync(\"git\", [\"cat-file\", \"-e\", END_OF_OPTIONS, `${sha}^{commit}`], { cwd }).status === 0;\n}\n\n/** The commits one pushed ref update sends that the remote lacks, oldest first. */\nexport function commitsForUpdate(cwd: string, remote: string, update: PushUpdate): string[] {\n if (isZeroSha(update.localSha)) return [];\n if (!isFullSha(update.localSha) || !isFullSha(update.remoteSha)) throw new ConfigError(\"push update is not a full sha\");\n if (!isRemoteName(remote)) throw new ConfigError(\"remote is not a remote name\");\n const known = !isZeroSha(update.remoteSha) && hasCommit(cwd, update.remoteSha);\n // The second `--not` flips back, so the local sha after it counts as included.\n const range = known\n ? [END_OF_OPTIONS, `${update.remoteSha}..${update.localSha}`]\n : [\"--not\", `--remotes=${remote}`, \"--not\", END_OF_OPTIONS, update.localSha];\n return lines(git(cwd, [\"rev-list\", \"--reverse\", ...range]));\n}\n\n/** The commits in `base..head`, oldest first; an all-zero base means `head` alone. */\nexport function commitsForRange(cwd: string, base: string, head: string): string[] {\n requireRevision(base, \"range base\");\n requireRevision(head, \"range head\");\n if (isZeroSha(base)) return lines(git(cwd, [\"rev-list\", \"--no-walk\", END_OF_OPTIONS, head]));\n return lines(git(cwd, [\"rev-list\", \"--reverse\", END_OF_OPTIONS, `${base}..${head}`]));\n}\n\n/** One commit's message and patch; `-c` makes a merge show the lines it brings in from any parent. */\nexport function readCommit(cwd: string, sha: string): ScanSource {\n requireRevision(sha, \"commit\");\n return parseCommit(sha, git(cwd, [\"show\", \"-c\", ...PATCH_FLAGS, \"--format=%B%x00\", END_OF_OPTIONS, sha]));\n}\n\n/** Every tracked file at HEAD, as one diff from the empty tree. */\nexport function readTree(cwd: string): ScanSource {\n const emptyTree = git(cwd, [\"hash-object\", \"-t\", \"tree\", \"--stdin\"], \"\").trim();\n return parseDiff(git(cwd, [\"diff\", ...PATCH_FLAGS, \"--no-renames\", END_OF_OPTIONS, emptyTree, \"HEAD\"]));\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { isCi } from \"./config.js\";\nimport { hooksDir } from \"./git.js\";\n\nexport const HOOK_MARKER = \"# titan-egress-scan managed pre-push hook\";\n\nexport type InstallOutcome = \"installed\" | \"updated\" | \"unchanged\" | \"skipped-in-ci\" | \"foreign-hook\";\n\nexport interface InstallResult {\n readonly outcome: InstallOutcome;\n readonly hookPath?: string;\n}\n\n/** The shipped hook body, one directory above both `src/` and `dist/`. */\nexport function hookBody(): string {\n return fs.readFileSync(new URL(\"../hooks/pre-push\", import.meta.url), \"utf-8\");\n}\n\nfunction existing(hookPath: string): string | undefined {\n return fs.existsSync(hookPath) ? fs.readFileSync(hookPath, \"utf-8\") : undefined;\n}\n\n/**\n * Writes the pre-push hook into git's hooks directory, which honours `core.hooksPath` and is\n * shared by every linked worktree. It never sets `core.hooksPath` and never replaces a hook it\n * did not write.\n */\nexport function installHook(cwd: string, env: Readonly<Record<string, string | undefined>>): InstallResult {\n if (isCi(env)) return { outcome: \"skipped-in-ci\" };\n const hookPath = path.join(hooksDir(cwd), \"pre-push\");\n const body = hookBody();\n const current = existing(hookPath);\n if (current !== undefined && !current.includes(HOOK_MARKER)) return { outcome: \"foreign-hook\", hookPath };\n if (current === body) return { outcome: \"unchanged\", hookPath };\n fs.mkdirSync(path.dirname(hookPath), { recursive: true });\n fs.writeFileSync(hookPath, body, { mode: 0o755 });\n fs.chmodSync(hookPath, 0o755);\n return { outcome: current === undefined ? \"installed\" : \"updated\", hookPath };\n}\n"],"mappings":";;;;;;;;;;;;;;AACA,YAAYA,SAAQ;;;ACDpB,SAAS,iBAAiB;;;ACA1B,YAAY,QAAQ;AACpB,YAAY,UAAU;AAMf,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAUA,IAAM,aAA0B,EAAE,OAAO,CAAC,GAAG,QAAQ,OAAO,SAAS,CAAC,EAAE;AAEjE,SAAS,KAAK,KAAmB;AACtC,QAAM,QAAQ,IAAI,IAAI,YAAY;AAClC,SAAO,UAAU,UAAa,UAAU,MAAM,UAAU,WAAW,UAAU;AAC/E;AAEO,SAAS,aAAa,KAA8B;AACzD,MAAI,IAAI,mBAAoB,QAAO,IAAI;AACvC,QAAM,aAAa,IAAI,mBAAoB,IAAI,QAAa,UAAK,IAAI,MAAM,SAAS;AACpF,SAAO,aAAkB,UAAK,YAAY,gBAAgB,eAAe,IAAI;AAC/E;AAEA,SAAS,cAAc,MAAc,MAAkC;AACrE,MAAI;AACF,WAAU,gBAAa,MAAM,OAAO;AAAA,EACtC,SAAS,OAAO;AACd,QAAK,MAAgC,SAAS,SAAU,QAAO;AAC/D,UAAM,IAAI,YAAY,GAAG,IAAI,oBAAoB;AAAA,EACnD;AACF;AAEA,IAAM,YAAY,oBAAI,IAAI,CAAC,KAAK,QAAQ,OAAO,IAAI,CAAC;AACpD,IAAM,aAAa,oBAAI,IAAI,CAAC,IAAI,KAAK,SAAS,MAAM,KAAK,CAAC;AAG1D,SAAS,cAAc,KAAmB;AACxC,QAAM,SAAS,IAAI,8BAA8B,IAAI,KAAK,EAAE,YAAY;AACxE,MAAI,UAAU,IAAI,KAAK,EAAG,QAAO;AACjC,MAAI,WAAW,IAAI,KAAK,EAAG,QAAO;AAClC,QAAM,IAAI,YAAY,8EAA8E;AACtG;AAEA,SAAS,aAAa,MAAc,UAAuC;AACzE,MAAI;AACF,WAAO,cAAc,MAAM,mBAAmB;AAAA,EAChD,SAAS,OAAO;AACd,QAAI,YAAY,iBAAiB,aAAa;AAC5C,YAAM,IAAI,YAAY,sEAAsE;AAAA,IAC9F;AACA,UAAM;AAAA,EACR;AACF;AAEA,SAAS,aAAa,UAAgC;AACpD,MAAI,UAAU;AACZ,UAAM,IAAI,YAAY,8DAA8D;AAAA,EACtF;AACA,SAAO,EAAE,GAAG,YAAY,SAAS,CAAC,iDAAiD,EAAE;AACvF;AAEA,SAAS,aAAa,MAAmC;AACvD,MAAI;AACF,WAAO,WAAW,IAAI;AAAA,EACxB,SAAS,OAAO;AACd,QAAI,iBAAiB,cAAe,OAAM,IAAI,YAAY,MAAM,OAAO;AACvE,UAAM;AAAA,EACR;AACF;AAMO,SAAS,UAAU,KAAuB;AAC/C,QAAM,WAAW,cAAc,GAAG;AAClC,MAAI,KAAK,GAAG,KAAK,CAAC,SAAU,QAAO;AACnC,QAAM,OAAO,aAAa,GAAG;AAC7B,QAAM,OAAO,SAAS,SAAY,SAAY,aAAa,MAAM,QAAQ;AACzE,MAAI,SAAS,UAAa,SAAS,OAAW,QAAO,aAAa,QAAQ;AAC1E,QAAM,eAAe,QAAQ,aAAa,YAAe,YAAS,IAAI,EAAE,OAAO,QAAW;AAC1F,QAAM,UAAU,eAAe,CAAC,4DAA4D,IAAI,CAAC;AACjG,QAAM,QAAQ,aAAa,IAAI;AAC/B,MAAI,MAAM,WAAW,KAAK,UAAU;AAClC,UAAM,IAAI,YAAY,mEAAmE;AAAA,EAC3F;AACA,SAAO,EAAE,OAAO,QAAQ,MAAM,QAAQ;AACxC;AAGO,SAAS,UAAU,MAAyB;AACjD,QAAM,OAAO,cAAmB,UAAK,MAAM,eAAe,GAAG,eAAe;AAC5E,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI;AACF,WAAO,WAAW,IAAI;AAAA,EACxB,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAgB,OAAM,IAAI,YAAY,MAAM,OAAO;AACxE,UAAM;AAAA,EACR;AACF;;;AC/GA,SAAS,iBAAiB;AAC1B,YAAYC,WAAU;AAKf,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YAAY,YAAoB,QAAuB;AACrD,UAAM,OAAO,UAAU,iBAAiB,UAAU,QAAQ,sCAAsC;AAChG,SAAK,OAAO;AAAA,EACd;AACF;AAQA,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,IAAM,aAAa,OAAO,OAAO;AAEjC,IAAM,iBAAiB;AAEvB,IAAM,WAAW;AACjB,IAAM,WAAW;AACjB,IAAM,cAAc;AAEb,SAAS,UAAU,OAAwB;AAChD,SAAO,SAAS,KAAK,KAAK;AAC5B;AAGO,SAAS,WAAW,OAAwB;AACjD,SAAO,SAAS,KAAK,KAAK;AAC5B;AAEO,SAAS,aAAa,OAAwB;AACnD,SAAO,YAAY,KAAK,KAAK;AAC/B;AAEA,SAAS,gBAAgB,OAAe,MAAoB;AAC1D,MAAI,CAAC,WAAW,KAAK,EAAG,OAAM,IAAI,YAAY,GAAG,IAAI,2BAA2B;AAClF;AAEO,SAAS,IAAI,KAAa,MAAyB,OAAwB;AAChF,QAAM,SAAS,UAAU,OAAO,MAAM,EAAE,KAAK,OAAO,UAAU,SAAS,WAAW,WAAW,CAAC;AAC9F,MAAI,OAAO,MAAO,OAAM,OAAO;AAC/B,MAAI,OAAO,WAAW,EAAG,OAAM,IAAI,SAAS,KAAK,CAAC,KAAK,IAAI,OAAO,MAAM;AACxE,SAAO,OAAO;AAChB;AAEA,SAAS,MAAM,MAAwB;AACrC,SAAO,KAAK,MAAM,IAAI,EAAE,OAAO,CAAC,SAAS,SAAS,EAAE;AACtD;AAEO,SAAS,UAAU,KAAsB;AAC9C,SAAO,OAAO,KAAK,GAAG;AACxB;AAEO,SAAS,SAAS,KAAqB;AAC5C,SAAO,IAAI,KAAK,CAAC,aAAa,iBAAiB,CAAC,EAAE,KAAK;AACzD;AAGO,SAAS,SAAS,KAAqB;AAC5C,SAAY,cAAQ,KAAK,IAAI,KAAK,CAAC,aAAa,cAAc,OAAO,CAAC,EAAE,KAAK,CAAC;AAChF;AAGO,SAAS,aAAa,OAA6B;AACxD,SAAO,MAAM,KAAK,EAAE,IAAI,CAAC,MAAM,MAAM;AACnC,UAAM,SAAS,KAAK,KAAK,EAAE,MAAM,KAAK;AACtC,QAAI,OAAO,WAAW,EAAG,OAAM,IAAI,YAAY,uBAAuB,IAAI,CAAC,wBAAwB;AACnG,eAAW,SAAS,CAAC,GAAG,CAAC,GAAG;AAC1B,UAAI,CAAC,UAAU,OAAO,KAAK,KAAK,EAAE,GAAG;AACnC,cAAM,IAAI,YAAY,uBAAuB,IAAI,CAAC,WAAW,QAAQ,CAAC,oBAAoB;AAAA,MAC5F;AAAA,IACF;AACA,WAAO,EAAE,UAAU,OAAO,CAAC,KAAK,IAAI,WAAW,OAAO,CAAC,KAAK,GAAG;AAAA,EACjE,CAAC;AACH;AAEA,SAAS,UAAU,KAAa,KAAsB;AACpD,SAAO,UAAU,OAAO,CAAC,YAAY,MAAM,gBAAgB,GAAG,GAAG,WAAW,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW;AACrG;AAGO,SAAS,iBAAiB,KAAa,QAAgB,QAA8B;AAC1F,MAAI,UAAU,OAAO,QAAQ,EAAG,QAAO,CAAC;AACxC,MAAI,CAAC,UAAU,OAAO,QAAQ,KAAK,CAAC,UAAU,OAAO,SAAS,EAAG,OAAM,IAAI,YAAY,+BAA+B;AACtH,MAAI,CAAC,aAAa,MAAM,EAAG,OAAM,IAAI,YAAY,6BAA6B;AAC9E,QAAM,QAAQ,CAAC,UAAU,OAAO,SAAS,KAAK,UAAU,KAAK,OAAO,SAAS;AAE7E,QAAM,QAAQ,QACV,CAAC,gBAAgB,GAAG,OAAO,SAAS,KAAK,OAAO,QAAQ,EAAE,IAC1D,CAAC,SAAS,aAAa,MAAM,IAAI,SAAS,gBAAgB,OAAO,QAAQ;AAC7E,SAAO,MAAM,IAAI,KAAK,CAAC,YAAY,aAAa,GAAG,KAAK,CAAC,CAAC;AAC5D;AAGO,SAAS,gBAAgB,KAAa,MAAc,MAAwB;AACjF,kBAAgB,MAAM,YAAY;AAClC,kBAAgB,MAAM,YAAY;AAClC,MAAI,UAAU,IAAI,EAAG,QAAO,MAAM,IAAI,KAAK,CAAC,YAAY,aAAa,gBAAgB,IAAI,CAAC,CAAC;AAC3F,SAAO,MAAM,IAAI,KAAK,CAAC,YAAY,aAAa,gBAAgB,GAAG,IAAI,KAAK,IAAI,EAAE,CAAC,CAAC;AACtF;AAGO,SAAS,WAAW,KAAa,KAAyB;AAC/D,kBAAgB,KAAK,QAAQ;AAC7B,SAAO,YAAY,KAAK,IAAI,KAAK,CAAC,QAAQ,MAAM,GAAG,aAAa,mBAAmB,gBAAgB,GAAG,CAAC,CAAC;AAC1G;AAGO,SAAS,SAAS,KAAyB;AAChD,QAAM,YAAY,IAAI,KAAK,CAAC,eAAe,MAAM,QAAQ,SAAS,GAAG,EAAE,EAAE,KAAK;AAC9E,SAAO,UAAU,IAAI,KAAK,CAAC,QAAQ,GAAG,aAAa,gBAAgB,gBAAgB,WAAW,MAAM,CAAC,CAAC;AACxG;;;AC9HA,YAAYC,SAAQ;AACpB,YAAYC,WAAU;AAIf,IAAM,cAAc;AAUpB,SAAS,WAAmB;AACjC,SAAU,iBAAa,IAAI,IAAI,qBAAqB,YAAY,GAAG,GAAG,OAAO;AAC/E;AAEA,SAAS,SAAS,UAAsC;AACtD,SAAU,eAAW,QAAQ,IAAO,iBAAa,UAAU,OAAO,IAAI;AACxE;AAOO,SAAS,YAAY,KAAa,KAAkE;AACzG,MAAI,KAAK,GAAG,EAAG,QAAO,EAAE,SAAS,gBAAgB;AACjD,QAAM,WAAgB,WAAK,SAAS,GAAG,GAAG,UAAU;AACpD,QAAM,OAAO,SAAS;AACtB,QAAM,UAAU,SAAS,QAAQ;AACjC,MAAI,YAAY,UAAa,CAAC,QAAQ,SAAS,WAAW,EAAG,QAAO,EAAE,SAAS,gBAAgB,SAAS;AACxG,MAAI,YAAY,KAAM,QAAO,EAAE,SAAS,aAAa,SAAS;AAC9D,EAAG,cAAe,cAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,EAAG,kBAAc,UAAU,MAAM,EAAE,MAAM,IAAM,CAAC;AAChD,EAAG,cAAU,UAAU,GAAK;AAC5B,SAAO,EAAE,SAAS,YAAY,SAAY,cAAc,WAAW,SAAS;AAC9E;;;AHdA,IAAM,QAAQ;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,SAAS;AAEf,SAAS,YAAY,MAAc,MAAuC;AACxE,SAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,WAAW,MAAM,GAAG,CAAC;AAC9D;AAEA,SAAS,eAAe,MAAc,QAAgB,OAA6B;AACjF,QAAM,UAAU,aAAa,KAAK;AAClC,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,QAAQ,CAAC,WAAW,iBAAiB,MAAM,QAAQ,MAAM,CAAC;AAAA,EACpE;AACF;AAEA,SAAS,QAAQ,IAAW,SAAiD;AAC3E,QAAM,OAAO,SAAS,GAAG,GAAG;AAC5B,QAAM,QAAQ,UAAU,IAAI;AAC5B,QAAM,EAAE,OAAO,QAAQ,QAAQ,IAAI,UAAU,GAAG,GAAG;AACnD,aAAW,UAAU,QAAS,IAAG,IAAI,SAAS,MAAM;AACpD,QAAM,SAAS,KAAK,QAAQ,IAAI,GAAG,EAAE,OAAO,MAAM,CAAC;AACnD,aAAW,QAAQ,aAAa,OAAO,UAAU,EAAE,GAAG,QAAQ,aAAa,OAAO,CAAC,EAAG,IAAG,IAAI,IAAI;AACjG,SAAO,OAAO,SAAS,SAAS,IAAI,IAAI;AAC1C;AAEA,IAAM,mBAA6D;AAAA,EACjE,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,gBAAgB;AAClB;AAEA,SAAS,WAAW,IAAmB;AACrC,QAAM,SAAS,YAAY,GAAG,KAAK,GAAG,GAAG;AACzC,QAAM,QAAQ,OAAO,aAAa,SAAY,KAAK,KAAK,OAAO,QAAQ;AACvE,MAAI,OAAO,YAAY,gBAAgB;AACrC,OAAG,IAAI,SAAS,iBAAiB,OAAO,OAAO,IAAI,KAAK;AACxD,WAAO;AAAA,EACT;AACA,KAAG,IAAI,SAAS,iBAAiB,OAAO,OAAO,IAAI,KAAK;AACxD,KAAG,IAAI,SAAS,kGAAkG;AAClH,SAAO;AACT;AAEA,SAAS,WAAW,SAAiB,MAAyB,KAAa,KAAmB;AAC5F,MAAI,KAAK,SAAS,OAAO,KAAK,SAAS,IAAK,OAAM,IAAI,YAAY,GAAG,OAAO,6BAA6B;AAC3G;AAGA,SAAS,YAAY,SAAiB,MAAyB,OAAmC,MAAoB;AACpH,OAAK,QAAQ,CAAC,OAAO,MAAM;AACzB,QAAI,CAAC,MAAM,KAAK,EAAG,OAAM,IAAI,YAAY,GAAG,OAAO,cAAc,IAAI,CAAC,WAAW,IAAI,EAAE;AAAA,EACzF,CAAC;AACH;AAEA,SAAS,SAAS,SAA6B,MAAyB,IAAmB;AACzF,UAAQ,SAAS;AAAA,IACf,KAAK;AAEH,iBAAW,SAAS,MAAM,GAAG,CAAC;AAC9B,kBAAY,SAAS,KAAK,MAAM,GAAG,CAAC,GAAG,cAAc,eAAe;AACpE,aAAO,QAAQ,IAAI,CAAC,SAAS,eAAe,MAAM,KAAK,CAAC,KAAK,IAAI,GAAG,UAAU,CAAC,CAAC;AAAA,IAClF,KAAK;AACH,iBAAW,SAAS,MAAM,GAAG,CAAC;AAC9B,kBAAY,SAAS,MAAM,YAAY,mBAAmB;AAC1D,aAAO,QAAQ,IAAI,CAAC,SAAS,YAAY,MAAM,gBAAgB,MAAM,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;AAAA,IACrG,KAAK;AACH,iBAAW,SAAS,MAAM,GAAG,CAAC;AAC9B,aAAO,QAAQ,IAAI,CAAC,SAAS,CAAC,SAAS,IAAI,CAAC,CAAC;AAAA,IAC/C,KAAK;AACH,iBAAW,SAAS,MAAM,GAAG,CAAC;AAC9B,aAAO,WAAW,EAAE;AAAA,IACtB;AACE,YAAM,IAAI,YAAY,YAAY,SAAY,oBAAoB,iBAAiB;AAAA,EACvF;AACF;AAGA,SAAS,iBAAiB,MAAmE;AAC3F,QAAM,EAAE,aAAa,OAAO,IAAI,UAAU;AAAA,IACxC,MAAM,CAAC,GAAG,IAAI;AAAA,IACd,kBAAkB;AAAA,IAClB,QAAQ;AAAA,IACR,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,OAAO,IAAI,EAAE;AAAA,EACnD,CAAC;AACD,QAAM,UAAU,OAAO,KAAK,CAAC,UAAU,MAAM,SAAS,YAAY;AAClE,QAAM,QAAQ,OAAO,OAAO,CAAC,UAAU,MAAM,SAAS,YAAY,MAAM,SAAS,MAAM;AACvF,MAAI,MAAM,KAAK,CAAC,UAAU,YAAY,UAAa,MAAM,QAAQ,QAAQ,KAAK,GAAG;AAC/E,UAAM,IAAI,YAAY,8CAA8C;AAAA,EACtE;AACA,SAAO,EAAE,aAAa,MAAM,MAAM,SAAS,EAAE;AAC/C;AAGO,SAAS,OAAO,MAAyB,IAAmB;AACjE,MAAI;AACF,UAAM,EAAE,aAAa,KAAK,IAAI,iBAAiB,IAAI;AACnD,QAAI,MAAM;AACR,iBAAW,QAAQ,MAAO,IAAG,IAAI,IAAI;AACrC,aAAO;AAAA,IACT;AACA,WAAO,SAAS,YAAY,CAAC,GAAG,YAAY,MAAM,CAAC,GAAG,EAAE;AAAA,EAC1D,SAAS,OAAO;AACd,OAAG,IAAI,UAAU,iBAAiB,QAAQ,MAAM,UAAU,mBAAmB;AAC7E,QAAI,iBAAiB,YAAa,IAAG,IAAI,MAAM,CAAC,KAAK,EAAE;AACvD,WAAO;AAAA,EACT;AACF;;;ADzIA,QAAQ,WAAW,OAAO,QAAQ,KAAK,MAAM,CAAC,GAAG;AAAA,EAC/C,KAAK,QAAQ,IAAI;AAAA,EACjB,KAAK,QAAQ;AAAA,EACb,WAAW,MAAS,iBAAa,GAAG,OAAO;AAAA,EAC3C,KAAK,CAAC,SAAS,QAAQ,OAAO,MAAM,GAAG,IAAI;AAAA,CAAI;AAAA,EAC/C,KAAK,CAAC,SAAS,QAAQ,OAAO,MAAM,GAAG,IAAI;AAAA,CAAI;AACjD,CAAC;","names":["fs","path","fs","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts","../src/cli.ts","../src/config.ts","../src/git.ts","../src/install.ts"],"sourcesContent":["#!/usr/bin/env node\nimport * as fs from \"node:fs\";\nimport { runCli } from \"./cli.js\";\n\nprocess.exitCode = runCli(process.argv.slice(2), {\n cwd: process.cwd(),\n env: process.env,\n readStdin: () => fs.readFileSync(0, \"utf-8\"),\n out: (line) => process.stdout.write(`${line}\\n`),\n err: (line) => process.stderr.write(`${line}\\n`),\n});\n","import { parseArgs } from \"node:util\";\nimport { ConfigError, loadAllow, loadTerms } from \"./config.js\";\nimport type { ScanSource } from \"./diff.js\";\nimport {\n commitsForRange,\n commitsForUpdate,\n isRemoteName,\n isRevision,\n parsePrePush,\n readCommit,\n readTree,\n repoRoot,\n} from \"./git.js\";\nimport { installHook, type InstallResult } from \"./install.js\";\nimport { formatReport } from \"./report.js\";\nimport { scan } from \"./scan.js\";\n\nexport interface CliIo {\n readonly cwd: string;\n readonly env: Readonly<Record<string, string | undefined>>;\n readStdin(): string;\n out(line: string): void;\n err(line: string): void;\n /** Overrides `MAX_PATCH_BYTES` for tests; the bin never sets it. */\n readonly maxPatchBytes?: number;\n}\n\nconst USAGE = [\n \"usage: titan-egress-scan <command>\",\n \" pre-push <remote> scan the commits a push sends (reads git's pre-push stdin)\",\n \" range <base> <head> scan every commit in base..head (CI)\",\n \" tree scan every tracked file at HEAD\",\n \" install-hook install the pre-push hook into git's hooks directory\",\n \"files git calls binary are scanned as text; a commit or tree over 128 MiB of patch text exits 2\",\n \"exit: 0 clean, 1 findings, 2 usage or configuration error\",\n];\n\nconst PREFIX = \"titan-egress-scan: \";\n\nfunction readCommits(root: string, shas: readonly string[], maxPatchBytes?: number): ScanSource[] {\n return [...new Set(shas)].map((sha) => readCommit(root, sha, maxPatchBytes));\n}\n\nfunction prePushSources(root: string, remote: string, io: CliIo): ScanSource[] {\n const updates = parsePrePush(io.readStdin());\n return readCommits(\n root,\n updates.flatMap((update) => commitsForUpdate(root, remote, update)),\n io.maxPatchBytes,\n );\n}\n\nfunction runScan(io: CliIo, collect: (root: string) => ScanSource[]): number {\n const root = repoRoot(io.cwd);\n const allow = loadAllow(root);\n const { terms, loaded, notices } = loadTerms(io.env);\n for (const notice of notices) io.err(PREFIX + notice);\n const result = scan(collect(root), { terms, allow });\n for (const line of formatReport(result.findings, { ...result, termsLoaded: loaded })) io.out(line);\n return result.findings.length > 0 ? 1 : 0;\n}\n\nconst INSTALL_MESSAGES: Record<InstallResult[\"outcome\"], string> = {\n installed: \"pre-push hook installed\",\n updated: \"pre-push hook updated\",\n unchanged: \"pre-push hook already installed\",\n \"skipped-in-ci\": \"CI detected; hook not installed\",\n \"foreign-hook\": \"an existing pre-push hook was not written by this tool; left it in place.\",\n};\n\nfunction runInstall(io: CliIo): number {\n const result = installHook(io.cwd, io.env);\n const where = result.hookPath === undefined ? \"\" : ` (${result.hookPath})`;\n if (result.outcome !== \"foreign-hook\") {\n io.out(PREFIX + INSTALL_MESSAGES[result.outcome] + where);\n return 0;\n }\n io.err(PREFIX + INSTALL_MESSAGES[result.outcome] + where);\n io.err(PREFIX + 'chain it: call `titan-egress-scan pre-push \"$1\"` from that hook with git\\'s stdin passed through');\n return 2;\n}\n\nfunction expectArgs(command: string, args: readonly string[], min: number, max: number): void {\n if (args.length < min || args.length > max) throw new ConfigError(`${command}: wrong number of arguments`);\n}\n\n/** Rejects a value by its position only, so the message never repeats what was passed. */\nfunction expectValid(command: string, args: readonly string[], valid: (value: string) => boolean, what: string): void {\n args.forEach((value, i) => {\n if (!valid(value)) throw new ConfigError(`${command}: argument ${i + 1} is not ${what}`);\n });\n}\n\nfunction dispatch(command: string | undefined, args: readonly string[], io: CliIo): number {\n switch (command) {\n case \"pre-push\":\n // git passes the remote name and its URL; only the name is used.\n expectArgs(command, args, 1, 2);\n expectValid(command, args.slice(0, 1), isRemoteName, \"a remote name\");\n return runScan(io, (root) => prePushSources(root, args[0] ?? \"\", io));\n case \"range\":\n expectArgs(command, args, 2, 2);\n expectValid(command, args, isRevision, \"a sha or ref name\");\n return runScan(io, (root) =>\n readCommits(root, commitsForRange(root, args[0] ?? \"\", args[1] ?? \"\"), io.maxPatchBytes),\n );\n case \"tree\":\n expectArgs(command, args, 0, 0);\n return runScan(io, (root) => [readTree(root, io.maxPatchBytes)]);\n case \"install-hook\":\n expectArgs(command, args, 0, 0);\n return runInstall(io);\n default:\n throw new ConfigError(command === undefined ? \"missing command\" : \"unknown command\");\n }\n}\n\n/** Help counts only before the command; after it, a help flag is a usage error, never a skipped scan. */\nfunction parseCommandLine(argv: readonly string[]): { positionals: string[]; help: boolean } {\n const { positionals, tokens } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n tokens: true,\n options: { help: { type: \"boolean\", short: \"h\" } },\n });\n const command = tokens.find((token) => token.kind === \"positional\");\n const helps = tokens.filter((token) => token.kind === \"option\" && token.name === \"help\");\n if (helps.some((token) => command !== undefined && token.index > command.index)) {\n throw new ConfigError(\"a help flag after the command is not allowed\");\n }\n return { positionals, help: helps.length > 0 };\n}\n\n/** Runs one command. Errors print as one line to stderr and exit 2; nothing echoes scanned text. */\nexport function runCli(argv: readonly string[], io: CliIo): number {\n try {\n const { positionals, help } = parseCommandLine(argv);\n if (help) {\n for (const line of USAGE) io.out(line);\n return 0;\n }\n return dispatch(positionals[0], positionals.slice(1), io);\n } catch (error) {\n io.err(PREFIX + (error instanceof Error ? error.message : \"unexpected error\"));\n if (error instanceof ConfigError) io.err(USAGE[0] ?? \"\");\n return 2;\n }\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { AllowFileError, EMPTY_ALLOW, parseAllow, type AllowList } from \"./allow.js\";\nimport type { TermRule } from \"./rules.js\";\nimport { parseTerms, TermFileError } from \"./terms.js\";\n\n/** A usage or configuration problem: exit 2. Messages never carry a term or scanned text. */\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\nexport interface LoadedTerms {\n readonly terms: readonly TermRule[];\n readonly loaded: boolean;\n readonly notices: readonly string[];\n}\n\ntype Env = Readonly<Record<string, string | undefined>>;\n\nconst NOT_LOADED: LoadedTerms = { terms: [], loaded: false, notices: [] };\n\nexport function isCi(env: Env): boolean {\n const value = env.CI?.toLowerCase();\n return value !== undefined && value !== \"\" && value !== \"false\" && value !== \"0\";\n}\n\nexport function termFilePath(env: Env): string | undefined {\n if (env.TITAN_EGRESS_TERMS) return env.TITAN_EGRESS_TERMS;\n const configHome = env.XDG_CONFIG_HOME || (env.HOME && path.join(env.HOME, \".config\"));\n return configHome ? path.join(configHome, \"titan-egress\", \"private-terms\") : undefined;\n}\n\nfunction readIfPresent(file: string, what: string): string | undefined {\n try {\n return fs.readFileSync(file, \"utf-8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw new ConfigError(`${what} could not be read`);\n }\n}\n\nconst ON_VALUES = new Set([\"1\", \"true\", \"yes\", \"on\"]);\nconst OFF_VALUES = new Set([\"\", \"0\", \"false\", \"no\", \"off\"]);\n\n/** An unrecognised value fails the scan: a typo must never read as off. The value is not echoed. */\nfunction requiresTerms(env: Env): boolean {\n const value = (env.TITAN_EGRESS_REQUIRE_TERMS ?? \"\").trim().toLowerCase();\n if (ON_VALUES.has(value)) return true;\n if (OFF_VALUES.has(value)) return false;\n throw new ConfigError(\"TITAN_EGRESS_REQUIRE_TERMS is not one of 1, true, yes, on, 0, false, no, off\");\n}\n\nfunction readTermFile(file: string, required: boolean): string | undefined {\n try {\n return readIfPresent(file, \"private term list\");\n } catch (error) {\n if (required && error instanceof ConfigError) {\n throw new ConfigError(\"private term list could not be read and TITAN_EGRESS_REQUIRE_TERMS=1\");\n }\n throw error;\n }\n}\n\nfunction missingTerms(required: boolean): LoadedTerms {\n if (required) {\n throw new ConfigError(\"private term list not found and TITAN_EGRESS_REQUIRE_TERMS=1\");\n }\n return { ...NOT_LOADED, notices: [\"private term list not found; generic rules only\"] };\n}\n\nfunction compileTerms(text: string): readonly TermRule[] {\n try {\n return parseTerms(text);\n } catch (error) {\n if (error instanceof TermFileError) throw new ConfigError(error.message);\n throw error;\n }\n}\n\n/**\n * Loads the private term list. In CI it is never looked up and an absent file is a notice, unless\n * TITAN_EGRESS_REQUIRE_TERMS=1, which always loads and fails on a missing, unreadable or empty list.\n */\nexport function loadTerms(env: Env): LoadedTerms {\n const required = requiresTerms(env);\n if (isCi(env) && !required) return NOT_LOADED;\n const file = termFilePath(env);\n const text = file === undefined ? undefined : readTermFile(file, required);\n if (file === undefined || text === undefined) return missingTerms(required);\n const openToOthers = process.platform !== \"win32\" && (fs.statSync(file).mode & 0o077) !== 0;\n const notices = openToOthers ? [\"private term list is readable by other users; chmod 600 it\"] : [];\n const terms = compileTerms(text);\n if (terms.length === 0 && required) {\n throw new ConfigError(\"private term list holds no terms and TITAN_EGRESS_REQUIRE_TERMS=1\");\n }\n return { terms, loaded: true, notices };\n}\n\n/** Loads `.egress-allow` from the repo root. A malformed file fails the scan, never reads as empty. */\nexport function loadAllow(root: string): AllowList {\n const text = readIfPresent(path.join(root, \".egress-allow\"), \".egress-allow\");\n if (text === undefined) return EMPTY_ALLOW;\n try {\n return parseAllow(text);\n } catch (error) {\n if (error instanceof AllowFileError) throw new ConfigError(error.message);\n throw error;\n }\n}\n","import { spawnSync, type SpawnSyncReturns } from \"node:child_process\";\nimport * as path from \"node:path\";\nimport { ConfigError } from \"./config.js\";\nimport { parseDiff, type ScanSource } from \"./diff.js\";\n\n/** A git command that failed. The message names the subcommand, never git's output. */\nexport class GitError extends Error {\n constructor(subcommand: string, status: number | null) {\n super(`git ${subcommand} failed (exit ${status ?? \"signal\"}); run it by hand to see git's error`);\n this.name = \"GitError\";\n }\n}\n\nexport interface PushUpdate {\n readonly localSha: string;\n readonly remoteSha: string;\n}\n\n/** A commit or tree whose patch text passes this many bytes is refused: over it, V8 cannot hold the text as one string. */\nexport const MAX_PATCH_BYTES = 128 * 1024 * 1024;\n\n/** A commit or tree too large to scan. The message names the source and the limit, never the content. */\nexport class PatchTooLargeError extends Error {\n constructor(sha: string | undefined, limit: number) {\n super(`${sha === undefined ? \"tree\" : `commit ${sha.slice(0, 7)}`}: patch text is over the scan limit of ${formatBytes(limit)}; refusing it`);\n this.name = \"PatchTooLargeError\";\n }\n}\n\nfunction formatBytes(bytes: number): string {\n const mib = 1024 * 1024;\n return bytes % mib === 0 ? `${bytes / mib} MiB` : `${bytes} bytes`;\n}\n\n// Pinned so user config (prefixes, textconv, relative, external diff) cannot reshape the patch text.\n// `--text` diffs a file git calls binary, so one NUL byte cannot hide the lines around it.\nconst PATCH_FLAGS = [\n \"--text\",\n \"-U0\",\n \"--no-color\",\n \"--no-ext-diff\",\n \"--no-textconv\",\n \"--no-relative\",\n \"--src-prefix=a/\",\n \"--dst-prefix=b/\",\n];\n// A message re-encoded by `i18n.logOutputEncoding` (UTF-16, say) would slip past every rule.\nconst MESSAGE_FLAGS = [\"--no-patch\", \"--encoding=UTF-8\", \"--format=%B\"];\n// A combined diff ignores `--text`, so a merge is diffed against each parent in turn instead.\nconst COMMIT_PATCH_FLAGS = [\"--diff-merges=separate\", \"--format=\", ...PATCH_FLAGS];\nconst MAX_BUFFER = 1024 * 1024 * 1024;\n// Every revision argument follows this, so a value that starts with a dash cannot become an option.\nconst END_OF_OPTIONS = \"--end-of-options\";\n\nconst FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;\nconst REVISION = /^(?:[0-9a-f]{7,64}|[^-\\s]\\S*)$/;\nconst REMOTE_NAME = /^[^-\\s]\\S*$/;\n\nexport function isFullSha(value: string): boolean {\n return FULL_SHA.test(value);\n}\n\n/** A hex sha of 7 to 64 characters, or a ref name that cannot be read as an option. */\nexport function isRevision(value: string): boolean {\n return REVISION.test(value);\n}\n\nexport function isRemoteName(value: string): boolean {\n return REMOTE_NAME.test(value);\n}\n\nfunction requireRevision(value: string, what: string): void {\n if (!isRevision(value)) throw new ConfigError(`${what} is not a sha or ref name`);\n}\n\nfunction runGit(cwd: string, args: readonly string[], maxBuffer: number, input?: string): SpawnSyncReturns<string> {\n return spawnSync(\"git\", args, { cwd, input, encoding: \"utf-8\", maxBuffer });\n}\n\nfunction stdoutOf(result: SpawnSyncReturns<string>, subcommand: string): string {\n if (result.error) throw result.error;\n if (result.status !== 0) throw new GitError(subcommand, result.status);\n return result.stdout;\n}\n\nexport function git(cwd: string, args: readonly string[], input?: string): string {\n return stdoutOf(runGit(cwd, args, MAX_BUFFER, input), args[0] ?? \"\");\n}\n\nfunction lines(text: string): string[] {\n return text.split(\"\\n\").filter((line) => line !== \"\");\n}\n\nexport function isZeroSha(sha: string): boolean {\n return /^0+$/.test(sha);\n}\n\nexport function repoRoot(cwd: string): string {\n return git(cwd, [\"rev-parse\", \"--show-toplevel\"]).trim();\n}\n\n/** Resolves the hooks directory as git does: `core.hooksPath` if set, else the common git dir's hooks. */\nexport function hooksDir(cwd: string): string {\n return path.resolve(cwd, git(cwd, [\"rev-parse\", \"--git-path\", \"hooks\"]).trim());\n}\n\n/** Parses git's pre-push stdin: `<local-ref> <local-sha> <remote-ref> <remote-sha>` per line. */\nexport function parsePrePush(stdin: string): PushUpdate[] {\n return lines(stdin).map((line, i) => {\n const fields = line.trim().split(/\\s+/);\n if (fields.length !== 4) throw new ConfigError(`pre-push stdin line ${i + 1}: expected four fields`);\n for (const field of [1, 3]) {\n if (!isFullSha(fields[field] ?? \"\")) {\n throw new ConfigError(`pre-push stdin line ${i + 1}: field ${field + 1} is not a full sha`);\n }\n }\n return { localSha: fields[1] ?? \"\", remoteSha: fields[3] ?? \"\" };\n });\n}\n\nfunction hasCommit(cwd: string, sha: string): boolean {\n return spawnSync(\"git\", [\"cat-file\", \"-e\", END_OF_OPTIONS, `${sha}^{commit}`], { cwd }).status === 0;\n}\n\n/** The commits one pushed ref update sends that the remote lacks, oldest first. */\nexport function commitsForUpdate(cwd: string, remote: string, update: PushUpdate): string[] {\n if (isZeroSha(update.localSha)) return [];\n if (!isFullSha(update.localSha) || !isFullSha(update.remoteSha)) throw new ConfigError(\"push update is not a full sha\");\n if (!isRemoteName(remote)) throw new ConfigError(\"remote is not a remote name\");\n const known = !isZeroSha(update.remoteSha) && hasCommit(cwd, update.remoteSha);\n // The second `--not` flips back, so the local sha after it counts as included.\n const range = known\n ? [END_OF_OPTIONS, `${update.remoteSha}..${update.localSha}`]\n : [\"--not\", `--remotes=${remote}`, \"--not\", END_OF_OPTIONS, update.localSha];\n return lines(git(cwd, [\"rev-list\", \"--reverse\", ...range]));\n}\n\n/** The commits in `base..head`, oldest first; an all-zero base means `head` alone. */\nexport function commitsForRange(cwd: string, base: string, head: string): string[] {\n requireRevision(base, \"range base\");\n requireRevision(head, \"range head\");\n if (isZeroSha(base)) return lines(git(cwd, [\"rev-list\", \"--no-walk\", END_OF_OPTIONS, head]));\n return lines(git(cwd, [\"rev-list\", \"--reverse\", END_OF_OPTIONS, `${base}..${head}`]));\n}\n\nfunction isOverBuffer(error: Error | undefined): boolean {\n return (error as NodeJS.ErrnoException | undefined)?.code === \"ENOBUFS\";\n}\n\n/** Refuses a source in which git still skipped a file as binary, since its lines went unscanned. */\nfunction requireAllText(source: ScanSource, what: string): ScanSource {\n if (source.binaryFiles > 0) throw new Error(`${what}: git printed a file as binary despite --text; refusing it`);\n return source;\n}\n\nfunction showCommit(cwd: string, sha: string, flags: readonly string[], maxBytes: number): string {\n const result = runGit(cwd, [\"show\", ...flags, END_OF_OPTIONS, sha], maxBytes);\n if (isOverBuffer(result.error)) throw new PatchTooLargeError(sha, maxBytes);\n return stdoutOf(result, \"show\");\n}\n\n/**\n * One commit's message and patch, read by separate calls so a merge's per-parent copies of the\n * message never land inside its patch. Throws `PatchTooLargeError` when either is over `maxPatchBytes`.\n */\nexport function readCommit(cwd: string, sha: string, maxPatchBytes = MAX_PATCH_BYTES): ScanSource {\n requireRevision(sha, \"commit\");\n const message = showCommit(cwd, sha, MESSAGE_FLAGS, maxPatchBytes).replace(/\\n+$/, \"\").split(\"\\n\");\n const patch = parseDiff(showCommit(cwd, sha, COMMIT_PATCH_FLAGS, maxPatchBytes));\n return requireAllText({ ...patch, sha, message }, `commit ${sha.slice(0, 7)}`);\n}\n\n/** Every tracked file at HEAD, as one diff from the empty tree. Throws `PatchTooLargeError` over `maxPatchBytes`. */\nexport function readTree(cwd: string, maxPatchBytes = MAX_PATCH_BYTES): ScanSource {\n const emptyTree = git(cwd, [\"hash-object\", \"-t\", \"tree\", \"--stdin\"], \"\").trim();\n const args = [\"diff\", ...PATCH_FLAGS, \"--no-renames\", END_OF_OPTIONS, emptyTree, \"HEAD\"];\n const result = runGit(cwd, args, maxPatchBytes);\n if (isOverBuffer(result.error)) throw new PatchTooLargeError(undefined, maxPatchBytes);\n return requireAllText(parseDiff(stdoutOf(result, \"diff\")), \"tree\");\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { isCi } from \"./config.js\";\nimport { hooksDir } from \"./git.js\";\n\nexport const HOOK_MARKER = \"# titan-egress-scan managed pre-push hook\";\n\nexport type InstallOutcome = \"installed\" | \"updated\" | \"unchanged\" | \"skipped-in-ci\" | \"foreign-hook\";\n\nexport interface InstallResult {\n readonly outcome: InstallOutcome;\n readonly hookPath?: string;\n}\n\n/** The shipped hook body, one directory above both `src/` and `dist/`. */\nexport function hookBody(): string {\n return fs.readFileSync(new URL(\"../hooks/pre-push\", import.meta.url), \"utf-8\");\n}\n\nfunction existing(hookPath: string): string | undefined {\n return fs.existsSync(hookPath) ? fs.readFileSync(hookPath, \"utf-8\") : undefined;\n}\n\n/**\n * Writes the pre-push hook into git's hooks directory, which honours `core.hooksPath` and is\n * shared by every linked worktree. It never sets `core.hooksPath` and never replaces a hook it\n * did not write.\n */\nexport function installHook(cwd: string, env: Readonly<Record<string, string | undefined>>): InstallResult {\n if (isCi(env)) return { outcome: \"skipped-in-ci\" };\n const hookPath = path.join(hooksDir(cwd), \"pre-push\");\n const body = hookBody();\n const current = existing(hookPath);\n if (current !== undefined && !current.includes(HOOK_MARKER)) return { outcome: \"foreign-hook\", hookPath };\n if (current === body) return { outcome: \"unchanged\", hookPath };\n fs.mkdirSync(path.dirname(hookPath), { recursive: true });\n fs.writeFileSync(hookPath, body, { mode: 0o755 });\n fs.chmodSync(hookPath, 0o755);\n return { outcome: current === undefined ? \"installed\" : \"updated\", hookPath };\n}\n"],"mappings":"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","names":["fs","path","fs","path"]}
|
|
@@ -171,6 +171,23 @@ function applyHeaderLine(line, file) {
|
|
|
171
171
|
else if (line.startsWith("@@")) return parseHunkHeader(line);
|
|
172
172
|
return void 0;
|
|
173
173
|
}
|
|
174
|
+
function mergeInto(target, file) {
|
|
175
|
+
target.pathAdded ||= file.pathAdded;
|
|
176
|
+
target.binary ||= file.binary;
|
|
177
|
+
const seen = new Set(target.lines.map((l) => `${l.line}
|
|
178
|
+
${l.text}`));
|
|
179
|
+
target.lines.push(...file.lines.filter((l) => !seen.has(`${l.line}
|
|
180
|
+
${l.text}`)));
|
|
181
|
+
}
|
|
182
|
+
function mergeSamePaths(files) {
|
|
183
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
const target = byPath.get(file.path);
|
|
186
|
+
if (target) mergeInto(target, file);
|
|
187
|
+
else byPath.set(file.path, { ...file, ordinal: byPath.size + 1, lines: [...file.lines] });
|
|
188
|
+
}
|
|
189
|
+
return [...byPath.values()];
|
|
190
|
+
}
|
|
174
191
|
function parseDiff(text) {
|
|
175
192
|
const files = [];
|
|
176
193
|
let hunk;
|
|
@@ -186,7 +203,8 @@ function parseDiff(text) {
|
|
|
186
203
|
files.push({ path: headerPath(line), ordinal: files.length + 1, pathAdded: false, binary: false, lines: [] });
|
|
187
204
|
} else if (file) hunk = applyHeaderLine(line, file);
|
|
188
205
|
}
|
|
189
|
-
|
|
206
|
+
const merged = mergeSamePaths(files);
|
|
207
|
+
return { files: merged, binaryFiles: merged.filter((f) => f.binary).length };
|
|
190
208
|
}
|
|
191
209
|
function parseCommit(sha, text) {
|
|
192
210
|
const cut = text.indexOf("\0");
|
|
@@ -319,4 +337,4 @@ export {
|
|
|
319
337
|
scan,
|
|
320
338
|
formatReport
|
|
321
339
|
};
|
|
322
|
-
//# sourceMappingURL=chunk-
|
|
340
|
+
//# sourceMappingURL=chunk-RK4WHDXR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/allow.ts","../src/terms.ts","../src/diff.ts","../src/rules.ts","../src/scan.ts","../src/report.ts"],"sourcesContent":["import type { RuleId } from \"./rules.js\";\n\nexport type AllowableRule = Exclude<RuleId, \"private-term\">;\n\nexport interface AllowEntry {\n readonly glob: string;\n readonly rule: AllowableRule;\n readonly line: number;\n readonly pattern: RegExp;\n}\n\nexport interface AllowList {\n readonly entries: readonly AllowEntry[];\n}\n\nexport const EMPTY_ALLOW: AllowList = { entries: [] };\n\n/** A malformed `.egress-allow`. Callers must fail the scan on it, never treat the file as empty. */\nexport class AllowFileError extends Error {\n constructor(\n readonly line: number,\n reason: string,\n ) {\n super(`.egress-allow line ${line}: ${reason}`);\n this.name = \"AllowFileError\";\n }\n}\n\nconst ALLOWABLE: readonly string[] = [\"home-path\", \"aw-data-path\"];\nconst TASK_ID = /\\b[A-Z]+-\\d+\\b/;\n\n/** Anchored glob over repo-relative paths: `**` spans directories, `*` and `?` stay within one. */\nexport function globToRegExp(glob: string): RegExp {\n let source = \"\";\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i] ?? \"\";\n if (glob.startsWith(\"**/\", i)) {\n source += \"(?:.*/)?\";\n i += 2;\n } else if (glob.startsWith(\"**\", i)) {\n source += \".*\";\n i += 1;\n } else if (char === \"*\") source += \"[^/]*\";\n else if (char === \"?\") source += \"[^/]\";\n else source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n return new RegExp(`^${source}$`);\n}\n\n// A glob made only of wildcard segments would allow a rule across the whole tree.\nfunction hasLiteralSegment(glob: string): boolean {\n return glob.split(\"/\").some((segment) => segment !== \"\" && !/[*?]/.test(segment));\n}\n\nfunction parseEntry(entry: string, line: number): AllowEntry {\n const fields = /^(\\S+)\\s+(\\S+)\\s+(.+)$/.exec(entry);\n if (!fields) throw new AllowFileError(line, \"expected <glob> <rule-id> <reason>\");\n const [, glob = \"\", rule = \"\", reason = \"\"] = fields;\n if (rule === \"private-term\") throw new AllowFileError(line, \"private-term is never allowable\");\n if (!ALLOWABLE.includes(rule)) throw new AllowFileError(line, \"unknown rule id\");\n if (!TASK_ID.test(reason)) throw new AllowFileError(line, \"reason must name a task id\");\n if (!hasLiteralSegment(glob)) throw new AllowFileError(line, \"glob must name at least one literal path segment\");\n return { glob, rule: rule as AllowableRule, line, pattern: globToRegExp(glob) };\n}\n\nexport function parseAllow(text: string): AllowList {\n const entries: AllowEntry[] = [];\n text.split(/\\r?\\n/).forEach((raw, i) => {\n const entry = raw.trim();\n if (entry === \"\" || entry.startsWith(\"#\")) return;\n entries.push(parseEntry(entry, i + 1));\n });\n return { entries };\n}\n\nexport function isAllowed(allow: AllowList, path: string, rule: RuleId): boolean {\n if (rule === \"private-term\") return false;\n return allow.entries.some((entry) => entry.rule === rule && entry.pattern.test(path));\n}\n","import type { TermRule } from \"./rules.js\";\n\n/** A term-file problem. The message names the line number only, never the term. */\nexport class TermFileError extends Error {\n constructor(\n readonly line: number,\n reason: string,\n ) {\n super(`private term list line ${line}: ${reason}`);\n this.name = \"TermFileError\";\n }\n}\n\nconst WORD_CHAR = String.raw`[\\p{L}\\p{N}_]`;\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\/]/g, \"\\\\$&\");\n}\n\nfunction compileRegex(source: string, line: number): RegExp {\n if (source === \"\") throw new TermFileError(line, \"empty regular expression\");\n try {\n return new RegExp(source, \"iu\");\n } catch {\n // The engine's message quotes the pattern, so it is dropped rather than wrapped.\n throw new TermFileError(line, \"invalid regular expression\");\n }\n}\n\nfunction compileTerm(entry: string, line: number): RegExp {\n if (entry.startsWith(\"re:\")) return compileRegex(entry.slice(3), line);\n return new RegExp(`(?<!${WORD_CHAR})${escapeRegExp(entry)}(?!${WORD_CHAR})`, \"iu\");\n}\n\n/**\n * Parses a private term list: one term per line, `#` comments, blank lines ignored, matched\n * case-insensitively on word boundaries; a `re:` line is a regex source compiled with `iu`.\n */\nexport function parseTerms(text: string): TermRule[] {\n const rules: TermRule[] = [];\n text\n .replace(/^\\uFEFF/, \"\")\n .split(/\\r?\\n/)\n .forEach((raw, i) => {\n const entry = raw.trim();\n if (entry === \"\" || entry.startsWith(\"#\")) return;\n const pattern = compileTerm(entry, i + 1);\n if (pattern.test(\"\")) throw new TermFileError(i + 1, \"matches the empty string, so it would match every line\");\n rules.push({ index: i + 1, matches: (candidate) => pattern.test(candidate) });\n });\n return rules;\n}\n","export interface AddedLine {\n readonly line: number;\n readonly text: string;\n}\n\nexport interface DiffFile {\n readonly path: string;\n /** 1-based position in its diff, used in place of a path that itself trips a rule. */\n readonly ordinal: number;\n /** True for a new, renamed or copied file, whose path is itself new text. */\n readonly pathAdded: boolean;\n readonly binary: boolean;\n readonly lines: readonly AddedLine[];\n}\n\n/** One unit of scanned text: a diff, or a commit's message plus its diff. */\nexport interface ScanSource {\n readonly sha?: string;\n readonly message?: readonly string[];\n readonly files: readonly DiffFile[];\n readonly binaryFiles: number;\n}\n\ninterface MutableFile {\n path: string;\n ordinal: number;\n pathAdded: boolean;\n binary: boolean;\n lines: AddedLine[];\n}\n\ninterface Hunk {\n columns: number;\n newLine: number;\n remainingNew: number;\n remainingOld: number[];\n}\n\nconst HUNK_HEADER = /^(@{2,}) ((?:-\\d+(?:,\\d+)? )+)\\+(\\d+)(?:,(\\d+))? \\1/;\nconst C_ESCAPES: Record<string, number> = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 };\n\n/** Undoes git's C-style path quoting (`core.quotePath`), octal UTF-8 bytes included. */\nexport function unquotePath(raw: string): string {\n if (raw.length < 2 || !raw.startsWith('\"') || !raw.endsWith('\"')) return raw;\n const body = raw.slice(1, -1);\n const bytes: number[] = [];\n for (let i = 0; i < body.length; i++) {\n const char = body[i] ?? \"\";\n const next = body[i + 1] ?? \"\";\n if (char !== \"\\\\\") bytes.push(...new TextEncoder().encode(char));\n else if (/[0-7]/.test(next)) {\n bytes.push(parseInt(body.slice(i + 1, i + 4), 8));\n i += 3;\n } else {\n bytes.push(C_ESCAPES[next] ?? next.charCodeAt(0));\n i += 1;\n }\n }\n return new TextDecoder().decode(new Uint8Array(bytes));\n}\n\nfunction stripSide(path: string): string {\n return path.replace(/^[ab]\\//, \"\");\n}\n\nfunction headerPath(header: string): string {\n const combined = /^diff --(?:cc|combined) (.+)$/.exec(header);\n if (combined) return unquotePath(combined[1] ?? \"\");\n const rest = header.slice(\"diff --git \".length);\n const quoted = / (\"(?:[^\"\\\\]|\\\\.)*\")$/.exec(rest);\n if (quoted) return stripSide(unquotePath(quoted[1] ?? \"\"));\n const half = (rest.length - 1) / 2;\n if (Number.isInteger(half) && rest.slice(2, half) === rest.slice(half + 3)) return rest.slice(2, half);\n return stripSide(rest.slice(rest.lastIndexOf(\" b/\") + 1));\n}\n\nfunction rangeCount(token: string): number {\n const count = token.split(\",\")[1];\n return count === undefined ? 1 : Number(count);\n}\n\nfunction parseHunkHeader(line: string): Hunk | undefined {\n const match = HUNK_HEADER.exec(line);\n if (!match) return undefined;\n const oldRanges = (match[2] ?? \"\").trim().split(\" \");\n return {\n columns: (match[1] ?? \"\").length - 1,\n newLine: Number(match[3]),\n remainingNew: match[4] === undefined ? 1 : Number(match[4]),\n remainingOld: oldRanges.map(rangeCount),\n };\n}\n\nfunction isHunkLine(hunk: Hunk, line: string): boolean {\n if (line.startsWith(\"\\\\\")) return true;\n return line.length >= hunk.columns && /^[ +-]*$/.test(line.slice(0, hunk.columns));\n}\n\n/** Applies one hunk body line; a `-` in any column means the line is not in the new file. */\nfunction consumeHunkLine(hunk: Hunk, line: string, file: MutableFile): void {\n if (line.startsWith(\"\\\\\")) return;\n const marks = [...line.slice(0, hunk.columns)];\n const removed = marks.includes(\"-\");\n const oldMark = removed ? \"-\" : \" \";\n hunk.remainingOld = hunk.remainingOld.map((n, i) => (marks[i] === oldMark ? n - 1 : n));\n if (removed) return;\n if (marks.includes(\"+\")) file.lines.push({ line: hunk.newLine, text: line.slice(hunk.columns) });\n hunk.newLine++;\n hunk.remainingNew--;\n}\n\nfunction hunkDone(hunk: Hunk): boolean {\n return hunk.remainingNew <= 0 && hunk.remainingOld.every((n) => n <= 0);\n}\n\nfunction applyHeaderLine(line: string, file: MutableFile): Hunk | undefined {\n if (file.binary) return undefined;\n const renamed = /^(?:rename|copy) to (.+)$/.exec(line);\n if (line.startsWith(\"new file mode \")) file.pathAdded = true;\n else if (renamed) {\n file.path = unquotePath(renamed[1] ?? \"\");\n file.pathAdded = true;\n } else if (line.startsWith(\"+++ \")) {\n const target = line.slice(4).replace(/\\t$/, \"\");\n if (target !== \"/dev/null\") file.path = stripSide(unquotePath(target));\n } else if (line.startsWith(\"Binary files \") || line === \"GIT binary patch\") file.binary = true;\n else if (line.startsWith(\"@@\")) return parseHunkHeader(line);\n return undefined;\n}\n\nfunction mergeInto(target: MutableFile, file: DiffFile): void {\n target.pathAdded ||= file.pathAdded;\n target.binary ||= file.binary;\n const seen = new Set(target.lines.map((l) => `${l.line}\\n${l.text}`));\n target.lines.push(...file.lines.filter((l) => !seen.has(`${l.line}\\n${l.text}`)));\n}\n\n/** Folds the per-parent diffs of one merge into one entry per path, so a line is reported once. */\nfunction mergeSamePaths(files: readonly DiffFile[]): DiffFile[] {\n const byPath = new Map<string, MutableFile>();\n for (const file of files) {\n const target = byPath.get(file.path);\n if (target) mergeInto(target, file);\n else byPath.set(file.path, { ...file, ordinal: byPath.size + 1, lines: [...file.lines] });\n }\n return [...byPath.values()];\n}\n\n/**\n * Parses `git diff -U0` or `git show -U0` patch text into the added lines of each file, with\n * new-file line numbers. Removed lines are dropped; binary files are kept for their path only.\n * A merge shown with `--diff-merges=separate` names a path once per parent; those fold into one.\n */\nexport function parseDiff(text: string): ScanSource {\n const files: MutableFile[] = [];\n let hunk: Hunk | undefined;\n for (const line of text.split(\"\\n\")) {\n const file = files.at(-1);\n if (hunk && file && isHunkLine(hunk, line)) {\n consumeHunkLine(hunk, line, file);\n if (hunkDone(hunk)) hunk = undefined;\n continue;\n }\n hunk = undefined;\n if (/^diff --(?:git|cc|combined) /.test(line)) {\n files.push({ path: headerPath(line), ordinal: files.length + 1, pathAdded: false, binary: false, lines: [] });\n } else if (file) hunk = applyHeaderLine(line, file);\n }\n const merged = mergeSamePaths(files);\n return { files: merged, binaryFiles: merged.filter((f) => f.binary).length };\n}\n\n/** Parses `git show -U0 --format=%B%x00 <sha>` output: the message, a NUL, then the patch. */\nexport function parseCommit(sha: string, text: string): ScanSource {\n const cut = text.indexOf(\"\\0\");\n if (cut < 0) throw new Error(`commit ${sha.slice(0, 7)}: no NUL after the message`);\n const message = text.slice(0, cut).replace(/\\n+$/, \"\").split(\"\\n\");\n return { ...parseDiff(text.slice(cut + 1)), sha, message };\n}\n","export type RuleId = \"home-path\" | \"aw-data-path\" | \"private-term\";\n\nexport const RULE_IDS: readonly RuleId[] = [\"home-path\", \"aw-data-path\", \"private-term\"];\n\n/** A compiled private term. It exposes its line number, never its text or pattern. */\nexport interface TermRule {\n readonly index: number;\n matches(text: string): boolean;\n}\n\nexport interface RuleHit {\n readonly rule: RuleId;\n readonly termIndex?: number;\n}\n\n/** Home-directory segments that are placeholders, not people. Compared case-insensitively. */\nexport const PLACEHOLDER_SEGMENTS: readonly string[] = [\n \"Shared\",\n \"runner\",\n \"you\",\n \"your-name\",\n \"user\",\n \"username\",\n \"me\",\n \"example\",\n \"name\",\n \"x\",\n \"alice\",\n \"bob\",\n];\n\n// One path separator: slash, backslash, or the JSON-escaped form of either.\nconst SEP = String.raw`(?:\\\\\\\\|\\\\/|/|\\\\)`;\nconst SEGMENT = String.raw`([^/\\\\\\s\"'\\`),;\\]]+)`;\nconst USERS = \"[Uu][Ss][Ee][Rr][Ss]\";\n// A root must start a path; after `file://` any host may precede it.\nconst PATH_START = String.raw`(?:(?<![\\w.-])|(?<=file://[^/\\s\"'\\`]*))`;\nconst HOME_ROOT = String.raw`(?:${PATH_START}${SEP}(?:${USERS}|[Hh][Oo][Mm][Ee])|(?<!\\w)[A-Za-z]:${SEP}${USERS})`;\nconst HOME_PATH = new RegExp(`${HOME_ROOT}${SEP}${SEGMENT}`, \"g\");\n\nconst SPACE = String.raw`(?: |%20|\\\\ )`;\nconst AW_DATA_ROOTS = [`Library${SEP}Application${SPACE}Support`, String.raw`\\.local${SEP}share`, `AppData${SEP}Local`];\n// The single definition of the active-work data directory shapes; narrow it here and nowhere else.\nconst AW_DATA_PATH = new RegExp(`(?:${AW_DATA_ROOTS.join(\"|\")})${SEP}active-work(?!\\\\w)`, \"i\");\n\nconst PLACEHOLDER_SET = new Set(PLACEHOLDER_SEGMENTS.map((s) => s.toLowerCase()));\n\nfunction isPlaceholder(rawSegment: string): boolean {\n const segment = rawSegment.replace(/[.:]+$/, \"\");\n if (segment === \"\") return true;\n if (/^<.*>$/.test(segment) || /^\\{.*\\}$/.test(segment) || segment.startsWith(\"$\")) return true;\n return PLACEHOLDER_SET.has(segment.toLowerCase());\n}\n\nexport function matchesHomePath(text: string): boolean {\n for (const match of text.matchAll(HOME_PATH)) {\n if (!isPlaceholder(match[1] ?? \"\")) return true;\n }\n return false;\n}\n\nexport function matchesAwDataPath(text: string): boolean {\n return AW_DATA_PATH.test(text);\n}\n\nexport function matchRules(text: string, terms: readonly TermRule[] = []): RuleHit[] {\n const hits: RuleHit[] = [];\n if (matchesHomePath(text)) hits.push({ rule: \"home-path\" });\n if (matchesAwDataPath(text)) hits.push({ rule: \"aw-data-path\" });\n for (const term of terms) {\n if (term.matches(text)) hits.push({ rule: \"private-term\", termIndex: term.index });\n }\n return hits;\n}\n","import { EMPTY_ALLOW, isAllowed, type AllowList } from \"./allow.js\";\nimport type { DiffFile, ScanSource } from \"./diff.js\";\nimport { matchRules, type RuleHit, type RuleId, type TermRule } from \"./rules.js\";\n\n/** Where a rule fired. By construction no field carries the matched text, term or line. */\nexport interface Finding {\n readonly location: string;\n readonly rule: RuleId;\n readonly termIndex?: number;\n}\n\nexport type RuleCounts = Record<RuleId, number>;\n\nexport interface ScanOptions {\n readonly terms?: readonly TermRule[];\n readonly allow?: AllowList;\n}\n\nexport interface ScanResult {\n readonly findings: Finding[];\n readonly allowed: RuleCounts;\n readonly binaryFilesSkipped: number;\n}\n\ninterface Context {\n readonly terms: readonly TermRule[];\n readonly allow: AllowList;\n readonly findings: Finding[];\n readonly allowed: RuleCounts;\n}\n\nexport function zeroCounts(): RuleCounts {\n return { \"home-path\": 0, \"aw-data-path\": 0, \"private-term\": 0 };\n}\n\nfunction record(ctx: Context, location: string, hits: readonly RuleHit[], path?: string): void {\n for (const { rule, termIndex } of hits) {\n if (path !== undefined && isAllowed(ctx.allow, path, rule)) ctx.allowed[rule]++;\n else ctx.findings.push(termIndex === undefined ? { location, rule } : { location, rule, termIndex });\n }\n}\n\nfunction scanFile(ctx: Context, file: DiffFile, prefix: string): void {\n const pathHits = matchRules(file.path, ctx.terms);\n // A path that trips a rule would leak through the location itself, so it is replaced by its ordinal.\n const name = pathHits.length > 0 ? `file #${file.ordinal}` : file.path;\n if (file.pathAdded) record(ctx, `${prefix}${name} path`, pathHits, file.path);\n for (const { line, text } of file.lines) {\n record(ctx, `${prefix}${name}:${line}`, matchRules(text, ctx.terms), file.path);\n }\n}\n\nfunction scanSource(ctx: Context, source: ScanSource): void {\n const prefix = source.sha === undefined ? \"\" : `commit ${source.sha.slice(0, 7)} `;\n source.message?.forEach((text, i) => record(ctx, `${prefix}message:${i + 1}`, matchRules(text, ctx.terms)));\n for (const file of source.files) scanFile(ctx, file, prefix);\n}\n\n/** Scans added lines, added file paths and commit messages. Messages are never allowable. */\nexport function scan(sources: readonly ScanSource[], options: ScanOptions = {}): ScanResult {\n const ctx: Context = {\n terms: options.terms ?? [],\n allow: options.allow ?? EMPTY_ALLOW,\n findings: [],\n allowed: zeroCounts(),\n };\n for (const source of sources) scanSource(ctx, source);\n const binaryFilesSkipped = sources.reduce((sum, source) => sum + source.binaryFiles, 0);\n return { findings: ctx.findings, allowed: ctx.allowed, binaryFilesSkipped };\n}\n","import { RULE_IDS } from \"./rules.js\";\nimport { zeroCounts, type Finding, type RuleCounts } from \"./scan.js\";\n\nexport interface ReportSummary {\n readonly allowed: RuleCounts;\n readonly binaryFilesSkipped: number;\n readonly termsLoaded: boolean;\n}\n\nfunction formatFinding(finding: Finding): string {\n const term = finding.termIndex === undefined ? \"\" : ` #${finding.termIndex}`;\n return `${finding.location} ${finding.rule}${term}`;\n}\n\nfunction formatCounts(counts: RuleCounts, rules: readonly string[]): string {\n return rules.map((rule) => `${rule} ${counts[rule as keyof RuleCounts]}`).join(\", \");\n}\n\n/**\n * Formats findings as `<location> <rule>[ #<termIndex>]` lines plus a summary. It takes no\n * scanned text, so it cannot echo a matched line, path fragment or term.\n */\nexport function formatReport(findings: readonly Finding[], summary: ReportSummary): string[] {\n const found = zeroCounts();\n for (const finding of findings) found[finding.rule]++;\n const plural = findings.length === 1 ? \"finding\" : \"findings\";\n return [\n ...findings.map(formatFinding),\n `egress-scan: ${findings.length} ${plural} (${formatCounts(found, RULE_IDS)})`,\n `allowed: ${formatCounts(summary.allowed, [\"home-path\", \"aw-data-path\"])}`,\n `binary files skipped: ${summary.binaryFilesSkipped}`,\n `private term list: ${summary.termsLoaded ? \"loaded\" : \"not loaded, generic rules only\"}`,\n ];\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface ScanSource {
|
|
|
38
38
|
/**
|
|
39
39
|
* Parses `git diff -U0` or `git show -U0` patch text into the added lines of each file, with
|
|
40
40
|
* new-file line numbers. Removed lines are dropped; binary files are kept for their path only.
|
|
41
|
+
* A merge shown with `--diff-merges=separate` names a path once per parent; those fold into one.
|
|
41
42
|
*/
|
|
42
43
|
declare function parseDiff(text: string): ScanSource;
|
|
43
44
|
/** Parses `git show -U0 --format=%B%x00 <sha>` output: the message, a NUL, then the patch. */
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/allow.ts","../src/terms.ts","../src/diff.ts","../src/rules.ts","../src/scan.ts","../src/report.ts"],"sourcesContent":["import type { RuleId } from \"./rules.js\";\n\nexport type AllowableRule = Exclude<RuleId, \"private-term\">;\n\nexport interface AllowEntry {\n readonly glob: string;\n readonly rule: AllowableRule;\n readonly line: number;\n readonly pattern: RegExp;\n}\n\nexport interface AllowList {\n readonly entries: readonly AllowEntry[];\n}\n\nexport const EMPTY_ALLOW: AllowList = { entries: [] };\n\n/** A malformed `.egress-allow`. Callers must fail the scan on it, never treat the file as empty. */\nexport class AllowFileError extends Error {\n constructor(\n readonly line: number,\n reason: string,\n ) {\n super(`.egress-allow line ${line}: ${reason}`);\n this.name = \"AllowFileError\";\n }\n}\n\nconst ALLOWABLE: readonly string[] = [\"home-path\", \"aw-data-path\"];\nconst TASK_ID = /\\b[A-Z]+-\\d+\\b/;\n\n/** Anchored glob over repo-relative paths: `**` spans directories, `*` and `?` stay within one. */\nexport function globToRegExp(glob: string): RegExp {\n let source = \"\";\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i] ?? \"\";\n if (glob.startsWith(\"**/\", i)) {\n source += \"(?:.*/)?\";\n i += 2;\n } else if (glob.startsWith(\"**\", i)) {\n source += \".*\";\n i += 1;\n } else if (char === \"*\") source += \"[^/]*\";\n else if (char === \"?\") source += \"[^/]\";\n else source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n return new RegExp(`^${source}$`);\n}\n\n// A glob made only of wildcard segments would allow a rule across the whole tree.\nfunction hasLiteralSegment(glob: string): boolean {\n return glob.split(\"/\").some((segment) => segment !== \"\" && !/[*?]/.test(segment));\n}\n\nfunction parseEntry(entry: string, line: number): AllowEntry {\n const fields = /^(\\S+)\\s+(\\S+)\\s+(.+)$/.exec(entry);\n if (!fields) throw new AllowFileError(line, \"expected <glob> <rule-id> <reason>\");\n const [, glob = \"\", rule = \"\", reason = \"\"] = fields;\n if (rule === \"private-term\") throw new AllowFileError(line, \"private-term is never allowable\");\n if (!ALLOWABLE.includes(rule)) throw new AllowFileError(line, \"unknown rule id\");\n if (!TASK_ID.test(reason)) throw new AllowFileError(line, \"reason must name a task id\");\n if (!hasLiteralSegment(glob)) throw new AllowFileError(line, \"glob must name at least one literal path segment\");\n return { glob, rule: rule as AllowableRule, line, pattern: globToRegExp(glob) };\n}\n\nexport function parseAllow(text: string): AllowList {\n const entries: AllowEntry[] = [];\n text.split(/\\r?\\n/).forEach((raw, i) => {\n const entry = raw.trim();\n if (entry === \"\" || entry.startsWith(\"#\")) return;\n entries.push(parseEntry(entry, i + 1));\n });\n return { entries };\n}\n\nexport function isAllowed(allow: AllowList, path: string, rule: RuleId): boolean {\n if (rule === \"private-term\") return false;\n return allow.entries.some((entry) => entry.rule === rule && entry.pattern.test(path));\n}\n","import type { TermRule } from \"./rules.js\";\n\n/** A term-file problem. The message names the line number only, never the term. */\nexport class TermFileError extends Error {\n constructor(\n readonly line: number,\n reason: string,\n ) {\n super(`private term list line ${line}: ${reason}`);\n this.name = \"TermFileError\";\n }\n}\n\nconst WORD_CHAR = String.raw`[\\p{L}\\p{N}_]`;\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\/]/g, \"\\\\$&\");\n}\n\nfunction compileRegex(source: string, line: number): RegExp {\n if (source === \"\") throw new TermFileError(line, \"empty regular expression\");\n try {\n return new RegExp(source, \"iu\");\n } catch {\n // The engine's message quotes the pattern, so it is dropped rather than wrapped.\n throw new TermFileError(line, \"invalid regular expression\");\n }\n}\n\nfunction compileTerm(entry: string, line: number): RegExp {\n if (entry.startsWith(\"re:\")) return compileRegex(entry.slice(3), line);\n return new RegExp(`(?<!${WORD_CHAR})${escapeRegExp(entry)}(?!${WORD_CHAR})`, \"iu\");\n}\n\n/**\n * Parses a private term list: one term per line, `#` comments, blank lines ignored, matched\n * case-insensitively on word boundaries; a `re:` line is a regex source compiled with `iu`.\n */\nexport function parseTerms(text: string): TermRule[] {\n const rules: TermRule[] = [];\n text\n .replace(/^\\uFEFF/, \"\")\n .split(/\\r?\\n/)\n .forEach((raw, i) => {\n const entry = raw.trim();\n if (entry === \"\" || entry.startsWith(\"#\")) return;\n const pattern = compileTerm(entry, i + 1);\n if (pattern.test(\"\")) throw new TermFileError(i + 1, \"matches the empty string, so it would match every line\");\n rules.push({ index: i + 1, matches: (candidate) => pattern.test(candidate) });\n });\n return rules;\n}\n","export interface AddedLine {\n readonly line: number;\n readonly text: string;\n}\n\nexport interface DiffFile {\n readonly path: string;\n /** 1-based position in its diff, used in place of a path that itself trips a rule. */\n readonly ordinal: number;\n /** True for a new, renamed or copied file, whose path is itself new text. */\n readonly pathAdded: boolean;\n readonly binary: boolean;\n readonly lines: readonly AddedLine[];\n}\n\n/** One unit of scanned text: a diff, or a commit's message plus its diff. */\nexport interface ScanSource {\n readonly sha?: string;\n readonly message?: readonly string[];\n readonly files: readonly DiffFile[];\n readonly binaryFiles: number;\n}\n\ninterface MutableFile {\n path: string;\n ordinal: number;\n pathAdded: boolean;\n binary: boolean;\n lines: AddedLine[];\n}\n\ninterface Hunk {\n columns: number;\n newLine: number;\n remainingNew: number;\n remainingOld: number[];\n}\n\nconst HUNK_HEADER = /^(@{2,}) ((?:-\\d+(?:,\\d+)? )+)\\+(\\d+)(?:,(\\d+))? \\1/;\nconst C_ESCAPES: Record<string, number> = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 };\n\n/** Undoes git's C-style path quoting (`core.quotePath`), octal UTF-8 bytes included. */\nexport function unquotePath(raw: string): string {\n if (raw.length < 2 || !raw.startsWith('\"') || !raw.endsWith('\"')) return raw;\n const body = raw.slice(1, -1);\n const bytes: number[] = [];\n for (let i = 0; i < body.length; i++) {\n const char = body[i] ?? \"\";\n const next = body[i + 1] ?? \"\";\n if (char !== \"\\\\\") bytes.push(...new TextEncoder().encode(char));\n else if (/[0-7]/.test(next)) {\n bytes.push(parseInt(body.slice(i + 1, i + 4), 8));\n i += 3;\n } else {\n bytes.push(C_ESCAPES[next] ?? next.charCodeAt(0));\n i += 1;\n }\n }\n return new TextDecoder().decode(new Uint8Array(bytes));\n}\n\nfunction stripSide(path: string): string {\n return path.replace(/^[ab]\\//, \"\");\n}\n\nfunction headerPath(header: string): string {\n const combined = /^diff --(?:cc|combined) (.+)$/.exec(header);\n if (combined) return unquotePath(combined[1] ?? \"\");\n const rest = header.slice(\"diff --git \".length);\n const quoted = / (\"(?:[^\"\\\\]|\\\\.)*\")$/.exec(rest);\n if (quoted) return stripSide(unquotePath(quoted[1] ?? \"\"));\n const half = (rest.length - 1) / 2;\n if (Number.isInteger(half) && rest.slice(2, half) === rest.slice(half + 3)) return rest.slice(2, half);\n return stripSide(rest.slice(rest.lastIndexOf(\" b/\") + 1));\n}\n\nfunction rangeCount(token: string): number {\n const count = token.split(\",\")[1];\n return count === undefined ? 1 : Number(count);\n}\n\nfunction parseHunkHeader(line: string): Hunk | undefined {\n const match = HUNK_HEADER.exec(line);\n if (!match) return undefined;\n const oldRanges = (match[2] ?? \"\").trim().split(\" \");\n return {\n columns: (match[1] ?? \"\").length - 1,\n newLine: Number(match[3]),\n remainingNew: match[4] === undefined ? 1 : Number(match[4]),\n remainingOld: oldRanges.map(rangeCount),\n };\n}\n\nfunction isHunkLine(hunk: Hunk, line: string): boolean {\n if (line.startsWith(\"\\\\\")) return true;\n return line.length >= hunk.columns && /^[ +-]*$/.test(line.slice(0, hunk.columns));\n}\n\n/** Applies one hunk body line; a `-` in any column means the line is not in the new file. */\nfunction consumeHunkLine(hunk: Hunk, line: string, file: MutableFile): void {\n if (line.startsWith(\"\\\\\")) return;\n const marks = [...line.slice(0, hunk.columns)];\n const removed = marks.includes(\"-\");\n const oldMark = removed ? \"-\" : \" \";\n hunk.remainingOld = hunk.remainingOld.map((n, i) => (marks[i] === oldMark ? n - 1 : n));\n if (removed) return;\n if (marks.includes(\"+\")) file.lines.push({ line: hunk.newLine, text: line.slice(hunk.columns) });\n hunk.newLine++;\n hunk.remainingNew--;\n}\n\nfunction hunkDone(hunk: Hunk): boolean {\n return hunk.remainingNew <= 0 && hunk.remainingOld.every((n) => n <= 0);\n}\n\nfunction applyHeaderLine(line: string, file: MutableFile): Hunk | undefined {\n if (file.binary) return undefined;\n const renamed = /^(?:rename|copy) to (.+)$/.exec(line);\n if (line.startsWith(\"new file mode \")) file.pathAdded = true;\n else if (renamed) {\n file.path = unquotePath(renamed[1] ?? \"\");\n file.pathAdded = true;\n } else if (line.startsWith(\"+++ \")) {\n const target = line.slice(4).replace(/\\t$/, \"\");\n if (target !== \"/dev/null\") file.path = stripSide(unquotePath(target));\n } else if (line.startsWith(\"Binary files \") || line === \"GIT binary patch\") file.binary = true;\n else if (line.startsWith(\"@@\")) return parseHunkHeader(line);\n return undefined;\n}\n\n/**\n * Parses `git diff -U0` or `git show -U0` patch text into the added lines of each file, with\n * new-file line numbers. Removed lines are dropped; binary files are kept for their path only.\n */\nexport function parseDiff(text: string): ScanSource {\n const files: MutableFile[] = [];\n let hunk: Hunk | undefined;\n for (const line of text.split(\"\\n\")) {\n const file = files.at(-1);\n if (hunk && file && isHunkLine(hunk, line)) {\n consumeHunkLine(hunk, line, file);\n if (hunkDone(hunk)) hunk = undefined;\n continue;\n }\n hunk = undefined;\n if (/^diff --(?:git|cc|combined) /.test(line)) {\n files.push({ path: headerPath(line), ordinal: files.length + 1, pathAdded: false, binary: false, lines: [] });\n } else if (file) hunk = applyHeaderLine(line, file);\n }\n return { files, binaryFiles: files.filter((f) => f.binary).length };\n}\n\n/** Parses `git show -U0 --format=%B%x00 <sha>` output: the message, a NUL, then the patch. */\nexport function parseCommit(sha: string, text: string): ScanSource {\n const cut = text.indexOf(\"\\0\");\n if (cut < 0) throw new Error(`commit ${sha.slice(0, 7)}: no NUL after the message`);\n const message = text.slice(0, cut).replace(/\\n+$/, \"\").split(\"\\n\");\n return { ...parseDiff(text.slice(cut + 1)), sha, message };\n}\n","export type RuleId = \"home-path\" | \"aw-data-path\" | \"private-term\";\n\nexport const RULE_IDS: readonly RuleId[] = [\"home-path\", \"aw-data-path\", \"private-term\"];\n\n/** A compiled private term. It exposes its line number, never its text or pattern. */\nexport interface TermRule {\n readonly index: number;\n matches(text: string): boolean;\n}\n\nexport interface RuleHit {\n readonly rule: RuleId;\n readonly termIndex?: number;\n}\n\n/** Home-directory segments that are placeholders, not people. Compared case-insensitively. */\nexport const PLACEHOLDER_SEGMENTS: readonly string[] = [\n \"Shared\",\n \"runner\",\n \"you\",\n \"your-name\",\n \"user\",\n \"username\",\n \"me\",\n \"example\",\n \"name\",\n \"x\",\n \"alice\",\n \"bob\",\n];\n\n// One path separator: slash, backslash, or the JSON-escaped form of either.\nconst SEP = String.raw`(?:\\\\\\\\|\\\\/|/|\\\\)`;\nconst SEGMENT = String.raw`([^/\\\\\\s\"'\\`),;\\]]+)`;\nconst USERS = \"[Uu][Ss][Ee][Rr][Ss]\";\n// A root must start a path; after `file://` any host may precede it.\nconst PATH_START = String.raw`(?:(?<![\\w.-])|(?<=file://[^/\\s\"'\\`]*))`;\nconst HOME_ROOT = String.raw`(?:${PATH_START}${SEP}(?:${USERS}|[Hh][Oo][Mm][Ee])|(?<!\\w)[A-Za-z]:${SEP}${USERS})`;\nconst HOME_PATH = new RegExp(`${HOME_ROOT}${SEP}${SEGMENT}`, \"g\");\n\nconst SPACE = String.raw`(?: |%20|\\\\ )`;\nconst AW_DATA_ROOTS = [`Library${SEP}Application${SPACE}Support`, String.raw`\\.local${SEP}share`, `AppData${SEP}Local`];\n// The single definition of the active-work data directory shapes; narrow it here and nowhere else.\nconst AW_DATA_PATH = new RegExp(`(?:${AW_DATA_ROOTS.join(\"|\")})${SEP}active-work(?!\\\\w)`, \"i\");\n\nconst PLACEHOLDER_SET = new Set(PLACEHOLDER_SEGMENTS.map((s) => s.toLowerCase()));\n\nfunction isPlaceholder(rawSegment: string): boolean {\n const segment = rawSegment.replace(/[.:]+$/, \"\");\n if (segment === \"\") return true;\n if (/^<.*>$/.test(segment) || /^\\{.*\\}$/.test(segment) || segment.startsWith(\"$\")) return true;\n return PLACEHOLDER_SET.has(segment.toLowerCase());\n}\n\nexport function matchesHomePath(text: string): boolean {\n for (const match of text.matchAll(HOME_PATH)) {\n if (!isPlaceholder(match[1] ?? \"\")) return true;\n }\n return false;\n}\n\nexport function matchesAwDataPath(text: string): boolean {\n return AW_DATA_PATH.test(text);\n}\n\nexport function matchRules(text: string, terms: readonly TermRule[] = []): RuleHit[] {\n const hits: RuleHit[] = [];\n if (matchesHomePath(text)) hits.push({ rule: \"home-path\" });\n if (matchesAwDataPath(text)) hits.push({ rule: \"aw-data-path\" });\n for (const term of terms) {\n if (term.matches(text)) hits.push({ rule: \"private-term\", termIndex: term.index });\n }\n return hits;\n}\n","import { EMPTY_ALLOW, isAllowed, type AllowList } from \"./allow.js\";\nimport type { DiffFile, ScanSource } from \"./diff.js\";\nimport { matchRules, type RuleHit, type RuleId, type TermRule } from \"./rules.js\";\n\n/** Where a rule fired. By construction no field carries the matched text, term or line. */\nexport interface Finding {\n readonly location: string;\n readonly rule: RuleId;\n readonly termIndex?: number;\n}\n\nexport type RuleCounts = Record<RuleId, number>;\n\nexport interface ScanOptions {\n readonly terms?: readonly TermRule[];\n readonly allow?: AllowList;\n}\n\nexport interface ScanResult {\n readonly findings: Finding[];\n readonly allowed: RuleCounts;\n readonly binaryFilesSkipped: number;\n}\n\ninterface Context {\n readonly terms: readonly TermRule[];\n readonly allow: AllowList;\n readonly findings: Finding[];\n readonly allowed: RuleCounts;\n}\n\nexport function zeroCounts(): RuleCounts {\n return { \"home-path\": 0, \"aw-data-path\": 0, \"private-term\": 0 };\n}\n\nfunction record(ctx: Context, location: string, hits: readonly RuleHit[], path?: string): void {\n for (const { rule, termIndex } of hits) {\n if (path !== undefined && isAllowed(ctx.allow, path, rule)) ctx.allowed[rule]++;\n else ctx.findings.push(termIndex === undefined ? { location, rule } : { location, rule, termIndex });\n }\n}\n\nfunction scanFile(ctx: Context, file: DiffFile, prefix: string): void {\n const pathHits = matchRules(file.path, ctx.terms);\n // A path that trips a rule would leak through the location itself, so it is replaced by its ordinal.\n const name = pathHits.length > 0 ? `file #${file.ordinal}` : file.path;\n if (file.pathAdded) record(ctx, `${prefix}${name} path`, pathHits, file.path);\n for (const { line, text } of file.lines) {\n record(ctx, `${prefix}${name}:${line}`, matchRules(text, ctx.terms), file.path);\n }\n}\n\nfunction scanSource(ctx: Context, source: ScanSource): void {\n const prefix = source.sha === undefined ? \"\" : `commit ${source.sha.slice(0, 7)} `;\n source.message?.forEach((text, i) => record(ctx, `${prefix}message:${i + 1}`, matchRules(text, ctx.terms)));\n for (const file of source.files) scanFile(ctx, file, prefix);\n}\n\n/** Scans added lines, added file paths and commit messages. Messages are never allowable. */\nexport function scan(sources: readonly ScanSource[], options: ScanOptions = {}): ScanResult {\n const ctx: Context = {\n terms: options.terms ?? [],\n allow: options.allow ?? EMPTY_ALLOW,\n findings: [],\n allowed: zeroCounts(),\n };\n for (const source of sources) scanSource(ctx, source);\n const binaryFilesSkipped = sources.reduce((sum, source) => sum + source.binaryFiles, 0);\n return { findings: ctx.findings, allowed: ctx.allowed, binaryFilesSkipped };\n}\n","import { RULE_IDS } from \"./rules.js\";\nimport { zeroCounts, type Finding, type RuleCounts } from \"./scan.js\";\n\nexport interface ReportSummary {\n readonly allowed: RuleCounts;\n readonly binaryFilesSkipped: number;\n readonly termsLoaded: boolean;\n}\n\nfunction formatFinding(finding: Finding): string {\n const term = finding.termIndex === undefined ? \"\" : ` #${finding.termIndex}`;\n return `${finding.location} ${finding.rule}${term}`;\n}\n\nfunction formatCounts(counts: RuleCounts, rules: readonly string[]): string {\n return rules.map((rule) => `${rule} ${counts[rule as keyof RuleCounts]}`).join(\", \");\n}\n\n/**\n * Formats findings as `<location> <rule>[ #<termIndex>]` lines plus a summary. It takes no\n * scanned text, so it cannot echo a matched line, path fragment or term.\n */\nexport function formatReport(findings: readonly Finding[], summary: ReportSummary): string[] {\n const found = zeroCounts();\n for (const finding of findings) found[finding.rule]++;\n const plural = findings.length === 1 ? \"finding\" : \"findings\";\n return [\n ...findings.map(formatFinding),\n `egress-scan: ${findings.length} ${plural} (${formatCounts(found, RULE_IDS)})`,\n `allowed: ${formatCounts(summary.allowed, [\"home-path\", \"aw-data-path\"])}`,\n `binary files skipped: ${summary.binaryFilesSkipped}`,\n `private term list: ${summary.termsLoaded ? \"loaded\" : \"not loaded, generic rules only\"}`,\n ];\n}\n"],"mappings":"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","names":[]}
|