residoo 0.1.0 → 0.3.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 +334 -46
- package/SECURITY.md +29 -22
- package/package.json +1 -1
- package/src/cli.js +249 -17
- package/src/integrity.js +689 -0
- package/src/patterns.js +78 -5
- package/src/report.js +188 -8
- package/src/rotation.js +834 -0
- package/src/sources/agent-configs.js +308 -0
- package/src/sources/aider.js +361 -0
- package/src/sources/amazon-q.js +199 -0
- package/src/sources/antigravity-cli.js +155 -0
- package/src/sources/cline.js +208 -0
- package/src/sources/codebuff.js +295 -0
- package/src/sources/codex-cli.js +258 -0
- package/src/sources/cody.js +325 -0
- package/src/sources/continue.js +408 -0
- package/src/sources/copilot-chat.js +272 -0
- package/src/sources/copilot-cli.js +300 -0
- package/src/sources/crush.js +364 -0
- package/src/sources/cursor.js +374 -0
- package/src/sources/devin-cli.js +241 -0
- package/src/sources/factory-droid.js +153 -0
- package/src/sources/fx.js +136 -0
- package/src/sources/gemini-cli.js +242 -0
- package/src/sources/goose.js +366 -0
- package/src/sources/grok-cli.js +267 -0
- package/src/sources/hermes.js +282 -0
- package/src/sources/index.js +172 -8
- package/src/sources/jetbrains-ai-assistant.js +343 -0
- package/src/sources/jetbrains-junie.js +292 -0
- package/src/sources/kilo-code.js +430 -0
- package/src/sources/kimi-code.js +147 -0
- package/src/sources/kiro-cli.js +393 -0
- package/src/sources/kiro-ide.js +230 -0
- package/src/sources/llm.js +328 -0
- package/src/sources/mentat.js +143 -0
- package/src/sources/open-interpreter.js +224 -0
- package/src/sources/openclaw.js +218 -0
- package/src/sources/opencode.js +379 -0
- package/src/sources/openhands.js +181 -0
- package/src/sources/pearai.js +151 -0
- package/src/sources/pi-agent.js +130 -0
- package/src/sources/project-artifacts.js +355 -0
- package/src/sources/qodo-gen.js +189 -0
- package/src/sources/qwen-code.js +244 -0
- package/src/sources/roo-code.js +239 -0
- package/src/sources/trae.js +294 -0
- package/src/sources/void.js +273 -0
- package/src/sources/warp.js +395 -0
- package/src/sources/windsurf.js +256 -0
- package/src/sources/zed.js +374 -0
package/src/cli.js
CHANGED
|
@@ -3,30 +3,100 @@
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { availableSources, ALL_SOURCES } = require("./sources");
|
|
5
5
|
const { scan, emptyResult } = require("./scan");
|
|
6
|
-
const { render, renderJson } = require("./report");
|
|
6
|
+
const { render, renderIntegrity, renderJson } = require("./report");
|
|
7
|
+
const { checkIntegrity } = require("./integrity");
|
|
8
|
+
const {
|
|
9
|
+
ROTATION_GUIDANCE, guidanceFor, loadAcks, ackFinding, renderRotation,
|
|
10
|
+
} = require("./rotation");
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
/**
|
|
13
|
+
* A source is unavailable for the ordinary reason (not installed — nothing
|
|
14
|
+
* more to say) far more often than for a reason worth surfacing. A handful
|
|
15
|
+
* of sources — the SQLite-backed ones gated on the built-in `node:sqlite`
|
|
16
|
+
* module (cursor.js, crush.js, cody.js, devin-cli.js, hermes.js, kiro-cli.js,
|
|
17
|
+
* llm.js, trae.js, void.js, warp.js, zed.js) — export the optional
|
|
18
|
+
* `unavailableReason()` for the one case worth calling out: the tool IS
|
|
19
|
+
* installed but this Node runtime is too old to read its database. Every
|
|
20
|
+
* other source can safely omit this export entirely; this stays a no-op for
|
|
21
|
+
* those rather than requiring every adapter to implement it.
|
|
22
|
+
*/
|
|
23
|
+
function sourceStatusLabel(source) {
|
|
24
|
+
const reason = typeof source.unavailableReason === "function" ? source.unavailableReason() : null;
|
|
25
|
+
return reason ? `${source.label()} (${reason})` : source.label();
|
|
26
|
+
}
|
|
27
|
+
function sourceStatusList() {
|
|
28
|
+
return ALL_SOURCES.map(sourceStatusLabel).join(", ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const HELP = `residoo: find secrets leaking through your AI agent's session history
|
|
9
32
|
|
|
10
33
|
Coding agents (Claude Code, Cursor, Copilot, ...) write everything you do
|
|
11
|
-
to a local transcript, including file contents your prompts touch
|
|
34
|
+
to a local transcript, including file contents your prompts touch. That
|
|
12
35
|
means real credentials sitting in plaintext on disk, indefinitely, in a
|
|
13
36
|
place nobody thinks to check. residoo scans those transcripts for them.
|
|
14
37
|
|
|
38
|
+
A scan also runs integrity checks over agent config locations, the 2026
|
|
39
|
+
supply-chain campaigns planted persistence exactly there: SessionStart
|
|
40
|
+
hooks, dropper scripts, folder-open tasks, zero-width Unicode instructions
|
|
41
|
+
hidden in memory/rules files. Every auto-executing hook found in the
|
|
42
|
+
checked locations is listed for your review; only published campaign IOCs
|
|
43
|
+
and campaign-shaped behaviors escalate to warnings.
|
|
44
|
+
|
|
45
|
+
Every finding comes with a rotation hint: the vendor's real revocation
|
|
46
|
+
path, verified against their docs, because a leaked key that is found but
|
|
47
|
+
never rotated is still leaked (64% of leaked secrets stay valid for years).
|
|
48
|
+
"residoo explain <rule-id>" prints the full runbook for one credential
|
|
49
|
+
type; "residoo ack <fingerprint>" records that you rotated one, in
|
|
50
|
+
~/.residoo/rotations.json, the only file residoo ever writes outside an
|
|
51
|
+
explicit --seal.
|
|
52
|
+
|
|
15
53
|
Scanning makes NO network calls and changes nothing on disk. Findings are
|
|
16
54
|
redacted in every output format. Sealing (--seal) writes NEW encrypted
|
|
17
|
-
files only
|
|
55
|
+
files only. It never modifies or deletes anything that already exists.
|
|
18
56
|
|
|
19
57
|
Usage:
|
|
20
58
|
residoo scan [options]
|
|
59
|
+
residoo explain <rule-id> (or: residoo explain --list)
|
|
60
|
+
residoo ack <fingerprint> [--note <text>]
|
|
21
61
|
residoo unseal <vault-dir> [--restore <n> --out <path>]
|
|
22
62
|
|
|
23
63
|
Scan options:
|
|
24
64
|
--json machine-readable output (full detail, still redacted)
|
|
65
|
+
--project [dir] scan a repository checkout instead of this machine
|
|
66
|
+
(default dir: current directory). Covers committed
|
|
67
|
+
agent transcripts, agent config/rules files, and
|
|
68
|
+
root-level .env files inside the checkout, plus
|
|
69
|
+
integrity checks anchored at that directory. The
|
|
70
|
+
home-level transcript and config sources are NOT
|
|
71
|
+
scanned in this mode, deliberately: in CI they
|
|
72
|
+
would scan the runner's home and say nothing about
|
|
73
|
+
the repo, and a clean project scan must never be
|
|
74
|
+
mistaken for a clean machine. Run residoo scan
|
|
75
|
+
without --project for the machine itself.
|
|
25
76
|
--include-noisy also run broad, false-positive-prone rules
|
|
26
77
|
--include-suppressed also show matches that looked like placeholder/example text
|
|
27
|
-
--fail-on-find exit code 1 if anything is found (for CI)
|
|
78
|
+
--fail-on-find exit code 1 if anything is found (for CI): secret
|
|
79
|
+
findings and integrity WARNINGS count; integrity
|
|
80
|
+
info-level review items do not
|
|
81
|
+
--allow-acked with --fail-on-find: findings whose fingerprint was
|
|
82
|
+
acknowledged via "residoo ack" no longer fail the
|
|
83
|
+
run; pending findings and integrity warnings still
|
|
84
|
+
do. Without this flag, --fail-on-find fails on
|
|
85
|
+
every finding, acknowledged or not.
|
|
86
|
+
--no-integrity skip the integrity checks (planted hooks, dropper
|
|
87
|
+
files, auto-run tasks, hidden Unicode)
|
|
28
88
|
--no-color disable ANSI colour
|
|
29
89
|
|
|
90
|
+
Rotation:
|
|
91
|
+
residoo explain <rule-id> full rotation runbook for one detection rule
|
|
92
|
+
(where to revoke, steps, what revocation does)
|
|
93
|
+
residoo explain --list every rule id with its credential label
|
|
94
|
+
residoo ack <fingerprint> mark one finding's rotation done; fingerprints
|
|
95
|
+
appear next to findings in the report and in
|
|
96
|
+
--json. Optional --note <text> is stored with
|
|
97
|
+
the acknowledgement (redacted if it matches a
|
|
98
|
+
secret pattern).
|
|
99
|
+
|
|
30
100
|
Seal options (used with scan):
|
|
31
101
|
--seal after scanning, encrypt every transcript that carried a
|
|
32
102
|
finding into a local vault directory (AES-256-GCM,
|
|
@@ -49,7 +119,7 @@ Unseal:
|
|
|
49
119
|
|
|
50
120
|
The passphrase is read from RESIDOO_PASSPHRASE, or prompted (hidden) on a TTY.
|
|
51
121
|
|
|
52
|
-
Sources checked on this machine: ${
|
|
122
|
+
Sources checked on this machine: ${sourceStatusList()}
|
|
53
123
|
`;
|
|
54
124
|
|
|
55
125
|
function argValue(args, flag) {
|
|
@@ -73,7 +143,7 @@ async function runSeal(result, args) {
|
|
|
73
143
|
|
|
74
144
|
const filesWithFindings = [...new Set(result.findings.map((f) => f.file))];
|
|
75
145
|
if (filesWithFindings.length === 0) {
|
|
76
|
-
process.stdout.write("Nothing to seal
|
|
146
|
+
process.stdout.write("Nothing to seal: no findings.\n");
|
|
77
147
|
return 0;
|
|
78
148
|
}
|
|
79
149
|
|
|
@@ -93,7 +163,7 @@ async function runSeal(result, args) {
|
|
|
93
163
|
`${(totalSealed / 1024 / 1024).toFixed(1)}MB encrypted.\n` +
|
|
94
164
|
`Originals were NOT touched. Once you've verified a restore works\n` +
|
|
95
165
|
`(residoo unseal ${path.basename(vaultDir)} --restore 0001.sealed --out /tmp/check), removing the\n` +
|
|
96
|
-
`plaintext originals is your call
|
|
166
|
+
`plaintext originals is your call; residoo never deletes anything itself.\n`
|
|
97
167
|
);
|
|
98
168
|
|
|
99
169
|
if (args.includes("--upload-cloudroam")) {
|
|
@@ -104,7 +174,7 @@ async function runSeal(result, args) {
|
|
|
104
174
|
process.stderr.write("--upload-cloudroam needs CLOUDROAM_API_KEY (env), --connector and --bucket.\n");
|
|
105
175
|
return 2;
|
|
106
176
|
}
|
|
107
|
-
process.stdout.write(`\nUploading sealed vault to CloudRoam (${bucket})
|
|
177
|
+
process.stdout.write(`\nUploading sealed vault to CloudRoam (${bucket}), ciphertext only:\n`);
|
|
108
178
|
const uploaded = await uploadVaultToCloudRoam({
|
|
109
179
|
vaultDir,
|
|
110
180
|
baseUrl: process.env.CLOUDROAM_BASE_URL || "https://cloudroam.io",
|
|
@@ -127,7 +197,7 @@ async function runUnseal(args) {
|
|
|
127
197
|
try {
|
|
128
198
|
manifest = openManifest(vaultDir, passphrase);
|
|
129
199
|
} catch {
|
|
130
|
-
process.stderr.write("Could not open vault
|
|
200
|
+
process.stderr.write("Could not open vault: wrong passphrase, or the vault is corrupted.\n");
|
|
131
201
|
return 1;
|
|
132
202
|
}
|
|
133
203
|
|
|
@@ -151,10 +221,82 @@ async function runUnseal(args) {
|
|
|
151
221
|
`verified byte-identical to the original (SHA-256 match).\n`);
|
|
152
222
|
return 0;
|
|
153
223
|
}
|
|
154
|
-
process.stderr.write(`Restored, but verification FAILED
|
|
224
|
+
process.stderr.write(`Restored, but verification FAILED: content does not match what was sealed. Do not trust this copy.\n`);
|
|
155
225
|
return 1;
|
|
156
226
|
}
|
|
157
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Full rotation runbook for one rule id, or the whole catalogue via --list.
|
|
230
|
+
* Prints for a HUMAN about to revoke a credential, so it stays plain text
|
|
231
|
+
* and never assumes the finding is still on screen.
|
|
232
|
+
*/
|
|
233
|
+
function runExplain(args) {
|
|
234
|
+
const ids = Object.keys(ROTATION_GUIDANCE);
|
|
235
|
+
const ruleId = args[1] && !args[1].startsWith("--") ? args[1] : null;
|
|
236
|
+
|
|
237
|
+
if (args.includes("--list") || ruleId === null) {
|
|
238
|
+
process.stdout.write("Rotation runbooks available (residoo explain <rule-id>):\n");
|
|
239
|
+
const width = Math.max(...ids.map((i) => i.length)) + 2;
|
|
240
|
+
for (const id of ids) {
|
|
241
|
+
process.stdout.write(` ${id.padEnd(width)}${ROTATION_GUIDANCE[id].label}\n`);
|
|
242
|
+
}
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const known = Object.prototype.hasOwnProperty.call(ROTATION_GUIDANCE, ruleId);
|
|
247
|
+
const g = guidanceFor(ruleId);
|
|
248
|
+
const out = [];
|
|
249
|
+
out.push(`${g.label} (rule: ${ruleId})`);
|
|
250
|
+
if (g.rotateUrl) out.push(` rotate/revoke docs: ${g.rotateUrl}`);
|
|
251
|
+
if (g.consolePath) out.push(` where: ${g.consolePath}`);
|
|
252
|
+
out.push(" steps:");
|
|
253
|
+
g.steps.forEach((s, i) => out.push(` ${i + 1}. ${s}`));
|
|
254
|
+
out.push(` note: ${g.revokeNote}`);
|
|
255
|
+
if (g.generic) {
|
|
256
|
+
out.push(" (generic guidance: this rule matches a shape, not a single vendor,");
|
|
257
|
+
out.push(" so the issuing service has to be identified from the finding's context)");
|
|
258
|
+
}
|
|
259
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
260
|
+
if (!known) {
|
|
261
|
+
process.stderr.write(`residoo: "${ruleId}" is not a known rule id; the guidance above is the generic fallback. residoo explain --list shows every known id.\n`);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Record that one finding's credential was rotated. Takes the fingerprint
|
|
269
|
+
* exactly as the report and --json print it; acks change what the report
|
|
270
|
+
* SAYS (pending vs acknowledged) but never what --fail-on-find DOES unless
|
|
271
|
+
* --allow-acked is also passed to the scan, which keeps a CI gate honest by
|
|
272
|
+
* default.
|
|
273
|
+
*/
|
|
274
|
+
function runAck(args) {
|
|
275
|
+
const fp = args[1] && !args[1].startsWith("--") ? args[1] : null;
|
|
276
|
+
if (!fp) {
|
|
277
|
+
process.stderr.write("usage: residoo ack <fingerprint> [--note <text>]\n" +
|
|
278
|
+
"Fingerprints (rf1-...) are shown next to findings in the scan report and in --json.\n");
|
|
279
|
+
return 2;
|
|
280
|
+
}
|
|
281
|
+
let res;
|
|
282
|
+
try {
|
|
283
|
+
res = ackFinding(fp, argValue(args, "--note"));
|
|
284
|
+
} catch (err) {
|
|
285
|
+
process.stderr.write(`residoo: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
286
|
+
return 2;
|
|
287
|
+
}
|
|
288
|
+
process.stdout.write(
|
|
289
|
+
`Acknowledged ${res.fingerprint} at ${res.at}` +
|
|
290
|
+
(res.note ? ` with note: ${res.note}` : "") + "\n" +
|
|
291
|
+
"The next scan reports this finding as acknowledged. It still counts toward\n" +
|
|
292
|
+
"--fail-on-find unless the scan is run with --allow-acked.\n" +
|
|
293
|
+
"(ack is stateless: it cannot check this fingerprint against a scan, so a\n" +
|
|
294
|
+
"mistyped one is recorded too; the intended finding would then still show\n" +
|
|
295
|
+
"as pending on the next scan.)\n"
|
|
296
|
+
);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
158
300
|
async function main(argv) {
|
|
159
301
|
const args = argv.slice(2);
|
|
160
302
|
if (args.includes("-h") || args.includes("--help") || args.length === 0) {
|
|
@@ -164,6 +306,8 @@ async function main(argv) {
|
|
|
164
306
|
|
|
165
307
|
const cmd = args[0];
|
|
166
308
|
if (cmd === "unseal") return runUnseal(args);
|
|
309
|
+
if (cmd === "explain") return runExplain(args);
|
|
310
|
+
if (cmd === "ack") return runAck(args);
|
|
167
311
|
if (cmd !== "scan") {
|
|
168
312
|
process.stderr.write(`Unknown command "${cmd}". Try "residoo --help".\n`);
|
|
169
313
|
return 2;
|
|
@@ -173,6 +317,16 @@ async function main(argv) {
|
|
|
173
317
|
const includeNoisy = args.includes("--include-noisy");
|
|
174
318
|
const includeSuppressed = args.includes("--include-suppressed");
|
|
175
319
|
const failOnFind = args.includes("--fail-on-find");
|
|
320
|
+
const allowAcked = args.includes("--allow-acked");
|
|
321
|
+
|
|
322
|
+
// --project [dir]: the dir is optional (CI passes ".", a bare --project
|
|
323
|
+
// means the current directory). null means machine mode.
|
|
324
|
+
let projectRoot = null;
|
|
325
|
+
const projectIdx = args.indexOf("--project");
|
|
326
|
+
if (projectIdx >= 0) {
|
|
327
|
+
const next = args[projectIdx + 1];
|
|
328
|
+
projectRoot = path.resolve(next && !next.startsWith("--") ? next : ".");
|
|
329
|
+
}
|
|
176
330
|
// Passed through explicitly to render() rather than mutating
|
|
177
331
|
// process.env.NO_COLOR — main() is an exported function a host process can
|
|
178
332
|
// legitimately call more than once (a wrapper CLI, a test runner), and a
|
|
@@ -180,32 +334,110 @@ async function main(argv) {
|
|
|
180
334
|
// color for a later call that never asked for that.
|
|
181
335
|
const noColor = args.includes("--no-color");
|
|
182
336
|
|
|
183
|
-
|
|
337
|
+
// Integrity runs by default: a scan that reports "no secrets leaked" while
|
|
338
|
+
// a planted SessionStart hook sits ready to re-leak them next session is
|
|
339
|
+
// an incomplete answer. Only warn-severity findings (verified campaign
|
|
340
|
+
// signatures, unverifiable configs) gate --fail-on-find — info items are
|
|
341
|
+
// the user's own hooks listed for review, and failing CI on those would
|
|
342
|
+
// train people to pass --no-integrity, which is worse than not checking.
|
|
343
|
+
const wantsIntegrity = !args.includes("--no-integrity");
|
|
344
|
+
const integrityWarnCount = (integ) =>
|
|
345
|
+
integ ? integ.findings.filter((f) => f.severity === "warn").length : 0;
|
|
346
|
+
// The integrity checker's inputs are, by its own threat model, attacker-
|
|
347
|
+
// plantable files — a hostile config must not be able to suppress the
|
|
348
|
+
// secrets report by crashing the checker after the scan already ran. A
|
|
349
|
+
// throw degrades to a warn-severity finding: the run stays alive, the
|
|
350
|
+
// failure stays loud (it still gates --fail-on-find), and it is never a
|
|
351
|
+
// silent all-clear.
|
|
352
|
+
// In project mode both anchors point at the project root: the transcript
|
|
353
|
+
// sources are already project-only, and letting the integrity pass read the
|
|
354
|
+
// invoking machine's home would fail a CI/pre-commit run on a developer's
|
|
355
|
+
// own home-level hooks — a verdict about the wrong thing. The root's own
|
|
356
|
+
// .claude/, .gemini/, .cursor/, .vscode/, CLAUDE.md and .cursorrules are
|
|
357
|
+
// exactly the committed plant sites the campaigns used.
|
|
358
|
+
const runIntegrity = () => {
|
|
359
|
+
try {
|
|
360
|
+
// projectMode additionally makes checkIntegrity ignore the machine's
|
|
361
|
+
// GEMINI_CLI_HOME override and suppress the home-anchored hook
|
|
362
|
+
// demotion — both are statements about this machine, and a committed
|
|
363
|
+
// repo config is not this user's standing config (see integrity.js).
|
|
364
|
+
const integ = checkIntegrity(projectRoot ? { home: projectRoot, cwd: projectRoot, projectMode: true } : {});
|
|
365
|
+
// checkIntegrity's stock scope note says "current working directory";
|
|
366
|
+
// in project mode that would misdescribe what was checked.
|
|
367
|
+
if (projectRoot) {
|
|
368
|
+
integ.scopeNote = "Integrity checks cover the --project directory only (paths shown relative to it); this machine's home-level agent configs were not examined on this run.";
|
|
369
|
+
}
|
|
370
|
+
return integ;
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
const why = String((e && e.message) || e).replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
374
|
+
return {
|
|
375
|
+
findings: [{
|
|
376
|
+
severity: "warn", kind: "integrity-crashed", file: "(integrity checker)",
|
|
377
|
+
detail: `integrity checks crashed (${why}). Config locations are UNVERIFIED, not clean; the secret-scan results are unaffected`,
|
|
378
|
+
}],
|
|
379
|
+
filesChecked: [],
|
|
380
|
+
scopeNote: "Integrity checks did not complete on this run.",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
let sources;
|
|
386
|
+
if (projectRoot) {
|
|
387
|
+
const projectArtifacts = require("./sources/project-artifacts");
|
|
388
|
+
const src = projectArtifacts.withRoot(projectRoot);
|
|
389
|
+
if (!src.available()) {
|
|
390
|
+
process.stderr.write(`--project: "${projectRoot}" is not a readable directory.\n`);
|
|
391
|
+
return 2;
|
|
392
|
+
}
|
|
393
|
+
sources = [src];
|
|
394
|
+
} else {
|
|
395
|
+
sources = availableSources();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// loadAcks degrades to {} (loudly, on stderr) if the state file is corrupt,
|
|
399
|
+
// so a broken ack ledger can never block or distort a scan.
|
|
400
|
+
const acks = loadAcks();
|
|
401
|
+
|
|
184
402
|
if (sources.length === 0) {
|
|
185
403
|
const empty = emptyResult();
|
|
404
|
+
const integrity = wantsIntegrity ? runIntegrity() : null;
|
|
186
405
|
if (wantsJson) {
|
|
187
406
|
// A --json caller (CI, a script piping into jq) must always get valid JSON
|
|
188
407
|
// on stdout, even on the "nothing to scan" path — a plain-text message on
|
|
189
408
|
// stderr with exit 0 silently breaks that contract.
|
|
190
|
-
process.stdout.write(renderJson(empty) + "\n");
|
|
409
|
+
process.stdout.write(renderJson(empty, integrity, renderRotation([], acks)) + "\n");
|
|
191
410
|
} else {
|
|
192
411
|
process.stderr.write(
|
|
193
412
|
"No known transcript sources found on this machine.\n" +
|
|
194
|
-
`Checked: ${
|
|
413
|
+
`Checked: ${sourceStatusList()}.\n`
|
|
195
414
|
);
|
|
415
|
+
// The integrity checks are not gated on transcript sources existing —
|
|
416
|
+
// a planted repo-level hook in the CWD is exactly as dangerous here.
|
|
417
|
+
if (integrity) process.stdout.write(renderIntegrity(integrity, { noColor }) + "\n");
|
|
196
418
|
}
|
|
197
|
-
return 0;
|
|
419
|
+
return failOnFind && integrityWarnCount(integrity) > 0 ? 1 : 0;
|
|
198
420
|
}
|
|
199
421
|
|
|
200
422
|
const result = await scan({ sources, includeNoisy, includeSuppressed });
|
|
201
|
-
|
|
423
|
+
const integrity = wantsIntegrity ? runIntegrity() : null;
|
|
424
|
+
const rotation = renderRotation(result.findings, acks);
|
|
425
|
+
process.stdout.write((wantsJson
|
|
426
|
+
? renderJson(result, integrity, rotation)
|
|
427
|
+
: render(result, { noColor, integrity, rotation })) + "\n");
|
|
202
428
|
|
|
203
429
|
if (args.includes("--seal")) {
|
|
204
430
|
const sealExit = await runSeal(result, args);
|
|
205
431
|
if (sealExit !== 0) return sealExit;
|
|
206
432
|
}
|
|
207
433
|
|
|
208
|
-
|
|
434
|
+
// --allow-acked narrows the SECRET gate only: an acknowledged rotation says
|
|
435
|
+
// nothing about a planted hook, so integrity warnings always fail. Without
|
|
436
|
+
// the flag, acks change what the report says, never what CI does — a gate
|
|
437
|
+
// that silently honored local ack state would let one developer's ledger
|
|
438
|
+
// green-light everyone's pipeline.
|
|
439
|
+
const secretGate = allowAcked ? rotation.counts.pending > 0 : result.findings.length > 0;
|
|
440
|
+
return failOnFind && (secretGate || integrityWarnCount(integrity) > 0) ? 1 : 0;
|
|
209
441
|
}
|
|
210
442
|
|
|
211
443
|
module.exports = { main };
|