finitemonkey 0.1.0-next.1 → 0.1.0-next.2
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.es.md +97 -60
- package/README.md +97 -60
- package/README.pt-BR.md +98 -61
- package/dist/finitemonkey.mjs +1702 -38
- package/package.json +18 -7
package/dist/finitemonkey.mjs
CHANGED
|
@@ -14,30 +14,121 @@ var isEntryPoint = (moduleUrl, argv1, realpath, platform) => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// src/core/node-runtime.ts
|
|
17
|
-
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
18
18
|
import { readFileSync, realpathSync } from "node:fs";
|
|
19
19
|
|
|
20
|
+
// src/core/node-fs.ts
|
|
21
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, readdirSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
22
|
+
import { dirname, join, relative } from "node:path";
|
|
23
|
+
|
|
20
24
|
// src/core/parsed.ts
|
|
21
25
|
var accept = (value) => ({ ok: true, value });
|
|
22
26
|
var reject = (error) => ({ ok: false, error });
|
|
23
27
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
-
var parseJson = (
|
|
28
|
+
var parseJson = (text2) => {
|
|
25
29
|
try {
|
|
26
|
-
return accept(JSON.parse(
|
|
30
|
+
return accept(JSON.parse(text2));
|
|
27
31
|
} catch (error) {
|
|
28
32
|
return reject(error instanceof Error ? error.message : String(error));
|
|
29
33
|
}
|
|
30
34
|
};
|
|
31
|
-
var parseJsonObject = (
|
|
32
|
-
const parsed = parseJson(
|
|
35
|
+
var parseJsonObject = (text2) => {
|
|
36
|
+
const parsed = parseJson(text2);
|
|
33
37
|
if (!parsed.ok) return parsed;
|
|
34
38
|
return isRecord(parsed.value) ? accept(parsed.value) : reject("expected a JSON object");
|
|
35
39
|
};
|
|
36
|
-
var optionalString = (
|
|
37
|
-
const value =
|
|
40
|
+
var optionalString = (record2, key) => {
|
|
41
|
+
const value = record2[key];
|
|
38
42
|
return typeof value === "string" ? value : void 0;
|
|
39
43
|
};
|
|
40
44
|
|
|
45
|
+
// src/core/node-fs.ts
|
|
46
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
47
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
48
|
+
var nodeWriteText = (path, text2) => {
|
|
49
|
+
const temp = `${path}.${String(process.pid)}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
52
|
+
const fd = openSync(temp, "w", 384);
|
|
53
|
+
try {
|
|
54
|
+
writeSync(fd, text2);
|
|
55
|
+
fsyncSync(fd);
|
|
56
|
+
} finally {
|
|
57
|
+
closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
renameSync(temp, path);
|
|
60
|
+
return accept(true);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
rmSync(temp, { force: true });
|
|
63
|
+
return reject(messageOf(error));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var nodeListDir = (path) => {
|
|
67
|
+
try {
|
|
68
|
+
return accept(readdirSync(path).sort());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return reject(messageOf(error));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var walkInto = (root, dir, out) => {
|
|
74
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
const full = join(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (!SKIPPED_DIRS.has(entry.name)) walkInto(root, full, out);
|
|
78
|
+
} else if (entry.isFile()) {
|
|
79
|
+
out.push(relative(root, full).split("\\").join("/"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var nodeWalkFiles = (root) => {
|
|
84
|
+
try {
|
|
85
|
+
const out = [];
|
|
86
|
+
walkInto(root, root, out);
|
|
87
|
+
return accept(out.sort());
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return reject(messageOf(error));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/core/node-git.ts
|
|
94
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
95
|
+
import { createInterface } from "node:readline";
|
|
96
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
97
|
+
var GIT_ENV = { ...process.env, GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
|
|
98
|
+
var notFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
99
|
+
var nodeGit = (args, cwd) => {
|
|
100
|
+
const result = spawnSync("git", [...args], {
|
|
101
|
+
cwd,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
maxBuffer: MAX_BUFFER,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
env: GIT_ENV
|
|
106
|
+
});
|
|
107
|
+
const stdout = result.stdout;
|
|
108
|
+
return {
|
|
109
|
+
found: !notFound(result.error),
|
|
110
|
+
ok: result.error === void 0 && result.status === 0,
|
|
111
|
+
stdout: typeof stdout === "string" ? stdout : ""
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
var nodeGitLines = (args, cwd, onLine) => new Promise((resolve2) => {
|
|
115
|
+
const child = spawn("git", [...args], {
|
|
116
|
+
cwd,
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
env: GIT_ENV,
|
|
119
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
120
|
+
});
|
|
121
|
+
let found = true;
|
|
122
|
+
child.on("error", (error) => {
|
|
123
|
+
found = !notFound(error);
|
|
124
|
+
});
|
|
125
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
126
|
+
lines.on("line", onLine);
|
|
127
|
+
child.on("close", (code) => {
|
|
128
|
+
resolve2({ found, ok: found && code === 0, stdout: "" });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
41
132
|
// src/core/node-runtime.ts
|
|
42
133
|
var SAFE_TOKEN = /^[A-Za-z0-9._-]+$/;
|
|
43
134
|
var WINDOWS_NOT_FOUND = 9009;
|
|
@@ -47,7 +138,7 @@ var errnoCode = (error) => {
|
|
|
47
138
|
};
|
|
48
139
|
var spawnProbe = (command, args, timeout) => {
|
|
49
140
|
const options = { encoding: "utf8", timeout, windowsHide: true };
|
|
50
|
-
return process.platform === "win32" ?
|
|
141
|
+
return process.platform === "win32" ? spawnSync2([command, ...args].join(" "), { ...options, shell: true }) : spawnSync2(command, [...args], options);
|
|
51
142
|
};
|
|
52
143
|
var nodeProbe = (command, args, timeoutMs) => {
|
|
53
144
|
if (![command, ...args].every((token) => SAFE_TOKEN.test(token))) {
|
|
@@ -86,7 +177,13 @@ var nodeRuntime = () => ({
|
|
|
86
177
|
return () => {
|
|
87
178
|
clearTimeout(timer);
|
|
88
179
|
};
|
|
89
|
-
}
|
|
180
|
+
},
|
|
181
|
+
writeText: nodeWriteText,
|
|
182
|
+
listDir: nodeListDir,
|
|
183
|
+
walkFiles: nodeWalkFiles,
|
|
184
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
185
|
+
git: nodeGit,
|
|
186
|
+
gitLines: nodeGitLines
|
|
90
187
|
});
|
|
91
188
|
|
|
92
189
|
// src/doctor/system.ts
|
|
@@ -301,6 +398,13 @@ var usageProblem = (code, messageKey, fix, params = {}) => ({
|
|
|
301
398
|
fix,
|
|
302
399
|
exitCode: ExitCode.Usage
|
|
303
400
|
});
|
|
401
|
+
var needsPersonProblem = (messageKey, fix, params = {}) => ({
|
|
402
|
+
code: "needs_confirmation",
|
|
403
|
+
messageKey,
|
|
404
|
+
params,
|
|
405
|
+
fix,
|
|
406
|
+
exitCode: ExitCode.NeedsPerson
|
|
407
|
+
});
|
|
304
408
|
var FmError = class extends Error {
|
|
305
409
|
problem;
|
|
306
410
|
constructor(problem) {
|
|
@@ -406,7 +510,7 @@ var GLOBAL_FLAGS = [
|
|
|
406
510
|
{ flag: "--help, -h", key: "flag.help" }
|
|
407
511
|
];
|
|
408
512
|
var exitCodeHelp = (ctx) => EXIT_CODE_NAMES.map((name) => ({ code: ExitCode[name], name, summary: ctx.t(exitCodeDocs[ExitCode[name]]) }));
|
|
409
|
-
var pad = (
|
|
513
|
+
var pad = (text2, width) => text2.padEnd(Math.max(width, text2.length + 1));
|
|
410
514
|
var describe = (ctx, spec) => ({
|
|
411
515
|
name: commandName(spec),
|
|
412
516
|
usage: spec.usage,
|
|
@@ -504,9 +608,1044 @@ var parseDeadline = (raw) => {
|
|
|
504
608
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HOOK_DEADLINE_MS;
|
|
505
609
|
};
|
|
506
610
|
|
|
611
|
+
// src/secrets/baseline.ts
|
|
612
|
+
import { join as join2 } from "node:path";
|
|
613
|
+
var BASELINE_FILE = ".finitemonkey/findings/baseline.json";
|
|
614
|
+
var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
|
|
615
|
+
var BASELINE_STATES = ["aceito", "falso-positivo"];
|
|
616
|
+
var EXCEPTION_MAX_DAYS = 90;
|
|
617
|
+
var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
|
|
618
|
+
var baselinePath = (root) => toAbsolutePath(join2(root, BASELINE_FILE));
|
|
619
|
+
var text = (record2, key) => {
|
|
620
|
+
const value = record2[key];
|
|
621
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
622
|
+
};
|
|
623
|
+
var stateOf = (value) => BASELINE_STATES.find((state) => state === value);
|
|
624
|
+
var parseEntry = (value) => {
|
|
625
|
+
if (!isRecord(value)) return void 0;
|
|
626
|
+
const fields = ["id", "rule", "file", "fingerprint", "owner", "reason", "acceptedAt", "expiresAt"];
|
|
627
|
+
const [id, rule, file, fingerprint, owner, reason, acceptedAt, expiresAt] = fields.map((key) => text(value, key));
|
|
628
|
+
const state = stateOf(value["state"]);
|
|
629
|
+
const line = value["line"];
|
|
630
|
+
if (id === void 0 || rule === void 0 || file === void 0 || fingerprint === void 0) return void 0;
|
|
631
|
+
if (owner === void 0 || reason === void 0 || acceptedAt === void 0 || expiresAt === void 0)
|
|
632
|
+
return void 0;
|
|
633
|
+
if (state === void 0 || typeof line !== "number") return void 0;
|
|
634
|
+
return { id, rule, file, line, fingerprint, state, owner, reason, acceptedAt, expiresAt };
|
|
635
|
+
};
|
|
636
|
+
var parseBaseline = (raw) => {
|
|
637
|
+
const parsed = parseJsonObject(raw);
|
|
638
|
+
if (!parsed.ok) return parsed;
|
|
639
|
+
if (parsed.value["schema"] !== BASELINE_SCHEMA) return reject(`schema must be ${BASELINE_SCHEMA}`);
|
|
640
|
+
const list2 = parsed.value["entries"];
|
|
641
|
+
if (!Array.isArray(list2)) return reject("entries must be a list");
|
|
642
|
+
const entries = [];
|
|
643
|
+
for (const [index, item] of list2.entries()) {
|
|
644
|
+
const entry = parseEntry(item);
|
|
645
|
+
if (entry === void 0) return reject(`entry ${String(index)} is incomplete`);
|
|
646
|
+
entries.push(entry);
|
|
647
|
+
}
|
|
648
|
+
return accept({ schema: BASELINE_SCHEMA, entries });
|
|
649
|
+
};
|
|
650
|
+
var loadBaseline = (runtime, root) => {
|
|
651
|
+
const raw = runtime.readText(baselinePath(root));
|
|
652
|
+
return raw.ok ? parseBaseline(raw.value) : accept(EMPTY_BASELINE);
|
|
653
|
+
};
|
|
654
|
+
var serializeBaseline = (baseline) => `${JSON.stringify({ schema: baseline.schema, entries: [...baseline.entries].sort((a, b) => a.id.localeCompare(b.id)) }, null, 2)}
|
|
655
|
+
`;
|
|
656
|
+
var saveBaseline = (runtime, root, baseline) => runtime.writeText(baselinePath(root), serializeBaseline(baseline));
|
|
657
|
+
var ratchet = (findings, baseline, now, inScope) => {
|
|
658
|
+
const byId = new Map(baseline.entries.map((entry) => [entry.id, entry]));
|
|
659
|
+
const seen = new Set(findings.map((finding) => finding.id));
|
|
660
|
+
const entryOf2 = (finding) => byId.get(finding.id);
|
|
661
|
+
return {
|
|
662
|
+
fresh: findings.filter((finding) => entryOf2(finding) === void 0),
|
|
663
|
+
known: findings.filter((finding) => (entryOf2(finding)?.expiresAt ?? "") >= now),
|
|
664
|
+
expired: findings.filter((finding) => {
|
|
665
|
+
const entry = entryOf2(finding);
|
|
666
|
+
return entry !== void 0 && entry.expiresAt < now;
|
|
667
|
+
}),
|
|
668
|
+
fixed: inScope === void 0 ? [] : baseline.entries.filter((entry) => !seen.has(entry.id) && inScope(entry.file))
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
var withoutFixed = (baseline, fixed) => {
|
|
672
|
+
const gone = new Set(fixed.map((entry) => entry.id));
|
|
673
|
+
return { schema: baseline.schema, entries: baseline.entries.filter((entry) => !gone.has(entry.id)) };
|
|
674
|
+
};
|
|
675
|
+
var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry) => entry.expiresAt >= now).map((entry) => entry.fingerprint));
|
|
676
|
+
|
|
677
|
+
// src/core/posix-path.ts
|
|
678
|
+
var toPosixPath = (path) => path.replace(/\\/g, "/");
|
|
679
|
+
var withoutCarriageReturn = (line) => line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
680
|
+
|
|
681
|
+
// src/secrets/allowlist.ts
|
|
682
|
+
var PLACEHOLDER_WORDS = [
|
|
683
|
+
"example",
|
|
684
|
+
"placeholder",
|
|
685
|
+
"your_",
|
|
686
|
+
"your-",
|
|
687
|
+
"yourkey",
|
|
688
|
+
"yourtoken",
|
|
689
|
+
"yoursecret",
|
|
690
|
+
"_here",
|
|
691
|
+
"-here",
|
|
692
|
+
"changeme",
|
|
693
|
+
"change_me",
|
|
694
|
+
"change-me",
|
|
695
|
+
"replace",
|
|
696
|
+
"redacted",
|
|
697
|
+
"dummy",
|
|
698
|
+
"fake",
|
|
699
|
+
"sample",
|
|
700
|
+
"mock",
|
|
701
|
+
"fixture",
|
|
702
|
+
"test",
|
|
703
|
+
"todo",
|
|
704
|
+
"xxxx",
|
|
705
|
+
"****",
|
|
706
|
+
"....",
|
|
707
|
+
"…",
|
|
708
|
+
"<",
|
|
709
|
+
">",
|
|
710
|
+
"${",
|
|
711
|
+
"{{",
|
|
712
|
+
"%s",
|
|
713
|
+
"process.env",
|
|
714
|
+
"secrets.",
|
|
715
|
+
"env("
|
|
716
|
+
];
|
|
717
|
+
var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set([
|
|
718
|
+
"password",
|
|
719
|
+
"passwd",
|
|
720
|
+
"secret",
|
|
721
|
+
"token",
|
|
722
|
+
"apikey",
|
|
723
|
+
"api_key",
|
|
724
|
+
"admin",
|
|
725
|
+
"root",
|
|
726
|
+
"postgres",
|
|
727
|
+
"changeit",
|
|
728
|
+
"user",
|
|
729
|
+
"pass"
|
|
730
|
+
]);
|
|
731
|
+
var ENV_REFERENCE = /^\$\{?[A-Z_][A-Z0-9_]*\}?$/;
|
|
732
|
+
var LOCKFILES = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|go\.sum|Cargo\.lock|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$/;
|
|
733
|
+
var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|__fixtures__|fixtures|spec|testdata|examples?)\/|\.(?:test|spec)\.[A-Za-z0-9]+$/;
|
|
734
|
+
var isPlaceholder = (value) => {
|
|
735
|
+
const lower = value.toLowerCase();
|
|
736
|
+
if (PLACEHOLDER_VALUES.has(lower) || ENV_REFERENCE.test(value)) return true;
|
|
737
|
+
if (PLACEHOLDER_WORDS.some((word) => lower.includes(word))) return true;
|
|
738
|
+
return new Set(lower).size < 4;
|
|
739
|
+
};
|
|
740
|
+
var isTestOrLockPath = (path) => path !== void 0 && (LOCKFILES.test(toPosixPath(path)) || TEST_PATH.test(toPosixPath(path)));
|
|
741
|
+
|
|
742
|
+
// src/secrets/entropy.ts
|
|
743
|
+
var shannonEntropy = (text2) => {
|
|
744
|
+
if (text2.length === 0) return 0;
|
|
745
|
+
const counts = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const char of text2) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
747
|
+
let entropy = 0;
|
|
748
|
+
for (const count2 of counts.values()) {
|
|
749
|
+
const p = count2 / text2.length;
|
|
750
|
+
entropy -= p * Math.log2(p);
|
|
751
|
+
}
|
|
752
|
+
return entropy;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/secrets/fingerprint.ts
|
|
756
|
+
import { createHash } from "node:crypto";
|
|
757
|
+
var sha256Hex = (text2) => createHash("sha256").update(text2).digest("hex");
|
|
758
|
+
var fingerprintOf = (secret) => sha256Hex(`finitemonkey:secret:${secret}`).slice(0, 16);
|
|
759
|
+
var ALNUM = /[A-Za-z0-9]/g;
|
|
760
|
+
var MIN_TAIL_LENGTH = 12;
|
|
761
|
+
var tailOf = (secret) => {
|
|
762
|
+
const visible = (secret.match(ALNUM) ?? []).join("");
|
|
763
|
+
return visible.length >= MIN_TAIL_LENGTH ? visible.slice(-4) : "****";
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/secrets/lines.ts
|
|
767
|
+
var lineLocator = (text2) => {
|
|
768
|
+
const starts = [0];
|
|
769
|
+
for (let index = text2.indexOf("\n"); index !== -1; index = text2.indexOf("\n", index + 1)) starts.push(index + 1);
|
|
770
|
+
return (offset) => {
|
|
771
|
+
let low = 0;
|
|
772
|
+
let high = starts.length - 1;
|
|
773
|
+
while (low < high) {
|
|
774
|
+
const mid = low + high + 1 >> 1;
|
|
775
|
+
if ((starts[mid] ?? 0) <= offset) low = mid;
|
|
776
|
+
else high = mid - 1;
|
|
777
|
+
}
|
|
778
|
+
return { line: low + 1, column: offset - (starts[low] ?? 0) + 1 };
|
|
779
|
+
};
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// src/secrets/rules/types.ts
|
|
783
|
+
var defineRule = (spec) => ({
|
|
784
|
+
id: spec.id,
|
|
785
|
+
summary: spec.summary,
|
|
786
|
+
kind: spec.kind ?? "credential",
|
|
787
|
+
severity: spec.severity ?? "high",
|
|
788
|
+
keywords: spec.keywords.map((keyword) => keyword.toLowerCase()),
|
|
789
|
+
pattern: new RegExp(spec.source, `gd${spec.flags ?? ""}`),
|
|
790
|
+
minLength: spec.minLength ?? 8,
|
|
791
|
+
minEntropy: spec.minEntropy ?? 0,
|
|
792
|
+
requireDigit: spec.requireDigit ?? false,
|
|
793
|
+
minRandomRun: spec.minRandomRun ?? 0,
|
|
794
|
+
ignoredNames: (spec.ignoredNames ?? []).map((name) => name.toLowerCase()),
|
|
795
|
+
skipTestPaths: spec.skipTestPaths ?? false,
|
|
796
|
+
placeholders: spec.placeholders ?? true
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// src/secrets/rules/providers.ts
|
|
800
|
+
var PROVIDER_RULES = [
|
|
801
|
+
defineRule({
|
|
802
|
+
id: "aws-access-token",
|
|
803
|
+
summary: "AWS access key id (AKIA, ASIA, ABIA, ACCA, A3T)",
|
|
804
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"],
|
|
805
|
+
source: String.raw`\b(?<secret>(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b`,
|
|
806
|
+
minLength: 20,
|
|
807
|
+
minEntropy: 3
|
|
808
|
+
}),
|
|
809
|
+
defineRule({
|
|
810
|
+
id: "aws-secret-access-key",
|
|
811
|
+
summary: "AWS secret access key assigned to aws_secret_access_key or a similar name",
|
|
812
|
+
keywords: ["secret"],
|
|
813
|
+
source: String.raw`(?:aws_?)?secret_?access_?key["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])`,
|
|
814
|
+
flags: "i",
|
|
815
|
+
minLength: 40,
|
|
816
|
+
minEntropy: 4
|
|
817
|
+
}),
|
|
818
|
+
defineRule({
|
|
819
|
+
id: "github-pat",
|
|
820
|
+
summary: "GitHub classic personal access token (ghp_)",
|
|
821
|
+
keywords: ["ghp_"],
|
|
822
|
+
source: String.raw`\b(?<secret>ghp_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
823
|
+
minEntropy: 3
|
|
824
|
+
}),
|
|
825
|
+
defineRule({
|
|
826
|
+
id: "github-oauth",
|
|
827
|
+
summary: "GitHub OAuth access token (gho_)",
|
|
828
|
+
keywords: ["gho_"],
|
|
829
|
+
source: String.raw`\b(?<secret>gho_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
830
|
+
minEntropy: 3
|
|
831
|
+
}),
|
|
832
|
+
defineRule({
|
|
833
|
+
id: "github-app-token",
|
|
834
|
+
summary: "GitHub App user or installation token (ghu_, ghs_)",
|
|
835
|
+
keywords: ["ghu_", "ghs_"],
|
|
836
|
+
source: String.raw`\b(?<secret>(?:ghu|ghs)_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
837
|
+
minEntropy: 3
|
|
838
|
+
}),
|
|
839
|
+
defineRule({
|
|
840
|
+
id: "github-refresh-token",
|
|
841
|
+
summary: "GitHub refresh token (ghr_)",
|
|
842
|
+
keywords: ["ghr_"],
|
|
843
|
+
source: String.raw`\b(?<secret>ghr_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
844
|
+
minEntropy: 3
|
|
845
|
+
}),
|
|
846
|
+
defineRule({
|
|
847
|
+
id: "github-fine-grained-pat",
|
|
848
|
+
summary: "GitHub fine-grained personal access token (github_pat_)",
|
|
849
|
+
keywords: ["github_pat_"],
|
|
850
|
+
source: String.raw`\b(?<secret>github_pat_[A-Za-z0-9_]{82})(?![A-Za-z0-9_])`,
|
|
851
|
+
minEntropy: 3
|
|
852
|
+
}),
|
|
853
|
+
defineRule({
|
|
854
|
+
id: "gitlab-pat",
|
|
855
|
+
summary: "GitLab personal access token (glpat-)",
|
|
856
|
+
keywords: ["glpat-"],
|
|
857
|
+
source: String.raw`\b(?<secret>glpat-[A-Za-z0-9_-]{20})(?![A-Za-z0-9_-])`,
|
|
858
|
+
minEntropy: 3
|
|
859
|
+
}),
|
|
860
|
+
defineRule({
|
|
861
|
+
id: "npm-access-token",
|
|
862
|
+
summary: "npm access token (npm_)",
|
|
863
|
+
keywords: ["npm_"],
|
|
864
|
+
source: String.raw`\b(?<secret>npm_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
865
|
+
flags: "i",
|
|
866
|
+
minEntropy: 3
|
|
867
|
+
}),
|
|
868
|
+
defineRule({
|
|
869
|
+
id: "slack-bot-token",
|
|
870
|
+
summary: "Slack bot token (xoxb-)",
|
|
871
|
+
keywords: ["xoxb-"],
|
|
872
|
+
source: String.raw`\b(?<secret>xoxb-[0-9]{10,13}-[0-9]{10,13}[A-Za-z0-9-]*)`,
|
|
873
|
+
minEntropy: 3
|
|
874
|
+
}),
|
|
875
|
+
defineRule({
|
|
876
|
+
id: "slack-user-token",
|
|
877
|
+
summary: "Slack user or export token (xoxp-, xoxe-)",
|
|
878
|
+
keywords: ["xoxp-", "xoxe-"],
|
|
879
|
+
source: String.raw`\b(?<secret>xox[pe](?:-[0-9]{10,13}){3}-[A-Za-z0-9-]{28,34})`,
|
|
880
|
+
minEntropy: 3
|
|
881
|
+
}),
|
|
882
|
+
defineRule({
|
|
883
|
+
id: "slack-app-token",
|
|
884
|
+
summary: "Slack app-level token (xapp-)",
|
|
885
|
+
keywords: ["xapp-"],
|
|
886
|
+
source: String.raw`\b(?<secret>xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+)`,
|
|
887
|
+
flags: "i",
|
|
888
|
+
minEntropy: 3
|
|
889
|
+
}),
|
|
890
|
+
defineRule({
|
|
891
|
+
id: "slack-webhook-url",
|
|
892
|
+
summary: "Slack incoming webhook URL",
|
|
893
|
+
keywords: ["hooks.slack.com"],
|
|
894
|
+
source: String.raw`(?<secret>hooks\.slack\.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56})`,
|
|
895
|
+
minEntropy: 3
|
|
896
|
+
}),
|
|
897
|
+
defineRule({
|
|
898
|
+
id: "stripe-access-token",
|
|
899
|
+
summary: "Stripe live secret or restricted key (sk_live_, rk_live_)",
|
|
900
|
+
keywords: ["sk_live_", "rk_live_", "sk_prod_", "rk_prod_"],
|
|
901
|
+
source: String.raw`\b(?<secret>(?:sk|rk)_(?:live|prod)_[A-Za-z0-9]{20,99})(?![A-Za-z0-9])`,
|
|
902
|
+
minEntropy: 3
|
|
903
|
+
}),
|
|
904
|
+
defineRule({
|
|
905
|
+
id: "anthropic-api-key",
|
|
906
|
+
summary: "Anthropic API or admin key (sk-ant-)",
|
|
907
|
+
keywords: ["sk-ant-"],
|
|
908
|
+
source: String.raw`\b(?<secret>sk-ant-(?:api|admin|oat)\d{2}-[A-Za-z0-9_-]{80,})`,
|
|
909
|
+
minEntropy: 3.5
|
|
910
|
+
}),
|
|
911
|
+
defineRule({
|
|
912
|
+
id: "openai-api-key",
|
|
913
|
+
summary: "OpenAI API key (sk-proj-, sk-svcacct-, sk-admin- or the T3BlbkFJ marker)",
|
|
914
|
+
keywords: ["t3blbkfj", "sk-proj-", "sk-svcacct-", "sk-admin-"],
|
|
915
|
+
source: String.raw`\b(?<secret>sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}|sk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{58,})`,
|
|
916
|
+
minEntropy: 3.5
|
|
917
|
+
}),
|
|
918
|
+
defineRule({
|
|
919
|
+
id: "gcp-api-key",
|
|
920
|
+
summary: "Google API key (AIza)",
|
|
921
|
+
keywords: ["aiza"],
|
|
922
|
+
source: String.raw`\b(?<secret>AIza[0-9A-Za-z_-]{35})(?![0-9A-Za-z_-])`,
|
|
923
|
+
minEntropy: 3
|
|
924
|
+
}),
|
|
925
|
+
defineRule({
|
|
926
|
+
id: "asaas-api-key",
|
|
927
|
+
summary: "Asaas API key ($aact_, $aact_prod_, $aact_hmlg_) with a body; the bare prefix is a placeholder",
|
|
928
|
+
keywords: ["$aact_"],
|
|
929
|
+
source: String.raw`(?<secret>\$aact_(?:prod_|hmlg_)?[A-Za-z0-9+/=_:$-]{32,})`,
|
|
930
|
+
minLength: 40,
|
|
931
|
+
minEntropy: 3.5
|
|
932
|
+
})
|
|
933
|
+
];
|
|
934
|
+
|
|
935
|
+
// src/secrets/rules/structural.ts
|
|
936
|
+
var STRUCTURAL_RULES = [
|
|
937
|
+
defineRule({
|
|
938
|
+
id: "private-key",
|
|
939
|
+
summary: "PEM private key block (RSA, EC, DSA, OPENSSH, PGP, PKCS#8)",
|
|
940
|
+
keywords: ["private key"],
|
|
941
|
+
source: String.raw`-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----(?<secret>[\s\S]{64,16384}?)-----END[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----`,
|
|
942
|
+
minLength: 64,
|
|
943
|
+
minEntropy: 4
|
|
944
|
+
}),
|
|
945
|
+
defineRule({
|
|
946
|
+
id: "jwt",
|
|
947
|
+
summary: "JSON Web Token with a header, a payload and a signature, checked for entropy",
|
|
948
|
+
keywords: ["eyj"],
|
|
949
|
+
source: String.raw`\b(?<secret>eyJ[A-Za-z0-9_-]{17,}\.eyJ[A-Za-z0-9_-]{17,}\.[A-Za-z0-9_-]{10,})`,
|
|
950
|
+
minLength: 40,
|
|
951
|
+
minEntropy: 3
|
|
952
|
+
}),
|
|
953
|
+
defineRule({
|
|
954
|
+
id: "credential-in-url",
|
|
955
|
+
summary: "Password or token inside a URL (scheme://user:secret@host), including git remotes",
|
|
956
|
+
keywords: ["://"],
|
|
957
|
+
source: String.raw`\b[a-z][a-z0-9+.-]{1,20}://[^\s:/@"'\x60]{1,64}:(?<secret>[^\s:/@"'\x60]{6,128})@[A-Za-z0-9.-]`,
|
|
958
|
+
flags: "i",
|
|
959
|
+
minLength: 6,
|
|
960
|
+
minEntropy: 2.5
|
|
961
|
+
}),
|
|
962
|
+
defineRule({
|
|
963
|
+
id: "default-secret-fallback",
|
|
964
|
+
summary: "JavaScript secret variable with a constant fallback (process.env.X ?? 'constant')",
|
|
965
|
+
kind: "practice",
|
|
966
|
+
severity: "medium",
|
|
967
|
+
keywords: ["process.env."],
|
|
968
|
+
source: String.raw`process\.env\.[A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Z0-9_]*[ \t]*(?:\?\?|\|\|)[ \t]*["'\x60](?<secret>[^"'\x60\n]{4,})["'\x60]`,
|
|
969
|
+
minLength: 4,
|
|
970
|
+
placeholders: false,
|
|
971
|
+
skipTestPaths: true
|
|
972
|
+
}),
|
|
973
|
+
defineRule({
|
|
974
|
+
id: "default-secret-fallback-python",
|
|
975
|
+
summary: "Python secret variable with a constant default (os.getenv('X', 'constant'))",
|
|
976
|
+
kind: "practice",
|
|
977
|
+
severity: "medium",
|
|
978
|
+
keywords: ["getenv", "environ.get"],
|
|
979
|
+
source: String.raw`os\.(?:getenv|environ\.get)\([ \t]*["'][A-Za-z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Za-z0-9_]*["'][ \t]*,[ \t]*["'](?<secret>[^"'\n]{4,})["']`,
|
|
980
|
+
minLength: 4,
|
|
981
|
+
placeholders: false,
|
|
982
|
+
skipTestPaths: true
|
|
983
|
+
}),
|
|
984
|
+
defineRule({
|
|
985
|
+
id: "generic-api-key",
|
|
986
|
+
summary: "High-entropy value assigned to a name with key, secret, token or credential",
|
|
987
|
+
keywords: ["key", "secret", "token", "credential"],
|
|
988
|
+
source: String.raw`(?:key|secret|token|credential)s?[A-Za-z0-9_.-]{0,30}["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[A-Za-z0-9_\-+/=.~$!@#%^&*]{16,200})(?![A-Za-z0-9_\-+/=.~$!@#%^&*(])`,
|
|
989
|
+
flags: "i",
|
|
990
|
+
minLength: 16,
|
|
991
|
+
minEntropy: 3.5,
|
|
992
|
+
requireDigit: true,
|
|
993
|
+
minRandomRun: 12,
|
|
994
|
+
ignoredNames: [
|
|
995
|
+
"key",
|
|
996
|
+
"keys",
|
|
997
|
+
"cachekey",
|
|
998
|
+
"querykey",
|
|
999
|
+
"storagekey",
|
|
1000
|
+
"sortkey",
|
|
1001
|
+
"idempotencykey",
|
|
1002
|
+
"keyname",
|
|
1003
|
+
"tokentype"
|
|
1004
|
+
],
|
|
1005
|
+
skipTestPaths: true
|
|
1006
|
+
}),
|
|
1007
|
+
defineRule({
|
|
1008
|
+
id: "password-assignment",
|
|
1009
|
+
summary: "Literal value assigned to a password name (password, passwd, pwd, senha, contraseña)",
|
|
1010
|
+
keywords: ["passw", "pwd", "senha", "contrase"],
|
|
1011
|
+
source: String.raw`(?:password|passwd|pwd|senha|contrase[ñn]a)[A-Za-z0-9_.-]{0,20}["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[^\s"'\x60,;()[\]{}<>]{8,128})`,
|
|
1012
|
+
flags: "i",
|
|
1013
|
+
minLength: 8,
|
|
1014
|
+
minEntropy: 3,
|
|
1015
|
+
requireDigit: true,
|
|
1016
|
+
skipTestPaths: true
|
|
1017
|
+
})
|
|
1018
|
+
];
|
|
1019
|
+
|
|
1020
|
+
// src/secrets/rules/catalog.ts
|
|
1021
|
+
var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES];
|
|
1022
|
+
var CREDENTIAL_RULES = RULES.filter((rule) => rule.kind === "credential");
|
|
1023
|
+
var ruleById = (id) => RULES.find((rule) => rule.id === id);
|
|
1024
|
+
|
|
1025
|
+
// src/secrets/engine.ts
|
|
1026
|
+
var DIGIT = /[0-9]/;
|
|
1027
|
+
var LETTER = /[A-Za-z]/;
|
|
1028
|
+
var RUN_ENTROPY = 3;
|
|
1029
|
+
var hasRandomRun = (secret, length) => (secret.match(new RegExp(`[A-Za-z0-9]{${String(length)},}`, "g")) ?? []).some(
|
|
1030
|
+
(run2) => DIGIT.test(run2) && LETTER.test(run2) && shannonEntropy(run2) >= RUN_ENTROPY
|
|
1031
|
+
);
|
|
1032
|
+
var accepted = (rule, secret) => secret.length >= rule.minLength && (!rule.requireDigit || DIGIT.test(secret)) && (rule.minRandomRun === 0 || hasRandomRun(secret, rule.minRandomRun)) && (!rule.placeholders || !isPlaceholder(secret)) && (rule.minEntropy === 0 || shannonEntropy(secret) >= rule.minEntropy);
|
|
1033
|
+
var NAME_CHAR = /[A-Za-z0-9_.-]/;
|
|
1034
|
+
var nameAround = (text2, at) => {
|
|
1035
|
+
let start = at;
|
|
1036
|
+
while (start > 0 && NAME_CHAR.test(text2.charAt(start - 1))) start -= 1;
|
|
1037
|
+
let end = at;
|
|
1038
|
+
while (end < text2.length && NAME_CHAR.test(text2.charAt(end))) end += 1;
|
|
1039
|
+
return text2.slice(start, end).toLowerCase();
|
|
1040
|
+
};
|
|
1041
|
+
var ignoredName = (rule, match) => rule.ignoredNames.length > 0 && rule.ignoredNames.includes(nameAround(match.input, match.index));
|
|
1042
|
+
var spanOf = (rule, match) => {
|
|
1043
|
+
const range = match.indices?.groups?.["secret"] ?? match.indices?.[0];
|
|
1044
|
+
if (range === void 0 || ignoredName(rule, match)) return void 0;
|
|
1045
|
+
const [start, end] = range;
|
|
1046
|
+
const secret = match.input.slice(start, end);
|
|
1047
|
+
return accepted(rule, secret) ? { rule, start, end, secret } : void 0;
|
|
1048
|
+
};
|
|
1049
|
+
var ruleSpans = (rule, text2) => {
|
|
1050
|
+
const spans = [];
|
|
1051
|
+
for (const match of text2.matchAll(rule.pattern)) {
|
|
1052
|
+
const span = spanOf(rule, match);
|
|
1053
|
+
if (span !== void 0) spans.push(span);
|
|
1054
|
+
}
|
|
1055
|
+
return spans;
|
|
1056
|
+
};
|
|
1057
|
+
var overlaps = (taken, span) => taken.some((other) => span.start < other.end && other.start < span.end);
|
|
1058
|
+
var applicable = (rules, lower, path) => {
|
|
1059
|
+
const testPath = isTestOrLockPath(path);
|
|
1060
|
+
return rules.filter(
|
|
1061
|
+
(rule) => !(testPath && rule.skipTestPaths) && rule.keywords.some((keyword) => lower.includes(keyword))
|
|
1062
|
+
);
|
|
1063
|
+
};
|
|
1064
|
+
var toFinding = (span, locate2) => {
|
|
1065
|
+
const position = locate2(span.start);
|
|
1066
|
+
return {
|
|
1067
|
+
ruleId: span.rule.id,
|
|
1068
|
+
severity: span.rule.severity,
|
|
1069
|
+
line: position.line,
|
|
1070
|
+
column: position.column,
|
|
1071
|
+
start: span.start,
|
|
1072
|
+
end: span.end,
|
|
1073
|
+
fingerprint: fingerprintOf(span.secret),
|
|
1074
|
+
tail: tailOf(span.secret)
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
var scanText = (text2, options = {}) => {
|
|
1078
|
+
if (text2.length === 0) return [];
|
|
1079
|
+
const rules = applicable(options.rules ?? RULES, text2.toLowerCase(), options.path);
|
|
1080
|
+
if (rules.length === 0) return [];
|
|
1081
|
+
const taken = [];
|
|
1082
|
+
for (const rule of rules) {
|
|
1083
|
+
for (const span of ruleSpans(rule, text2)) if (!overlaps(taken, span)) taken.push(span);
|
|
1084
|
+
}
|
|
1085
|
+
if (taken.length === 0) return [];
|
|
1086
|
+
const locate2 = lineLocator(text2);
|
|
1087
|
+
return taken.sort((a, b) => a.start - b.start).map((span) => toFinding(span, locate2));
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// src/secrets/incidents.ts
|
|
1091
|
+
import { join as join3 } from "node:path";
|
|
1092
|
+
var INCIDENTS_DIR = ".finitemonkey/incidents";
|
|
1093
|
+
var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
|
|
1094
|
+
var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
|
|
1095
|
+
var ID = /^inc-[0-9a-f]{12}$/;
|
|
1096
|
+
var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
|
|
1097
|
+
var incidentsDir = (root) => toAbsolutePath(join3(root, INCIDENTS_DIR));
|
|
1098
|
+
var incidentFile = (root, id) => toAbsolutePath(join3(incidentsDir(root), `${id}.json`));
|
|
1099
|
+
var pick = (options, value) => options.find((option) => option === value);
|
|
1100
|
+
var str = (record2, key) => {
|
|
1101
|
+
const value = record2[key];
|
|
1102
|
+
return typeof value === "string" ? value : void 0;
|
|
1103
|
+
};
|
|
1104
|
+
var parseEvent = (value) => {
|
|
1105
|
+
if (!isRecord(value)) return void 0;
|
|
1106
|
+
const state = pick(INCIDENT_STATES, value["state"]);
|
|
1107
|
+
const at = str(value, "at");
|
|
1108
|
+
const by = str(value, "by");
|
|
1109
|
+
return state === void 0 || at === void 0 || by === void 0 ? void 0 : { state, at, by };
|
|
1110
|
+
};
|
|
1111
|
+
var parseHistory = (value) => {
|
|
1112
|
+
if (!Array.isArray(value)) return void 0;
|
|
1113
|
+
const events = value.map(parseEvent);
|
|
1114
|
+
return events.every((event) => event !== void 0) ? events : void 0;
|
|
1115
|
+
};
|
|
1116
|
+
var parseIncident = (raw) => {
|
|
1117
|
+
const parsed = parseJsonObject(raw);
|
|
1118
|
+
if (!parsed.ok) return parsed;
|
|
1119
|
+
const record2 = parsed.value;
|
|
1120
|
+
const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str(record2, key));
|
|
1121
|
+
const state = pick(INCIDENT_STATES, record2["state"]);
|
|
1122
|
+
const source = pick(INCIDENT_SOURCES, record2["source"]);
|
|
1123
|
+
const history = parseHistory(record2["history"]);
|
|
1124
|
+
const firstCommit = str(record2, "firstCommit");
|
|
1125
|
+
if (id === void 0 || !ID.test(id) || rule === void 0 || where === void 0) return reject("incomplete");
|
|
1126
|
+
if (fingerprint === void 0 || createdAt === void 0 || updatedAt === void 0) return reject("incomplete");
|
|
1127
|
+
if (state === void 0 || source === void 0 || history === void 0) return reject("incomplete");
|
|
1128
|
+
const base = {
|
|
1129
|
+
schemaVersion: 1,
|
|
1130
|
+
id,
|
|
1131
|
+
rule,
|
|
1132
|
+
where,
|
|
1133
|
+
fingerprint,
|
|
1134
|
+
source,
|
|
1135
|
+
state,
|
|
1136
|
+
createdAt,
|
|
1137
|
+
updatedAt,
|
|
1138
|
+
history
|
|
1139
|
+
};
|
|
1140
|
+
return accept(firstCommit === void 0 ? base : { ...base, firstCommit });
|
|
1141
|
+
};
|
|
1142
|
+
var readIncident = (runtime, root, id) => {
|
|
1143
|
+
if (!ID.test(id)) return reject("not an incident id");
|
|
1144
|
+
const raw = runtime.readText(incidentFile(root, id));
|
|
1145
|
+
return raw.ok ? parseIncident(raw.value) : reject(raw.error);
|
|
1146
|
+
};
|
|
1147
|
+
var listIncidents = (runtime, root) => {
|
|
1148
|
+
const names = runtime.listDir(incidentsDir(root));
|
|
1149
|
+
if (!names.ok) return [];
|
|
1150
|
+
return names.value.filter((name) => name.endsWith(".json")).map((name) => readIncident(runtime, root, name.slice(0, -".json".length))).flatMap((parsed) => parsed.ok ? [parsed.value] : []).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1151
|
+
};
|
|
1152
|
+
var saveIncident = (runtime, root, incident) => {
|
|
1153
|
+
const ignore = runtime.writeText(toAbsolutePath(join3(incidentsDir(root), ".gitignore")), "*\n");
|
|
1154
|
+
if (!ignore.ok) return ignore;
|
|
1155
|
+
return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
|
|
1156
|
+
`);
|
|
1157
|
+
};
|
|
1158
|
+
var created = (draft, now, by) => ({
|
|
1159
|
+
schemaVersion: 1,
|
|
1160
|
+
id: incidentId(draft.rule, draft.fingerprint),
|
|
1161
|
+
rule: draft.rule,
|
|
1162
|
+
where: draft.where,
|
|
1163
|
+
fingerprint: draft.fingerprint,
|
|
1164
|
+
source: draft.source,
|
|
1165
|
+
state: draft.state,
|
|
1166
|
+
createdAt: now,
|
|
1167
|
+
updatedAt: now,
|
|
1168
|
+
...draft.firstCommit === void 0 ? {} : { firstCommit: draft.firstCommit },
|
|
1169
|
+
history: [{ state: draft.state, at: now, by }]
|
|
1170
|
+
});
|
|
1171
|
+
var recordIncident = (runtime, root, draft, by) => {
|
|
1172
|
+
const now = runtime.now();
|
|
1173
|
+
const existing = readIncident(runtime, root, incidentId(draft.rule, draft.fingerprint));
|
|
1174
|
+
if (existing.ok && existing.value.state !== "fechado") return existing;
|
|
1175
|
+
const incident = existing.ok ? {
|
|
1176
|
+
...existing.value,
|
|
1177
|
+
state: draft.state,
|
|
1178
|
+
updatedAt: now,
|
|
1179
|
+
history: [...existing.value.history, { state: draft.state, at: now, by }]
|
|
1180
|
+
} : created(draft, now, by);
|
|
1181
|
+
const saved = saveIncident(runtime, root, incident);
|
|
1182
|
+
return saved.ok ? accept(incident) : reject(saved.error);
|
|
1183
|
+
};
|
|
1184
|
+
var ALLOWED_FROM = {
|
|
1185
|
+
aberto: [],
|
|
1186
|
+
"exposicao-local": [],
|
|
1187
|
+
rotacionado: ["aberto", "exposicao-local"],
|
|
1188
|
+
fechado: ["rotacionado"]
|
|
1189
|
+
};
|
|
1190
|
+
var canMove = (from, to) => ALLOWED_FROM[to].includes(from);
|
|
1191
|
+
var moveIncident = (incident, to, now, by) => ({
|
|
1192
|
+
...incident,
|
|
1193
|
+
state: to,
|
|
1194
|
+
updatedAt: now,
|
|
1195
|
+
history: [...incident.history, { state: to, at: now, by }]
|
|
1196
|
+
});
|
|
1197
|
+
var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
|
|
1198
|
+
|
|
1199
|
+
// src/secrets/guards/describe.ts
|
|
1200
|
+
var MAX_LISTED = 5;
|
|
1201
|
+
var describeFindings = (t, items) => {
|
|
1202
|
+
const listed2 = items.slice(0, MAX_LISTED).map(({ ruleId, where }) => t("secrets.found_item", { rule: ruleId, where }));
|
|
1203
|
+
const rest = items.length - listed2.length;
|
|
1204
|
+
return rest > 0 ? `${listed2.join("; ")}; ${t("secrets.found_more", { count: rest })}` : listed2.join("; ");
|
|
1205
|
+
};
|
|
1206
|
+
var lineWhere = (t, finding) => t("secrets.where.line", { line: finding.line, column: finding.column });
|
|
1207
|
+
|
|
1208
|
+
// src/secrets/guards/tool-input.ts
|
|
1209
|
+
var toolName = (payload) => optionalString(payload.raw, "tool_name") ?? "";
|
|
1210
|
+
var toolInput = (payload) => {
|
|
1211
|
+
const input = payload.raw["tool_input"];
|
|
1212
|
+
return isRecord(input) ? input : {};
|
|
1213
|
+
};
|
|
1214
|
+
var editTexts = (input, file) => {
|
|
1215
|
+
const edits = input["edits"];
|
|
1216
|
+
if (!Array.isArray(edits)) return [];
|
|
1217
|
+
return edits.flatMap((edit) => {
|
|
1218
|
+
if (!isRecord(edit)) return [];
|
|
1219
|
+
const text2 = optionalString(edit, "new_string");
|
|
1220
|
+
return text2 === void 0 ? [] : [{ file, text: text2, whole: false }];
|
|
1221
|
+
});
|
|
1222
|
+
};
|
|
1223
|
+
var writtenTexts = (tool, input) => {
|
|
1224
|
+
const file = optionalString(input, "file_path") ?? optionalString(input, "notebook_path") ?? "";
|
|
1225
|
+
const single = (key, whole) => {
|
|
1226
|
+
const text2 = optionalString(input, key);
|
|
1227
|
+
return text2 === void 0 ? [] : [{ file, text: text2, whole }];
|
|
1228
|
+
};
|
|
1229
|
+
switch (tool) {
|
|
1230
|
+
case "Write":
|
|
1231
|
+
return single("content", true);
|
|
1232
|
+
case "Edit":
|
|
1233
|
+
return single("new_string", false);
|
|
1234
|
+
case "MultiEdit":
|
|
1235
|
+
return editTexts(input, file);
|
|
1236
|
+
case "NotebookEdit":
|
|
1237
|
+
return single("new_source", false);
|
|
1238
|
+
default:
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
var stringLeaves = (value, out = []) => {
|
|
1243
|
+
if (typeof value === "string") out.push(value);
|
|
1244
|
+
else if (Array.isArray(value)) for (const item of value) stringLeaves(item, out);
|
|
1245
|
+
else if (isRecord(value)) for (const item of Object.values(value)) stringLeaves(item, out);
|
|
1246
|
+
return out;
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
// src/secrets/guards/exposure.ts
|
|
1250
|
+
var HOOK_ACTOR = "finitemonkey";
|
|
1251
|
+
var liveFindings = (input, texts) => {
|
|
1252
|
+
const baseline = loadBaseline(input.runtime, input.cwd);
|
|
1253
|
+
const accepted2 = acceptedFingerprints(baseline.ok ? baseline.value : EMPTY_BASELINE, input.runtime.now());
|
|
1254
|
+
return texts.flatMap((text2) => scanText(text2, { rules: CREDENTIAL_RULES })).filter((finding) => !accepted2.has(finding.fingerprint));
|
|
1255
|
+
};
|
|
1256
|
+
var record = (input, findings, draft, where) => {
|
|
1257
|
+
const ids = findings.map((finding) => {
|
|
1258
|
+
const saved = recordIncident(
|
|
1259
|
+
input.runtime,
|
|
1260
|
+
input.cwd,
|
|
1261
|
+
{ ...draft, rule: finding.ruleId, fingerprint: finding.fingerprint, where: `${where}:${String(finding.line)}` },
|
|
1262
|
+
HOOK_ACTOR
|
|
1263
|
+
);
|
|
1264
|
+
return saved.ok ? saved.value.id : input.t("secrets.incident.unsaved", { detail: saved.error });
|
|
1265
|
+
});
|
|
1266
|
+
return [...new Set(ids)].join(", ");
|
|
1267
|
+
};
|
|
1268
|
+
var secretsUserPromptSubmit = (input) => {
|
|
1269
|
+
const prompt = optionalString(input.payload.raw, "prompt") ?? "";
|
|
1270
|
+
const findings = liveFindings(input, [prompt]);
|
|
1271
|
+
if (findings.length === 0) return { kind: "allow" };
|
|
1272
|
+
const incident = record(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
|
|
1273
|
+
const items = findings.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
|
|
1274
|
+
return {
|
|
1275
|
+
kind: "block",
|
|
1276
|
+
reason: input.t("secrets.block.prompt", { found: describeFindings(input.t, items), incident })
|
|
1277
|
+
};
|
|
1278
|
+
};
|
|
1279
|
+
var secretsPostToolUse = (input) => {
|
|
1280
|
+
const findings = liveFindings(input, stringLeaves(input.payload.raw["tool_response"]));
|
|
1281
|
+
if (findings.length === 0) return { kind: "allow" };
|
|
1282
|
+
const tool = toolName(input.payload) || "tool";
|
|
1283
|
+
const incident = record(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
|
|
1284
|
+
const items = findings.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
|
|
1285
|
+
const context = input.t("secrets.post.context", { tool, found: describeFindings(input.t, items), incident });
|
|
1286
|
+
return {
|
|
1287
|
+
kind: "allow",
|
|
1288
|
+
output: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: context } }
|
|
1289
|
+
};
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// src/secrets/guards/pre-tool.ts
|
|
1293
|
+
import { relative as relative3 } from "node:path";
|
|
1294
|
+
|
|
1295
|
+
// src/secrets/scan.ts
|
|
1296
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
1297
|
+
|
|
1298
|
+
// src/secrets/located.ts
|
|
1299
|
+
var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
|
|
1300
|
+
var locate = (finding, rawFile, line, commit) => {
|
|
1301
|
+
const file = toPosixPath(rawFile);
|
|
1302
|
+
return {
|
|
1303
|
+
id: findingId(finding.ruleId, file, finding.fingerprint),
|
|
1304
|
+
ruleId: finding.ruleId,
|
|
1305
|
+
severity: finding.severity,
|
|
1306
|
+
file,
|
|
1307
|
+
line,
|
|
1308
|
+
column: finding.column,
|
|
1309
|
+
fingerprint: finding.fingerprint,
|
|
1310
|
+
tail: finding.tail,
|
|
1311
|
+
...commit === void 0 ? {} : { commit }
|
|
1312
|
+
};
|
|
1313
|
+
};
|
|
1314
|
+
var whereOf = (finding) => `${finding.file}:${String(finding.line)}`;
|
|
1315
|
+
|
|
1316
|
+
// src/secrets/diff.ts
|
|
1317
|
+
var COMMIT = /^commit ([0-9a-f]{7,64})\b/;
|
|
1318
|
+
var HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
1319
|
+
var DEV_NULL = "/dev/null";
|
|
1320
|
+
var fresh = () => ({ file: void 0, inHunk: false, next: 0, text: [], lines: [] });
|
|
1321
|
+
var targetOf = (line) => {
|
|
1322
|
+
const path = line.slice(4).replace(/\t.*$/, "");
|
|
1323
|
+
if (path === DEV_NULL) return void 0;
|
|
1324
|
+
return path.startsWith("b/") ? path.slice(2) : path;
|
|
1325
|
+
};
|
|
1326
|
+
var collect = (chunk, commit, found) => {
|
|
1327
|
+
const { file } = chunk;
|
|
1328
|
+
if (file === void 0 || chunk.text.length === 0) return;
|
|
1329
|
+
for (const finding of scanText(chunk.text.join("\n"), { path: file })) {
|
|
1330
|
+
const located = locate(finding, file, chunk.lines[finding.line - 1] ?? finding.line, commit);
|
|
1331
|
+
found.set(located.id, located);
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
var addLine = (chunk, line) => {
|
|
1335
|
+
const hunk = HUNK.exec(line);
|
|
1336
|
+
if (hunk !== null) {
|
|
1337
|
+
chunk.inHunk = true;
|
|
1338
|
+
chunk.next = Number(hunk[1]);
|
|
1339
|
+
} else if (!chunk.inHunk && line.startsWith("+++ ")) {
|
|
1340
|
+
chunk.file = targetOf(line);
|
|
1341
|
+
} else if (chunk.inHunk && line.startsWith("+")) {
|
|
1342
|
+
chunk.text.push(line.slice(1));
|
|
1343
|
+
chunk.lines.push(chunk.next);
|
|
1344
|
+
chunk.next += 1;
|
|
1345
|
+
} else if (chunk.inHunk && line.startsWith(" ")) {
|
|
1346
|
+
chunk.next += 1;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
var diffScanner = () => {
|
|
1350
|
+
const found = /* @__PURE__ */ new Map();
|
|
1351
|
+
let commit;
|
|
1352
|
+
let chunk = fresh();
|
|
1353
|
+
const flush2 = () => {
|
|
1354
|
+
collect(chunk, commit, found);
|
|
1355
|
+
chunk = fresh();
|
|
1356
|
+
};
|
|
1357
|
+
const header = (line) => {
|
|
1358
|
+
const match = COMMIT.exec(line);
|
|
1359
|
+
if (match !== null) {
|
|
1360
|
+
flush2();
|
|
1361
|
+
commit = match[1];
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
if (line.startsWith("diff --git ")) {
|
|
1365
|
+
flush2();
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
return false;
|
|
1369
|
+
};
|
|
1370
|
+
return {
|
|
1371
|
+
push: (raw) => {
|
|
1372
|
+
const line = withoutCarriageReturn(raw);
|
|
1373
|
+
if (!header(line)) addLine(chunk, line);
|
|
1374
|
+
},
|
|
1375
|
+
end: () => {
|
|
1376
|
+
flush2();
|
|
1377
|
+
return [...found.values()];
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
// src/secrets/scan.ts
|
|
1383
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1384
|
+
var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
|
|
1385
|
+
var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
|
|
1386
|
+
var binary = (text2) => text2.slice(0, 8e3).includes("\0");
|
|
1387
|
+
var listed = (runtime, root, target) => {
|
|
1388
|
+
const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target], root);
|
|
1389
|
+
if (git.ok)
|
|
1390
|
+
return accept(
|
|
1391
|
+
git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
|
|
1392
|
+
);
|
|
1393
|
+
const base = toAbsolutePath(target, root);
|
|
1394
|
+
const walked = runtime.walkFiles(base);
|
|
1395
|
+
if (!walked.ok) return walked;
|
|
1396
|
+
return accept(walked.value.map((name) => toPosixPath(relative2(root, join4(base, toPosixPath(name))))));
|
|
1397
|
+
};
|
|
1398
|
+
var scanFile = (runtime, root, file) => {
|
|
1399
|
+
const text2 = runtime.readText(toAbsolutePath(file, root));
|
|
1400
|
+
if (!text2.ok || text2.value.length > MAX_FILE_BYTES || binary(text2.value)) return [];
|
|
1401
|
+
return scanText(text2.value, { path: file }).map((finding) => locate(finding, file, finding.line));
|
|
1402
|
+
};
|
|
1403
|
+
var scanPath = (runtime, root, target) => {
|
|
1404
|
+
const files = listed(runtime, root, target);
|
|
1405
|
+
if (!files.ok) return files;
|
|
1406
|
+
return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
|
|
1407
|
+
};
|
|
1408
|
+
var diffFindings = (diff) => {
|
|
1409
|
+
const scanner = diffScanner();
|
|
1410
|
+
for (const line of diff.split("\n")) scanner.push(line);
|
|
1411
|
+
return scanner.end().filter((finding) => !SKIPPED.test(finding.file));
|
|
1412
|
+
};
|
|
1413
|
+
var scanStaged = (runtime, root, includeUnstaged = false) => {
|
|
1414
|
+
const args = includeUnstaged ? ["diff", "HEAD", ...DIFF_FLAGS] : ["diff", "--cached", ...DIFF_FLAGS];
|
|
1415
|
+
const git = runtime.git(args, root);
|
|
1416
|
+
if (!git.ok) return reject(git.found ? "git diff failed" : "git not found");
|
|
1417
|
+
return accept(diffFindings(git.stdout));
|
|
1418
|
+
};
|
|
1419
|
+
var scanHistory = async (runtime, root) => {
|
|
1420
|
+
const scanner = diffScanner();
|
|
1421
|
+
const args = ["log", "-p", "--all", "--format=commit %H", ...DIFF_FLAGS];
|
|
1422
|
+
const git = await runtime.gitLines(args, root, scanner.push);
|
|
1423
|
+
if (!git.ok) return reject(git.found ? "git log failed" : "git not found");
|
|
1424
|
+
return accept(scanner.end().filter((finding) => !SKIPPED.test(finding.file)));
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
// src/secrets/guards/paths.ts
|
|
1428
|
+
var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
|
|
1429
|
+
var ENV_FILE = /^\.env(?:\..+)?$/i;
|
|
1430
|
+
var KEY_FILE = /\.(?:pem|key|p12|pfx)$/i;
|
|
1431
|
+
var SSH_KEY = /^id_(?:rsa|dsa|ecdsa|ed25519)(?:_[A-Za-z0-9_-]+)?$/;
|
|
1432
|
+
var basename = (path) => path.split(/[\\/]/).pop() ?? path;
|
|
1433
|
+
var isSensitivePath = (path) => {
|
|
1434
|
+
const name = basename(path.replace(/^["']|["']$/g, ""));
|
|
1435
|
+
if (ENV_FILE.test(name)) return !SAFE_ENV.test(name);
|
|
1436
|
+
return KEY_FILE.test(name) || SSH_KEY.test(name);
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/secrets/guards/bash-rules.ts
|
|
1440
|
+
var READERS = /* @__PURE__ */ new Set([
|
|
1441
|
+
"cat",
|
|
1442
|
+
"bat",
|
|
1443
|
+
"batcat",
|
|
1444
|
+
"less",
|
|
1445
|
+
"more",
|
|
1446
|
+
"head",
|
|
1447
|
+
"tail",
|
|
1448
|
+
"nl",
|
|
1449
|
+
"tac",
|
|
1450
|
+
"strings",
|
|
1451
|
+
"xxd",
|
|
1452
|
+
"hexdump",
|
|
1453
|
+
"od",
|
|
1454
|
+
"grep",
|
|
1455
|
+
"egrep",
|
|
1456
|
+
"fgrep",
|
|
1457
|
+
"rg",
|
|
1458
|
+
"ag",
|
|
1459
|
+
"ack",
|
|
1460
|
+
"awk",
|
|
1461
|
+
"gawk",
|
|
1462
|
+
"sed",
|
|
1463
|
+
"cut",
|
|
1464
|
+
"sort",
|
|
1465
|
+
"uniq",
|
|
1466
|
+
"jq",
|
|
1467
|
+
"yq",
|
|
1468
|
+
"base64",
|
|
1469
|
+
"type",
|
|
1470
|
+
"get-content",
|
|
1471
|
+
"gc",
|
|
1472
|
+
"select-string",
|
|
1473
|
+
"sls",
|
|
1474
|
+
"diff",
|
|
1475
|
+
"vim",
|
|
1476
|
+
"vi",
|
|
1477
|
+
"nano",
|
|
1478
|
+
"view",
|
|
1479
|
+
"dotenv"
|
|
1480
|
+
]);
|
|
1481
|
+
var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
|
|
1482
|
+
var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
|
|
1483
|
+
var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
|
|
1484
|
+
var onlyFlags = (args) => args.every((arg) => arg.startsWith("-"));
|
|
1485
|
+
var readTarget = (segment) => {
|
|
1486
|
+
const redirected = segment.redirectedFrom.find(isSensitivePath);
|
|
1487
|
+
if (redirected !== void 0) return redirected;
|
|
1488
|
+
const program = segment.program.toLowerCase();
|
|
1489
|
+
if (!READERS.has(program)) return void 0;
|
|
1490
|
+
if (program === "sed" && segment.args.some((arg) => arg.startsWith("-i"))) return void 0;
|
|
1491
|
+
return segment.args.find((arg) => !arg.startsWith("-") && isSensitivePath(arg));
|
|
1492
|
+
};
|
|
1493
|
+
var dumpsEnv = (segment) => {
|
|
1494
|
+
const program = segment.program.toLowerCase();
|
|
1495
|
+
if (program === "env" || program === "printenv") {
|
|
1496
|
+
if (onlyFlags(segment.args)) return true;
|
|
1497
|
+
return program === "printenv" && segment.args.some((arg) => SECRET_NAME.test(arg));
|
|
1498
|
+
}
|
|
1499
|
+
if (program === "set" || program === "export" || program === "declare") {
|
|
1500
|
+
return segment.args.length === 0 || segment.args.every((arg) => arg === "-p" || arg === "-x");
|
|
1501
|
+
}
|
|
1502
|
+
return LISTS_ENV.has(program) && segment.args.some((arg) => /^env:/i.test(arg));
|
|
1503
|
+
};
|
|
1504
|
+
var bashFindings = (command, segments) => {
|
|
1505
|
+
const person = PERSON_ONLY.exec(command);
|
|
1506
|
+
if (person !== null) return [{ kind: "person-only", command: person[0] }];
|
|
1507
|
+
return segments.flatMap((segment) => {
|
|
1508
|
+
const target = readTarget(segment);
|
|
1509
|
+
if (target !== void 0) return [{ kind: "read", target }];
|
|
1510
|
+
if (dumpsEnv(segment)) return [{ kind: "env-dump", command: [segment.program, ...segment.args].join(" ") }];
|
|
1511
|
+
return [];
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
var GIT_GLOBALS_WITH_VALUE = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree"]);
|
|
1515
|
+
var commitIntent = (segments) => {
|
|
1516
|
+
for (const segment of segments) {
|
|
1517
|
+
if (segment.program !== "git") continue;
|
|
1518
|
+
let index = 0;
|
|
1519
|
+
while (index < segment.args.length && (segment.args[index] ?? "").startsWith("-")) {
|
|
1520
|
+
index += GIT_GLOBALS_WITH_VALUE.has(segment.args[index] ?? "") ? 2 : 1;
|
|
1521
|
+
}
|
|
1522
|
+
if (segment.args[index] !== "commit") continue;
|
|
1523
|
+
const flags = segment.args.slice(index + 1);
|
|
1524
|
+
const all = flags.some((flag2) => flag2 === "--all" || /^-[A-Za-z]*a[A-Za-z]*$/.test(flag2));
|
|
1525
|
+
return { includeUnstaged: all };
|
|
1526
|
+
}
|
|
1527
|
+
return void 0;
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
// src/secrets/guards/shell.ts
|
|
1531
|
+
var SEPARATORS = /\|\||&&|[|;&\n]/;
|
|
1532
|
+
var TOKEN = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
|
|
1533
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
1534
|
+
var OUTPUT_REDIRECT = /^(?:\d|&)?>{1,2}\|?$/;
|
|
1535
|
+
var OUTPUT_ATTACHED = /^(?:\d|&)?>{1,2}\|?\S/;
|
|
1536
|
+
var WRAPPERS = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "nice", "nohup", "builtin"]);
|
|
1537
|
+
var tokensOf = (text2) => Array.from(text2.matchAll(TOKEN), (match) => match[1] ?? match[2] ?? match[3] ?? "");
|
|
1538
|
+
var splitRedirects = (tokens) => {
|
|
1539
|
+
const words = [];
|
|
1540
|
+
const inputs = [];
|
|
1541
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1542
|
+
const token = tokens[index] ?? "";
|
|
1543
|
+
if (OUTPUT_REDIRECT.test(token)) {
|
|
1544
|
+
index += 1;
|
|
1545
|
+
} else if (OUTPUT_ATTACHED.test(token)) {
|
|
1546
|
+
continue;
|
|
1547
|
+
} else if (token === "<") {
|
|
1548
|
+
inputs.push(tokens[index + 1] ?? "");
|
|
1549
|
+
index += 1;
|
|
1550
|
+
} else if (token.startsWith("<") && !token.startsWith("<<")) {
|
|
1551
|
+
inputs.push(token.slice(1));
|
|
1552
|
+
} else {
|
|
1553
|
+
words.push(token);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return { words, inputs };
|
|
1557
|
+
};
|
|
1558
|
+
var segmentOf = (text2) => {
|
|
1559
|
+
const { words, inputs } = splitRedirects(tokensOf(text2));
|
|
1560
|
+
let index = 0;
|
|
1561
|
+
while (index < words.length && (ASSIGNMENT.test(words[index] ?? "") || WRAPPERS.has(words[index] ?? ""))) index += 1;
|
|
1562
|
+
const program = words[index];
|
|
1563
|
+
if (program === void 0) return inputs.length > 0 ? { program: "", args: [], redirectedFrom: inputs } : void 0;
|
|
1564
|
+
return { program: program.split(/[\\/]/).pop() ?? program, args: words.slice(index + 1), redirectedFrom: inputs };
|
|
1565
|
+
};
|
|
1566
|
+
var segmentsOf = (command) => command.split(SEPARATORS).flatMap((part) => {
|
|
1567
|
+
const segment = segmentOf(part);
|
|
1568
|
+
return segment === void 0 ? [] : [segment];
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// src/secrets/guards/pre-tool.ts
|
|
1572
|
+
var ALLOW = { kind: "allow" };
|
|
1573
|
+
var relativeFile = (cwd, file) => {
|
|
1574
|
+
if (file === "") return file;
|
|
1575
|
+
const rel = relative3(cwd, file).split("\\").join("/");
|
|
1576
|
+
return rel.startsWith("..") || rel === "" ? file : rel;
|
|
1577
|
+
};
|
|
1578
|
+
var writeDecision = (input, tool) => {
|
|
1579
|
+
const { t } = input;
|
|
1580
|
+
for (const written of writtenTexts(tool, toolInput(input.payload))) {
|
|
1581
|
+
const file = relativeFile(input.cwd, written.file);
|
|
1582
|
+
const found = scanText(written.text, { path: file });
|
|
1583
|
+
if (found.length === 0) continue;
|
|
1584
|
+
const items = found.map((finding) => ({
|
|
1585
|
+
ruleId: finding.ruleId,
|
|
1586
|
+
where: written.whole ? t("secrets.where.file", { file, line: finding.line, column: finding.column }) : t("secrets.where.new_text", { file, line: finding.line })
|
|
1587
|
+
}));
|
|
1588
|
+
return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
|
|
1589
|
+
}
|
|
1590
|
+
return ALLOW;
|
|
1591
|
+
};
|
|
1592
|
+
var bashMessage = (t, finding) => {
|
|
1593
|
+
switch (finding.kind) {
|
|
1594
|
+
case "read":
|
|
1595
|
+
return t("secrets.block.read", { target: finding.target });
|
|
1596
|
+
case "env-dump":
|
|
1597
|
+
return t("secrets.block.env_dump", { command: finding.command });
|
|
1598
|
+
case "person-only":
|
|
1599
|
+
return t("secrets.block.person_only", { command: finding.command });
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
var commitDecision = (input, includeUnstaged) => {
|
|
1603
|
+
const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
|
|
1604
|
+
if (!staged.ok || staged.value.length === 0) return ALLOW;
|
|
1605
|
+
const items = staged.value.map((finding) => ({
|
|
1606
|
+
ruleId: finding.ruleId,
|
|
1607
|
+
where: input.t("secrets.where.file", { file: finding.file, line: finding.line, column: finding.column })
|
|
1608
|
+
}));
|
|
1609
|
+
return { kind: "block", reason: input.t("secrets.block.commit", { found: describeFindings(input.t, items) }) };
|
|
1610
|
+
};
|
|
1611
|
+
var bashDecision = (input) => {
|
|
1612
|
+
const command = optionalString(toolInput(input.payload), "command") ?? "";
|
|
1613
|
+
if (command === "") return ALLOW;
|
|
1614
|
+
const segments = segmentsOf(command);
|
|
1615
|
+
const [first] = bashFindings(command, segments);
|
|
1616
|
+
if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
|
|
1617
|
+
const found = scanText(command);
|
|
1618
|
+
if (found.length > 0) {
|
|
1619
|
+
const items = found.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
|
|
1620
|
+
return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
|
|
1621
|
+
}
|
|
1622
|
+
const commit = commitIntent(segments);
|
|
1623
|
+
return commit === void 0 ? ALLOW : commitDecision(input, commit.includeUnstaged);
|
|
1624
|
+
};
|
|
1625
|
+
var readDecision = (input) => {
|
|
1626
|
+
const inputs = toolInput(input.payload);
|
|
1627
|
+
const target = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
|
|
1628
|
+
if (target === "" || !isSensitivePath(target)) return ALLOW;
|
|
1629
|
+
return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target) }) };
|
|
1630
|
+
};
|
|
1631
|
+
var WRITERS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
1632
|
+
var SHELLS = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
|
|
1633
|
+
var READS = /* @__PURE__ */ new Set(["Read", "Grep"]);
|
|
1634
|
+
var secretsPreToolUse = (input) => {
|
|
1635
|
+
const tool = toolName(input.payload);
|
|
1636
|
+
if (WRITERS.has(tool)) return writeDecision(input, tool);
|
|
1637
|
+
if (SHELLS.has(tool)) return bashDecision(input);
|
|
1638
|
+
if (READS.has(tool)) return readDecision(input);
|
|
1639
|
+
return ALLOW;
|
|
1640
|
+
};
|
|
1641
|
+
|
|
507
1642
|
// src/hooks/handlers.ts
|
|
508
1643
|
var allow = () => ({ kind: "allow" });
|
|
509
|
-
var DEFAULT_HANDLERS = {
|
|
1644
|
+
var DEFAULT_HANDLERS = {
|
|
1645
|
+
PreToolUse: [secretsPreToolUse],
|
|
1646
|
+
UserPromptSubmit: [secretsUserPromptSubmit],
|
|
1647
|
+
PostToolUse: [secretsPostToolUse]
|
|
1648
|
+
};
|
|
510
1649
|
|
|
511
1650
|
// src/hooks/payload.ts
|
|
512
1651
|
var EMPTY_PAYLOAD = {
|
|
@@ -516,9 +1655,9 @@ var EMPTY_PAYLOAD = {
|
|
|
516
1655
|
stopHookActive: false,
|
|
517
1656
|
raw: {}
|
|
518
1657
|
};
|
|
519
|
-
var parseHookPayload = (event,
|
|
520
|
-
if (interactive &&
|
|
521
|
-
const parsed = parseJsonObject(
|
|
1658
|
+
var parseHookPayload = (event, text2, interactive) => {
|
|
1659
|
+
if (interactive && text2.trim() === "") return accept(EMPTY_PAYLOAD);
|
|
1660
|
+
const parsed = parseJsonObject(text2);
|
|
522
1661
|
if (!parsed.ok) return reject(`stdin: ${parsed.error}`);
|
|
523
1662
|
const payload = {
|
|
524
1663
|
hookEventName: optionalString(parsed.value, "hook_event_name"),
|
|
@@ -574,7 +1713,7 @@ var withDeadline = (work, ms, startTimer) => new Promise((resolve2, reject2) =>
|
|
|
574
1713
|
});
|
|
575
1714
|
|
|
576
1715
|
// src/hooks/runner.ts
|
|
577
|
-
var
|
|
1716
|
+
var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
|
|
578
1717
|
var decide = async (input, handlers) => {
|
|
579
1718
|
let output = {};
|
|
580
1719
|
for (const handler of handlers[input.event] ?? [allow]) {
|
|
@@ -585,14 +1724,21 @@ var decide = async (input, handlers) => {
|
|
|
585
1724
|
return { kind: "allow", output };
|
|
586
1725
|
};
|
|
587
1726
|
var evaluate = async (event, options) => {
|
|
588
|
-
const
|
|
589
|
-
const parsed = parseHookPayload(event,
|
|
1727
|
+
const text2 = await readAll(options.stdin);
|
|
1728
|
+
const parsed = parseHookPayload(event, text2, options.stdin.isTTY === true);
|
|
590
1729
|
if (!parsed.ok) return { kind: "failed", payload: void 0, detail: parsed.error };
|
|
591
1730
|
const payload = parsed.value;
|
|
592
1731
|
try {
|
|
593
|
-
return {
|
|
1732
|
+
return {
|
|
1733
|
+
kind: "decided",
|
|
1734
|
+
payload,
|
|
1735
|
+
decision: await decide(
|
|
1736
|
+
{ event, payload, cwd: options.cwd, runtime: options.runtime, t: options.t },
|
|
1737
|
+
options.handlers
|
|
1738
|
+
)
|
|
1739
|
+
};
|
|
594
1740
|
} catch (error) {
|
|
595
|
-
return { kind: "failed", payload, detail:
|
|
1741
|
+
return { kind: "failed", payload, detail: messageOf2(error) };
|
|
596
1742
|
}
|
|
597
1743
|
};
|
|
598
1744
|
var ALLOW_STDOUT = `${JSON.stringify({ continue: true })}
|
|
@@ -643,7 +1789,7 @@ var runHook = async (event, options) => {
|
|
|
643
1789
|
options.t
|
|
644
1790
|
);
|
|
645
1791
|
} catch (error) {
|
|
646
|
-
return failed(event,
|
|
1792
|
+
return failed(event, messageOf2(error), options.t);
|
|
647
1793
|
}
|
|
648
1794
|
};
|
|
649
1795
|
|
|
@@ -670,7 +1816,8 @@ var hookCommand = {
|
|
|
670
1816
|
handlers: DEFAULT_HANDLERS,
|
|
671
1817
|
cwd: ctx.cwd,
|
|
672
1818
|
t: ctx.t,
|
|
673
|
-
startTimer: ctx.runtime.startTimer
|
|
1819
|
+
startTimer: ctx.runtime.startTimer,
|
|
1820
|
+
runtime: ctx.runtime
|
|
674
1821
|
});
|
|
675
1822
|
const data = { event, verdict: outcome.verdict };
|
|
676
1823
|
const output = { stdout: outcome.stdout, stderr: outcome.stderr };
|
|
@@ -687,7 +1834,7 @@ var hookCommand = {
|
|
|
687
1834
|
};
|
|
688
1835
|
|
|
689
1836
|
// src/commands/i18n-check.ts
|
|
690
|
-
import { join } from "node:path";
|
|
1837
|
+
import { join as join5 } from "node:path";
|
|
691
1838
|
|
|
692
1839
|
// src/i18n/en.json
|
|
693
1840
|
var en_default = {
|
|
@@ -772,7 +1919,52 @@ var en_default = {
|
|
|
772
1919
|
"exit.internal": "5 internal error: Finite Monkey itself failed; please report it",
|
|
773
1920
|
"help.exit_codes": "Exit codes",
|
|
774
1921
|
"hook.stop_released": "the {event} hook would block again ({detail}), but Claude Code is already continuing from a stop hook, so it lets the session stop. Run `finitemonkey doctor`",
|
|
775
|
-
"error.needs_confirmation": '"{command}" needs your decision before it changes anything: {question}'
|
|
1922
|
+
"error.needs_confirmation": '"{command}" needs your decision before it changes anything: {question}',
|
|
1923
|
+
"secrets.command.scan": "Find secrets in files, the staged changes or the git history (local only)",
|
|
1924
|
+
"secrets.command.accept": "Accept one finding into the baseline, with a reason and an expiry (the person only)",
|
|
1925
|
+
"secrets.command.incident": "List secret incidents, mark them rotated or closed, and show the history purge steps",
|
|
1926
|
+
"secrets.found_item": "{rule} at {where}",
|
|
1927
|
+
"secrets.found_more": "and {count} more",
|
|
1928
|
+
"secrets.where.line": "line {line}, column {column}",
|
|
1929
|
+
"secrets.where.file": "{file}:{line}:{column}",
|
|
1930
|
+
"secrets.where.new_text": "{file} (line {line} of the new text)",
|
|
1931
|
+
"secrets.block.write": "blocked: the new text for {file} has a credential ({found}). Nothing was written. Keep the value in .env (outside git) and read it by its variable name. If the key is real, treat it as exposed and rotate it.",
|
|
1932
|
+
"secrets.block.bash": "blocked: this command has a credential ({found}) and did not run. Read the value from an environment variable instead of writing it in the command. If the key is real, treat it as exposed and rotate it.",
|
|
1933
|
+
"secrets.block.commit": "blocked: the staged changes have a credential ({found}), so the commit did not run. Remove it from the staged files, keep it in .env (outside git) and rotate it if it is real.",
|
|
1934
|
+
"secrets.block.read": "blocked: {target} holds secrets, and reading it would put their values in the conversation. Ask the person for the variable names, or read .env.example.",
|
|
1935
|
+
"secrets.block.env_dump": 'blocked: `{command}` prints every environment variable, secrets included. Print only the variable you need by name, or check that it exists with `test -n "$NAME"`.',
|
|
1936
|
+
"secrets.block.person_only": "blocked: `{command}` is a decision for the person, not the model. Ask the person to run it in their own terminal.",
|
|
1937
|
+
"secrets.block.prompt": "your message was not sent to the model: it has a credential ({found}). If this key was pasted anywhere else, treat it as exposed and rotate it. To use it, put it in .env and tell me only the variable name. Never paste keys into the chat. Incident: {incident}.",
|
|
1938
|
+
"secrets.post.context": "Finite Monkey: the {tool} output contained a credential ({found}). Incident {incident} is open; rotating the key is the person's decision. Finite Monkey output never repeats the value.",
|
|
1939
|
+
"secrets.incident.unsaved": "not recorded ({detail})",
|
|
1940
|
+
"secrets.scan.title": "Secrets scan ({mode}): {total} found, {new} new, {known} known, {expired} expired, {fixed} fixed",
|
|
1941
|
+
"secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
|
|
1942
|
+
"secrets.scan.clean": "No secrets found. The monkey keeps its bananas to itself.",
|
|
1943
|
+
"secrets.accept.done": "Accepted {id} ({rule} at {where}) as {state} until {expires}, by {owner}.",
|
|
1944
|
+
"secrets.action.accept": "accept a finding",
|
|
1945
|
+
"secrets.action.rotated": "mark an incident as rotated",
|
|
1946
|
+
"secrets.action.close": "close an incident",
|
|
1947
|
+
"secrets.incident.title": "{count} incident(s)",
|
|
1948
|
+
"secrets.incident.item": " {id} {state} {rule} {where} since {created}",
|
|
1949
|
+
"secrets.incident.none": "No secret incidents.",
|
|
1950
|
+
"secrets.incident.marked": "Incident {id} is now {state}.",
|
|
1951
|
+
"secrets.purge.title": "History purge for {id} ({where}, first commit {commit}). You run these steps; Finite Monkey never runs them:",
|
|
1952
|
+
"secrets.purge.step1": "1. Confirm the old key is revoked at the provider and the new one works in every consumer.",
|
|
1953
|
+
"secrets.purge.step2": "2. Outside the repository, write a replacements file with one line per leaked value: <value>==>REMOVED. Copy the value from your own records; Finite Monkey never stores or prints it.",
|
|
1954
|
+
"secrets.purge.step3": "3. In a fresh mirror clone, run: git filter-repo --replace-text <replacements file>",
|
|
1955
|
+
"secrets.purge.step4": "4. Relax branch protection yourself, push the rewritten refs, restore the protection and tell everyone with a clone to clone again.",
|
|
1956
|
+
"secrets.purge.step5": "5. Close the incident: finitemonkey secrets incident close {id}",
|
|
1957
|
+
"secrets.error.found": "{count} secret finding(s) outside the baseline or with an expired acceptance",
|
|
1958
|
+
"secrets.error.one_mode": "Choose only one of --staged, --history and --path",
|
|
1959
|
+
"secrets.error.git_unavailable": "The scan could not read git ({detail})",
|
|
1960
|
+
"secrets.error.baseline_invalid": "The baseline {file} is invalid: {detail}",
|
|
1961
|
+
"secrets.error.person_only": "Only the person can {action}, and this does not look like their terminal (no TTY, or running inside Claude Code)",
|
|
1962
|
+
"secrets.error.reason_required": "--reason is required: say why this finding is acceptable",
|
|
1963
|
+
"secrets.error.finding_not_found": "No current finding has the id {id}",
|
|
1964
|
+
"secrets.error.incident_not_found": "No incident has the id {id}",
|
|
1965
|
+
"secrets.error.not_rotated": "Incident {id} is {state}: rotate the key at the provider first, then mark it rotated",
|
|
1966
|
+
"secrets.error.bad_transition": "Incident {id} cannot go from {from} to {to}",
|
|
1967
|
+
"secrets.error.write_failed": "Could not write {file}: {detail}"
|
|
776
1968
|
};
|
|
777
1969
|
|
|
778
1970
|
// src/i18n/es.json
|
|
@@ -858,7 +2050,52 @@ var es_default = {
|
|
|
858
2050
|
"exit.internal": "5 error interno: el propio Finite Monkey falló; por favor, repórtalo",
|
|
859
2051
|
"help.exit_codes": "Códigos de salida",
|
|
860
2052
|
"hook.stop_released": "el hook {event} bloquearía otra vez ({detail}), pero Claude Code ya está continuando por un hook de parada, así que deja que la sesión termine. Ejecuta `finitemonkey doctor`",
|
|
861
|
-
"error.needs_confirmation": '"{command}" necesita tu decisión antes de cambiar algo: {question}'
|
|
2053
|
+
"error.needs_confirmation": '"{command}" necesita tu decisión antes de cambiar algo: {question}',
|
|
2054
|
+
"secrets.command.scan": "Busca secretos en los archivos, en los cambios preparados o en el historial de git (solo local)",
|
|
2055
|
+
"secrets.command.accept": "Acepta un hallazgo en el baseline, con motivo y vencimiento (solo la persona)",
|
|
2056
|
+
"secrets.command.incident": "Lista incidentes de secretos, los marca como rotados o cerrados y muestra los pasos para limpiar el historial",
|
|
2057
|
+
"secrets.found_item": "{rule} en {where}",
|
|
2058
|
+
"secrets.found_more": "y {count} más",
|
|
2059
|
+
"secrets.where.line": "línea {line}, columna {column}",
|
|
2060
|
+
"secrets.where.file": "{file}:{line}:{column}",
|
|
2061
|
+
"secrets.where.new_text": "{file} (línea {line} del texto nuevo)",
|
|
2062
|
+
"secrets.block.write": "bloqueado: el texto nuevo de {file} tiene una credencial ({found}). No se escribió nada. Deja el valor en .env (fuera de git) y léelo por el nombre de la variable. Si la clave es real, trátala como expuesta y rótala.",
|
|
2063
|
+
"secrets.block.bash": "bloqueado: este comando tiene una credencial ({found}) y no se ejecutó. Lee el valor de una variable de entorno en lugar de escribirlo en el comando. Si la clave es real, trátala como expuesta y rótala.",
|
|
2064
|
+
"secrets.block.commit": "bloqueado: los cambios preparados tienen una credencial ({found}), así que el commit no se ejecutó. Quítala de los archivos preparados, déjala en .env (fuera de git) y rótala si es real.",
|
|
2065
|
+
"secrets.block.read": "bloqueado: {target} guarda secretos, y leerlo pondría sus valores en la conversación. Pide a la persona los nombres de las variables, o lee .env.example.",
|
|
2066
|
+
"secrets.block.env_dump": 'bloqueado: `{command}` imprime todas las variables de entorno, secretos incluidos. Imprime solo la variable que necesitas por su nombre, o comprueba que existe con `test -n "$NAME"`.',
|
|
2067
|
+
"secrets.block.person_only": "bloqueado: `{command}` es una decisión de la persona, no del modelo. Pide a la persona que lo ejecute en su propia terminal.",
|
|
2068
|
+
"secrets.block.prompt": "tu mensaje no se envió al modelo: tiene una credencial ({found}). Si esta clave se pegó en otro lugar, trátala como expuesta y rótala. Para usarla, ponla en .env y dime solo el nombre de la variable. Nunca pegues claves en el chat. Incidente: {incident}.",
|
|
2069
|
+
"secrets.post.context": "Finite Monkey: la salida de {tool} contenía una credencial ({found}). El incidente {incident} está abierto; rotar la clave es decisión de la persona. La salida de Finite Monkey nunca repite el valor.",
|
|
2070
|
+
"secrets.incident.unsaved": "no registrado ({detail})",
|
|
2071
|
+
"secrets.scan.title": "Búsqueda de secretos ({mode}): {total} hallados, {new} nuevos, {known} conocidos, {expired} vencidos, {fixed} corregidos",
|
|
2072
|
+
"secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
|
|
2073
|
+
"secrets.scan.clean": "No se encontraron secretos. El mono se guarda sus bananas.",
|
|
2074
|
+
"secrets.accept.done": "Aceptado {id} ({rule} en {where}) como {state} hasta {expires}, por {owner}.",
|
|
2075
|
+
"secrets.action.accept": "aceptar un hallazgo",
|
|
2076
|
+
"secrets.action.rotated": "marcar un incidente como rotado",
|
|
2077
|
+
"secrets.action.close": "cerrar un incidente",
|
|
2078
|
+
"secrets.incident.title": "{count} incidente(s)",
|
|
2079
|
+
"secrets.incident.item": " {id} {state} {rule} {where} desde {created}",
|
|
2080
|
+
"secrets.incident.none": "No hay incidentes de secretos.",
|
|
2081
|
+
"secrets.incident.marked": "El incidente {id} ahora está {state}.",
|
|
2082
|
+
"secrets.purge.title": "Limpieza del historial de {id} ({where}, primer commit {commit}). Estos pasos los ejecutas tú; Finite Monkey nunca los ejecuta:",
|
|
2083
|
+
"secrets.purge.step1": "1. Confirma que la clave vieja está revocada en el proveedor y que la nueva funciona en todos los consumidores.",
|
|
2084
|
+
"secrets.purge.step2": "2. Fuera del repositorio, escribe un archivo de reemplazos con una línea por valor filtrado: <valor>==>REMOVED. Copia el valor de tus propios registros; Finite Monkey nunca lo guarda ni lo imprime.",
|
|
2085
|
+
"secrets.purge.step3": "3. En un clon espejo nuevo, ejecuta: git filter-repo --replace-text <archivo de reemplazos>",
|
|
2086
|
+
"secrets.purge.step4": "4. Relaja tú la protección de la rama, envía las refs reescritas, restaura la protección y avisa a quien tenga un clon que vuelva a clonar.",
|
|
2087
|
+
"secrets.purge.step5": "5. Cierra el incidente: finitemonkey secrets incident close {id}",
|
|
2088
|
+
"secrets.error.found": "{count} hallazgo(s) de secretos fuera del baseline o con aceptación vencida",
|
|
2089
|
+
"secrets.error.one_mode": "Elige solo uno entre --staged, --history y --path",
|
|
2090
|
+
"secrets.error.git_unavailable": "La búsqueda no pudo leer git ({detail})",
|
|
2091
|
+
"secrets.error.baseline_invalid": "El baseline {file} no es válido: {detail}",
|
|
2092
|
+
"secrets.error.person_only": "Solo la persona puede {action}, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
|
|
2093
|
+
"secrets.error.reason_required": "--reason es obligatorio: di por qué este hallazgo es aceptable",
|
|
2094
|
+
"secrets.error.finding_not_found": "Ningún hallazgo actual tiene el id {id}",
|
|
2095
|
+
"secrets.error.incident_not_found": "Ningún incidente tiene el id {id}",
|
|
2096
|
+
"secrets.error.not_rotated": "El incidente {id} está {state}: rota la clave en el proveedor primero y luego márcalo como rotado",
|
|
2097
|
+
"secrets.error.bad_transition": "El incidente {id} no puede pasar de {from} a {to}",
|
|
2098
|
+
"secrets.error.write_failed": "No se pudo escribir {file}: {detail}"
|
|
862
2099
|
};
|
|
863
2100
|
|
|
864
2101
|
// src/i18n/pt-BR.json
|
|
@@ -944,7 +2181,52 @@ var pt_BR_default = {
|
|
|
944
2181
|
"exit.internal": "5 erro interno: o próprio Finite Monkey falhou; por favor, reporte",
|
|
945
2182
|
"help.exit_codes": "Códigos de saída",
|
|
946
2183
|
"hook.stop_released": "o hook {event} bloquearia de novo ({detail}), mas o Claude Code já está continuando por causa de um hook de parada, então ele deixa a sessão parar. Rode `finitemonkey doctor`",
|
|
947
|
-
"error.needs_confirmation": '"{command}" precisa da sua decisão antes de mudar qualquer coisa: {question}'
|
|
2184
|
+
"error.needs_confirmation": '"{command}" precisa da sua decisão antes de mudar qualquer coisa: {question}',
|
|
2185
|
+
"secrets.command.scan": "Procura segredos nos arquivos, nas mudanças preparadas ou no histórico do git (só local)",
|
|
2186
|
+
"secrets.command.accept": "Aceita um achado no baseline, com motivo e validade (só a pessoa)",
|
|
2187
|
+
"secrets.command.incident": "Lista incidentes de segredo, marca como rotacionado ou fechado e mostra os passos de limpeza do histórico",
|
|
2188
|
+
"secrets.found_item": "{rule} em {where}",
|
|
2189
|
+
"secrets.found_more": "e mais {count}",
|
|
2190
|
+
"secrets.where.line": "linha {line}, coluna {column}",
|
|
2191
|
+
"secrets.where.file": "{file}:{line}:{column}",
|
|
2192
|
+
"secrets.where.new_text": "{file} (linha {line} do texto novo)",
|
|
2193
|
+
"secrets.block.write": "bloqueado: o texto novo de {file} tem uma credencial ({found}). Nada foi gravado. Deixe o valor no .env (fora do git) e leia pelo nome da variável. Se a chave é real, trate como exposta e troque.",
|
|
2194
|
+
"secrets.block.bash": "bloqueado: este comando tem uma credencial ({found}) e não rodou. Leia o valor de uma variável de ambiente em vez de escrevê-lo no comando. Se a chave é real, trate como exposta e troque.",
|
|
2195
|
+
"secrets.block.commit": "bloqueado: as mudanças preparadas têm uma credencial ({found}), então o commit não rodou. Tire-a dos arquivos preparados, deixe no .env (fora do git) e troque a chave se for real.",
|
|
2196
|
+
"secrets.block.read": "bloqueado: {target} guarda segredos, e lê-lo colocaria os valores na conversa. Peça à pessoa os nomes das variáveis, ou leia o .env.example.",
|
|
2197
|
+
"secrets.block.env_dump": 'bloqueado: `{command}` imprime todas as variáveis de ambiente, segredos inclusive. Imprima só a variável que precisa, pelo nome, ou confira que ela existe com `test -n "$NAME"`.',
|
|
2198
|
+
"secrets.block.person_only": "bloqueado: `{command}` é decisão da pessoa, não do modelo. Peça à pessoa que rode no terminal dela.",
|
|
2199
|
+
"secrets.block.prompt": "sua mensagem não foi enviada ao modelo: ela tem uma credencial ({found}). Se essa chave foi colada em outro lugar, trate como exposta e troque. Para usá-la, ponha no .env e me diga só o nome da variável. Nunca cole chaves na conversa. Incidente: {incident}.",
|
|
2200
|
+
"secrets.post.context": "Finite Monkey: a saída de {tool} continha uma credencial ({found}). O incidente {incident} está aberto; trocar a chave é decisão da pessoa. A saída do Finite Monkey nunca repete o valor.",
|
|
2201
|
+
"secrets.incident.unsaved": "não registrado ({detail})",
|
|
2202
|
+
"secrets.scan.title": "Varredura de segredos ({mode}): {total} achados, {new} novos, {known} conhecidos, {expired} vencidos, {fixed} corrigidos",
|
|
2203
|
+
"secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
|
|
2204
|
+
"secrets.scan.clean": "Nenhum segredo encontrado. O macaco guarda as bananas para si.",
|
|
2205
|
+
"secrets.accept.done": "Aceito {id} ({rule} em {where}) como {state} até {expires}, por {owner}.",
|
|
2206
|
+
"secrets.action.accept": "aceitar um achado",
|
|
2207
|
+
"secrets.action.rotated": "marcar um incidente como rotacionado",
|
|
2208
|
+
"secrets.action.close": "fechar um incidente",
|
|
2209
|
+
"secrets.incident.title": "{count} incidente(s)",
|
|
2210
|
+
"secrets.incident.item": " {id} {state} {rule} {where} desde {created}",
|
|
2211
|
+
"secrets.incident.none": "Nenhum incidente de segredo.",
|
|
2212
|
+
"secrets.incident.marked": "O incidente {id} agora está {state}.",
|
|
2213
|
+
"secrets.purge.title": "Limpeza do histórico de {id} ({where}, primeiro commit {commit}). Quem roda estes passos é você; o Finite Monkey nunca os roda:",
|
|
2214
|
+
"secrets.purge.step1": "1. Confirme que a chave antiga foi revogada no provedor e que a nova funciona em todos os consumidores.",
|
|
2215
|
+
"secrets.purge.step2": "2. Fora do repositório, escreva um arquivo de substituições com uma linha por valor vazado: <valor>==>REMOVED. Copie o valor dos seus registros; o Finite Monkey nunca o guarda nem imprime.",
|
|
2216
|
+
"secrets.purge.step3": "3. Num clone espelho novo, rode: git filter-repo --replace-text <arquivo de substituições>",
|
|
2217
|
+
"secrets.purge.step4": "4. Afrouxe a proteção da branch você mesmo, envie as refs reescritas, restaure a proteção e avise quem tem clone para clonar de novo.",
|
|
2218
|
+
"secrets.purge.step5": "5. Feche o incidente: finitemonkey secrets incident close {id}",
|
|
2219
|
+
"secrets.error.found": "{count} achado(s) de segredo fora do baseline ou com aceite vencido",
|
|
2220
|
+
"secrets.error.one_mode": "Escolha só um entre --staged, --history e --path",
|
|
2221
|
+
"secrets.error.git_unavailable": "A varredura não conseguiu ler o git ({detail})",
|
|
2222
|
+
"secrets.error.baseline_invalid": "O baseline {file} é inválido: {detail}",
|
|
2223
|
+
"secrets.error.person_only": "Só a pessoa pode {action}, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
|
|
2224
|
+
"secrets.error.reason_required": "--reason é obrigatório: diga por que este achado é aceitável",
|
|
2225
|
+
"secrets.error.finding_not_found": "Nenhum achado atual tem o id {id}",
|
|
2226
|
+
"secrets.error.incident_not_found": "Nenhum incidente tem o id {id}",
|
|
2227
|
+
"secrets.error.not_rotated": "O incidente {id} está {state}: troque a chave no provedor primeiro e depois marque como rotacionado",
|
|
2228
|
+
"secrets.error.bad_transition": "O incidente {id} não pode ir de {from} para {to}",
|
|
2229
|
+
"secrets.error.write_failed": "Não foi possível gravar {file}: {detail}"
|
|
948
2230
|
};
|
|
949
2231
|
|
|
950
2232
|
// src/i18n/catalogs.ts
|
|
@@ -1022,10 +2304,10 @@ var unreadable = (dir, file, detail) => ({
|
|
|
1022
2304
|
exitCode: ExitCode.NotConfigured
|
|
1023
2305
|
});
|
|
1024
2306
|
var readCatalog = (ctx, dir, lang) => {
|
|
1025
|
-
const file = toAbsolutePath(
|
|
1026
|
-
const
|
|
1027
|
-
if (!
|
|
1028
|
-
const parsed = parseJsonObject(
|
|
2307
|
+
const file = toAbsolutePath(join5(dir, `${lang}.json`));
|
|
2308
|
+
const text2 = ctx.runtime.readText(file);
|
|
2309
|
+
if (!text2.ok) return reject(unreadable(dir, file, text2.error));
|
|
2310
|
+
const parsed = parseJsonObject(text2.value);
|
|
1029
2311
|
return parsed.ok ? parsed : reject(unreadable(dir, file, parsed.error));
|
|
1030
2312
|
};
|
|
1031
2313
|
var loadCatalogs = (ctx, dir) => {
|
|
@@ -1104,6 +2386,382 @@ var uninstallCommand = planned("uninstall", "command.uninstall.summary", {
|
|
|
1104
2386
|
"keep-generated": { type: "boolean" }
|
|
1105
2387
|
});
|
|
1106
2388
|
|
|
2389
|
+
// src/secrets/project.ts
|
|
2390
|
+
var projectRoot = (ctx) => {
|
|
2391
|
+
const top = ctx.runtime.git(["rev-parse", "--show-toplevel"], ctx.cwd);
|
|
2392
|
+
const path = top.stdout.trim();
|
|
2393
|
+
return top.ok && path !== "" ? toAbsolutePath(path) : ctx.cwd;
|
|
2394
|
+
};
|
|
2395
|
+
var isPerson = (ctx) => ctx.io.stdin.isTTY === true && ctx.env["CLAUDECODE"] === void 0 && ctx.env["CLAUDE_CODE_ENTRYPOINT"] === void 0;
|
|
2396
|
+
var personName = (ctx) => {
|
|
2397
|
+
const name = ctx.runtime.git(["config", "user.name"], ctx.cwd).stdout.trim();
|
|
2398
|
+
return name !== "" ? name : ctx.env["USER"] ?? ctx.env["USERNAME"] ?? "person";
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
// src/commands/secrets-problems.ts
|
|
2402
|
+
var baselineInvalid = (detail) => ({
|
|
2403
|
+
code: "secrets_baseline_invalid",
|
|
2404
|
+
messageKey: "secrets.error.baseline_invalid",
|
|
2405
|
+
params: { file: BASELINE_FILE, detail },
|
|
2406
|
+
fix: `git checkout -- ${BASELINE_FILE}`,
|
|
2407
|
+
exitCode: ExitCode.NotConfigured
|
|
2408
|
+
});
|
|
2409
|
+
var writeFailed = (file, detail) => ({
|
|
2410
|
+
code: "secrets_write_failed",
|
|
2411
|
+
messageKey: "secrets.error.write_failed",
|
|
2412
|
+
params: { file, detail },
|
|
2413
|
+
fix: "finitemonkey doctor",
|
|
2414
|
+
exitCode: ExitCode.Internal
|
|
2415
|
+
});
|
|
2416
|
+
var gitUnavailable = (detail) => ({
|
|
2417
|
+
code: "secrets_git_unavailable",
|
|
2418
|
+
messageKey: "secrets.error.git_unavailable",
|
|
2419
|
+
params: { detail },
|
|
2420
|
+
fix: "finitemonkey doctor",
|
|
2421
|
+
exitCode: ExitCode.NotConfigured
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
// src/commands/secrets-accept.ts
|
|
2425
|
+
var DAY_MS = 864e5;
|
|
2426
|
+
var daysOf = (values) => {
|
|
2427
|
+
const raw = stringOption(values, "days");
|
|
2428
|
+
if (raw === void 0) return EXCEPTION_MAX_DAYS;
|
|
2429
|
+
const days = Number.parseInt(raw, 10);
|
|
2430
|
+
return Number.isInteger(days) && days > 0 && days <= EXCEPTION_MAX_DAYS ? days : void 0;
|
|
2431
|
+
};
|
|
2432
|
+
var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS).toISOString();
|
|
2433
|
+
var failure = (ctx, problem) => failureFrom(ctx.t, problem);
|
|
2434
|
+
var inputProblem = (ctx, id, reason, days) => {
|
|
2435
|
+
const fix = "finitemonkey secrets accept <id> --reason <why> [--days 1-90] [--false-positive]";
|
|
2436
|
+
if (!isPerson(ctx)) {
|
|
2437
|
+
return needsPersonProblem("secrets.error.person_only", fix, { action: ctx.t("secrets.action.accept") });
|
|
2438
|
+
}
|
|
2439
|
+
if (id === "") return usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail: "<id>" });
|
|
2440
|
+
if (reason === void 0 || reason.trim() === "") {
|
|
2441
|
+
return usageProblem("invalid_arguments", "secrets.error.reason_required", fix);
|
|
2442
|
+
}
|
|
2443
|
+
if (days === void 0) {
|
|
2444
|
+
return usageProblem("invalid_arguments", "error.invalid_arguments", fix, {
|
|
2445
|
+
detail: `--days 1-${String(EXCEPTION_MAX_DAYS)}`
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
return void 0;
|
|
2449
|
+
};
|
|
2450
|
+
var entryOf = (ctx, finding, args) => {
|
|
2451
|
+
const now = ctx.runtime.now();
|
|
2452
|
+
return {
|
|
2453
|
+
id: finding.id,
|
|
2454
|
+
rule: finding.ruleId,
|
|
2455
|
+
file: finding.file,
|
|
2456
|
+
line: finding.line,
|
|
2457
|
+
fingerprint: finding.fingerprint,
|
|
2458
|
+
state: args.falsePositive ? "falso-positivo" : "aceito",
|
|
2459
|
+
owner: personName(ctx),
|
|
2460
|
+
reason: args.reason,
|
|
2461
|
+
acceptedAt: now,
|
|
2462
|
+
expiresAt: plusDays(now, args.days)
|
|
2463
|
+
};
|
|
2464
|
+
};
|
|
2465
|
+
var acceptEntry = (ctx, args) => {
|
|
2466
|
+
const root = projectRoot(ctx);
|
|
2467
|
+
const baseline = loadBaseline(ctx.runtime, root);
|
|
2468
|
+
if (!baseline.ok) {
|
|
2469
|
+
return failure(ctx, baselineInvalid(baseline.error));
|
|
2470
|
+
}
|
|
2471
|
+
const scanned = scanPath(ctx.runtime, root, ".");
|
|
2472
|
+
const finding = scanned.ok ? scanned.value.find((item) => item.id === args.id) : void 0;
|
|
2473
|
+
if (finding === void 0) {
|
|
2474
|
+
return failure(
|
|
2475
|
+
ctx,
|
|
2476
|
+
usageProblem("secrets_finding_not_found", "secrets.error.finding_not_found", "finitemonkey secrets scan", {
|
|
2477
|
+
id: args.id
|
|
2478
|
+
})
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
const entry = entryOf(ctx, finding, args);
|
|
2482
|
+
const entries = [...baseline.value.entries.filter((item) => item.id !== entry.id), entry];
|
|
2483
|
+
const saved = saveBaseline(ctx.runtime, root, { schema: baseline.value.schema, entries });
|
|
2484
|
+
if (!saved.ok) {
|
|
2485
|
+
return failure(ctx, writeFailed(BASELINE_FILE, saved.error));
|
|
2486
|
+
}
|
|
2487
|
+
const line = ctx.t("secrets.accept.done", {
|
|
2488
|
+
id: entry.id,
|
|
2489
|
+
rule: entry.rule,
|
|
2490
|
+
where: whereOf(finding),
|
|
2491
|
+
state: entry.state,
|
|
2492
|
+
expires: entry.expiresAt.slice(0, 10),
|
|
2493
|
+
owner: entry.owner
|
|
2494
|
+
});
|
|
2495
|
+
return success(entry, [line]);
|
|
2496
|
+
};
|
|
2497
|
+
var secretsAcceptCommand = {
|
|
2498
|
+
path: ["secrets", "accept"],
|
|
2499
|
+
summaryKey: "secrets.command.accept",
|
|
2500
|
+
usage: "finitemonkey secrets accept <finding-id> --reason <why> [--days 1-90] [--false-positive]",
|
|
2501
|
+
status: "ready",
|
|
2502
|
+
maxPositionals: 1,
|
|
2503
|
+
options: { reason: { type: "string" }, days: { type: "string" }, "false-positive": { type: "boolean" } },
|
|
2504
|
+
run: (ctx, args) => {
|
|
2505
|
+
const id = args.positionals[0] ?? "";
|
|
2506
|
+
const reason = stringOption(args.values, "reason");
|
|
2507
|
+
const days = daysOf(args.values);
|
|
2508
|
+
const problem = inputProblem(ctx, id, reason, days);
|
|
2509
|
+
if (problem !== void 0 || reason === void 0 || days === void 0) {
|
|
2510
|
+
return Promise.resolve(
|
|
2511
|
+
failure(
|
|
2512
|
+
ctx,
|
|
2513
|
+
problem ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
|
|
2514
|
+
)
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
return Promise.resolve(
|
|
2518
|
+
acceptEntry(ctx, { id, reason, days, falsePositive: args.values["false-positive"] === true })
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
// src/commands/secrets-incident.ts
|
|
2524
|
+
var INCIDENT_ACTIONS = ["list", "rotated", "close", "purge"];
|
|
2525
|
+
var USAGE = "finitemonkey secrets incident [list | rotated <id> | close <id> | purge <id>]";
|
|
2526
|
+
var TARGET = { rotated: "rotacionado", close: "fechado" };
|
|
2527
|
+
var PURGE_STEPS = [
|
|
2528
|
+
"secrets.purge.step1",
|
|
2529
|
+
"secrets.purge.step2",
|
|
2530
|
+
"secrets.purge.step3",
|
|
2531
|
+
"secrets.purge.step4",
|
|
2532
|
+
"secrets.purge.step5"
|
|
2533
|
+
];
|
|
2534
|
+
var fail = (ctx, problem) => failureFrom(ctx.t, problem);
|
|
2535
|
+
var notFound2 = (ctx, id) => fail(
|
|
2536
|
+
ctx,
|
|
2537
|
+
usageProblem("secrets_incident_not_found", "secrets.error.incident_not_found", `finitemonkey secrets incident`, {
|
|
2538
|
+
id
|
|
2539
|
+
})
|
|
2540
|
+
);
|
|
2541
|
+
var itemLine = (ctx, incident) => ctx.t("secrets.incident.item", {
|
|
2542
|
+
id: incident.id,
|
|
2543
|
+
state: incident.state,
|
|
2544
|
+
rule: incident.rule,
|
|
2545
|
+
where: incident.where,
|
|
2546
|
+
created: incident.createdAt.slice(0, 10)
|
|
2547
|
+
});
|
|
2548
|
+
var list = (ctx, root) => {
|
|
2549
|
+
const incidents = listIncidents(ctx.runtime, root);
|
|
2550
|
+
const human = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [ctx.t("secrets.incident.title", { count: incidents.length }), ...incidents.map((item) => itemLine(ctx, item))];
|
|
2551
|
+
return success({ schema: "finitemonkey/secrets-incidents@1", incidents }, human);
|
|
2552
|
+
};
|
|
2553
|
+
var notRotated = (ctx, incident) => fail(ctx, {
|
|
2554
|
+
code: "secrets_not_rotated",
|
|
2555
|
+
messageKey: "secrets.error.not_rotated",
|
|
2556
|
+
params: { id: incident.id, state: incident.state },
|
|
2557
|
+
fix: `finitemonkey secrets incident rotated ${incident.id}`,
|
|
2558
|
+
exitCode: ExitCode.NeedsPerson
|
|
2559
|
+
});
|
|
2560
|
+
var move = (ctx, root, incident, action) => {
|
|
2561
|
+
const to = TARGET[action];
|
|
2562
|
+
if (!isPerson(ctx)) {
|
|
2563
|
+
return fail(
|
|
2564
|
+
ctx,
|
|
2565
|
+
needsPersonProblem("secrets.error.person_only", `finitemonkey secrets incident ${action} ${incident.id}`, {
|
|
2566
|
+
action: ctx.t(`secrets.action.${action}`)
|
|
2567
|
+
})
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
if (!canMove(incident.state, to))
|
|
2571
|
+
return action === "close" ? notRotated(ctx, incident) : fail(
|
|
2572
|
+
ctx,
|
|
2573
|
+
usageProblem("invalid_arguments", "secrets.error.bad_transition", USAGE, {
|
|
2574
|
+
id: incident.id,
|
|
2575
|
+
from: incident.state,
|
|
2576
|
+
to
|
|
2577
|
+
})
|
|
2578
|
+
);
|
|
2579
|
+
const moved = moveIncident(incident, to, ctx.runtime.now(), personName(ctx));
|
|
2580
|
+
const saved = saveIncident(ctx.runtime, root, moved);
|
|
2581
|
+
if (!saved.ok) {
|
|
2582
|
+
return fail(ctx, writeFailed(incident.id, saved.error));
|
|
2583
|
+
}
|
|
2584
|
+
return success(moved, [ctx.t("secrets.incident.marked", { id: moved.id, state: moved.state })]);
|
|
2585
|
+
};
|
|
2586
|
+
var purge = (ctx, incident) => {
|
|
2587
|
+
if (!isRotated(incident)) return notRotated(ctx, incident);
|
|
2588
|
+
const params = { id: incident.id, where: incident.where, commit: incident.firstCommit ?? "?" };
|
|
2589
|
+
const steps = PURGE_STEPS.map((key) => ctx.t(key, params));
|
|
2590
|
+
return success({ id: incident.id, state: incident.state, steps }, [ctx.t("secrets.purge.title", params), ...steps]);
|
|
2591
|
+
};
|
|
2592
|
+
var actionOf = (raw) => INCIDENT_ACTIONS.find((action) => action === (raw ?? "list"));
|
|
2593
|
+
var secretsIncidentCommand = {
|
|
2594
|
+
path: ["secrets", "incident"],
|
|
2595
|
+
summaryKey: "secrets.command.incident",
|
|
2596
|
+
usage: USAGE,
|
|
2597
|
+
status: "ready",
|
|
2598
|
+
maxPositionals: 2,
|
|
2599
|
+
run: (ctx, args) => {
|
|
2600
|
+
const action = actionOf(args.positionals[0]);
|
|
2601
|
+
const root = projectRoot(ctx);
|
|
2602
|
+
if (action === void 0)
|
|
2603
|
+
return Promise.resolve(
|
|
2604
|
+
fail(
|
|
2605
|
+
ctx,
|
|
2606
|
+
usageProblem("invalid_arguments", "error.invalid_arguments", USAGE, { detail: args.positionals[0] ?? "" })
|
|
2607
|
+
)
|
|
2608
|
+
);
|
|
2609
|
+
if (action === "list") return Promise.resolve(list(ctx, root));
|
|
2610
|
+
const id = args.positionals[1] ?? "";
|
|
2611
|
+
const incident = readIncident(ctx.runtime, root, id);
|
|
2612
|
+
if (!incident.ok) return Promise.resolve(notFound2(ctx, id));
|
|
2613
|
+
return Promise.resolve(action === "purge" ? purge(ctx, incident.value) : move(ctx, root, incident.value, action));
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
// src/mask/mask.ts
|
|
2618
|
+
var maskToken = (finding) => `«secret:${finding.ruleId}:${finding.tail}»`;
|
|
2619
|
+
var maskText = (text2) => {
|
|
2620
|
+
const findings = scanText(text2, { rules: CREDENTIAL_RULES });
|
|
2621
|
+
if (findings.length === 0) return text2;
|
|
2622
|
+
let out = "";
|
|
2623
|
+
let cursor = 0;
|
|
2624
|
+
for (const finding of findings) {
|
|
2625
|
+
out += text2.slice(cursor, finding.start) + maskToken(finding);
|
|
2626
|
+
cursor = finding.end;
|
|
2627
|
+
}
|
|
2628
|
+
return out + text2.slice(cursor);
|
|
2629
|
+
};
|
|
2630
|
+
var maskDeep = (value) => {
|
|
2631
|
+
if (typeof value === "string") return maskText(value);
|
|
2632
|
+
if (Array.isArray(value)) return value.map(maskDeep);
|
|
2633
|
+
if (typeof value === "object" && value !== null) {
|
|
2634
|
+
return Object.fromEntries(Object.entries(value).map(([key, inner]) => [maskText(key), maskDeep(inner)]));
|
|
2635
|
+
}
|
|
2636
|
+
return value;
|
|
2637
|
+
};
|
|
2638
|
+
|
|
2639
|
+
// src/commands/secrets-scan.ts
|
|
2640
|
+
var SECRETS_SCAN_SCHEMA = "finitemonkey/secrets-scan@1";
|
|
2641
|
+
var view = (finding, status) => ({
|
|
2642
|
+
id: finding.id,
|
|
2643
|
+
rule: finding.ruleId,
|
|
2644
|
+
severity: finding.severity,
|
|
2645
|
+
file: finding.file,
|
|
2646
|
+
line: finding.line,
|
|
2647
|
+
column: finding.column,
|
|
2648
|
+
fingerprint: finding.fingerprint,
|
|
2649
|
+
mask: maskToken(finding),
|
|
2650
|
+
status,
|
|
2651
|
+
...finding.commit === void 0 ? {} : { commit: finding.commit }
|
|
2652
|
+
});
|
|
2653
|
+
var modeOf = (values) => {
|
|
2654
|
+
const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
|
|
2655
|
+
if (chosen.filter(Boolean).length > 1) return void 0;
|
|
2656
|
+
if (chosen[0] === true) return "staged";
|
|
2657
|
+
return chosen[1] === true ? "history" : "path";
|
|
2658
|
+
};
|
|
2659
|
+
var normalizedTarget = (target) => target.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
|
|
2660
|
+
var scopeOf = (target) => {
|
|
2661
|
+
const prefix = normalizedTarget(target);
|
|
2662
|
+
return (file) => prefix === "" || file === prefix || file.startsWith(`${prefix}/`);
|
|
2663
|
+
};
|
|
2664
|
+
var runScan = async (ctx, root, mode, target) => {
|
|
2665
|
+
if (mode === "staged") return scanStaged(ctx.runtime, root);
|
|
2666
|
+
if (mode === "history") return scanHistory(ctx.runtime, root);
|
|
2667
|
+
return scanPath(ctx.runtime, root, normalizedTarget(target) || ".");
|
|
2668
|
+
};
|
|
2669
|
+
var historyIncidents = (ctx, root, fresh2) => fresh2.flatMap((finding) => {
|
|
2670
|
+
if (ruleById(finding.ruleId)?.kind !== "credential") return [];
|
|
2671
|
+
const draft = {
|
|
2672
|
+
rule: finding.ruleId,
|
|
2673
|
+
fingerprint: finding.fingerprint,
|
|
2674
|
+
where: whereOf(finding),
|
|
2675
|
+
source: "history",
|
|
2676
|
+
state: "aberto",
|
|
2677
|
+
...finding.commit === void 0 ? {} : { firstCommit: finding.commit }
|
|
2678
|
+
};
|
|
2679
|
+
const saved = recordIncident(ctx.runtime, root, draft, HOOK_ACTOR);
|
|
2680
|
+
return saved.ok ? [saved.value.id] : [];
|
|
2681
|
+
});
|
|
2682
|
+
var dataOf = (mode, outcome, incidents) => {
|
|
2683
|
+
const findings = [
|
|
2684
|
+
...outcome.fresh.map((finding) => view(finding, "new")),
|
|
2685
|
+
...outcome.known.map((finding) => view(finding, "known")),
|
|
2686
|
+
...outcome.expired.map((finding) => view(finding, "expired"))
|
|
2687
|
+
];
|
|
2688
|
+
return {
|
|
2689
|
+
schema: SECRETS_SCAN_SCHEMA,
|
|
2690
|
+
mode,
|
|
2691
|
+
baseline: BASELINE_FILE,
|
|
2692
|
+
findings,
|
|
2693
|
+
fixed: outcome.fixed.map((entry) => entry.id),
|
|
2694
|
+
incidents,
|
|
2695
|
+
summary: {
|
|
2696
|
+
total: findings.length,
|
|
2697
|
+
new: outcome.fresh.length,
|
|
2698
|
+
known: outcome.known.length,
|
|
2699
|
+
expired: outcome.expired.length,
|
|
2700
|
+
fixed: outcome.fixed.length
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
};
|
|
2704
|
+
var humanOf = (ctx, data) => [
|
|
2705
|
+
ctx.t("secrets.scan.title", { mode: data.mode, ...data.summary }),
|
|
2706
|
+
...data.findings.map(
|
|
2707
|
+
(item) => ctx.t("secrets.scan.item", {
|
|
2708
|
+
status: item.status,
|
|
2709
|
+
rule: item.rule,
|
|
2710
|
+
where: `${item.file}:${String(item.line)}`,
|
|
2711
|
+
id: item.id,
|
|
2712
|
+
mask: item.mask
|
|
2713
|
+
})
|
|
2714
|
+
),
|
|
2715
|
+
...data.summary.total === 0 ? [ctx.t("secrets.scan.clean")] : []
|
|
2716
|
+
];
|
|
2717
|
+
var resultOf = (ctx, data) => {
|
|
2718
|
+
const human = humanOf(ctx, data);
|
|
2719
|
+
const failing = data.summary.new + data.summary.expired;
|
|
2720
|
+
if (failing === 0) return success(data, human);
|
|
2721
|
+
return failureFrom(
|
|
2722
|
+
ctx.t,
|
|
2723
|
+
{
|
|
2724
|
+
code: "secrets_found",
|
|
2725
|
+
messageKey: "secrets.error.found",
|
|
2726
|
+
params: { count: failing },
|
|
2727
|
+
fix: "finitemonkey secrets accept <id> --reason <why>",
|
|
2728
|
+
exitCode: ExitCode.CheckFailed,
|
|
2729
|
+
data
|
|
2730
|
+
},
|
|
2731
|
+
human
|
|
2732
|
+
);
|
|
2733
|
+
};
|
|
2734
|
+
var prune = (ctx, root, baseline, outcome) => {
|
|
2735
|
+
if (outcome.fixed.length > 0) saveBaseline(ctx.runtime, root, withoutFixed(baseline, outcome.fixed));
|
|
2736
|
+
};
|
|
2737
|
+
var secretsScanCommand = {
|
|
2738
|
+
path: ["secrets", "scan"],
|
|
2739
|
+
summaryKey: "secrets.command.scan",
|
|
2740
|
+
usage: "finitemonkey secrets scan [--staged | --history | --path <dir>] [--json]",
|
|
2741
|
+
status: "ready",
|
|
2742
|
+
options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
|
|
2743
|
+
run: async (ctx, args) => {
|
|
2744
|
+
const mode = modeOf(args.values);
|
|
2745
|
+
const fix = "finitemonkey help secrets scan";
|
|
2746
|
+
if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
|
|
2747
|
+
const root = projectRoot(ctx);
|
|
2748
|
+
const target = stringOption(args.values, "path") ?? ".";
|
|
2749
|
+
const scanned = await runScan(ctx, root, mode, target);
|
|
2750
|
+
if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
|
|
2751
|
+
const baseline = loadBaseline(ctx.runtime, root);
|
|
2752
|
+
if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
|
|
2753
|
+
const outcome = ratchet(
|
|
2754
|
+
scanned.value,
|
|
2755
|
+
baseline.value,
|
|
2756
|
+
ctx.runtime.now(),
|
|
2757
|
+
mode === "path" ? scopeOf(target) : void 0
|
|
2758
|
+
);
|
|
2759
|
+
if (mode === "path") prune(ctx, root, baseline.value, outcome);
|
|
2760
|
+
const incidents = mode === "history" ? historyIncidents(ctx, root, outcome.fresh) : [];
|
|
2761
|
+
return resultOf(ctx, dataOf(mode, outcome, incidents));
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
1107
2765
|
// src/commands/version.ts
|
|
1108
2766
|
var versionCommand = {
|
|
1109
2767
|
path: ["version"],
|
|
@@ -1124,6 +2782,9 @@ var REGISTRY = [
|
|
|
1124
2782
|
uninstallCommand,
|
|
1125
2783
|
i18nCheckCommand,
|
|
1126
2784
|
hookCommand,
|
|
2785
|
+
secretsScanCommand,
|
|
2786
|
+
secretsAcceptCommand,
|
|
2787
|
+
secretsIncidentCommand,
|
|
1127
2788
|
versionCommand,
|
|
1128
2789
|
helpCommand
|
|
1129
2790
|
];
|
|
@@ -1205,23 +2866,26 @@ var resultFromThrown = (thrown, t) => {
|
|
|
1205
2866
|
};
|
|
1206
2867
|
var block = (items) => items.length > 0 ? `${items.join("\n")}
|
|
1207
2868
|
` : "";
|
|
2869
|
+
var write = (sink, text2) => {
|
|
2870
|
+
if (text2 !== "") sink.write(maskText(text2));
|
|
2871
|
+
};
|
|
1208
2872
|
var render = (ctx, command, result) => {
|
|
1209
2873
|
if (result.raw !== void 0) {
|
|
1210
|
-
ctx.io.stdout
|
|
1211
|
-
ctx.io.stderr
|
|
2874
|
+
write(ctx.io.stdout, result.raw.stdout);
|
|
2875
|
+
write(ctx.io.stderr, result.raw.stderr);
|
|
1212
2876
|
return;
|
|
1213
2877
|
}
|
|
1214
2878
|
if (ctx.json) {
|
|
1215
|
-
ctx.io.stdout.write(`${JSON.stringify(toEnvelope(command, result))}
|
|
2879
|
+
ctx.io.stdout.write(`${JSON.stringify(maskDeep(toEnvelope(command, result)))}
|
|
1216
2880
|
`);
|
|
1217
2881
|
return;
|
|
1218
2882
|
}
|
|
1219
|
-
ctx.io.stdout
|
|
2883
|
+
write(ctx.io.stdout, block(result.human ?? []));
|
|
1220
2884
|
if (!result.ok) {
|
|
1221
2885
|
const { message, fix } = result.error;
|
|
1222
|
-
ctx.io.stderr
|
|
2886
|
+
write(ctx.io.stderr, block([ctx.t("output.error", { message }), ctx.t("output.fix", { fix })]));
|
|
1223
2887
|
}
|
|
1224
|
-
if (result.next !== void 0) ctx.io.stdout
|
|
2888
|
+
if (result.next !== void 0) write(ctx.io.stdout, block([ctx.t("output.next", { next: result.next })]));
|
|
1225
2889
|
};
|
|
1226
2890
|
|
|
1227
2891
|
// src/core/run.ts
|
|
@@ -1268,9 +2932,9 @@ var run = async (options) => {
|
|
|
1268
2932
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1269
2933
|
var UNKNOWN_VERSION = "0.0.0-unknown";
|
|
1270
2934
|
var readPackageVersion = (readText, moduleUrl) => {
|
|
1271
|
-
const
|
|
1272
|
-
if (!
|
|
1273
|
-
const parsed = parseJson(
|
|
2935
|
+
const text2 = readText(toAbsolutePath(fileURLToPath2(new URL("../package.json", moduleUrl))));
|
|
2936
|
+
if (!text2.ok) return UNKNOWN_VERSION;
|
|
2937
|
+
const parsed = parseJson(text2.value);
|
|
1274
2938
|
if (!parsed.ok || !isRecord(parsed.value)) return UNKNOWN_VERSION;
|
|
1275
2939
|
const version = parsed.value["version"];
|
|
1276
2940
|
return typeof version === "string" ? version : UNKNOWN_VERSION;
|