failproofai 1.0.4-beta.2 → 1.0.4-beta.6
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
- package/.next/standalone/.next/server/app/audit/page.js +3 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1lh7m5d._.js → [externals]__0v1hz15._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1rqkg_y._.js → [externals]__1nb206a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_08w6xzm._.js → _0otft92._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__17xnxlx._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__1ckpmkk._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00-s7h8._.js → [root-of-the-server]__1fvfw59._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ui5ur8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_02b3i98._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0oq1dh6._.js → _04ng_6x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
- package/.next/standalone/.next/static/chunks/{1pb1oztsbwcss.js → 07c6l1lq5_69b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0o-hh5_turzlz.css → 0jpd8dv2vk930.css} +1 -1
- package/.next/standalone/.next/static/chunks/{32spub4wqjem-.js → 12bsgz8czvcnn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3m4upvybtrexd.js → 1gn08w2pgqh09.js} +1 -1
- package/.next/standalone/.next/static/chunks/{010bv1w6j171t.js → 1xgq22tri86an.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2aquitk72k2op.js → 20xkiqzvfkuvm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bi_1y0a_smt7.js → 2a84ug32tzik2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2ej3b8gk5ittu.js → 32m2ryfankf_2.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ioub_lpen-sq.js +1 -0
- package/.next/standalone/.next/static/chunks/{0wz8yftk18ts2.js → 3jxd133cw2vm3.js} +1 -1
- package/.next/standalone/.next/static/chunks/44ncu88eq6z71.js +1 -0
- package/.next/standalone/app/actions/get-leaks.ts +5 -1
- package/.next/standalone/app/actions/update-scheduled-audit.ts +4 -56
- package/.next/standalone/app/api/auth/status/route.ts +3 -2
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +29 -2
- package/.next/standalone/app/audit/_components/leak-section.tsx +15 -2
- package/.next/standalone/app/audit/audit-styles.css +18 -0
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +13 -39
- package/.next/standalone/lib/auth/api-server-client.ts +6 -6
- package/.next/standalone/lib/codex-sessions.ts +20 -4
- package/.next/standalone/lib/copilot-sessions.ts +32 -1
- package/.next/standalone/lib/cursor-sessions.ts +62 -5
- package/.next/standalone/lib/sqlite-reader.ts +78 -1
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -1
- package/dist/cli.mjs +1191 -1130
- package/dist/worker.mjs +198 -359
- package/lib/auth/api-server-client.ts +6 -6
- package/lib/codex-sessions.ts +20 -4
- package/lib/copilot-sessions.ts +32 -1
- package/lib/cursor-sessions.ts +62 -5
- package/lib/sqlite-reader.ts +78 -1
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -4
- package/src/audit/cache.ts +6 -0
- package/src/audit/cli-adapters/claude.ts +13 -0
- package/src/audit/cli-login.ts +4 -1
- package/src/audit/cli.ts +67 -51
- package/src/audit/harm-report.ts +12 -6
- package/src/audit/index.ts +151 -8
- package/src/audit/leak-fingerprint.ts +2 -4
- package/src/audit/leak-record.ts +13 -9
- package/src/audit/leak-scan.ts +20 -44
- package/src/audit/leak-store.ts +5 -2
- package/src/audit/machine-store.ts +8 -9
- package/src/audit/macos-notifier.ts +2 -2
- package/src/audit/redact-example.ts +62 -5
- package/src/audit/report-harm.ts +31 -46
- package/src/audit/schedule-cli.ts +17 -33
- package/src/audit/types.ts +11 -0
- package/src/hooks/configure-wizard.ts +28 -0
- package/src/hooks/fp-config.ts +29 -54
- package/src/hooks/handler.ts +1 -59
- package/src/hooks/integrations.ts +4 -1
- package/src/hooks/notice.ts +23 -154
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +0 -23
- package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +0 -1
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +0 -1
- package/src/audit/leak-notice.ts +0 -161
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_ssgManifest.js +0 -0
package/src/audit/leak-scan.ts
CHANGED
|
@@ -24,18 +24,26 @@
|
|
|
24
24
|
* them apart so the report can tell a user which of the two happened.
|
|
25
25
|
*/
|
|
26
26
|
import { SECRET_PATTERNS } from "../hooks/builtin-policies";
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cache key for the credential detector.
|
|
30
|
+
*
|
|
31
|
+
* The transcript cache used to key only on builtin policies and behavioural
|
|
32
|
+
* audit detectors. Changing THIS scanner therefore kept serving the old
|
|
33
|
+
* `result.leaks` forever, which made a precision fix appear to do nothing on a
|
|
34
|
+
* warm machine. Bump this whenever matching or suppression semantics change.
|
|
35
|
+
*/
|
|
36
|
+
export const LEAK_SCAN_VERSION = 6;
|
|
28
37
|
|
|
29
38
|
/** One credential value found in one event. */
|
|
30
39
|
export interface SecretMatch {
|
|
31
40
|
/** The raw value. Held in memory only — the caller fingerprints it and must
|
|
32
41
|
* never persist or transmit this. */
|
|
33
42
|
value: string;
|
|
34
|
-
/** The identifier it was assigned to, when
|
|
35
|
-
*
|
|
36
|
-
* pattern) this is the only actionable label there is. */
|
|
43
|
+
/** The identifier it was assigned to, when a recognized shaped value also
|
|
44
|
+
* appeared in an assignment. Used only as an actionable label. */
|
|
37
45
|
name: string | null;
|
|
38
|
-
/** Which
|
|
46
|
+
/** Which recognizable credential shape found it. */
|
|
39
47
|
rule: string;
|
|
40
48
|
/** Whether the value carried a recognised vendor shape. Drives the record's
|
|
41
49
|
* confidence, and therefore whether an alert may name a console. */
|
|
@@ -69,36 +77,6 @@ export interface SecretMatch {
|
|
|
69
77
|
const ASSIGNMENT_RE =
|
|
70
78
|
/(["']?)\b([A-Za-z_][A-Za-z0-9_]{0,127})\1([ \t]*(?::(?!\/\/)|=)[ \t]*)("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
|
|
71
79
|
|
|
72
|
-
/** Shortest value worth reporting. Below this it is a flag, a boolean or a
|
|
73
|
-
* placeholder far more often than a credential. */
|
|
74
|
-
const MIN_VALUE_LEN = 8;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Values that are structurally incapable of being a leaked credential.
|
|
78
|
-
*
|
|
79
|
-
* Deliberately structural, never entropy: the corpus's one confirmed-live
|
|
80
|
-
* password measures 3.19 bits per character while 1.3 million UUIDs in the same
|
|
81
|
-
* corpus sit at 3.72, so any entropy threshold that catches the password also
|
|
82
|
-
* catches every UUID. These tests are decisive instead — a `$VAR` reference
|
|
83
|
-
* contains no secret by construction, and `process.env.X` is the CORRECT secure
|
|
84
|
-
* form, which the redactor currently flags 7,870 times.
|
|
85
|
-
*/
|
|
86
|
-
function isNotACredential(value: string): boolean {
|
|
87
|
-
if (value.length < MIN_VALUE_LEN) return true;
|
|
88
|
-
// Shell/template indirection: the value is a reference, not a secret.
|
|
89
|
-
if (/^[$<{(`%]/.test(value)) return true;
|
|
90
|
-
if (value.includes("${") || value.includes("{{") || value.includes("%(")) return true;
|
|
91
|
-
// A code expression — `process.env.DISCORD_TOKEN` is how you SHOULD do it.
|
|
92
|
-
if (/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)+$/.test(value)) return true;
|
|
93
|
-
// Already masked, by us or by anyone else.
|
|
94
|
-
if (value.includes("[REDACTED") || /^[•*#.]+$/.test(value)) return true;
|
|
95
|
-
if (/(.)\1{5,}/.test(value)) return true; // xxxxxxxx, 00000000
|
|
96
|
-
if (/^(true|false|null|undefined|none)$/i.test(value)) return true;
|
|
97
|
-
if (/^-?\d+(\.\d+)?$/.test(value)) return true; // pure number
|
|
98
|
-
if (/^\/|^~\/|^\.\.?\//.test(value)) return true; // a path
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
80
|
/** Strip one layer of surrounding quotes. */
|
|
103
81
|
function unquote(v: string): string {
|
|
104
82
|
return (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))
|
|
@@ -249,19 +227,17 @@ export function findSecrets(text: string): SecretMatch[] {
|
|
|
249
227
|
}
|
|
250
228
|
}
|
|
251
229
|
|
|
230
|
+
// Names annotate a value the shape layer already proved; they no longer
|
|
231
|
+
// create findings. Even after increasingly elaborate value heuristics, the
|
|
232
|
+
// name-only lane still produced hundreds of code literals and test fixtures
|
|
233
|
+
// on the measured machine. The product now deliberately prefers missing an
|
|
234
|
+
// unrecognisable first-party password over telling somebody they leaked 500
|
|
235
|
+
// credentials they do not have.
|
|
252
236
|
for (const m of text.matchAll(ASSIGNMENT_RE)) {
|
|
253
237
|
const name = m[2];
|
|
254
238
|
const value = unquote(m[4]);
|
|
255
|
-
if (!isSecretName(name) || isNotACredential(value) || isDocsLiteral(value)) continue;
|
|
256
239
|
const existing = byValue.get(value);
|
|
257
|
-
if (existing)
|
|
258
|
-
// A vendor-shaped value that also has a name: keep the vendor rule (it
|
|
259
|
-
// can name a console) and gain the name (it says which of the user's
|
|
260
|
-
// variables to change).
|
|
261
|
-
existing.name ??= name;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
byValue.set(value, { value, name, rule: "assigned secret", shaped: false });
|
|
240
|
+
if (existing) existing.name ??= name;
|
|
265
241
|
}
|
|
266
242
|
|
|
267
243
|
return [...byValue.values()];
|
package/src/audit/leak-store.ts
CHANGED
|
@@ -74,7 +74,10 @@ function readJson<T>(path: string): T | null {
|
|
|
74
74
|
export function readLeakIdentity(home?: string): LeakIdentity {
|
|
75
75
|
const existing = readJson<Partial<LeakIdentity>>(auditLeakIdentityFile(home));
|
|
76
76
|
if (existing && typeof existing.salt === "string" && existing.salt.length >= 32) {
|
|
77
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
salt: existing.salt,
|
|
79
|
+
dismissed: existing.dismissed ?? [],
|
|
80
|
+
};
|
|
78
81
|
}
|
|
79
82
|
const minted: LeakIdentity = { salt: randomBytes(32).toString("hex"), dismissed: [] };
|
|
80
83
|
writeLeakIdentity(minted, home);
|
|
@@ -121,7 +124,7 @@ export function writeLeakIdentity(identity: LeakIdentity, home?: string): boolea
|
|
|
121
124
|
function sanitizeFinding(raw: unknown): LeakFinding | null {
|
|
122
125
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
123
126
|
const f = raw as Record<string, unknown>;
|
|
124
|
-
// The id becomes a
|
|
127
|
+
// The id becomes a filename in the macOS notification queue, so anything
|
|
125
128
|
// that is not the exact minted shape disqualifies the whole entry.
|
|
126
129
|
if (typeof f.id !== "string" || !isFindingId(f.id)) return null;
|
|
127
130
|
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* - `machine_id` is what the api-server keys reports on. Regenerate it and the
|
|
8
8
|
* server sees a brand-new machine, which burns a slot off the account's cap
|
|
9
9
|
* on every logout and splits one box's history into two.
|
|
10
|
-
* - `last_reported_at` is how far the last
|
|
11
|
-
* next report re-
|
|
12
|
-
* everything that ever happened as though it just did.
|
|
10
|
+
* - `last_reported_at` is how far the last credential alert reached. Reset it
|
|
11
|
+
* and the next report can re-cover old exposures as though they just happened.
|
|
13
12
|
*
|
|
14
13
|
* That is why this is a separate file from `session.json` rather than two more
|
|
15
14
|
* keys in it: signing out deletes the session, and neither of these may go with
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
* `state/telemetry-id` is already a stable per-machine random id and would have
|
|
23
22
|
* been free to reuse. It is deliberately not reused: that id is the anonymous
|
|
24
23
|
* PostHog person, and sending it alongside a verified email address would link
|
|
25
|
-
* the two the moment somebody
|
|
24
|
+
* the two the moment somebody adds email alerts. Opting into email
|
|
26
25
|
* should not de-anonymise telemetry, so this feature gets its own id and the
|
|
27
26
|
* two never meet.
|
|
28
27
|
*/
|
|
@@ -66,8 +65,8 @@ export function readMachineIdentity(home?: string): MachineIdentity | null {
|
|
|
66
65
|
/**
|
|
67
66
|
* Read the identity, creating it on first call.
|
|
68
67
|
*
|
|
69
|
-
* Only ever called from the reporting path, so a machine
|
|
70
|
-
*
|
|
68
|
+
* Only ever called from the email-reporting path, so a machine without a valid
|
|
69
|
+
* email identity never gets an id at all — there is nothing to mint one for.
|
|
71
70
|
*/
|
|
72
71
|
export function ensureMachineIdentity(home?: string): MachineIdentity {
|
|
73
72
|
const existing = readMachineIdentity(home);
|
|
@@ -81,13 +80,13 @@ export function ensureMachineIdentity(home?: string): MachineIdentity {
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
/**
|
|
84
|
-
* Record how far the last
|
|
83
|
+
* Record how far the last delivered credential alert reached.
|
|
85
84
|
*
|
|
86
85
|
* The value is the server's `next_window_from`, not the window this run
|
|
87
86
|
* scanned. The server is authoritative because it knows which reports actually
|
|
88
87
|
* produced an email — a report held by the cooldown, or one whose send failed,
|
|
89
88
|
* must not advance the watermark or its findings are silently dropped from every
|
|
90
|
-
* future
|
|
89
|
+
* future alert.
|
|
91
90
|
*/
|
|
92
91
|
export function recordReportWatermark(nextWindowFrom: string, home?: string): void {
|
|
93
92
|
const current = ensureMachineIdentity(home);
|
|
@@ -105,7 +104,7 @@ export function deleteMachineIdentity(home?: string): void {
|
|
|
105
104
|
/**
|
|
106
105
|
* A display name for this machine — its hostname.
|
|
107
106
|
*
|
|
108
|
-
* Shown in the
|
|
107
|
+
* Shown in the email alert so somebody with three boxes can tell which one is
|
|
109
108
|
* misbehaving, which is the whole reason it is sent. Falls back to `undefined`
|
|
110
109
|
* rather than a placeholder: the server keeps whatever label it already has when
|
|
111
110
|
* one is omitted, so guessing here would overwrite a good name with a bad one.
|
|
@@ -255,8 +255,8 @@ export function queueMacNotification(
|
|
|
255
255
|
body: string,
|
|
256
256
|
home?: string,
|
|
257
257
|
): boolean {
|
|
258
|
-
// The id becomes a filename in a watched directory
|
|
259
|
-
//
|
|
258
|
+
// The id becomes a filename in a watched directory, so validate the path
|
|
259
|
+
// component before resolving it — see `isFindingId`.
|
|
260
260
|
if (!isFindingId(id)) return false;
|
|
261
261
|
const dir = macNotifyDir(home);
|
|
262
262
|
const tmp = resolve(dir, "..", `notify-${id}.tmp`);
|
|
@@ -147,14 +147,40 @@ const SECRET_NAME_SUBSTRINGS = [
|
|
|
147
147
|
"APIKEY",
|
|
148
148
|
"PRIVATEKEY",
|
|
149
149
|
];
|
|
150
|
-
const SECRET_NAME_COMPONENTS = ["
|
|
150
|
+
const SECRET_NAME_COMPONENTS = ["PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "COOKIE"];
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
153
|
+
* Names that mean "credential" only inside a COMPOUND identifier.
|
|
154
|
+
*
|
|
155
|
+
* Every entry here was measured as a dominant false positive on a real 2,664-
|
|
156
|
+
* transcript corpus, and the reason they are demotions rather than deletions is
|
|
157
|
+
* that the SHAPE layer already catches real vendor keys regardless of what they
|
|
158
|
+
* are called. The name layer exists for FIRST-PARTY secrets with no recognisable
|
|
159
|
+
* format — and those essentially always carry a qualified name
|
|
160
|
+
* (`COMPOSIO_API_KEY`, `DB_PASSWORD`), never a bare one. So requiring a compound
|
|
161
|
+
* costs almost nothing real and removes most of the noise.
|
|
162
|
+
*
|
|
163
|
+
* PWD `MYSQL_PWD` is MySQL's documented password variable; a bare `PWD`
|
|
164
|
+
* is the shell's working directory, on every second line of a
|
|
165
|
+
* captured session.
|
|
166
|
+
* KEY a bare `key=` is a React list prop, a map entry, a sort field.
|
|
167
|
+
* 80 findings on the measured corpus, none of them credentials.
|
|
168
|
+
*
|
|
169
|
+
* `SIG` was demoted here too and put back: a bare `sig=` in a URL query IS a
|
|
170
|
+
* request signature, and an existing test says so. It also never appeared in
|
|
171
|
+
* the measured noise, unlike the two above — the rule is that a demotion needs
|
|
172
|
+
* evidence, not a plausible story.
|
|
173
|
+
*
|
|
174
|
+
* `SESSION` is REMOVED from both lists rather than demoted, and the difference
|
|
175
|
+
* matters: demoting it would have changed nothing, because
|
|
176
|
+
* `DBUS_SESSION_BUS_ADDRESS` is already a compound. It was the single largest
|
|
177
|
+
* source of noise measured — 254 of 500 findings, every one of them
|
|
178
|
+
* `DBUS_SESSION_BUS_ADDRESS`, `XDG_SESSION_TYPE`, `SESSION_MANAGER`,
|
|
179
|
+
* `session_id` or `sessionUpdate`. A session IDENTIFIER is not a rotatable
|
|
180
|
+
* credential; a session SECRET still matches through `SECRET`, `TOKEN`, or
|
|
181
|
+
* `KEY` in `sessionKey`.
|
|
156
182
|
*/
|
|
157
|
-
const COMPOUND_ONLY_COMPONENTS = ["PWD"];
|
|
183
|
+
const COMPOUND_ONLY_COMPONENTS = ["PWD", "KEY"];
|
|
158
184
|
|
|
159
185
|
/**
|
|
160
186
|
* Split an identifier into its uppercased components, on `_`, `-`, `.` AND
|
|
@@ -300,6 +326,35 @@ export function isPublishedByDesign(name: string): boolean {
|
|
|
300
326
|
return PUBLISHED_MARKERS.test(upper);
|
|
301
327
|
}
|
|
302
328
|
|
|
329
|
+
/**
|
|
330
|
+
* How confidently the NAME alone says "credential".
|
|
331
|
+
*
|
|
332
|
+
* `strong` — the word means secret and nothing else: `DB_PASSWORD`,
|
|
333
|
+
* `STRIPE_SECRET`, `PRIVATE_KEY`. A short value here is still worth flagging;
|
|
334
|
+
* `hunter2` assigned to `PASSWORD` is a leaked password.
|
|
335
|
+
*
|
|
336
|
+
* `weak` — the word is ALSO ordinary programming vocabulary: `key`, `token`,
|
|
337
|
+
* `auth`, `sig`. Measured on a real 2,664-transcript machine these produced
|
|
338
|
+
* `keyType`, `tokenLimitCancelled`, `max_output_tokens`, `resultKey`,
|
|
339
|
+
* `configDirKey` — 394 findings from this layer, of which 389 had no vendor
|
|
340
|
+
* prefix and 227 were under 24 characters. Nothing that short is an API key.
|
|
341
|
+
* A weak name has to be backed by a value that actually looks like a secret.
|
|
342
|
+
*/
|
|
343
|
+
export type SecretNameStrength = "strong" | "weak" | "none";
|
|
344
|
+
|
|
345
|
+
const STRONG_NAME_WORDS = ["SECRET", "PASSWORD", "PASSWD", "PASSPHRASE", "CREDENTIAL", "APIKEY", "PRIVATEKEY"];
|
|
346
|
+
|
|
347
|
+
export function secretNameStrength(name: string): SecretNameStrength {
|
|
348
|
+
if (!isSecretName(name)) return "none";
|
|
349
|
+
const upper = name.toUpperCase();
|
|
350
|
+
if (STRONG_NAME_WORDS.some((w) => upper.includes(w))) return "strong";
|
|
351
|
+
// `API_KEY` / `apiKey` split into components rather than matching APIKEY as a
|
|
352
|
+
// substring, and they are as unambiguous as the substring form.
|
|
353
|
+
const parts = identifierComponents(name);
|
|
354
|
+
if (parts.includes("API") && parts.includes("KEY")) return "strong";
|
|
355
|
+
return "weak";
|
|
356
|
+
}
|
|
357
|
+
|
|
303
358
|
export function isSecretName(name: string): boolean {
|
|
304
359
|
// Runs first and beats every other signal: a value the framework compiles
|
|
305
360
|
// into the browser bundle is public no matter what the rest of the name says.
|
|
@@ -518,6 +573,7 @@ export function redactAuditResult(result: AuditResult, home = homedir()): AuditR
|
|
|
518
573
|
// Salted, machine-local ids. They carry no path and cannot be reversed to
|
|
519
574
|
// a credential, so they pass through — but the decision is recorded here
|
|
520
575
|
// rather than inherited from a spread.
|
|
576
|
+
leakIds: result.leakIds,
|
|
521
577
|
newLeakIds: result.newLeakIds,
|
|
522
578
|
|
|
523
579
|
// Everything below carries a path or user content and is rewritten.
|
|
@@ -560,5 +616,6 @@ export const REDACTED_AUDIT_RESULT_KEYS: ReadonlyArray<keyof AuditResult> = [
|
|
|
560
616
|
"projectsScanned",
|
|
561
617
|
"eventsScanned",
|
|
562
618
|
"enabledBuiltinNames",
|
|
619
|
+
"leakIds",
|
|
563
620
|
"newLeakIds",
|
|
564
621
|
];
|
package/src/audit/report-harm.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The side effect a scheduled audit
|
|
3
|
-
*
|
|
2
|
+
* The optional remote side effect of a scheduled audit: telling the api-server
|
|
3
|
+
* about the newest masked credential exposure so an email alert can be sent.
|
|
4
4
|
*
|
|
5
5
|
* Separated from `harm-report.ts` on purpose. That module is pure — result in,
|
|
6
6
|
* payload out — and is where the windowing rules live and are tested. This one
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* token expired, whose network is down, or whose api-server is having a bad day
|
|
16
16
|
* must keep auditing itself locally and keep showing results on its own
|
|
17
17
|
* dashboard — the local feature does not depend on the remote one, and a person
|
|
18
|
-
* who never
|
|
18
|
+
* who never added an email must never have the local scan depend on this code.
|
|
19
19
|
*
|
|
20
20
|
* ## Why the CHILD does this and not the daemon
|
|
21
21
|
*
|
|
@@ -35,6 +35,7 @@ import { activeFindings, readLeakRecord } from "./leak-store";
|
|
|
35
35
|
import {
|
|
36
36
|
ensureMachineIdentity,
|
|
37
37
|
machineLabel,
|
|
38
|
+
readMachineIdentity,
|
|
38
39
|
recordReportWatermark,
|
|
39
40
|
} from "./machine-store";
|
|
40
41
|
import type { AuditResult } from "./types";
|
|
@@ -42,31 +43,26 @@ import type { AuditResult } from "./types";
|
|
|
42
43
|
/** What happened, for the one line the scheduled run prints. */
|
|
43
44
|
export type HarmReportOutcome =
|
|
44
45
|
| { kind: "disabled" }
|
|
45
|
-
| { kind: "
|
|
46
|
+
| { kind: "no-leak" }
|
|
46
47
|
| { kind: "signed-out" }
|
|
47
|
-
| { kind: "sent";
|
|
48
|
-
| { kind: "held";
|
|
48
|
+
| { kind: "sent"; leaks: number }
|
|
49
|
+
| { kind: "held"; leaks: number; reason: string }
|
|
49
50
|
| { kind: "failed"; error: string };
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* Report this scan's
|
|
53
|
+
* Report this scan's newest credential exposure when an email identity exists.
|
|
53
54
|
*
|
|
54
55
|
* Returns an outcome rather than a boolean so the caller can say something
|
|
55
|
-
* truthful. "held" in particular is not a failure — a machine
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* truthful. "held" in particular is not a failure — a machine inside its
|
|
57
|
+
* cooldown is working exactly as intended, and a line that called that an error
|
|
58
|
+
* would train people to ignore the line.
|
|
58
59
|
*/
|
|
59
60
|
export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome> {
|
|
60
|
-
// ONE switch. `auto` means "scan on a timer AND tell me", because the reason
|
|
61
|
-
// to put a scan on a timer is to be told — a machine scanning quietly with
|
|
62
|
-
// nothing to report to is a feature that looks on and does nothing visible.
|
|
63
61
|
let auto = false;
|
|
64
|
-
let consentedAt: number | undefined;
|
|
65
62
|
let intervalDays = 7;
|
|
66
63
|
try {
|
|
67
64
|
const config = readConfig();
|
|
68
65
|
auto = config.audit.auto;
|
|
69
|
-
consentedAt = config.audit.reportsConsentedAt;
|
|
70
66
|
// Also the width of a FIRST report's window, so a new machine's opening
|
|
71
67
|
// digest covers the same period every later one will.
|
|
72
68
|
intervalDays = config.audit.intervalDays;
|
|
@@ -76,13 +72,18 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
76
72
|
}
|
|
77
73
|
if (!auto) return { kind: "disabled" };
|
|
78
74
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
// Decide whether there is anything emailable before touching auth or the
|
|
76
|
+
// network. Scheduled scans are local by default; an email identity is an
|
|
77
|
+
// optional delivery channel, and a scan with no credential leak must make no
|
|
78
|
+
// report request at all.
|
|
79
|
+
const prior = readMachineIdentity();
|
|
80
|
+
const report = buildHarmReport(
|
|
81
|
+
result,
|
|
82
|
+
prior?.last_reported_at,
|
|
83
|
+
intervalDays,
|
|
84
|
+
activeFindings(readLeakRecord()),
|
|
85
|
+
);
|
|
86
|
+
if (report.leaks.length === 0) return { kind: "no-leak" };
|
|
86
87
|
|
|
87
88
|
const auth = await getValidAccessToken();
|
|
88
89
|
if (!auth) {
|
|
@@ -102,18 +103,6 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
102
103
|
return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
// The leak record is read here rather than carried on the AuditResult: the
|
|
106
|
-
// digest wants every credential still live in this window, not only the ones
|
|
107
|
-
// this particular scan happened to re-encounter. A key that leaked last month
|
|
108
|
-
// and was not touched by today's transcripts is still a key to rotate.
|
|
109
|
-
const report = buildHarmReport(
|
|
110
|
-
result,
|
|
111
|
-
identity.last_reported_at,
|
|
112
|
-
intervalDays,
|
|
113
|
-
activeFindings(readLeakRecord()),
|
|
114
|
-
);
|
|
115
|
-
const hits = report.harmful.reduce((n, p) => n + p.hits, 0);
|
|
116
|
-
|
|
117
106
|
try {
|
|
118
107
|
const res = await submitAuditReport(auth.access_token, {
|
|
119
108
|
machine_id: identity.machine_id,
|
|
@@ -139,8 +128,8 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
139
128
|
}
|
|
140
129
|
|
|
141
130
|
return res.emailed
|
|
142
|
-
? { kind: "sent",
|
|
143
|
-
: { kind: "held",
|
|
131
|
+
? { kind: "sent", leaks: report.leaks.length }
|
|
132
|
+
: { kind: "held", leaks: report.leaks.length, reason: res.reason ?? "not_sent" };
|
|
144
133
|
} catch (err) {
|
|
145
134
|
// A 401 here means the session died between `getValidAccessToken` and this
|
|
146
135
|
// call — rare, and indistinguishable from any other failure as far as this
|
|
@@ -160,19 +149,15 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
160
149
|
export function describeOutcome(outcome: HarmReportOutcome): string | null {
|
|
161
150
|
switch (outcome.kind) {
|
|
162
151
|
case "disabled":
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return "failproofai: scheduled scans are on, but emailing what they find needs a fresh opt-in — run `failproofai audit --schedule` (or visit /settings) to turn digests on";
|
|
152
|
+
case "no-leak":
|
|
153
|
+
return null;
|
|
166
154
|
case "signed-out":
|
|
167
|
-
|
|
168
|
-
// from the audit page", which stopped being true when this release moved
|
|
169
|
-
// that dialog behind "invite a friend".
|
|
170
|
-
return "failproofai: emailed reports are on but this machine is signed out — run `failproofai audit --schedule` or visit /settings to resume them";
|
|
155
|
+
return "failproofai: a credential exposure was found, but no email is configured — run `failproofai audit --schedule --email you@example.com`";
|
|
171
156
|
case "sent":
|
|
172
|
-
return
|
|
157
|
+
return "failproofai: emailed the most recent credential exposure";
|
|
173
158
|
case "held":
|
|
174
|
-
return `failproofai:
|
|
159
|
+
return `failproofai: credential exposure reported, no email (${outcome.reason})`;
|
|
175
160
|
case "failed":
|
|
176
|
-
return `failproofai: could not send the
|
|
161
|
+
return `failproofai: could not send the credential alert: ${outcome.error}`;
|
|
177
162
|
}
|
|
178
163
|
}
|
|
@@ -68,13 +68,9 @@ const dim = (s: string) => wrap(ANSI_DIM, s);
|
|
|
68
68
|
const bold = (s: string) => wrap(ANSI_BOLD, s);
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Turn scheduled audits on
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* is to be told what it found — so this requires a session, exactly as the
|
|
75
|
-
* dashboard's `setAutoAuditAction` does. A timer set with nobody to tell is a
|
|
76
|
-
* switch that reads as on and produces nothing, discoverable only by noticing
|
|
77
|
-
* that no digest ever arrives.
|
|
71
|
+
* Turn scheduled audits on. Email is optional: `--email` signs in, while a
|
|
72
|
+
* plain `--schedule` enables the local scan and desktop notification without
|
|
73
|
+
* prompting for an account.
|
|
78
74
|
*
|
|
79
75
|
* The interval is written and then RE-READ, so what is printed is what the
|
|
80
76
|
* config actually kept — `readIntervalDays` owns the 1..90 clamp and a second
|
|
@@ -108,17 +104,13 @@ export async function runScheduleOn(
|
|
|
108
104
|
if (bad) throw new ScheduleCliError(bad);
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
const
|
|
107
|
+
const signedIn = emailArg === undefined
|
|
108
|
+
? { user: readAuth()?.user ?? null, prompted: false }
|
|
109
|
+
: await ensureSignedIn(emailArg);
|
|
112
110
|
|
|
113
111
|
const next = updateConfig({
|
|
114
|
-
// Stamped in the SAME call that sets `auto`, never separately: this records
|
|
115
|
-
// that a person completed a sign-in and read the disclosure printed below,
|
|
116
|
-
// and it is what `reportHarm` gates sending on. A machine that inherited
|
|
117
|
-
// `auto` from a release where it meant "scan locally" has no stamp and
|
|
118
|
-
// sends nothing until it comes through here.
|
|
119
112
|
audit: {
|
|
120
113
|
auto: true,
|
|
121
|
-
reportsConsentedAt: Date.now(),
|
|
122
114
|
...(days !== undefined ? { intervalDays: days } : {}),
|
|
123
115
|
},
|
|
124
116
|
});
|
|
@@ -134,12 +126,14 @@ export async function runScheduleOn(
|
|
|
134
126
|
// The list is the real payload from `report-harm.ts`: machine id, hostname,
|
|
135
127
|
// platform, the window bounds and the redacted examples.
|
|
136
128
|
const summary = [
|
|
137
|
-
`every ${interval} day${interval === 1 ? "" : "s"} ·
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
`every ${interval} day${interval === 1 ? "" : "s"} · desktop notification on leaks`,
|
|
130
|
+
signedIn.user
|
|
131
|
+
? `masked leak alerts also go to ${signedIn.user.email}`
|
|
132
|
+
: "email is optional · add it with --email you@example.com",
|
|
133
|
+
"email contains only the most recent masked credential exposure",
|
|
140
134
|
];
|
|
141
135
|
|
|
142
|
-
if (prompted) {
|
|
136
|
+
if (signedIn.prompted) {
|
|
143
137
|
// A sign-in just drew the frame, so the result continues it and the `└`
|
|
144
138
|
// closes both at once — rather than the frame ending and a loose line
|
|
145
139
|
// appearing underneath.
|
|
@@ -158,7 +152,7 @@ export async function runScheduleOn(
|
|
|
158
152
|
// exists to make visible.
|
|
159
153
|
warnIfDaemonWontRun();
|
|
160
154
|
|
|
161
|
-
if (prompted) {
|
|
155
|
+
if (signedIn.prompted) {
|
|
162
156
|
outro("failproofai audit --status · when the next scan is due");
|
|
163
157
|
} else {
|
|
164
158
|
process.stdout.write(dim(`\n failproofai audit --status when the next scan is due\n\n`));
|
|
@@ -217,7 +211,7 @@ export function runScheduleStatus(): void {
|
|
|
217
211
|
|
|
218
212
|
// A session whose refresh window has closed cannot mint another access token,
|
|
219
213
|
// so it is a destination in name only. Showing the address for one would tell
|
|
220
|
-
// somebody their
|
|
214
|
+
// somebody their alerts are going somewhere they are not.
|
|
221
215
|
const live = auth && auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
|
|
222
216
|
// Beside the schedule, because they are the two halves of "what does this
|
|
223
217
|
// machine do on its own" — one decides whether it scans, the other whether it
|
|
@@ -230,19 +224,9 @@ export function runScheduleStatus(): void {
|
|
|
230
224
|
detail.push(["reports to", live ? live.user.email : dim("— signed out")]);
|
|
231
225
|
if (on && !live) {
|
|
232
226
|
// The state the reporter surfaces as "signed-out". Named here for the same
|
|
233
|
-
// reason the settings panel names it: the scans
|
|
234
|
-
// about
|
|
235
|
-
detail.push(["", pink("scans continue;
|
|
236
|
-
} else if (on && config.audit.reportsConsentedAt === undefined) {
|
|
237
|
-
// Signed in, scheduled, and still not sending: this machine set `audit.auto`
|
|
238
|
-
// when it only meant "scan locally", so nothing has consented to the digest
|
|
239
|
-
// leaving the box. Without this row the status screen would show a healthy
|
|
240
|
-
// schedule and a live address and still mail nothing, with no explanation
|
|
241
|
-
// anywhere the user can see.
|
|
242
|
-
detail.push([
|
|
243
|
-
"",
|
|
244
|
-
pink("scans continue; digests need a fresh opt-in — run `--schedule` to turn them on"),
|
|
245
|
-
]);
|
|
227
|
+
// reason the settings panel names it: the scans and desktop notifications
|
|
228
|
+
// keep running, so silence about email would look like the feature failing.
|
|
229
|
+
detail.push(["", pink("scans and desktop alerts continue; email alerts are paused")]);
|
|
246
230
|
}
|
|
247
231
|
|
|
248
232
|
detail.push(["daemon", describeDaemon(daemon)]);
|
package/src/audit/types.ts
CHANGED
|
@@ -45,6 +45,9 @@ export interface TranscriptMetadata {
|
|
|
45
45
|
mtimeMs: number;
|
|
46
46
|
/** Byte size of the transcript file. Used for cache invalidation. */
|
|
47
47
|
sizeBytes: number;
|
|
48
|
+
/** Optional host-authored session purpose. Used only to distinguish
|
|
49
|
+
* deliberate credential-scanner research from evidence of a leak. */
|
|
50
|
+
sessionDescription?: string;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** Per-session detector state. Detectors mutate this freely. */
|
|
@@ -172,6 +175,9 @@ export interface TranscriptAuditResult {
|
|
|
172
175
|
* absent reads as "this transcript predates leak scanning", not "clean".
|
|
173
176
|
*/
|
|
174
177
|
leaks?: TranscriptLeak[];
|
|
178
|
+
/** The session was explicitly generating/researching credential fixtures,
|
|
179
|
+
* so its matches are examples rather than evidence of user leakage. */
|
|
180
|
+
leakScanSuppressed?: "credential-research";
|
|
175
181
|
/** First/last timestamp per name. */
|
|
176
182
|
rangeByName: Record<string, { first: string; last: string }>;
|
|
177
183
|
}
|
|
@@ -211,6 +217,11 @@ export interface AuditResult {
|
|
|
211
217
|
* Lets the dashboard answer "is this policy already on?" for
|
|
212
218
|
* detector-mapped policies that may not have hit during this audit. */
|
|
213
219
|
enabledBuiltinNames: string[];
|
|
220
|
+
/** Ids of credentials encountered by this scan, including ones already in
|
|
221
|
+
* the persistent leak record. Scheduled desktop notifications key on this
|
|
222
|
+
* list so every completed scan that still sees a leak can notify; the
|
|
223
|
+
* narrower `newLeakIds` remains for first-seen-only surfaces. */
|
|
224
|
+
leakIds?: string[];
|
|
214
225
|
/**
|
|
215
226
|
* Ids of credentials seen for the FIRST time in this scan.
|
|
216
227
|
*
|
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
type RetryProbe,
|
|
107
107
|
} from "./onboarding-attempt";
|
|
108
108
|
import { acquireOnboardingLock } from "./onboarding-lock";
|
|
109
|
+
import { readConfigRaw, updateConfig } from "./fp-config";
|
|
109
110
|
|
|
110
111
|
export interface WizardIO {
|
|
111
112
|
stdin?: TTYIn;
|
|
@@ -755,6 +756,19 @@ export async function runConfigureWizard(
|
|
|
755
756
|
// question, and no flag can supply it.
|
|
756
757
|
const preAnswered = Boolean(answers.token);
|
|
757
758
|
|
|
759
|
+
// Capture the preference BEFORE the daemon setup writes config.json. A fresh
|
|
760
|
+
// machine reaches `setDaemonConfigured(true)` with no config file; that helper
|
|
761
|
+
// necessarily materialises the conservative no-file default (`auto: false`).
|
|
762
|
+
// Looking after that write cannot distinguish "the user switched it off"
|
|
763
|
+
// from "setup just wrote its fallback", which is how new installs ended up
|
|
764
|
+
// with a seven-day scheduler that was explicitly disabled.
|
|
765
|
+
const initialAudit = readConfigRaw().raw.audit;
|
|
766
|
+
const hadExplicitAuditAuto =
|
|
767
|
+
!!initialAudit &&
|
|
768
|
+
typeof initialAudit === "object" &&
|
|
769
|
+
!Array.isArray(initialAudit) &&
|
|
770
|
+
typeof (initialAudit as Record<string, unknown>).auto === "boolean";
|
|
771
|
+
|
|
758
772
|
|
|
759
773
|
// Running the wizard itself under sudo configures the WRONG ACCOUNT, and
|
|
760
774
|
// does it silently: homedir() becomes /root, so the hooks land in root's
|
|
@@ -1439,6 +1453,20 @@ export async function runConfigureWizard(
|
|
|
1439
1453
|
// binary filename are both derived from it.
|
|
1440
1454
|
if (daemonInstalled) {
|
|
1441
1455
|
setDaemonConfigured(true, cliVersion);
|
|
1456
|
+
// A completed setup is the default opt-in to the LOCAL seven-day scan. It
|
|
1457
|
+
// does not imply an email address or make a network request: reporting is
|
|
1458
|
+
// independently gated by the signed-in audit identity. Reconfiguration
|
|
1459
|
+
// preserves an explicit `auto: false`, so the user's off switch remains an
|
|
1460
|
+
// off switch instead of being undone whenever they repair or upgrade the
|
|
1461
|
+
// daemon.
|
|
1462
|
+
if (!hadExplicitAuditAuto) {
|
|
1463
|
+
try {
|
|
1464
|
+
updateConfig({ audit: { auto: true } });
|
|
1465
|
+
} catch {
|
|
1466
|
+
// The daemon and hooks are already installed. Keep setup successful;
|
|
1467
|
+
// the status screen will expose that scheduling could not be persisted.
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1442
1470
|
// Only now — the unit points at the new binary, so older ones are no
|
|
1443
1471
|
// longer referenced by anything. Keeps the previous version for an
|
|
1444
1472
|
// offline rollback.
|