@indigoai-us/hq-cli 5.345.170 → 5.345.172
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 +84 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +206 -7
- package/dist/command-catalog.generated.js +252 -8
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +179 -18
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +228 -10
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +124 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +142 -0
- package/dist/lib/bot/fleet-local.js +689 -0
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -24
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/sync-core/engine-home.d.ts +44 -0
- package/dist/lib/sync-core/engine-home.js +193 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +196 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +140 -0
- package/dist/lib/sync-core/rollout-state.d.ts +24 -0
- package/dist/lib/sync-core/rollout-state.js +94 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
- package/dist/lib/daemon/sync-core-canary.js +0 -56
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range scan: the lines a branch added between <base> and <head>, as
|
|
3
|
+
* `commit-secret-scan.sh pr-check` needs them.
|
|
4
|
+
*
|
|
5
|
+
* What is scanned:
|
|
6
|
+
* - The added lines of every non-merge commit in base..head, each against
|
|
7
|
+
* its own parent. A secret committed in one commit and deleted in a later
|
|
8
|
+
* one is still reported: it stays in the branch history.
|
|
9
|
+
* - For every merge commit in base..head, the lines that differ from all of
|
|
10
|
+
* its parents (git's combined diff, "++" lines): content the merge itself
|
|
11
|
+
* introduced, such as a conflict resolution. Lines a merge only carries
|
|
12
|
+
* over from one parent are either base content or scanned in that
|
|
13
|
+
* parent's own commits.
|
|
14
|
+
* - The added lines of the net diff merge-base(base, head)..head.
|
|
15
|
+
* - Binary files those diffs add or change: path rules, plus their bytes
|
|
16
|
+
* scanned as text up to MAX_BINARY_BYTES (larger ones are warned about).
|
|
17
|
+
* Lines that already existed on the base, and earlier versions of lines the
|
|
18
|
+
* branch did not touch, are not scanned.
|
|
19
|
+
*
|
|
20
|
+
* Config comes from the repo's .gitleaks.toml:
|
|
21
|
+
* - Allowlists: the exact-value exemptions committed on the base (the same
|
|
22
|
+
* contract as `scan secrets --repo --allowlist-rev`). A branch cannot
|
|
23
|
+
* exempt its own commits by adding an allowlist.
|
|
24
|
+
* - Rules: [[rules]] committed on the base, added to the built-in rules.
|
|
25
|
+
* Rules the branch adds or changes are not compiled or run: a branch-added
|
|
26
|
+
* regex could be written to backtrack for minutes (ReDoS), and the base
|
|
27
|
+
* copy has been reviewed.
|
|
28
|
+
* A rejected allowlist or rules section is warned about and dropped; the scan
|
|
29
|
+
* still runs with the built-in rules (stricter, never looser).
|
|
30
|
+
*
|
|
31
|
+
* Range errors: an unknown rev, a reversed range (head behind base) and a
|
|
32
|
+
* merge-base failure other than "no common history" exit 2. base == head is an
|
|
33
|
+
* empty range: a warning and no findings.
|
|
34
|
+
*/
|
|
35
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
36
|
+
import { scanFragment, scanPath } from "./detect.js";
|
|
37
|
+
import { InvalidAllowlistError, InvalidRepoRulesError, ScanConfigError, loadCommittedAllowlists, loadCommittedRules } from "./repo-config.js";
|
|
38
|
+
import { compiledRules } from "./rules.js";
|
|
39
|
+
import { ALLOWLIST_IGNORED_WARNING, parseUnifiedDiff, repoRoot } from "./staged.js";
|
|
40
|
+
export const RULES_IGNORED_WARNING = "HQ secret scan: warning: the repo .gitleaks.toml [[rules]] were rejected; scanning with the built-in rules only";
|
|
41
|
+
export const EMPTY_RANGE_WARNING = "HQ secret scan: warning: the range is empty (base and head are the same commit); nothing to scan";
|
|
42
|
+
/** Binary blobs larger than this are not read; the scan warns instead. */
|
|
43
|
+
export const MAX_BINARY_BYTES = 10 * 1024 * 1024;
|
|
44
|
+
const PATCH_ARGS = [
|
|
45
|
+
"--no-color",
|
|
46
|
+
"--no-ext-diff",
|
|
47
|
+
"--no-textconv",
|
|
48
|
+
"--unified=0",
|
|
49
|
+
"--diff-filter=ACMR",
|
|
50
|
+
"--src-prefix=a/",
|
|
51
|
+
"--dst-prefix=b/",
|
|
52
|
+
];
|
|
53
|
+
function git(root, args) {
|
|
54
|
+
return execFileSync("git", ["-c", "core.quotePath=false", ...args], {
|
|
55
|
+
cwd: root,
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function commit(root, rev, label) {
|
|
62
|
+
if (!rev || rev.startsWith("-"))
|
|
63
|
+
throw new ScanConfigError(`${label} ${JSON.stringify(rev)} is not a commit`);
|
|
64
|
+
try {
|
|
65
|
+
return git(root, ["rev-parse", "--verify", "-q", `${rev}^{commit}`]).trim();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new ScanConfigError(`${label} ${JSON.stringify(rev)} is not a commit in ${root}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** `<base>..<head>` or `<base>` (head = HEAD). A three-dot range is rejected: the net diff is always taken from the merge base. */
|
|
72
|
+
export function parseRange(range) {
|
|
73
|
+
if (range.includes("..."))
|
|
74
|
+
throw new ScanConfigError(`--range takes <base>..<head> or <base>, not ${JSON.stringify(range)}`);
|
|
75
|
+
const i = range.indexOf("..");
|
|
76
|
+
if (i < 0)
|
|
77
|
+
return { base: range, head: "HEAD" };
|
|
78
|
+
return { base: range.slice(0, i), head: range.slice(i + 2) || "HEAD" };
|
|
79
|
+
}
|
|
80
|
+
function warnAndDrop(load, warning, kind) {
|
|
81
|
+
try {
|
|
82
|
+
return load();
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (!(err instanceof kind))
|
|
86
|
+
throw err;
|
|
87
|
+
process.stderr.write(`${warning}\n`);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function rangeRules(root, base) {
|
|
92
|
+
const builtIn = compiledRules();
|
|
93
|
+
const repo = warnAndDrop(() => loadCommittedAllowlists(root, base), ALLOWLIST_IGNORED_WARNING, InvalidAllowlistError);
|
|
94
|
+
const extra = warnAndDrop(() => loadCommittedRules(root, base), RULES_IGNORED_WARNING, InvalidRepoRulesError);
|
|
95
|
+
return { ...builtIn, rules: [...builtIn.rules, ...extra], repo };
|
|
96
|
+
}
|
|
97
|
+
/** merge-base, "" when the two have no common history; any other git failure is an error. */
|
|
98
|
+
function mergeBase(root, base, head) {
|
|
99
|
+
const r = spawnSync("git", ["merge-base", base, head], { cwd: root, encoding: "utf8" });
|
|
100
|
+
if (r.status === 0)
|
|
101
|
+
return r.stdout.trim();
|
|
102
|
+
if (r.status === 1 && !r.stdout.trim() && !r.stderr.trim())
|
|
103
|
+
return "";
|
|
104
|
+
throw new ScanConfigError(`git merge-base failed: ${(r.stderr || String(r.error ?? "")).trim() || `exit ${r.status}`}`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Lines a merge introduced, from `git diff-tree --cc` output: a line counts
|
|
108
|
+
* only when every parent column is "+", i.e. it is in none of the parents.
|
|
109
|
+
*/
|
|
110
|
+
export function parseCombinedDiff(diff) {
|
|
111
|
+
const fragments = [];
|
|
112
|
+
let file = null;
|
|
113
|
+
let parents = 0;
|
|
114
|
+
let nextLine = 0;
|
|
115
|
+
let cur = null;
|
|
116
|
+
const flush = () => {
|
|
117
|
+
if (file && cur && cur.lines.length)
|
|
118
|
+
fragments.push({ file, text: cur.lines.join("\n"), startLine: cur.startLine });
|
|
119
|
+
cur = null;
|
|
120
|
+
};
|
|
121
|
+
for (const raw of diff.split("\n")) {
|
|
122
|
+
if (raw.startsWith("diff --cc ") || raw.startsWith("diff --combined ")) {
|
|
123
|
+
flush();
|
|
124
|
+
file = raw.replace(/^diff --(?:cc|combined) /, "");
|
|
125
|
+
parents = 0;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (parents === 0 && raw.startsWith("+++ ")) {
|
|
129
|
+
const p = raw.slice(4);
|
|
130
|
+
file = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const h = /^(@{3,}) .*?\+(\d+)(?:,\d+)? \1/.exec(raw);
|
|
134
|
+
if (h) {
|
|
135
|
+
flush();
|
|
136
|
+
parents = h[1].length - 1;
|
|
137
|
+
nextLine = Number(h[2]);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (parents === 0)
|
|
141
|
+
continue;
|
|
142
|
+
const cols = raw.slice(0, parents);
|
|
143
|
+
if (cols.length < parents) {
|
|
144
|
+
flush();
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (cols.includes("-")) {
|
|
148
|
+
flush();
|
|
149
|
+
continue;
|
|
150
|
+
} // a line removed from some parent: not in the result
|
|
151
|
+
if (/^\++$/.test(cols)) {
|
|
152
|
+
if (!cur)
|
|
153
|
+
cur = { lines: [], startLine: nextLine };
|
|
154
|
+
cur.lines.push(raw.slice(parents));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
flush();
|
|
158
|
+
}
|
|
159
|
+
nextLine++;
|
|
160
|
+
}
|
|
161
|
+
flush();
|
|
162
|
+
return fragments;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Binary files a merge commit changed relative to every parent. The combined
|
|
166
|
+
* diff only says "Binary files differ" for them, so their content is read
|
|
167
|
+
* from the merge's tree instead. Deleted files are left out.
|
|
168
|
+
*/
|
|
169
|
+
function mergeBinaryFiles(root, merge) {
|
|
170
|
+
const files = git(root, ["diff-tree", "-c", "--name-only", "--no-commit-id", "-r", merge]).split("\n").filter(Boolean);
|
|
171
|
+
return files.filter((file) => {
|
|
172
|
+
try {
|
|
173
|
+
// git's own binary test: a NUL byte in the first 8000 bytes of the blob.
|
|
174
|
+
const head = execFileSync("git", ["cat-file", "blob", `${merge}:${file}`], {
|
|
175
|
+
cwd: root, maxBuffer: MAX_BINARY_BYTES + 1024, stdio: ["ignore", "pipe", "ignore"],
|
|
176
|
+
}).subarray(0, 8000);
|
|
177
|
+
return head.includes(0);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
// Too large to read here: still a binary candidate, so binaryText warns about it.
|
|
181
|
+
if (err.code === "ENOBUFS")
|
|
182
|
+
return true;
|
|
183
|
+
return false; // deleted in the merge, or a submodule
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function patches(root, base, head) {
|
|
188
|
+
const out = [];
|
|
189
|
+
const mb = mergeBase(root, base, head);
|
|
190
|
+
// No common history: everything on head is new.
|
|
191
|
+
const net = mb ? git(root, ["diff", ...PATCH_ARGS, mb, head]) : git(root, ["show", "--format=", ...PATCH_ARGS, "--root", head]);
|
|
192
|
+
out.push({ rev: head, ...parseUnifiedDiff(net) });
|
|
193
|
+
const commits = git(root, ["rev-list", "--reverse", "--parents", `${base}..${head}`]).split("\n").filter(Boolean);
|
|
194
|
+
for (const line of commits) {
|
|
195
|
+
const [c, ...parents] = line.split(" ");
|
|
196
|
+
if (parents.length > 1) {
|
|
197
|
+
const cc = git(root, ["diff-tree", "--cc", "-p", "--no-commit-id", "--no-color", "--no-ext-diff", "--no-textconv", "--unified=0", "--src-prefix=a/", "--dst-prefix=b/", c]);
|
|
198
|
+
out.push({ commit: c, rev: c, fragments: parseCombinedDiff(cc), binaryFiles: mergeBinaryFiles(root, c) });
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
out.push({ commit: c, rev: c, ...parseUnifiedDiff(git(root, ["diff-tree", "-p", "--root", "--no-commit-id", ...PATCH_ARGS, c])) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
/** A binary blob's bytes as text (latin1 keeps every byte), or null when it is too large to read. */
|
|
207
|
+
function binaryText(root, rev, file) {
|
|
208
|
+
let size;
|
|
209
|
+
try {
|
|
210
|
+
size = Number(git(root, ["cat-file", "-s", `${rev}:${file}`]).trim());
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
if (!(size <= MAX_BINARY_BYTES))
|
|
216
|
+
return null;
|
|
217
|
+
const buf = execFileSync("git", ["cat-file", "blob", `${rev}:${file}`], { cwd: root, maxBuffer: MAX_BINARY_BYTES + 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
218
|
+
return buf.toString("latin1");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Findings for the lines added on <head> since <base>. A finding from a
|
|
222
|
+
* commit that the net diff no longer shows is named `<file>@<commit12>`, so
|
|
223
|
+
* the reader can find the version that held it.
|
|
224
|
+
*/
|
|
225
|
+
export function scanRange(cwd, range) {
|
|
226
|
+
const root = repoRoot(cwd);
|
|
227
|
+
const parsed = parseRange(range);
|
|
228
|
+
const base = commit(root, parsed.base, "range base");
|
|
229
|
+
const head = commit(root, parsed.head, "range head");
|
|
230
|
+
if (base === head) {
|
|
231
|
+
process.stderr.write(`${EMPTY_RANGE_WARNING}\n`);
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
if (!git(root, ["rev-list", "-n", "1", `${base}..${head}`]).trim()) {
|
|
235
|
+
throw new ScanConfigError(`range ${JSON.stringify(range)} has no commits: head is behind base (reversed range?)`);
|
|
236
|
+
}
|
|
237
|
+
const rules = rangeRules(root, base);
|
|
238
|
+
const out = [];
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
const tooLarge = new Set();
|
|
241
|
+
for (const patch of patches(root, base, head)) {
|
|
242
|
+
const found = [...patch.fragments.flatMap((f) => scanFragment(f, rules))];
|
|
243
|
+
for (const file of patch.binaryFiles) {
|
|
244
|
+
found.push(...scanPath(file, rules));
|
|
245
|
+
const text = binaryText(root, patch.rev, file);
|
|
246
|
+
if (text === null)
|
|
247
|
+
tooLarge.add(file);
|
|
248
|
+
else if (text)
|
|
249
|
+
found.push(...scanFragment({ file, text, startLine: 1 }, rules));
|
|
250
|
+
}
|
|
251
|
+
for (const f of found) {
|
|
252
|
+
// The same rule on the same file at the same line is one report. A
|
|
253
|
+
// repeat never hides the only finding, so a dirty range always fails.
|
|
254
|
+
const key = `${f.ruleId}\u0000${f.file}\u0000${f.line}`;
|
|
255
|
+
if (seen.has(key))
|
|
256
|
+
continue;
|
|
257
|
+
seen.add(key);
|
|
258
|
+
out.push(patch.commit ? { ...f, file: `${f.file}@${patch.commit.slice(0, 12)}` } : f);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const file of tooLarge) {
|
|
262
|
+
process.stderr.write(`HQ secret scan: warning: binary ${file} is larger than ${MAX_BINARY_BYTES} bytes; its content was not scanned\n`);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=range.js.map
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
|
|
4
4
|
* `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
|
|
5
5
|
* `condition`. They apply on top of the built-in rules and allowlists, to that
|
|
6
|
-
* repo only.
|
|
7
|
-
*
|
|
6
|
+
* repo only. `[extend]` and the rest of the file are ignored: a repo can allow
|
|
7
|
+
* known false positives, not switch built-in detection off. Repo `[[rules]]`
|
|
8
|
+
* are used by range scans only (`--range`, pr-check), and only add detection
|
|
9
|
+
* on top of the built-in rules (parseRepoRules below).
|
|
8
10
|
*
|
|
9
11
|
* The file is read from git, never the working tree:
|
|
10
12
|
* - The staged (index) copy applies only when it is identical to the copy
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
* scan exits 2.
|
|
34
36
|
*/
|
|
35
37
|
import { type Probe } from "./probes.js";
|
|
36
|
-
import { type CompiledAllowlist } from "./rules.js";
|
|
38
|
+
import { type CompiledAllowlist, type CompiledRule } from "./rules.js";
|
|
37
39
|
export declare class ScanConfigError extends Error {
|
|
38
40
|
}
|
|
39
41
|
/** The repo's .gitleaks.toml cannot be used; the scan ignores it (see the top of the file). */
|
|
@@ -68,4 +70,17 @@ export declare function loadRepoAllowlists(repoRoot: string): CompiledAllowlist[
|
|
|
68
70
|
* its own commits by adding an allowlist. An unknown rev is an error.
|
|
69
71
|
*/
|
|
70
72
|
export declare function loadCommittedAllowlists(repoRoot: string, rev?: string): CompiledAllowlist[];
|
|
73
|
+
/** The repo's [[rules]] cannot be used; a range scan warns and keeps the built-in rules only. */
|
|
74
|
+
export declare class InvalidRepoRulesError extends ScanConfigError {
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extra detection rules from the `[[rules]]` of a .gitleaks.toml. They are
|
|
78
|
+
* added to the built-in rules and can only report more: a repo rule cannot
|
|
79
|
+
* replace or disable a built-in one (a built-in id is refused, `[extend]` and
|
|
80
|
+
* `useDefault` are ignored), and a rule's own allowlists apply to that rule
|
|
81
|
+
* only. [] when the file has no rules.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseRepoRules(text: string, file?: string): CompiledRule[];
|
|
84
|
+
/** Repo rules from the copy of .gitleaks.toml committed at `rev`. [] when there is none. */
|
|
85
|
+
export declare function loadCommittedRules(repoRoot: string, rev: string): CompiledRule[];
|
|
71
86
|
//# sourceMappingURL=repo-config.d.ts.map
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
|
|
4
4
|
* `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
|
|
5
5
|
* `condition`. They apply on top of the built-in rules and allowlists, to that
|
|
6
|
-
* repo only.
|
|
7
|
-
*
|
|
6
|
+
* repo only. `[extend]` and the rest of the file are ignored: a repo can allow
|
|
7
|
+
* known false positives, not switch built-in detection off. Repo `[[rules]]`
|
|
8
|
+
* are used by range scans only (`--range`, pr-check), and only add detection
|
|
9
|
+
* on top of the built-in rules (parseRepoRules below).
|
|
8
10
|
*
|
|
9
11
|
* The file is read from git, never the working tree:
|
|
10
12
|
* - The staged (index) copy applies only when it is identical to the copy
|
|
@@ -361,6 +363,137 @@ export function loadCommittedAllowlists(repoRoot, rev = "HEAD") {
|
|
|
361
363
|
}
|
|
362
364
|
return revAllowlists(repoRoot, rev, parseExactAllowlists);
|
|
363
365
|
}
|
|
366
|
+
/** The repo's [[rules]] cannot be used; a range scan warns and keeps the built-in rules only. */
|
|
367
|
+
export class InvalidRepoRulesError extends ScanConfigError {
|
|
368
|
+
}
|
|
369
|
+
const RULE_KEYS = new Set(["id", "description", "regex", "path", "entropy", "secretGroup", "keywords", "allowlists", "tags"]);
|
|
370
|
+
const RULE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
371
|
+
/** Detection regexes are longer than allowlist ones; gitleaks' own rules run to a few hundred characters. */
|
|
372
|
+
const MAX_RULE_REGEX_LENGTH = 1024;
|
|
373
|
+
function failRules(file, where, msg) {
|
|
374
|
+
throw new InvalidRepoRulesError(`${file}: ${where}: ${msg}`);
|
|
375
|
+
}
|
|
376
|
+
function ruleRegex(file, where, key, src, flags = "") {
|
|
377
|
+
if (typeof src !== "string" || !src)
|
|
378
|
+
failRules(file, where, `${key} must be a non-empty string`);
|
|
379
|
+
if (src.length > MAX_RULE_REGEX_LENGTH)
|
|
380
|
+
failRules(file, where, `${key} is longer than ${MAX_RULE_REGEX_LENGTH} characters`);
|
|
381
|
+
if (NESTED_QUANTIFIER.test(src))
|
|
382
|
+
failRules(file, where, `${key} has a nested quantifier; simplify it`);
|
|
383
|
+
try {
|
|
384
|
+
return compileGoRegex(src, flags);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
failRules(file, where, `${key} is not a supported regex`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function ruleAllowlist(file, where, raw) {
|
|
391
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
392
|
+
failRules(file, where, "must be a table");
|
|
393
|
+
const t = raw;
|
|
394
|
+
for (const k of Object.keys(t))
|
|
395
|
+
if (!KEYS.has(k) || k === "targetRules")
|
|
396
|
+
failRules(file, where, `unknown key "${k}"`);
|
|
397
|
+
const list = (key) => {
|
|
398
|
+
const v = t[key];
|
|
399
|
+
if (v === undefined)
|
|
400
|
+
return undefined;
|
|
401
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string"))
|
|
402
|
+
failRules(file, where, `${key} must be a list of strings`);
|
|
403
|
+
return v;
|
|
404
|
+
};
|
|
405
|
+
const regexes = list("regexes");
|
|
406
|
+
const paths = list("paths");
|
|
407
|
+
regexes?.forEach((src, i) => ruleRegex(file, where, `regexes[${i}]`, src));
|
|
408
|
+
paths?.forEach((src, i) => ruleRegex(file, where, `paths[${i}]`, src));
|
|
409
|
+
const condition = typeof t.condition === "string" && t.condition.trim().toUpperCase() === "AND" ? "AND" : "OR";
|
|
410
|
+
const target = t.regexTarget;
|
|
411
|
+
if (target !== undefined && target !== "secret" && target !== "match" && target !== "line") {
|
|
412
|
+
failRules(file, where, 'regexTarget must be "secret", "match" or "line"');
|
|
413
|
+
}
|
|
414
|
+
return compileAllowlist({
|
|
415
|
+
condition,
|
|
416
|
+
regexTarget: target,
|
|
417
|
+
paths,
|
|
418
|
+
regexes,
|
|
419
|
+
stopwords: list("stopwords"),
|
|
420
|
+
commits: list("commits"),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Extra detection rules from the `[[rules]]` of a .gitleaks.toml. They are
|
|
425
|
+
* added to the built-in rules and can only report more: a repo rule cannot
|
|
426
|
+
* replace or disable a built-in one (a built-in id is refused, `[extend]` and
|
|
427
|
+
* `useDefault` are ignored), and a rule's own allowlists apply to that rule
|
|
428
|
+
* only. [] when the file has no rules.
|
|
429
|
+
*/
|
|
430
|
+
export function parseRepoRules(text, file = REPO_CONFIG_FILE) {
|
|
431
|
+
let doc;
|
|
432
|
+
try {
|
|
433
|
+
doc = parse(text);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
throw new InvalidRepoRulesError(`${file}: could not be parsed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
437
|
+
}
|
|
438
|
+
if (doc.rules === undefined)
|
|
439
|
+
return [];
|
|
440
|
+
if (!Array.isArray(doc.rules))
|
|
441
|
+
failRules(file, "rules", "must be an array of tables ([[rules]])");
|
|
442
|
+
const builtIn = new Set(compiledRules().rules.map((r) => r.id));
|
|
443
|
+
const seen = new Set();
|
|
444
|
+
return doc.rules.map((raw, i) => {
|
|
445
|
+
const where = `[[rules]] #${i + 1}`;
|
|
446
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
447
|
+
failRules(file, where, "must be a table");
|
|
448
|
+
const t = raw;
|
|
449
|
+
for (const k of Object.keys(t))
|
|
450
|
+
if (!RULE_KEYS.has(k))
|
|
451
|
+
failRules(file, where, `unknown key "${k}"`);
|
|
452
|
+
if (typeof t.id !== "string" || !RULE_ID.test(t.id))
|
|
453
|
+
failRules(file, where, "id must be 1-64 letters, digits, dots, dashes or underscores");
|
|
454
|
+
if (builtIn.has(t.id))
|
|
455
|
+
failRules(file, where, `id "${t.id}" is a built-in rule; a repo rule can add detection, not replace it`);
|
|
456
|
+
if (seen.has(t.id))
|
|
457
|
+
failRules(file, where, `id "${t.id}" is used twice`);
|
|
458
|
+
seen.add(t.id);
|
|
459
|
+
if (t.regex === undefined && t.path === undefined)
|
|
460
|
+
failRules(file, where, "needs regex or path");
|
|
461
|
+
if (t.description !== undefined && typeof t.description !== "string")
|
|
462
|
+
failRules(file, where, "description must be a string");
|
|
463
|
+
if (t.entropy !== undefined && (typeof t.entropy !== "number" || !(t.entropy >= 0)))
|
|
464
|
+
failRules(file, where, "entropy must be a number >= 0");
|
|
465
|
+
if (t.secretGroup !== undefined && (!Number.isInteger(t.secretGroup) || t.secretGroup < 0)) {
|
|
466
|
+
failRules(file, where, "secretGroup must be an integer >= 0");
|
|
467
|
+
}
|
|
468
|
+
if (t.keywords !== undefined && (!Array.isArray(t.keywords) || t.keywords.some((k) => typeof k !== "string" || !k))) {
|
|
469
|
+
failRules(file, where, "keywords must be a list of non-empty strings");
|
|
470
|
+
}
|
|
471
|
+
if (t.allowlists !== undefined && !Array.isArray(t.allowlists))
|
|
472
|
+
failRules(file, where, "allowlists must be an array of tables");
|
|
473
|
+
return {
|
|
474
|
+
id: t.id,
|
|
475
|
+
description: typeof t.description === "string" && t.description ? t.description : `Repo rule ${t.id}`,
|
|
476
|
+
regex: t.regex === undefined ? undefined : ruleRegex(file, where, "regex", t.regex, "g"),
|
|
477
|
+
path: t.path === undefined ? undefined : ruleRegex(file, where, "path", t.path),
|
|
478
|
+
entropy: t.entropy,
|
|
479
|
+
secretGroup: t.secretGroup,
|
|
480
|
+
keywords: (t.keywords ?? []).map((k) => k.toLowerCase()),
|
|
481
|
+
allowlists: (t.allowlists ?? []).map((a, j) => ruleAllowlist(file, `${where} allowlists #${j + 1}`, a)),
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/** Repo rules from the copy of .gitleaks.toml committed at `rev`. [] when there is none. */
|
|
486
|
+
export function loadCommittedRules(repoRoot, rev) {
|
|
487
|
+
const e = entry(git(repoRoot, ["ls-tree", rev, "--", REPO_CONFIG_FILE]), 2);
|
|
488
|
+
if (!e)
|
|
489
|
+
return [];
|
|
490
|
+
if (e.mode === "120000")
|
|
491
|
+
throw new InvalidRepoRulesError(`${REPO_CONFIG_FILE} (${rev}) is a symlink; commit a regular file`);
|
|
492
|
+
const text = git(repoRoot, ["cat-file", "blob", e.blob]);
|
|
493
|
+
if (text === null)
|
|
494
|
+
throw new InvalidRepoRulesError(`${REPO_CONFIG_FILE} (${rev}) could not be read from git`);
|
|
495
|
+
return parseRepoRules(text, `${REPO_CONFIG_FILE} (${rev})`);
|
|
496
|
+
}
|
|
364
497
|
function headAllowlists(repoRoot, parser = parseRepoAllowlists) {
|
|
365
498
|
if (git(repoRoot, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]) === null)
|
|
366
499
|
return [];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type SyncCoreVaultKind = "personal" | "company";
|
|
2
|
+
export type SyncCoreVaultPhase = "warming" | "full";
|
|
3
|
+
export interface SyncCoreVault {
|
|
4
|
+
uid: string;
|
|
5
|
+
kind: SyncCoreVaultKind;
|
|
6
|
+
slug?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SyncCoreVaultPlan extends SyncCoreVault {
|
|
9
|
+
phase: SyncCoreVaultPhase;
|
|
10
|
+
engineHome: string;
|
|
11
|
+
binaryPath: string;
|
|
12
|
+
release: string;
|
|
13
|
+
legacyOffSince?: string;
|
|
14
|
+
blockedReason?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const SYNC_CORE_REQUESTS_PER_DAY = 20000;
|
|
17
|
+
export declare function syncCoreEngineHome(home: string, vaultUid: string): string;
|
|
18
|
+
export declare function syncCoreUnitId(vaultUid: string): string;
|
|
19
|
+
export declare function isSyncCoreVaultUid(value: unknown): value is string;
|
|
20
|
+
export declare function engineConfigToml(vault: SyncCoreVault, hqRoot: string, phase: SyncCoreVaultPhase): string;
|
|
21
|
+
export interface WriteEngineHomeOptions {
|
|
22
|
+
home: string;
|
|
23
|
+
hqRoot: string;
|
|
24
|
+
vault: SyncCoreVault;
|
|
25
|
+
phase: SyncCoreVaultPhase;
|
|
26
|
+
tokenFile: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function writeEngineHome(opts: WriteEngineHomeOptions): {
|
|
29
|
+
engineHome: string;
|
|
30
|
+
changed: boolean;
|
|
31
|
+
};
|
|
32
|
+
export interface SyncCorePlanFigures {
|
|
33
|
+
at: string;
|
|
34
|
+
uploads: number;
|
|
35
|
+
downloads: number;
|
|
36
|
+
deletes: number;
|
|
37
|
+
remoteRows: number;
|
|
38
|
+
localFiles: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function engineReadOnlyPlan(engineHome: string, vaultUid: string): SyncCorePlanFigures | null;
|
|
41
|
+
export declare function clearEngineStatus(home: string, vaultUid: string): void;
|
|
42
|
+
export declare function removeEngineOwnership(home: string, vaultUid: string): void;
|
|
43
|
+
export declare function inspectEngineHome(home: string, hqRoot: string | undefined, plan: SyncCoreVaultPlan, platform: NodeJS.Platform): SyncCoreVaultPlan;
|
|
44
|
+
//# sourceMappingURL=engine-home.d.ts.map
|