@codyswann/lisa 4.69.5 → 4.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/all/copy-overwrite/scripts/lib/history-secret-git.mjs +212 -0
  2. package/all/copy-overwrite/scripts/lib/history-secret-policy.mjs +39 -0
  3. package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +353 -0
  4. package/all/copy-overwrite/scripts/lisa-gates.mjs +26 -10
  5. package/all/copy-overwrite/scripts/lisa-history-secrets.mjs +122 -0
  6. package/all/copy-overwrite/scripts/lisa-rails-prepush.mjs +90 -0
  7. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  8. package/dist/core/lisa-owned-hash-ledger.js +46 -0
  9. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  10. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  11. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  12. package/dist/core/upstream-evidence-manifest.js +28 -4
  13. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  14. package/dist/migrations/ensure-required-history-secret-gate.d.ts +19 -0
  15. package/dist/migrations/ensure-required-history-secret-gate.d.ts.map +1 -0
  16. package/dist/migrations/ensure-required-history-secret-gate.js +94 -0
  17. package/dist/migrations/ensure-required-history-secret-gate.js.map +1 -0
  18. package/dist/migrations/index.d.ts +1 -0
  19. package/dist/migrations/index.d.ts.map +1 -1
  20. package/dist/migrations/index.js +3 -0
  21. package/dist/migrations/index.js.map +1 -1
  22. package/package.json +4 -4
  23. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-agy/plugin.json +1 -1
  36. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  56. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-agy/plugin.json +1 -1
  61. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  66. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  71. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  73. package/rails/copy-overwrite/lefthook.yml +2 -1
  74. package/scripts/two-channel-couplings.json +31 -7
  75. package/typescript/copy-overwrite/.prettierignore +2 -0
@@ -0,0 +1,212 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /**
5
+ * @file history-secret-git.mjs
6
+ * @description Prove complete pairwise introduced reachability before scanning.
7
+ * @module history-secrets
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ /** Authored guidance is distinguishable from filesystem/vendor exceptions. */
14
+ export class HistorySecretError extends Error {}
15
+
16
+ /** Deterministic Git plumbing must ignore replacements and diff transforms. */
17
+ export const gitEnvironment = () => {
18
+ const environment = { ...process.env };
19
+ for (const key of Object.keys(environment)) {
20
+ if (
21
+ key.startsWith("GIT_CONFIG_") ||
22
+ key.startsWith("GITLEAKS_") ||
23
+ [
24
+ "GIT_DIR",
25
+ "GIT_COMMON_DIR",
26
+ "GIT_WORK_TREE",
27
+ "GIT_INDEX_FILE",
28
+ "GIT_OBJECT_DIRECTORY",
29
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
30
+ "GIT_SHALLOW_FILE",
31
+ "GIT_REPLACE_REF_BASE",
32
+ "GIT_NAMESPACE",
33
+ ].includes(key)
34
+ )
35
+ delete environment[key];
36
+ }
37
+ return {
38
+ ...environment,
39
+ GIT_NO_REPLACE_OBJECTS: "1",
40
+ GIT_CONFIG_NOSYSTEM: "1",
41
+ GIT_CONFIG_GLOBAL: "/dev/null",
42
+ GIT_CONFIG_COUNT: "0",
43
+ GIT_OPTIONAL_LOCKS: "0",
44
+ };
45
+ };
46
+
47
+ /** Git errors can contain arbitrary payloads; only authored guidance leaves here. */
48
+ export const gitRead = (args, cwd) => {
49
+ const result = spawnSync(
50
+ "git",
51
+ [
52
+ "--no-replace-objects",
53
+ "-c",
54
+ "diff.external=",
55
+ "-c",
56
+ "core.quotePath=true",
57
+ ...args,
58
+ ],
59
+ {
60
+ cwd,
61
+ env: gitEnvironment(),
62
+ encoding: "utf8",
63
+ timeout: 120000,
64
+ maxBuffer: 64 * 1024 * 1024,
65
+ }
66
+ );
67
+ if (result.error || result.signal || result.status !== 0)
68
+ throw new HistorySecretError(
69
+ "Cannot prove complete Git history. Fetch the actual before/head objects and complete history, then retry; repair missing or corrupt objects."
70
+ );
71
+ return result.stdout.trim();
72
+ };
73
+
74
+ /** Validate the entire stream before any scanner or deletion shortcut. */
75
+ export const parsePush = (input, width, cwd = process.cwd()) => {
76
+ if (input === "") return [];
77
+ const lines = input.endsWith("\n")
78
+ ? input.slice(0, -1).split("\n")
79
+ : input.split("\n");
80
+ return lines.map(line => {
81
+ const fields = line.split(" ");
82
+ if (fields.length !== 4 || fields.some(field => !field))
83
+ throw new HistorySecretError(
84
+ "Malformed pre-push input. Supply Git's complete four-field ref records without extra or missing fields."
85
+ );
86
+ const [localRef, after, remoteRef, before] = fields;
87
+ const oid = new RegExp(`^[a-f0-9]{${width}}$`, "u");
88
+ if (!oid.test(before) || !oid.test(after))
89
+ throw new HistorySecretError(
90
+ "Malformed object ID. Supply full lowercase IDs matching the repository object format."
91
+ );
92
+ const validRef = ref =>
93
+ ref.startsWith("refs/") &&
94
+ spawnSync("git", ["check-ref-format", ref], {
95
+ env: gitEnvironment(),
96
+ timeout: 10000,
97
+ stdio: "ignore",
98
+ }).status === 0;
99
+ // Git preserves an explicit revision expression (HEAD~1 or a raw ID) in
100
+ // local-ref. Its actual supplied object ID remains the range boundary.
101
+ const validSource = () => {
102
+ if (localRef.startsWith("refs/")) return validRef(localRef);
103
+ if (localRef === "(delete)") return /^0+$/u.test(after);
104
+ try {
105
+ return (
106
+ gitRead(
107
+ [
108
+ "rev-parse",
109
+ "--verify",
110
+ "--end-of-options",
111
+ `${localRef}^{object}`,
112
+ ],
113
+ cwd
114
+ ) === after
115
+ );
116
+ } catch {
117
+ return false;
118
+ }
119
+ };
120
+ if (!validRef(remoteRef) || !validSource())
121
+ throw new HistorySecretError(
122
+ "Malformed ref record. Supply the original Git pre-push stream with valid refs."
123
+ );
124
+ return { before, after };
125
+ });
126
+ };
127
+
128
+ /** Events carry actual IDs, never a guessed checkout HEAD. */
129
+ export const eventPairs = (event, name, width) => {
130
+ if (name === "pull_request")
131
+ return [
132
+ {
133
+ before: event.pull_request?.base?.sha,
134
+ after: event.pull_request?.head?.sha,
135
+ },
136
+ ];
137
+ if (name === "push") {
138
+ const oid = new RegExp(`^[a-f0-9]{${width}}$`, "u");
139
+ if (!oid.test(event.before ?? "") || !oid.test(event.after ?? ""))
140
+ throw new HistorySecretError(
141
+ "Malformed event object IDs. Supply full actual before/after IDs matching the repository object format."
142
+ );
143
+ if (event.deleted === true && event.after !== "0".repeat(width))
144
+ throw new HistorySecretError(
145
+ "Inconsistent deletion event. Supply the original actual push event with a zero after ID for deletion."
146
+ );
147
+ return [{ before: event.before, after: event.after }];
148
+ }
149
+ throw new HistorySecretError(
150
+ "Unsupported CI event. Supply actual pull_request base/head or push before/after event data."
151
+ );
152
+ };
153
+
154
+ /** Pairwise subtraction prevents one ref's old tip from hiding another update. */
155
+ export const introducedCommits = (pairs, cwd, width) => {
156
+ const oid = new RegExp(`^[a-f0-9]{${width}}$`, "u");
157
+ for (const pair of pairs) {
158
+ if (!oid.test(pair.before ?? "") || !oid.test(pair.after ?? ""))
159
+ throw new HistorySecretError(
160
+ "Malformed event object IDs. Supply full actual before/base/head IDs matching the repository object format."
161
+ );
162
+ }
163
+ const updates = pairs.filter(pair => !/^0+$/u.test(pair.after));
164
+ if (updates.length === 0) return [];
165
+ if (gitRead(["rev-parse", "--is-shallow-repository"], cwd) !== "false")
166
+ throw new HistorySecretError(
167
+ "Incomplete shallow history. Fetch complete required history (unshallow the checkout) and actual before/head objects before retrying."
168
+ );
169
+ const metadata = gitRead(["rev-parse", "--absolute-git-dir"], cwd);
170
+ if (
171
+ existsSync(join(metadata, "info/grafts")) &&
172
+ readFileSync(join(metadata, "info/grafts"), "utf8").trim()
173
+ )
174
+ throw new HistorySecretError(
175
+ "Legacy Git grafts prevent complete history proof. Use an unmodified complete repository checkout."
176
+ );
177
+ const commits = new Set();
178
+ for (const { before, after } of updates) {
179
+ const next = gitRead(["rev-parse", "--verify", `${after}^{commit}`], cwd);
180
+ const previous = /^0+$/u.test(before)
181
+ ? null
182
+ : gitRead(["rev-parse", "--verify", `${before}^{commit}`], cwd);
183
+ for (const tip of [next, previous].filter(Boolean)) {
184
+ const objects = gitRead(
185
+ ["rev-list", "--objects", "--missing=print", tip],
186
+ cwd
187
+ );
188
+ if (objects.split("\n").some(line => line.startsWith("?")))
189
+ throw new HistorySecretError(
190
+ "Required Git trees or blobs are missing. Fetch complete objects, repair the checkout and retry."
191
+ );
192
+ }
193
+ const list = gitRead(
194
+ ["rev-list", next, ...(previous ? [`^${previous}`] : [])],
195
+ cwd
196
+ );
197
+ for (const commit of list.split("\n").filter(Boolean)) commits.add(commit);
198
+ }
199
+ return [...commits].sort((left, right) =>
200
+ left < right ? -1 : left > right ? 1 : 0
201
+ );
202
+ };
203
+
204
+ /** Git's negotiated storage format determines accepted IDs. */
205
+ export const objectWidth = cwd => {
206
+ const format = gitRead(["rev-parse", "--show-object-format"], cwd);
207
+ if (format === "sha1") return 40;
208
+ if (format === "sha256") return 64;
209
+ throw new HistorySecretError(
210
+ "Unsupported Git object format. Use a supported SHA-1 or SHA-256 checkout."
211
+ );
212
+ };
@@ -0,0 +1,39 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /**
5
+ * @file history-secret-policy.mjs
6
+ * @description Resolve only this owned facade without changing unrelated declarations.
7
+ * @module history-secrets
8
+ */
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { readGates, projectScripts, resolveMoment } from "../lisa-gates.mjs";
12
+ import { HistorySecretError } from "./history-secret-git.mjs";
13
+ export const HISTORY_PROPERTY = "introduced-history-credential-leakage";
14
+ /** Unknown or malformed manifests cannot manufacture a required facade pass. */
15
+ export const historyPolicy = (cwd, moment) => {
16
+ try {
17
+ if (existsSync(join(cwd, "package.json")))
18
+ JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
19
+ const { gates, runner } = readGates(cwd);
20
+ const policy = resolveMoment({
21
+ gates,
22
+ runner,
23
+ scripts: projectScripts(cwd),
24
+ moment,
25
+ includeOff: true,
26
+ }).find(entry => entry.id === HISTORY_PROPERTY);
27
+ if (!policy || policy.level === "off") return null;
28
+ if (policy.level !== "required" || policy.mode !== "builtin")
29
+ throw new HistorySecretError(
30
+ "The authored required history route is incompatible. Run full Lisa apply and review its required managed facade declaration."
31
+ );
32
+ return policy;
33
+ } catch (error) {
34
+ if (error instanceof HistorySecretError) throw error;
35
+ throw new HistorySecretError(
36
+ "Required history policy could not be resolved. Repair project JSON and run full Lisa apply; raw configuration errors are withheld."
37
+ );
38
+ }
39
+ };
@@ -0,0 +1,353 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /**
5
+ * @file history-secret-scanner.mjs
6
+ * @description Pin offline scanner identity and contain all scanner output.
7
+ * @module history-secrets
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { spawnSync } from "node:child_process";
11
+ import {
12
+ accessSync,
13
+ chmodSync,
14
+ constants,
15
+ existsSync,
16
+ mkdirSync,
17
+ mkdtempSync,
18
+ readFileSync,
19
+ rmSync,
20
+ statSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { delimiter, join, resolve } from "node:path";
25
+ import {
26
+ gitEnvironment,
27
+ gitRead,
28
+ HistorySecretError,
29
+ } from "./history-secret-git.mjs";
30
+
31
+ export const VERSION = "8.30.1";
32
+ /** Archive and extracted executable pins both come from the same official release. */
33
+ export const PINS = Object.freeze({
34
+ darwin_arm64: {
35
+ archive: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
36
+ binary: "ba52fb1bfabbcde42f032afad3d6e0b19dff8ed105229a16e7caa338bbc0e84f",
37
+ },
38
+ darwin_x64: {
39
+ archive: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
40
+ binary: "cee01fea7173f1b779dff188e1c26ecbcb4027d394acc573b23aaf0be260e291",
41
+ },
42
+ linux_x64: {
43
+ archive: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb",
44
+ binary: "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509",
45
+ },
46
+ linux_arm64: {
47
+ archive: "e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080",
48
+ binary: "00e91bbe655bd7c47753e8cfe61cb76ea1a5d7e7702fe161ee40102b46b3823b",
49
+ },
50
+ });
51
+ const digest = bytes => createHash("sha256").update(bytes).digest("hex");
52
+ const shellQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
53
+
54
+ /** Audit the actual vendor Git children: Gitleaks can swallow their failures. */
55
+ const auditedGit = scratch => {
56
+ const environment = gitEnvironment();
57
+ const binary = (environment.PATH ?? "")
58
+ .split(delimiter)
59
+ .map(directory => resolve(directory || ".", "git"))
60
+ .find(candidate => {
61
+ try {
62
+ accessSync(candidate, constants.X_OK);
63
+ return statSync(candidate).isFile();
64
+ } catch {
65
+ return false;
66
+ }
67
+ });
68
+ if (!binary)
69
+ throw new HistorySecretError(
70
+ "Git is unavailable for scanner execution. Restore Git on PATH and retry; safety is unproved."
71
+ );
72
+ const directory = join(scratch, "audited-git");
73
+ mkdirSync(directory, { mode: 0o700 });
74
+ const record = join(directory, "status.jsonl");
75
+ writeFileSync(record, "", { mode: 0o600 });
76
+ const launcher = join(directory, "audit.mjs");
77
+ // Inherit streaming stdin/stdout/stderr. Only status is captured here; the
78
+ // parent contains vendor diagnostics. The child deadline precedes the
79
+ // vendor's 120-second deadline, which precedes the outer 130-second deadline.
80
+ writeFileSync(
81
+ launcher,
82
+ `import { spawn } from "node:child_process";
83
+ import { appendFileSync } from "node:fs";
84
+ const args = process.argv.slice(2);
85
+ let failure = false;
86
+ const child = spawn(${JSON.stringify(binary)}, args, {
87
+ stdio: "inherit", timeout: 110000, killSignal: "SIGKILL"
88
+ });
89
+ child.once("error", () => { failure = true; });
90
+ child.once("close", (status, signal) => {
91
+ appendFileSync(${JSON.stringify(record)}, JSON.stringify({
92
+ log: args[0] === "-C" && args[2] === "log",
93
+ status, signal, failure
94
+ }) + "\\n", { mode: 0o600 });
95
+ process.exitCode = !failure && !signal && status === 0 ? 0 : 1;
96
+ });
97
+ `,
98
+ { mode: 0o600 }
99
+ );
100
+ writeFileSync(
101
+ join(directory, "git"),
102
+ `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(launcher)} "$@"\n`,
103
+ { mode: 0o700 }
104
+ );
105
+ return {
106
+ environment: {
107
+ ...environment,
108
+ PATH: `${directory}${delimiter}${environment.PATH ?? ""}`,
109
+ },
110
+ begin: () => writeFileSync(record, "", { mode: 0o600 }),
111
+ succeeded: () => {
112
+ try {
113
+ const rows = readFileSync(record, "utf8")
114
+ .trim()
115
+ .split("\n")
116
+ .filter(Boolean)
117
+ .map(line => JSON.parse(line));
118
+ return (
119
+ rows.some(row => row.log === true) &&
120
+ rows.every(
121
+ row =>
122
+ row.status === 0 && row.signal === null && row.failure === false
123
+ )
124
+ );
125
+ } catch {
126
+ // probe-direction: fail-closed — unreadable or malformed audit denies
127
+ // scanner success because successful Git execution cannot be proved.
128
+ return false;
129
+ }
130
+ },
131
+ };
132
+ };
133
+ const pin = () => {
134
+ const selected = PINS[`${process.platform}_${process.arch}`];
135
+ if (!selected)
136
+ throw new HistorySecretError(
137
+ "Unsupported scanner platform. Use supported Linux/macOS x64/arm64 with pinned Gitleaks 8.30.1."
138
+ );
139
+ return selected;
140
+ };
141
+
142
+ /** Provisioning is explicit; scanning itself stays offline and fails missing tools. */
143
+ export const provisionScanner = async destination => {
144
+ const selected = pin();
145
+ const archiveName = `gitleaks_${VERSION}_${process.platform}_${process.arch}.tar.gz`;
146
+ const response = await fetch(
147
+ `https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${archiveName}`,
148
+ { signal: AbortSignal.timeout(120000) }
149
+ );
150
+ if (!response.ok)
151
+ throw new HistorySecretError(
152
+ "Pinned scanner download failed. Restore access to the official release and retry provisioning."
153
+ );
154
+ const bytes = Buffer.from(await response.arrayBuffer());
155
+ if (digest(bytes) !== selected.archive)
156
+ throw new HistorySecretError(
157
+ "Scanner archive checksum mismatch. Discard the download and restore the official pinned release."
158
+ );
159
+ const scratch = mkdtempSync(join(tmpdir(), "lisa-scanner-provision-"));
160
+ chmodSync(scratch, 0o700);
161
+ try {
162
+ const archive = join(scratch, "release.tar.gz");
163
+ writeFileSync(archive, bytes, { mode: 0o600 });
164
+ const extraction = spawnSync("tar", ["-xOf", archive, "gitleaks"], {
165
+ maxBuffer: 64 * 1024 * 1024,
166
+ timeout: 120000,
167
+ });
168
+ if (
169
+ extraction.error ||
170
+ extraction.status !== 0 ||
171
+ digest(extraction.stdout) !== selected.binary
172
+ )
173
+ throw new HistorySecretError(
174
+ "Pinned scanner extraction failed identity verification. Repair tar/provisioning and retry."
175
+ );
176
+ writeFileSync(destination, extraction.stdout, { mode: 0o700 });
177
+ chmodSync(destination, 0o700);
178
+ qualifyScanner(destination);
179
+ } finally {
180
+ rmSync(scratch, { recursive: true, force: true });
181
+ }
182
+ };
183
+
184
+ /** A forged same-version executable cannot satisfy the required scanner identity. */
185
+ export const qualifyScanner = requested => {
186
+ const binary = requested
187
+ ? resolve(requested)
188
+ : (process.env.PATH ?? "")
189
+ .split(delimiter)
190
+ .map(directory => join(directory, "gitleaks"))
191
+ .find(existsSync);
192
+ if (!binary)
193
+ throw new HistorySecretError(
194
+ "Gitleaks 8.30.1 is unavailable. Run node scripts/lisa-history-secrets.mjs provision <executable-path>, then put that executable on PATH or supply --scanner."
195
+ );
196
+ let bytes;
197
+ try {
198
+ bytes = readFileSync(binary);
199
+ } catch {
200
+ throw new HistorySecretError(
201
+ "Scanner executable is unreadable. Provision the pinned Gitleaks 8.30.1 executable and retry."
202
+ );
203
+ }
204
+ if (digest(bytes) !== pin().binary)
205
+ throw new HistorySecretError(
206
+ "Scanner executable checksum/version mismatch. Provision the official pinned Gitleaks 8.30.1 release; required scanning cannot use an unqualified executable."
207
+ );
208
+ const version = spawnSync(binary, ["version"], {
209
+ env: gitEnvironment(),
210
+ encoding: "utf8",
211
+ timeout: 10000,
212
+ });
213
+ if (
214
+ version.error ||
215
+ version.signal ||
216
+ version.status !== 0 ||
217
+ version.stdout.trim() !== VERSION
218
+ )
219
+ throw new HistorySecretError(
220
+ "Scanner version execution failed. Repair permissions and provision pinned Gitleaks 8.30.1."
221
+ );
222
+ return binary;
223
+ };
224
+
225
+ /** Only structured safe attribution is serialized, never stderr or metadata strings. */
226
+ export const scanCommits = (commits, cwd, requested) => {
227
+ const binary = qualifyScanner(requested);
228
+ const scratch = mkdtempSync(join(tmpdir(), "lisa-history-scan-"));
229
+ chmodSync(scratch, 0o700);
230
+ try {
231
+ const scanner = join(scratch, "gitleaks");
232
+ const executable = readFileSync(binary);
233
+ if (digest(executable) !== pin().binary)
234
+ throw new HistorySecretError(
235
+ "Scanner identity changed during qualification. Provision the pinned executable and retry."
236
+ );
237
+ writeFileSync(scanner, executable, { mode: 0o700 });
238
+ const config = join(scratch, "default.toml");
239
+ const report = join(scratch, "report.json");
240
+ const ignore = join(scratch, "ignore");
241
+ writeFileSync(config, "[extend]\nuseDefault = true\n", { mode: 0o600 });
242
+ // Gitleaks truncates this existing file: its report keeps private mode even
243
+ // when the caller's umask would otherwise create world-readable metadata.
244
+ writeFileSync(report, "[]", { mode: 0o600 });
245
+ writeFileSync(ignore, "", { mode: 0o600 });
246
+ // An isolated bare view excludes caller ignore files even in Git metadata.
247
+ const source = join(scratch, "repository");
248
+ gitRead(
249
+ [
250
+ "init",
251
+ "--bare",
252
+ `--object-format=${gitRead(["rev-parse", "--show-object-format"], cwd)}`,
253
+ source,
254
+ ],
255
+ scratch
256
+ );
257
+ const objects = gitRead(
258
+ ["rev-parse", "--path-format=absolute", "--git-path", "objects"],
259
+ cwd
260
+ );
261
+ writeFileSync(join(source, "objects/info/alternates"), `${objects}\n`, {
262
+ mode: 0o600,
263
+ });
264
+ const findings = [];
265
+ const audit = auditedGit(scratch);
266
+ for (let offset = 0; offset < commits.length; offset += 100) {
267
+ const group = commits.slice(offset, offset + 100);
268
+ audit.begin();
269
+ const result = spawnSync(
270
+ scanner,
271
+ [
272
+ "git",
273
+ source,
274
+ "--config",
275
+ config,
276
+ "--gitleaks-ignore-path",
277
+ ignore,
278
+ "--ignore-gitleaks-allow",
279
+ "--redact=100",
280
+ "--no-banner",
281
+ "--no-color",
282
+ "--log-level",
283
+ "error",
284
+ "--platform",
285
+ "none",
286
+ "--exit-code",
287
+ "42",
288
+ "--timeout",
289
+ "120",
290
+ "--report-format",
291
+ "json",
292
+ "--report-path",
293
+ report,
294
+ "--log-opts",
295
+ `--no-walk=unsorted --root --diff-merges=separate --no-ext-diff --no-textconv --no-renames ${group.join(" ")}`,
296
+ ],
297
+ {
298
+ cwd: scratch,
299
+ env: audit.environment,
300
+ timeout: 130000,
301
+ maxBuffer: 16 * 1024 * 1024,
302
+ }
303
+ );
304
+ if (
305
+ result.error ||
306
+ result.signal ||
307
+ ![0, 42].includes(result.status) ||
308
+ result.stdout.length ||
309
+ result.stderr.length ||
310
+ !audit.succeeded()
311
+ )
312
+ throw new HistorySecretError(
313
+ "Gitleaks 8.30.1 execution/configuration failed. Repair the pinned scanner and complete Git objects; no safety result was established."
314
+ );
315
+ let rows;
316
+ try {
317
+ rows = JSON.parse(readFileSync(report, "utf8"));
318
+ } catch {
319
+ throw new HistorySecretError(
320
+ "Scanner report is missing or malformed. Repair pinned scanner execution and retry; safety is unproved."
321
+ );
322
+ }
323
+ if (!Array.isArray(rows) || (result.status === 42) !== rows.length > 0)
324
+ throw new HistorySecretError(
325
+ "Scanner exit/report disagreement. Repair pinned scanner execution; required safety is unproved."
326
+ );
327
+ for (const row of rows) {
328
+ if (
329
+ !group.includes(row.Commit) ||
330
+ !/^[a-z][a-z0-9-]{1,64}$/u.test(row.RuleID) ||
331
+ !Number.isSafeInteger(row.StartLine) ||
332
+ row.StartLine < 1
333
+ )
334
+ throw new HistorySecretError(
335
+ "Scanner attribution is invalid. Repair pinned scanner execution; no raw report is published."
336
+ );
337
+ findings.push({
338
+ rule: row.RuleID,
339
+ commit: row.Commit,
340
+ line: row.StartLine,
341
+ });
342
+ }
343
+ }
344
+ return {
345
+ scanner: "Gitleaks",
346
+ version: VERSION,
347
+ commits: commits.length,
348
+ findings,
349
+ };
350
+ } finally {
351
+ rmSync(scratch, { recursive: true, force: true });
352
+ }
353
+ };
@@ -876,6 +876,18 @@ export const REGISTRY = Object.freeze({
876
876
  "Proved by the gitleaks binary in the pre-commit built-in and by a hosted scanner in CI. Neither is a package script.",
877
877
  moments: COMMIT_ONWARD,
878
878
  },
879
+ "introduced-history-credential-leakage": {
880
+ label: "🔐 Introduced History Credential Leakage",
881
+ summary:
882
+ "Every actual introduced Git history range is scanner-clean and fully redacted.",
883
+ task: "security:check-introduced-history",
884
+ declareOnly:
885
+ "When declared, the emitted scripts/lisa-history-secrets.mjs proves the actual range with a pinned offline scanner, without a package script.",
886
+ ownedFacadeAt: [PUSH, PULL_REQUEST],
887
+ needs: NO_INSTALL,
888
+ moments: [PUSH, PULL_REQUEST],
889
+ work: "introduced commits scanned",
890
+ },
879
891
  "dependency-vulnerability": {
880
892
  label: "🔒 Security Scan",
881
893
  summary: "No known high or critical advisory in shipped dependencies.",
@@ -1091,6 +1103,7 @@ export const REGISTRY = Object.freeze({
1091
1103
  * the two disagree, which is what keeps a static copy true.
1092
1104
  */
1093
1105
  export const QUALITY_JOB_GATES = Object.freeze({
1106
+ history_secrets: "introduced-history-credential-leakage",
1094
1107
  lint: CODE_STYLE,
1095
1108
  lint_slow: "code-style-slow",
1096
1109
  typecheck: "type-correctness",
@@ -3123,6 +3136,7 @@ export const REUSE_CLASSES = Object.freeze(Object.values(REUSE_CLASS));
3123
3136
  * A project overrides a row with `gates.<id>.reuse` in `.lisa.config.json`.
3124
3137
  */
3125
3138
  export const GATE_REUSE_CLASS = Object.freeze({
3139
+ "introduced-history-credential-leakage": { class: REUSE_CLASS.NEVER },
3126
3140
  accessibility: { class: REUSE_CLASS.NEVER },
3127
3141
  // A guard whose job is to detect a stale generated artifact must not itself
3128
3142
  // be satisfied by a record of a previous run, and it costs seconds.
@@ -5441,16 +5455,18 @@ export function resolveMoment({
5441
5455
  declared === null &&
5442
5456
  definition?.declareOnly !== undefined &&
5443
5457
  definition?.shippedAs === undefined &&
5444
- scripts !== null &&
5445
- typeof scripts === "object" &&
5446
- !Object.hasOwn(scripts, task) &&
5447
- HARDCODED_INVOCATIONS.some(
5448
- invocation =>
5449
- invocation.gate === id &&
5450
- invocation.moment === moment &&
5451
- invocation.facade === CONSULTS_THEN_FALLS_BACK &&
5452
- invocation.unconditional === true
5453
- );
5458
+ ((scripts !== null &&
5459
+ typeof scripts === "object" &&
5460
+ !Object.hasOwn(scripts, task)) ||
5461
+ (id === "introduced-history-credential-leakage" && scripts === null)) &&
5462
+ (definition?.ownedFacadeAt?.includes(moment) ||
5463
+ HARDCODED_INVOCATIONS.some(
5464
+ invocation =>
5465
+ invocation.gate === id &&
5466
+ invocation.moment === moment &&
5467
+ invocation.facade === CONSULTS_THEN_FALLS_BACK &&
5468
+ invocation.unconditional === true
5469
+ ));
5454
5470
 
5455
5471
  resolved.push({
5456
5472
  id,