codiedev 0.9.80 → 0.9.81
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/dist/cli.js +47 -0
- package/dist/commands/agentTemplates.generated.d.ts +11 -0
- package/dist/commands/agentTemplates.generated.js +139 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.js +1130 -0
- package/dist/commands/agentsSdk.generated.d.ts +4 -0
- package/dist/commands/agentsSdk.generated.js +10 -0
- package/dist/commands/scrub.d.ts +11 -0
- package/dist/commands/scrub.js +102 -0
- package/dist/hook.js +18 -0
- package/dist/knownSecrets.d.ts +43 -0
- package/dist/knownSecrets.js +241 -0
- package/dist/localScrub.d.ts +172 -0
- package/dist/localScrub.js +820 -0
- package/dist/localScrubReport.d.ts +83 -0
- package/dist/localScrubReport.js +268 -0
- package/dist/scrub/extraRules.d.ts +4 -0
- package/dist/scrub/extraRules.js +17 -0
- package/dist/scrub/rules.d.ts +86 -0
- package/dist/scrub/rules.js +645 -0
- package/dist/scrub/scrubProviderRules.d.ts +14 -0
- package/dist/scrub/scrubProviderRules.js +149 -0
- package/package.json +5 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type SessionStatus = "removed" | "not_removed";
|
|
2
|
+
export type NotRemovedReason = "cursor_chat_store";
|
|
3
|
+
export interface KeyHit {
|
|
4
|
+
rule: string;
|
|
5
|
+
/** Last 4 alphanumerics, same as the server's secretFindings hint. */
|
|
6
|
+
hint: string;
|
|
7
|
+
count: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionDetection {
|
|
10
|
+
harness: string;
|
|
11
|
+
/** Stable id of the session within its harness (uuid, file name, chat id). */
|
|
12
|
+
sessionKey: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
keys: KeyHit[];
|
|
16
|
+
status: SessionStatus;
|
|
17
|
+
reason?: NotRemovedReason;
|
|
18
|
+
}
|
|
19
|
+
/** Keys in `text` as (rule, last 4, count). Never returns a value. */
|
|
20
|
+
export declare function keysIn(text: string): KeyHit[];
|
|
21
|
+
export declare function mergeKeys(a: KeyHit[], b: KeyHit[]): KeyHit[];
|
|
22
|
+
/** Title and working folder of a session file, from its cleaned text. */
|
|
23
|
+
export declare function describeText(kind: string, cleaned: string): {
|
|
24
|
+
title?: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Which session a file belongs to, per harness layout. */
|
|
28
|
+
export declare function sessionKeyOf(harness: string, file: string): string;
|
|
29
|
+
/** Drop titles and folders for sessions the person excluded from capture.
|
|
30
|
+
* With any exclusion set, a session whose folder we can't tell is treated
|
|
31
|
+
* as excluded too. */
|
|
32
|
+
export declare function redactExcluded(s: SessionDetection, ignored: string[] | undefined): SessionDetection;
|
|
33
|
+
/** A random id for this machine, kept in the scrub's state folder. */
|
|
34
|
+
export declare function machineId(dir: string): string;
|
|
35
|
+
export interface ReportPayload {
|
|
36
|
+
machine: {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
};
|
|
40
|
+
at: number;
|
|
41
|
+
complete: boolean;
|
|
42
|
+
scanned: number;
|
|
43
|
+
cleaned: number;
|
|
44
|
+
redactions: number;
|
|
45
|
+
errors: number;
|
|
46
|
+
skippedRecent: number;
|
|
47
|
+
skippedLarge: number;
|
|
48
|
+
byHarness: Array<{
|
|
49
|
+
harness: string;
|
|
50
|
+
files: number;
|
|
51
|
+
redactions: number;
|
|
52
|
+
}>;
|
|
53
|
+
leftInCursor: {
|
|
54
|
+
keys: number;
|
|
55
|
+
chats: number;
|
|
56
|
+
};
|
|
57
|
+
sessions: Array<Omit<SessionDetection, "cwd">>;
|
|
58
|
+
}
|
|
59
|
+
export declare function buildReport(input: {
|
|
60
|
+
machineId: string;
|
|
61
|
+
at: number;
|
|
62
|
+
complete: boolean;
|
|
63
|
+
summary: {
|
|
64
|
+
scanned: number;
|
|
65
|
+
cleaned: number;
|
|
66
|
+
redactions: number;
|
|
67
|
+
errors: number;
|
|
68
|
+
skippedRecent: number;
|
|
69
|
+
skippedLarge: number;
|
|
70
|
+
byHarness: Partial<Record<string, {
|
|
71
|
+
files: number;
|
|
72
|
+
redactions: number;
|
|
73
|
+
}>>;
|
|
74
|
+
leftInCursor: {
|
|
75
|
+
keys: number;
|
|
76
|
+
chats: number;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
sessions: SessionDetection[];
|
|
80
|
+
ignoredPaths?: string[];
|
|
81
|
+
}): ReportPayload;
|
|
82
|
+
/** Best-effort POST to Codiedev. Never throws. */
|
|
83
|
+
export declare function sendReport(backendUrl: string, token: string, payload: ReportPayload, cliVersion: string, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.keysIn = keysIn;
|
|
37
|
+
exports.mergeKeys = mergeKeys;
|
|
38
|
+
exports.describeText = describeText;
|
|
39
|
+
exports.sessionKeyOf = sessionKeyOf;
|
|
40
|
+
exports.redactExcluded = redactExcluded;
|
|
41
|
+
exports.machineId = machineId;
|
|
42
|
+
exports.buildReport = buildReport;
|
|
43
|
+
exports.sendReport = sendReport;
|
|
44
|
+
// What the local scrub tells Codiedev, for the Security tab's audit: per
|
|
45
|
+
// session, when keys were found, which (type + last 4, never the key), and
|
|
46
|
+
// whether they were removed or left (and why). Titles are the harness's own
|
|
47
|
+
// title or the first prompt, taken from the CLEANED text so a title never
|
|
48
|
+
// carries a key, and dropped for sessions in folders excluded from capture.
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const os = __importStar(require("os"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const rules_1 = require("./scrub/rules");
|
|
54
|
+
const extraRules_1 = require("./scrub/extraRules");
|
|
55
|
+
const captureExclusions_1 = require("./captureExclusions");
|
|
56
|
+
const MAX_KEYS_PER_SESSION = 20;
|
|
57
|
+
const TITLE_LEN = 80;
|
|
58
|
+
/** Keys in `text` as (rule, last 4, count). Never returns a value. */
|
|
59
|
+
function keysIn(text) {
|
|
60
|
+
const { redactions } = (0, extraRules_1.scrub)(text);
|
|
61
|
+
if (redactions.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const hints = new Map();
|
|
64
|
+
// secretFindings gives each redaction's hint the same way the server does.
|
|
65
|
+
for (const r of redactions) {
|
|
66
|
+
const [f] = (0, rules_1.secretFindings)(text, [r]);
|
|
67
|
+
if (!f)
|
|
68
|
+
continue;
|
|
69
|
+
const k = `${f.rule}\u0000${f.hint}`;
|
|
70
|
+
const hit = hints.get(k) ?? { rule: f.rule, hint: f.hint, count: 0 };
|
|
71
|
+
hit.count += 1;
|
|
72
|
+
hints.set(k, hit);
|
|
73
|
+
}
|
|
74
|
+
return [...hints.values()].slice(0, MAX_KEYS_PER_SESSION);
|
|
75
|
+
}
|
|
76
|
+
function mergeKeys(a, b) {
|
|
77
|
+
const out = new Map(a.map((k) => [`${k.rule}\u0000${k.hint}`, { ...k }]));
|
|
78
|
+
for (const k of b) {
|
|
79
|
+
const id = `${k.rule}\u0000${k.hint}`;
|
|
80
|
+
const cur = out.get(id);
|
|
81
|
+
if (cur)
|
|
82
|
+
cur.count += k.count;
|
|
83
|
+
else
|
|
84
|
+
out.set(id, { ...k });
|
|
85
|
+
}
|
|
86
|
+
return [...out.values()].slice(0, MAX_KEYS_PER_SESSION);
|
|
87
|
+
}
|
|
88
|
+
function tidy(s) {
|
|
89
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
90
|
+
if (!t)
|
|
91
|
+
return undefined;
|
|
92
|
+
return t.length > TITLE_LEN ? `${t.slice(0, TITLE_LEN - 1)}…` : t;
|
|
93
|
+
}
|
|
94
|
+
/** Injected context the harnesses put in user-role messages. */
|
|
95
|
+
const NOT_A_PROMPT = /^(?:<|# AGENTS\.md|Caveat:|\[Request interrupted)/;
|
|
96
|
+
function textOf(content) {
|
|
97
|
+
if (typeof content === "string")
|
|
98
|
+
return content;
|
|
99
|
+
if (Array.isArray(content)) {
|
|
100
|
+
for (const part of content) {
|
|
101
|
+
const p = part;
|
|
102
|
+
if (p && typeof p.text === "string" && (p.type === undefined || /text/.test(String(p.type))))
|
|
103
|
+
return p.text;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/** The first thing the person typed, across every harness's JSONL shape. */
|
|
109
|
+
function firstPrompt(obj) {
|
|
110
|
+
const msg = obj.message;
|
|
111
|
+
const payload = obj.payload;
|
|
112
|
+
const data = obj.data;
|
|
113
|
+
const candidates = [];
|
|
114
|
+
if (msg?.role === "user")
|
|
115
|
+
candidates.push(msg.content); // Claude Code, pi
|
|
116
|
+
if (obj.role === "user")
|
|
117
|
+
candidates.push(obj.message?.content ?? obj.content); // Cursor
|
|
118
|
+
if (payload?.type === "message" && payload.role === "user")
|
|
119
|
+
candidates.push(payload.content); // Codex
|
|
120
|
+
if (obj.type === "user.message")
|
|
121
|
+
candidates.push(data?.content); // Copilot
|
|
122
|
+
for (const c of candidates) {
|
|
123
|
+
const t = textOf(c);
|
|
124
|
+
if (t && !NOT_A_PROMPT.test(t.trimStart()))
|
|
125
|
+
return t;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/** Title and working folder of a session file, from its cleaned text. */
|
|
130
|
+
function describeText(kind, cleaned) {
|
|
131
|
+
let title;
|
|
132
|
+
let named;
|
|
133
|
+
let cwd;
|
|
134
|
+
if (kind === "yaml") {
|
|
135
|
+
for (const line of cleaned.split(/\r?\n/)) {
|
|
136
|
+
const m = line.match(/^(summary|name|cwd):\s*(.*)$/);
|
|
137
|
+
if (!m)
|
|
138
|
+
continue;
|
|
139
|
+
const v = m[2].trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
140
|
+
if (m[1] === "cwd")
|
|
141
|
+
cwd ??= v;
|
|
142
|
+
else
|
|
143
|
+
named ??= v;
|
|
144
|
+
}
|
|
145
|
+
return { title: named ? tidy(named) : undefined, cwd };
|
|
146
|
+
}
|
|
147
|
+
const docs = [];
|
|
148
|
+
if (kind === "jsonl") {
|
|
149
|
+
const lines = cleaned.split("\n");
|
|
150
|
+
for (let i = 0; i < lines.length && i < 2000; i++) {
|
|
151
|
+
if (!lines[i].trim())
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
docs.push(JSON.parse(lines[i]));
|
|
155
|
+
}
|
|
156
|
+
catch { /* not JSON */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (kind === "json") {
|
|
160
|
+
try {
|
|
161
|
+
const d = JSON.parse(cleaned);
|
|
162
|
+
if (Array.isArray(d))
|
|
163
|
+
docs.push(...d);
|
|
164
|
+
else
|
|
165
|
+
docs.push(d);
|
|
166
|
+
}
|
|
167
|
+
catch { /* not JSON */ }
|
|
168
|
+
}
|
|
169
|
+
for (const d of docs) {
|
|
170
|
+
const o = d;
|
|
171
|
+
if (!o || typeof o !== "object")
|
|
172
|
+
continue;
|
|
173
|
+
// Harness-provided titles win over the first prompt.
|
|
174
|
+
if (!named && o.type === "summary" && typeof o.summary === "string")
|
|
175
|
+
named = o.summary; // Claude Code
|
|
176
|
+
if (!named && typeof o.thread_name === "string")
|
|
177
|
+
named = o.thread_name; // Codex
|
|
178
|
+
if (!cwd) {
|
|
179
|
+
const c = o.cwd ?? o.payload?.cwd;
|
|
180
|
+
if (typeof c === "string")
|
|
181
|
+
cwd = c;
|
|
182
|
+
}
|
|
183
|
+
if (!title) {
|
|
184
|
+
// OpenCode capture copy: [{ info: { role }, parts: [{ type: "text", text }] }]
|
|
185
|
+
const info = o.info;
|
|
186
|
+
const t = info?.role === "user" ? textOf(o.parts) : firstPrompt(o);
|
|
187
|
+
if (t)
|
|
188
|
+
title = t;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { title: tidy(named ?? title ?? ""), cwd };
|
|
192
|
+
}
|
|
193
|
+
/** Which session a file belongs to, per harness layout. */
|
|
194
|
+
function sessionKeyOf(harness, file) {
|
|
195
|
+
const base = path.basename(file);
|
|
196
|
+
if (base === "history.jsonl")
|
|
197
|
+
return "history";
|
|
198
|
+
if (harness === "copilot")
|
|
199
|
+
return path.basename(path.dirname(file)); // session-state/<id>/…
|
|
200
|
+
const uuid = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
201
|
+
if (uuid)
|
|
202
|
+
return uuid[1];
|
|
203
|
+
return base.replace(/\.(jsonl|json|ya?ml|txt)$/, "");
|
|
204
|
+
}
|
|
205
|
+
/** Drop titles and folders for sessions the person excluded from capture.
|
|
206
|
+
* With any exclusion set, a session whose folder we can't tell is treated
|
|
207
|
+
* as excluded too. */
|
|
208
|
+
function redactExcluded(s, ignored) {
|
|
209
|
+
if (!ignored?.length)
|
|
210
|
+
return s;
|
|
211
|
+
if (s.cwd && !(0, captureExclusions_1.isCaptureIgnored)(s.cwd, ignored))
|
|
212
|
+
return s;
|
|
213
|
+
const { title: _t, cwd: _c, ...rest } = s;
|
|
214
|
+
return rest;
|
|
215
|
+
}
|
|
216
|
+
/** A random id for this machine, kept in the scrub's state folder. */
|
|
217
|
+
function machineId(dir) {
|
|
218
|
+
const file = path.join(dir, "machine-id");
|
|
219
|
+
try {
|
|
220
|
+
const id = fs.readFileSync(file, "utf8").trim();
|
|
221
|
+
if (/^[0-9a-f-]{36}$/.test(id))
|
|
222
|
+
return id;
|
|
223
|
+
}
|
|
224
|
+
catch { /* first run */ }
|
|
225
|
+
const id = crypto.randomUUID();
|
|
226
|
+
try {
|
|
227
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
228
|
+
fs.writeFileSync(file, id, { mode: 0o600 });
|
|
229
|
+
}
|
|
230
|
+
catch { /* fall back to a per-run id */ }
|
|
231
|
+
return id;
|
|
232
|
+
}
|
|
233
|
+
const MAX_SESSIONS = 200;
|
|
234
|
+
function buildReport(input) {
|
|
235
|
+
const s = input.summary;
|
|
236
|
+
return {
|
|
237
|
+
machine: { id: input.machineId, name: os.hostname().slice(0, 80) },
|
|
238
|
+
at: input.at,
|
|
239
|
+
complete: input.complete,
|
|
240
|
+
scanned: s.scanned,
|
|
241
|
+
cleaned: s.cleaned,
|
|
242
|
+
redactions: s.redactions,
|
|
243
|
+
errors: s.errors,
|
|
244
|
+
skippedRecent: s.skippedRecent,
|
|
245
|
+
skippedLarge: s.skippedLarge,
|
|
246
|
+
byHarness: Object.entries(s.byHarness).map(([harness, t]) => ({ harness, files: t.files, redactions: t.redactions })),
|
|
247
|
+
leftInCursor: s.leftInCursor,
|
|
248
|
+
sessions: input.sessions
|
|
249
|
+
.slice(0, MAX_SESSIONS)
|
|
250
|
+
.map((d) => redactExcluded(d, input.ignoredPaths))
|
|
251
|
+
.map(({ cwd: _cwd, ...rest }) => rest),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Best-effort POST to Codiedev. Never throws. */
|
|
255
|
+
async function sendReport(backendUrl, token, payload, cliVersion, fetchImpl = fetch) {
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetchImpl(`${backendUrl}/api/cli/localScrub`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "X-Codiedev-Cli-Version": cliVersion },
|
|
260
|
+
body: JSON.stringify(payload),
|
|
261
|
+
signal: AbortSignal.timeout(15_000),
|
|
262
|
+
});
|
|
263
|
+
return res.ok;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setExtraRules = setExtraRules;
|
|
4
|
+
exports.scrub = scrub;
|
|
5
|
+
// Rules added for this run of the laptop scrub on top of the built-in ones:
|
|
6
|
+
// the company's own token formats (from the server) and the exact values of
|
|
7
|
+
// this person's own secrets (knownSecrets.ts). Set once by the detached
|
|
8
|
+
// scrub child before it runs; empty everywhere else.
|
|
9
|
+
const rules_1 = require("./rules");
|
|
10
|
+
let extra = [];
|
|
11
|
+
function setExtraRules(rules) {
|
|
12
|
+
extra = rules;
|
|
13
|
+
}
|
|
14
|
+
/** scrubSecrets with this run's extra rules. */
|
|
15
|
+
function scrub(text) {
|
|
16
|
+
return extra.length ? (0, rules_1.scrubSecrets)(text, { extraRules: extra }) : (0, rules_1.scrubSecrets)(text);
|
|
17
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface Redaction {
|
|
2
|
+
rule: string;
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ScrubResult {
|
|
7
|
+
text: string;
|
|
8
|
+
redactions: Redaction[];
|
|
9
|
+
}
|
|
10
|
+
/** Rules a caller adds for one scrub (a company's own token formats). */
|
|
11
|
+
export interface ExtraRule {
|
|
12
|
+
name: string;
|
|
13
|
+
re: RegExp;
|
|
14
|
+
keywords?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/** Shannon entropy in bits per character, as Gitleaks measures it. */
|
|
17
|
+
export declare function shannonEntropy(s: string): number;
|
|
18
|
+
export declare function scrubSecrets(input: string, opts?: {
|
|
19
|
+
extraRules?: readonly ExtraRule[];
|
|
20
|
+
}): ScrubResult;
|
|
21
|
+
/** Rewrite `input` with each span replaced by `[REDACTED:<rule>]`. Spans must
|
|
22
|
+
* be sorted by start and not overlap. One forward pass: repeated
|
|
23
|
+
* slice+concat would copy the whole string per redaction (quadratic). */
|
|
24
|
+
export declare function redactSpans(input: string, spans: readonly Redaction[]): string;
|
|
25
|
+
/** Where in a session a secret appeared. */
|
|
26
|
+
export type SecretWhere = "prompt" | "tool_call" | "commit" | "tool_output" | "assistant" | "unknown";
|
|
27
|
+
export interface SecretLocation {
|
|
28
|
+
where: SecretWhere;
|
|
29
|
+
/** Tool or MCP tool name for tool calls/outputs when the transcript has it. */
|
|
30
|
+
tool?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SecretFinding {
|
|
33
|
+
rule: string;
|
|
34
|
+
/** Last four alphanumeric characters of the redacted value — enough for a
|
|
35
|
+
* person to recognise which key to rotate, never enough to use it. */
|
|
36
|
+
hint: string;
|
|
37
|
+
/** Distinct places in the session the key appeared. */
|
|
38
|
+
locations: SecretLocation[];
|
|
39
|
+
/** The variable or field it was assigned to (`X_BEARER_TOKEN`), when the
|
|
40
|
+
* text shows one. A name, never the value. */
|
|
41
|
+
name?: string;
|
|
42
|
+
/** The file it was written to or read from (`altcap-bot/.env`), when the
|
|
43
|
+
* text shows one: last two path parts only. */
|
|
44
|
+
file?: string;
|
|
45
|
+
}
|
|
46
|
+
/** What a caller may check before a key becomes a finding (exceptions).
|
|
47
|
+
* `value` is the key itself: use it to compare, never store it. */
|
|
48
|
+
export interface KeyContext {
|
|
49
|
+
rule: string;
|
|
50
|
+
value: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
file?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare const GENERIC_RULES: ReadonlySet<string>;
|
|
55
|
+
/** Values that share a key's shape but are test or sample values: words
|
|
56
|
+
* joined by _ or - with only digits or one repeated character between
|
|
57
|
+
* (`cdv_local_pilot_token_2026`, `…_token_000000`), a long counting run
|
|
58
|
+
* (`sk-1234567890…`), or a long repeated tail. Still removed from sessions;
|
|
59
|
+
* just not listed as a key to rotate. */
|
|
60
|
+
export declare function looksLikeTestValue(value: string): boolean;
|
|
61
|
+
/** One finding per distinct (rule, last-4) in a scrub result, with every
|
|
62
|
+
* distinct place it appeared. */
|
|
63
|
+
export declare function secretFindings(input: string, redactions: Redaction[], opts?: {
|
|
64
|
+
skip?: (k: KeyContext) => boolean;
|
|
65
|
+
}): SecretFinding[];
|
|
66
|
+
export interface PossibleSecret {
|
|
67
|
+
label: string;
|
|
68
|
+
prefix?: string;
|
|
69
|
+
hint: string;
|
|
70
|
+
length: number;
|
|
71
|
+
shape: "hex" | "base64" | "alnum";
|
|
72
|
+
snippet: string;
|
|
73
|
+
locations: SecretLocation[];
|
|
74
|
+
/** Spans in the text this was read from, for redaction once confirmed. */
|
|
75
|
+
spans: Array<{
|
|
76
|
+
start: number;
|
|
77
|
+
end: number;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export declare function possibleSecrets(text: string, limit?: number): PossibleSecret[];
|
|
81
|
+
/** Redact every occurrence of an already-reviewed possible secret. */
|
|
82
|
+
export declare function redactPossible(text: string, match: {
|
|
83
|
+
label: string;
|
|
84
|
+
hint: string;
|
|
85
|
+
length: number;
|
|
86
|
+
}, rule?: string): ScrubResult;
|