@indigoai-us/hq-cli 5.345.84 → 5.345.86
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/CHANGELOG.md +11 -0
- package/dist/command-catalog.generated.d.ts +41 -0
- package/dist/command-catalog.generated.js +50 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/lanes-checks.js +32 -2
- package/dist/commands/lanes-session-start.d.ts +1 -1
- package/dist/commands/lanes-session-start.js +57 -7
- package/dist/commands/scan.d.ts +23 -0
- package/dist/commands/scan.js +59 -0
- package/dist/lib/lanes/flag-keys.d.ts +8 -0
- package/dist/lib/lanes/flag-keys.js +8 -0
- package/dist/lib/lanes/legacy-pool.d.ts +35 -0
- package/dist/lib/lanes/legacy-pool.js +209 -0
- package/dist/lib/lanes/reaper.d.ts +13 -0
- package/dist/lib/lanes/reaper.js +43 -5
- package/dist/lib/secret-scan/detect.d.ts +27 -0
- package/dist/lib/secret-scan/detect.js +129 -0
- package/dist/lib/secret-scan/gitleaks-rules.generated.d.ts +6 -0
- package/dist/lib/secret-scan/gitleaks-rules.generated.js +3602 -0
- package/dist/lib/secret-scan/go-regex.d.ts +22 -0
- package/dist/lib/secret-scan/go-regex.js +250 -0
- package/dist/lib/secret-scan/rules.d.ts +31 -0
- package/dist/lib/secret-scan/rules.js +84 -0
- package/dist/lib/secret-scan/staged.d.ts +17 -0
- package/dist/lib/secret-scan/staged.js +112 -0
- package/dist/lib/secret-scan/types.d.ts +34 -0
- package/dist/lib/secret-scan/types.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate a Go RE2 regular expression (as written in the gitleaks config)
|
|
3
|
+
* into an equivalent JavaScript RegExp source that compiles on Node 20+.
|
|
4
|
+
*
|
|
5
|
+
* Node 20/22 do not support scoped modifiers like `(?i:...)` or a mid-pattern
|
|
6
|
+
* `(?i)`, so case-insensitivity is expanded inline: inside a case-insensitive
|
|
7
|
+
* scope every ASCII letter `a` becomes `[aA]`, and character classes gain the
|
|
8
|
+
* other-case counterpart of each letter and letter range. Patterns are then
|
|
9
|
+
* compiled with no flags, which keeps `(?-i:...)` exact.
|
|
10
|
+
*
|
|
11
|
+
* Other RE2 differences handled:
|
|
12
|
+
* (?s:...) / (?s) `.` in scope becomes `[\s\S]`
|
|
13
|
+
* (?P<name> becomes `(?<name>`
|
|
14
|
+
* \z \A become `$` `^` (no `m` flag, so both anchor the input)
|
|
15
|
+
* [[:alnum:]] etc. POSIX classes inside a bracket expression
|
|
16
|
+
*
|
|
17
|
+
* Throws on syntax this translator does not understand rather than guessing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function translateGoRegex(src: string): string;
|
|
20
|
+
/** Compile a Go RE2 pattern to a JavaScript RegExp (extra flags e.g. "g"). */
|
|
21
|
+
export declare function compileGoRegex(src: string, flags?: string): RegExp;
|
|
22
|
+
//# sourceMappingURL=go-regex.d.ts.map
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate a Go RE2 regular expression (as written in the gitleaks config)
|
|
3
|
+
* into an equivalent JavaScript RegExp source that compiles on Node 20+.
|
|
4
|
+
*
|
|
5
|
+
* Node 20/22 do not support scoped modifiers like `(?i:...)` or a mid-pattern
|
|
6
|
+
* `(?i)`, so case-insensitivity is expanded inline: inside a case-insensitive
|
|
7
|
+
* scope every ASCII letter `a` becomes `[aA]`, and character classes gain the
|
|
8
|
+
* other-case counterpart of each letter and letter range. Patterns are then
|
|
9
|
+
* compiled with no flags, which keeps `(?-i:...)` exact.
|
|
10
|
+
*
|
|
11
|
+
* Other RE2 differences handled:
|
|
12
|
+
* (?s:...) / (?s) `.` in scope becomes `[\s\S]`
|
|
13
|
+
* (?P<name> becomes `(?<name>`
|
|
14
|
+
* \z \A become `$` `^` (no `m` flag, so both anchor the input)
|
|
15
|
+
* [[:alnum:]] etc. POSIX classes inside a bracket expression
|
|
16
|
+
*
|
|
17
|
+
* Throws on syntax this translator does not understand rather than guessing.
|
|
18
|
+
*/
|
|
19
|
+
const POSIX = {
|
|
20
|
+
alnum: "a-zA-Z0-9",
|
|
21
|
+
alpha: "a-zA-Z",
|
|
22
|
+
digit: "0-9",
|
|
23
|
+
lower: "a-z",
|
|
24
|
+
upper: "A-Z",
|
|
25
|
+
space: "\\s",
|
|
26
|
+
word: "\\w",
|
|
27
|
+
xdigit: "0-9A-Fa-f",
|
|
28
|
+
punct: "!-\\/:-@\\[-`{-~",
|
|
29
|
+
};
|
|
30
|
+
const isLower = (c) => c >= "a" && c <= "z";
|
|
31
|
+
const isUpper = (c) => c >= "A" && c <= "Z";
|
|
32
|
+
const isLetter = (c) => isLower(c) || isUpper(c);
|
|
33
|
+
const swap = (c) => (isLower(c) ? c.toUpperCase() : c.toLowerCase());
|
|
34
|
+
function parseFlags(spec, base) {
|
|
35
|
+
const out = { ...base };
|
|
36
|
+
let on = true;
|
|
37
|
+
for (const ch of spec) {
|
|
38
|
+
if (ch === "-")
|
|
39
|
+
on = false;
|
|
40
|
+
else if (ch === "i")
|
|
41
|
+
out.i = on;
|
|
42
|
+
else if (ch === "s")
|
|
43
|
+
out.s = on;
|
|
44
|
+
else if (ch === "m" || ch === "U") {
|
|
45
|
+
throw new Error(`unsupported RE2 flag '${ch}'`);
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
throw new Error(`unknown RE2 flag '${ch}'`);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/** Translate one bracket expression starting at src[start] === "[". */
|
|
53
|
+
function translateClass(src, start, flags) {
|
|
54
|
+
let i = start + 1;
|
|
55
|
+
let out = "[";
|
|
56
|
+
if (src[i] === "^") {
|
|
57
|
+
out += "^";
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
// A leading "]" is a literal in RE2.
|
|
61
|
+
if (src[i] === "]") {
|
|
62
|
+
out += "\\]";
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
const extra = [];
|
|
66
|
+
while (i < src.length && src[i] !== "]") {
|
|
67
|
+
if (src.startsWith("[:", i)) {
|
|
68
|
+
const close = src.indexOf(":]", i + 2);
|
|
69
|
+
if (close < 0)
|
|
70
|
+
throw new Error("unterminated POSIX class");
|
|
71
|
+
const name = src.slice(i + 2, close);
|
|
72
|
+
const neg = name.startsWith("^");
|
|
73
|
+
const body = POSIX[neg ? name.slice(1) : name];
|
|
74
|
+
if (!body || neg)
|
|
75
|
+
throw new Error(`unsupported POSIX class [:${name}:]`);
|
|
76
|
+
out += body;
|
|
77
|
+
i = close + 2;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (src[i] === "\\") {
|
|
81
|
+
const esc = src.slice(i, i + 2);
|
|
82
|
+
if (esc === "\\x" && src[i + 2] === "{")
|
|
83
|
+
throw new Error("unsupported \\x{...} escape");
|
|
84
|
+
if (esc === "\\p" || esc === "\\P" || esc === "\\Q")
|
|
85
|
+
throw new Error(`unsupported escape ${esc}`);
|
|
86
|
+
// \xHH keeps its two hex digits.
|
|
87
|
+
if (esc === "\\x") {
|
|
88
|
+
out += src.slice(i, i + 4);
|
|
89
|
+
i += 4;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
out += esc;
|
|
93
|
+
i += 2;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const c = src[i];
|
|
98
|
+
if (c === "[") {
|
|
99
|
+
out += "\\[";
|
|
100
|
+
i++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Range lo-hi (hi is not "]").
|
|
104
|
+
if (src[i + 1] === "-" && i + 2 < src.length && src[i + 2] !== "]") {
|
|
105
|
+
let hi = src[i + 2];
|
|
106
|
+
let consumed = 3;
|
|
107
|
+
if (hi === "\\") {
|
|
108
|
+
hi = src.slice(i + 2, i + 4);
|
|
109
|
+
consumed = 4;
|
|
110
|
+
}
|
|
111
|
+
out += `${c}-${hi}`;
|
|
112
|
+
if (flags.i && hi.length === 1 && isLetter(c) && isLetter(hi) && isLower(c) === isLower(hi)) {
|
|
113
|
+
extra.push(`${swap(c)}-${swap(hi)}`);
|
|
114
|
+
}
|
|
115
|
+
i += consumed;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// A literal "-" is escaped so the case-folded extras appended below can
|
|
119
|
+
// never turn it into a range operator.
|
|
120
|
+
out += c === "-" ? "\\-" : c;
|
|
121
|
+
if (flags.i && isLetter(c))
|
|
122
|
+
extra.push(swap(c));
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
if (src[i] !== "]")
|
|
126
|
+
throw new Error("unterminated character class");
|
|
127
|
+
out += extra.join("") + "]";
|
|
128
|
+
return { out, end: i + 1 };
|
|
129
|
+
}
|
|
130
|
+
export function translateGoRegex(src) {
|
|
131
|
+
const stack = [{ i: false, s: false }];
|
|
132
|
+
let out = "";
|
|
133
|
+
let i = 0;
|
|
134
|
+
const top = () => stack[stack.length - 1];
|
|
135
|
+
while (i < src.length) {
|
|
136
|
+
const c = src[i];
|
|
137
|
+
const flags = top();
|
|
138
|
+
if (c === "\\") {
|
|
139
|
+
const n = src[i + 1];
|
|
140
|
+
if (n === undefined)
|
|
141
|
+
throw new Error("trailing backslash");
|
|
142
|
+
if (n === "z")
|
|
143
|
+
out += "$";
|
|
144
|
+
else if (n === "A")
|
|
145
|
+
out += "^";
|
|
146
|
+
else if (n === "Q" || n === "p" || n === "P" || n === "C")
|
|
147
|
+
throw new Error(`unsupported escape \\${n}`);
|
|
148
|
+
else if (n === "x") {
|
|
149
|
+
if (src[i + 2] === "{")
|
|
150
|
+
throw new Error("unsupported \\x{...} escape");
|
|
151
|
+
out += src.slice(i, i + 4);
|
|
152
|
+
i += 4;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
out += `\\${n}`;
|
|
157
|
+
i += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (c === "[") {
|
|
161
|
+
const cls = translateClass(src, i, flags);
|
|
162
|
+
out += cls.out;
|
|
163
|
+
i = cls.end;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (c === "(") {
|
|
167
|
+
if (src[i + 1] === "?") {
|
|
168
|
+
// (?P<name>...)
|
|
169
|
+
if (src[i + 2] === "P" && src[i + 3] === "<") {
|
|
170
|
+
out += "(?<";
|
|
171
|
+
stack.push({ ...flags });
|
|
172
|
+
i += 4;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (src[i + 2] === "<" && src[i + 3] !== "=" && src[i + 3] !== "!") {
|
|
176
|
+
out += "(?<";
|
|
177
|
+
stack.push({ ...flags });
|
|
178
|
+
i += 3;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// Flag group: (?flags) or (?flags:...)
|
|
182
|
+
const m = /^\(\?([a-zA-Z-]*)([:)])/.exec(src.slice(i));
|
|
183
|
+
if (m) {
|
|
184
|
+
const next = parseFlags(m[1], flags);
|
|
185
|
+
if (m[2] === ")") {
|
|
186
|
+
// Applies to the rest of the enclosing group.
|
|
187
|
+
stack[stack.length - 1] = next;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
out += "(?:";
|
|
191
|
+
stack.push(next);
|
|
192
|
+
}
|
|
193
|
+
i += m[0].length;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
throw new Error(`unsupported group syntax near '${src.slice(i, i + 6)}'`);
|
|
197
|
+
}
|
|
198
|
+
out += "(";
|
|
199
|
+
stack.push({ ...flags });
|
|
200
|
+
i++;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (c === ")") {
|
|
204
|
+
if (stack.length === 1)
|
|
205
|
+
throw new Error("unbalanced ')'");
|
|
206
|
+
stack.pop();
|
|
207
|
+
out += ")";
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (c === ".") {
|
|
212
|
+
out += flags.s ? "[\\s\\S]" : ".";
|
|
213
|
+
i++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (c === "{") {
|
|
217
|
+
// Quantifier {n}, {n,}, {n,m} copies through; any other "{" is literal.
|
|
218
|
+
const q = /^\{\d+(,\d*)?\}/.exec(src.slice(i));
|
|
219
|
+
if (q) {
|
|
220
|
+
out += q[0];
|
|
221
|
+
i += q[0].length;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
out += "\\{";
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (c === "}") {
|
|
230
|
+
out += "\\}";
|
|
231
|
+
i++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (flags.i && isLetter(c)) {
|
|
235
|
+
out += `[${c}${swap(c)}]`;
|
|
236
|
+
i++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
out += c;
|
|
240
|
+
i++;
|
|
241
|
+
}
|
|
242
|
+
if (stack.length !== 1)
|
|
243
|
+
throw new Error("unbalanced '('");
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** Compile a Go RE2 pattern to a JavaScript RegExp (extra flags e.g. "g"). */
|
|
247
|
+
export function compileGoRegex(src, flags = "") {
|
|
248
|
+
return new RegExp(translateGoRegex(src), flags);
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=go-regex.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled rule set: the vendored gitleaks default rules plus HQ's own token
|
|
3
|
+
* patterns (src/lib/core-utils/secret-patterns.ts). Compiled once per process.
|
|
4
|
+
*/
|
|
5
|
+
import type { GitleaksRule } from "./types.js";
|
|
6
|
+
export interface CompiledAllowlist {
|
|
7
|
+
condition: "OR" | "AND";
|
|
8
|
+
target: "secret" | "match" | "line";
|
|
9
|
+
paths: RegExp[];
|
|
10
|
+
regexes: RegExp[];
|
|
11
|
+
stopwords: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface CompiledRule {
|
|
14
|
+
id: string;
|
|
15
|
+
description: string;
|
|
16
|
+
/** Global-flagged, for iterating matches. */
|
|
17
|
+
regex?: RegExp;
|
|
18
|
+
path?: RegExp;
|
|
19
|
+
entropy?: number;
|
|
20
|
+
secretGroup?: number;
|
|
21
|
+
keywords: string[];
|
|
22
|
+
allowlists: CompiledAllowlist[];
|
|
23
|
+
}
|
|
24
|
+
export interface CompiledRuleSet {
|
|
25
|
+
global: CompiledAllowlist;
|
|
26
|
+
rules: CompiledRule[];
|
|
27
|
+
}
|
|
28
|
+
/** HQ's own patterns, expressed as gitleaks-shaped rules. */
|
|
29
|
+
export declare function hqRules(): GitleaksRule[];
|
|
30
|
+
export declare function compiledRules(): CompiledRuleSet;
|
|
31
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled rule set: the vendored gitleaks default rules plus HQ's own token
|
|
3
|
+
* patterns (src/lib/core-utils/secret-patterns.ts). Compiled once per process.
|
|
4
|
+
*/
|
|
5
|
+
import { SECRET_PATTERNS } from "../core-utils/secret-patterns.js";
|
|
6
|
+
import { GITLEAKS_CONFIG } from "./gitleaks-rules.generated.js";
|
|
7
|
+
import { compileGoRegex } from "./go-regex.js";
|
|
8
|
+
function compileAllowlist(a) {
|
|
9
|
+
return {
|
|
10
|
+
condition: a.condition === "AND" ? "AND" : "OR",
|
|
11
|
+
target: a.regexTarget ?? "secret",
|
|
12
|
+
paths: (a.paths ?? []).map((p) => compileGoRegex(p)),
|
|
13
|
+
regexes: (a.regexes ?? []).map((r) => compileGoRegex(r)),
|
|
14
|
+
stopwords: (a.stopwords ?? []).map((s) => s.toLowerCase()),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function compileRule(r) {
|
|
18
|
+
return {
|
|
19
|
+
id: r.id,
|
|
20
|
+
description: r.description,
|
|
21
|
+
regex: r.regex === undefined ? undefined : compileGoRegex(r.regex, "g"),
|
|
22
|
+
path: r.path === undefined ? undefined : compileGoRegex(r.path),
|
|
23
|
+
entropy: r.entropy,
|
|
24
|
+
secretGroup: r.secretGroup,
|
|
25
|
+
keywords: (r.keywords ?? []).map((k) => k.toLowerCase()),
|
|
26
|
+
allowlists: (r.allowlists ?? []).map(compileAllowlist),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Literal prefix of an HQ pattern, used as its keyword prefilter. */
|
|
30
|
+
function literalPrefix(pattern) {
|
|
31
|
+
const m = /^[A-Za-z0-9_-]+/.exec(pattern.replace(/^-+/, ""));
|
|
32
|
+
return (m ? m[0] : "").replace(/\[.*$/, "").toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
/** HQ's own patterns, expressed as gitleaks-shaped rules. */
|
|
35
|
+
export function hqRules() {
|
|
36
|
+
return SECRET_PATTERNS.map(({ pattern, name }) => {
|
|
37
|
+
const id = `hq-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
|
|
38
|
+
const prefix = literalPrefix(pattern);
|
|
39
|
+
const rule = {
|
|
40
|
+
id,
|
|
41
|
+
description: `HQ pattern: ${name}`,
|
|
42
|
+
regex: `(?:^|[^a-zA-Z0-9])(${pattern})`,
|
|
43
|
+
secretGroup: 1,
|
|
44
|
+
keywords: prefix.length >= 3 ? [prefix] : undefined,
|
|
45
|
+
};
|
|
46
|
+
if (name === "Bearer token") {
|
|
47
|
+
// An auth header built from a variable is not a credential: a real token
|
|
48
|
+
// carries a digit, looks random, and is not an environment lookup.
|
|
49
|
+
rule.entropy = 4;
|
|
50
|
+
rule.allowlists = [
|
|
51
|
+
{ regexes: ["^[^0-9]*$", "(?i)process\\.env|import\\.meta|os\\.environ|ENV\\["] },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
if (name === "Slack token") {
|
|
55
|
+
// Real Slack tokens are long (team id, user id, then a random part).
|
|
56
|
+
// Short test fixtures are not. gitleaks' Slack rules cover the exact
|
|
57
|
+
// formats; this fallback only catches long, digit-bearing tokens.
|
|
58
|
+
rule.regex = `(?:^|[^a-zA-Z0-9])(${pattern.replace("+", "{30,}")})`;
|
|
59
|
+
rule.entropy = 3.5;
|
|
60
|
+
rule.allowlists = [{ regexes: ["^[^0-9]*$"] }];
|
|
61
|
+
}
|
|
62
|
+
return rule;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The scanner's own rule sources contain example token shapes; gitleaks skips
|
|
67
|
+
* its gitleaks.toml the same way.
|
|
68
|
+
*/
|
|
69
|
+
const SELF_PATHS = [
|
|
70
|
+
"(?:^|/)vendor/gitleaks/",
|
|
71
|
+
"(?:^|/)src/lib/secret-scan/gitleaks-rules\\.generated\\.ts$",
|
|
72
|
+
];
|
|
73
|
+
let cached;
|
|
74
|
+
export function compiledRules() {
|
|
75
|
+
if (!cached) {
|
|
76
|
+
const global = GITLEAKS_CONFIG.global;
|
|
77
|
+
cached = {
|
|
78
|
+
global: compileAllowlist({ ...global, paths: [...(global.paths ?? []), ...SELF_PATHS] }),
|
|
79
|
+
rules: [...GITLEAKS_CONFIG.rules, ...hqRules()].map(compileRule),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan inputs: the staged diff of a git repo (pre-commit), or whole files.
|
|
3
|
+
*/
|
|
4
|
+
import { type Fragment } from "./detect.js";
|
|
5
|
+
import type { SecretFinding } from "./types.js";
|
|
6
|
+
export declare class ScanInputError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export interface ParsedDiff {
|
|
9
|
+
fragments: Fragment[];
|
|
10
|
+
binaryFiles: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Parse `git diff --unified=0` output into one fragment of added lines per hunk. */
|
|
13
|
+
export declare function parseUnifiedDiff(diff: string): ParsedDiff;
|
|
14
|
+
export declare function stagedDiff(cwd: string): string;
|
|
15
|
+
export declare function scanStaged(cwd: string): SecretFinding[];
|
|
16
|
+
export declare function scanFiles(paths: string[]): SecretFinding[];
|
|
17
|
+
//# sourceMappingURL=staged.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan inputs: the staged diff of a git repo (pre-commit), or whole files.
|
|
3
|
+
*/
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { scanFragment, scanPath } from "./detect.js";
|
|
7
|
+
export class ScanInputError extends Error {
|
|
8
|
+
}
|
|
9
|
+
const DIFF_ARGS = [
|
|
10
|
+
"-c",
|
|
11
|
+
"core.quotePath=false",
|
|
12
|
+
"diff",
|
|
13
|
+
"--cached",
|
|
14
|
+
"--no-color",
|
|
15
|
+
"--no-ext-diff",
|
|
16
|
+
"--no-textconv",
|
|
17
|
+
"--unified=0",
|
|
18
|
+
"--diff-filter=ACMR",
|
|
19
|
+
"--src-prefix=a/",
|
|
20
|
+
"--dst-prefix=b/",
|
|
21
|
+
];
|
|
22
|
+
/** Parse `git diff --unified=0` output into one fragment of added lines per hunk. */
|
|
23
|
+
export function parseUnifiedDiff(diff) {
|
|
24
|
+
const fragments = [];
|
|
25
|
+
const binaryFiles = [];
|
|
26
|
+
let file = null;
|
|
27
|
+
let inHunk = false;
|
|
28
|
+
let nextLine = 0;
|
|
29
|
+
let cur = null;
|
|
30
|
+
const flush = () => {
|
|
31
|
+
if (file && cur && cur.lines.length) {
|
|
32
|
+
fragments.push({ file, text: cur.lines.join("\n"), startLine: cur.startLine });
|
|
33
|
+
}
|
|
34
|
+
cur = null;
|
|
35
|
+
};
|
|
36
|
+
for (const raw of diff.split("\n")) {
|
|
37
|
+
if (raw.startsWith("diff --git ")) {
|
|
38
|
+
flush();
|
|
39
|
+
file = null;
|
|
40
|
+
inHunk = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!inHunk && raw.startsWith("+++ ")) {
|
|
44
|
+
const p = raw.slice(4);
|
|
45
|
+
file = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!inHunk && raw.startsWith("Binary files ")) {
|
|
49
|
+
const m = / and b\/(.*) differ$/.exec(raw);
|
|
50
|
+
if (m)
|
|
51
|
+
binaryFiles.push(m[1]);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
55
|
+
if (h) {
|
|
56
|
+
flush();
|
|
57
|
+
inHunk = true;
|
|
58
|
+
nextLine = Number(h[1]);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!inHunk)
|
|
62
|
+
continue;
|
|
63
|
+
if (raw.startsWith("+")) {
|
|
64
|
+
if (!cur)
|
|
65
|
+
cur = { lines: [], startLine: nextLine };
|
|
66
|
+
cur.lines.push(raw.slice(1));
|
|
67
|
+
nextLine++;
|
|
68
|
+
}
|
|
69
|
+
else if (raw.startsWith(" ")) {
|
|
70
|
+
flush();
|
|
71
|
+
nextLine++;
|
|
72
|
+
}
|
|
73
|
+
// "-" removed lines and "" are skipped.
|
|
74
|
+
}
|
|
75
|
+
flush();
|
|
76
|
+
return { fragments, binaryFiles };
|
|
77
|
+
}
|
|
78
|
+
export function stagedDiff(cwd) {
|
|
79
|
+
try {
|
|
80
|
+
return execFileSync("git", DIFF_ARGS, {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
84
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const detail = err instanceof Error && "stderr" in err ? String(err.stderr).trim() : String(err);
|
|
89
|
+
throw new ScanInputError(`could not read the staged changes: ${detail || "git diff failed"}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function scanStaged(cwd) {
|
|
93
|
+
const { fragments, binaryFiles } = parseUnifiedDiff(stagedDiff(cwd));
|
|
94
|
+
return [...fragments.flatMap((f) => scanFragment(f)), ...binaryFiles.flatMap((f) => scanPath(f))];
|
|
95
|
+
}
|
|
96
|
+
export function scanFiles(paths) {
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const p of paths) {
|
|
99
|
+
let text;
|
|
100
|
+
try {
|
|
101
|
+
if (!statSync(p).isFile())
|
|
102
|
+
continue;
|
|
103
|
+
text = readFileSync(p, "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
throw new ScanInputError(`could not read ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
out.push(...scanPath(p), ...scanFragment({ file: p, text, startLine: 1 }));
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=staged.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Shapes of the vendored gitleaks config (see vendor/gitleaks/). */
|
|
2
|
+
export interface GitleaksAllowlist {
|
|
3
|
+
description?: string;
|
|
4
|
+
/** "OR" (default): any check allows. "AND": every defined check must allow. */
|
|
5
|
+
condition?: "OR" | "AND";
|
|
6
|
+
/** What `regexes` test: the secret (default), the full match, or the line. */
|
|
7
|
+
regexTarget?: "secret" | "match" | "line";
|
|
8
|
+
paths?: string[];
|
|
9
|
+
regexes?: string[];
|
|
10
|
+
stopwords?: string[];
|
|
11
|
+
commits?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface GitleaksRule {
|
|
14
|
+
id: string;
|
|
15
|
+
description: string;
|
|
16
|
+
regex?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
entropy?: number;
|
|
19
|
+
secretGroup?: number;
|
|
20
|
+
keywords?: string[];
|
|
21
|
+
allowlists?: GitleaksAllowlist[];
|
|
22
|
+
}
|
|
23
|
+
export interface GitleaksConfig {
|
|
24
|
+
global: GitleaksAllowlist;
|
|
25
|
+
rules: GitleaksRule[];
|
|
26
|
+
}
|
|
27
|
+
/** One finding. Never carries the secret value. */
|
|
28
|
+
export interface SecretFinding {
|
|
29
|
+
ruleId: string;
|
|
30
|
+
description: string;
|
|
31
|
+
file: string;
|
|
32
|
+
line: number;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=types.d.ts.map
|