failproofai 1.0.4-beta.8 → 1.0.4
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 +5 -5
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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 +4 -4
- 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 +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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +4 -37
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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 +4 -4
- 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/build-manifest.json +2 -2
- 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 +5 -5
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -15
- package/.next/standalone/.next/server/app/settings/page.js +4 -4
- 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]__0hmcwgg._.js → [externals]__0-9ti7z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__14b7cc6._.js → [externals]__0u3wrvq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.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]__1le9jqc._.js +3 -0
- 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 +5 -0
- 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/{_1_eakl4._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +10 -10
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__187jks6._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1fa1jd9._.js → [root-of-the-server]__09flfsx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kokc8z._.js → [root-of-the-server]__0n_lxhg._.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]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1kpd1za._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00y9ww2._.js → [root-of-the-server]__1sc84bb._.js} +2 -21
- package/.next/standalone/.next/server/chunks/ssr/_02yoeej._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_00bqkhj._.js → _04rbnaq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0s-77v5._.js → _0h_douw._.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 +14 -51
- package/.next/standalone/.next/server/chunks/ssr/{_14c6q09._.js → _1if9ixi._.js} +2 -21
- package/.next/standalone/.next/server/chunks/ssr/{_07t3jol._.js → _1u8-lu2._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +3 -0
- 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 +69 -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/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.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 +5 -5
- 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 +21 -65
- package/.next/standalone/.next/static/chunks/{1302duobnz07l.js → 0-tyrpwuc3lla.js} +1 -1
- package/.next/standalone/.next/static/chunks/0ca7octhod0g2.js +69 -0
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{2n85g-crgcx5j.js → 1j6o9min_6qa9.js} +1 -1
- package/.next/standalone/.next/static/chunks/27cl8h-c2w7cd.js +1 -0
- package/.next/standalone/.next/static/chunks/{0jpd8dv2vk930.css → 29-iqnp9v_axt.css} +1 -1
- package/.next/standalone/.next/static/chunks/{3uyhmm01g64k4.css → 2k9f4tyv04809.css} +0 -1
- package/.next/standalone/.next/static/chunks/{1chm9piluggc-.js → 2lwipmv5etc8m.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +1 -0
- package/.next/standalone/.next/static/chunks/{2ukucf6g-kbs5.js → 2u8vh73tpw28_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1_x7pqemw5n_w.js → 31u94yukw9fz-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2mizt96igig1y.js → 3p9_8x5ezce-9.js} +1 -1
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0vm4wdx3w-2_w.js → 3tjji451hrgc0.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-3s3c-u_u_l0u9.js → turbopack-3p73obe0xmrxm.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +27 -51
- package/.next/standalone/app/actions/get-scheduled-audit.ts +0 -5
- package/.next/standalone/app/actions/update-scheduled-audit.ts +57 -18
- package/.next/standalone/app/api/auth/status/route.ts +2 -3
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +34 -140
- package/.next/standalone/app/audit/_components/audit-poster.tsx +10 -29
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -6
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -4
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +5 -19
- package/.next/standalone/app/audit/_components/share-templates.ts +44 -110
- package/.next/standalone/app/audit/audit-styles.css +1 -132
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +45 -50
- package/.next/standalone/lib/auth/api-server-client.ts +5 -33
- package/.next/standalone/lib/claude-sessions.ts +27 -101
- package/.next/standalone/lib/codex-sessions.ts +4 -20
- package/.next/standalone/lib/copilot-sessions.ts +1 -32
- package/.next/standalone/lib/cursor-sessions.ts +5 -62
- package/.next/standalone/lib/download-session.ts +13 -58
- package/.next/standalone/lib/openclaw-projects.ts +18 -54
- package/.next/standalone/lib/openclaw-sessions.ts +17 -75
- package/.next/standalone/lib/sqlite-reader.ts +1 -78
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -1
- package/dist/cli.mjs +664 -2121
- package/dist/worker.mjs +105 -422
- package/lib/auth/api-server-client.ts +5 -33
- package/lib/claude-sessions.ts +27 -101
- package/lib/codex-sessions.ts +4 -20
- package/lib/copilot-sessions.ts +1 -32
- package/lib/cursor-sessions.ts +5 -62
- package/lib/download-session.ts +13 -58
- package/lib/openclaw-projects.ts +18 -54
- package/lib/openclaw-sessions.ts +17 -75
- package/lib/sqlite-reader.ts +1 -78
- package/package.json +9 -9
- package/pi-extension/index.ts +5 -50
- package/src/audit/cache.ts +0 -6
- package/src/audit/cli-adapters/claude.ts +0 -13
- package/src/audit/cli-login.ts +1 -4
- package/src/audit/cli.ts +7 -157
- package/src/audit/harm-report.ts +4 -111
- package/src/audit/index.ts +15 -286
- package/src/audit/machine-store.ts +9 -8
- package/src/audit/redact-example.ts +15 -324
- package/src/audit/report-harm.ts +37 -33
- package/src/audit/report.ts +3 -6
- package/src/audit/schedule-cli.ts +33 -54
- package/src/audit/scoring.ts +0 -49
- package/src/audit/types.ts +0 -52
- package/src/hooks/builtin-policies.ts +2 -67
- package/src/hooks/configure-wizard.ts +0 -55
- package/src/hooks/fp-config.ts +48 -83
- package/src/hooks/fp-home.ts +0 -27
- package/src/hooks/integrations.ts +14 -51
- package/src/hooks/uninstall-cli.ts +2 -17
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0faqz9x._.js +0 -22
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__112b4go._.js +0 -24
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l71g3y._.js +0 -22
- package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-4ja6q._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cr3xu._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04px77e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egr5s1._.js +0 -6
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0i0l1di._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n2txhk._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0r3c1_0._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17xnxlx._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ckpmkk._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kaotix._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +0 -3
- package/.next/standalone/.next/static/chunks/0igq884ziuv-8.js +0 -1
- package/.next/standalone/.next/static/chunks/349jkb79v5g6z.js +0 -1
- package/.next/standalone/.next/static/chunks/3a4gth64hwtwf.js +0 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -104
- package/.next/standalone/app/audit/_components/leak-section.tsx +0 -192
- package/.next/standalone/lib/openclaw-db.ts +0 -271
- package/lib/openclaw-db.ts +0 -271
- package/src/audit/desktop-notify.ts +0 -420
- package/src/audit/leak-fingerprint.ts +0 -198
- package/src/audit/leak-record.ts +0 -248
- package/src/audit/leak-scan.ts +0 -268
- package/src/audit/leak-store.ts +0 -220
- package/src/audit/macos-notifier.ts +0 -298
- package/src/hooks/notice.ts +0 -24
- /package/.next/standalone/.next/static/{DNhaAH8iPGH0tIuxee5_2 → dg8VQ7Ejlo0Qsh_x92oIt}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{DNhaAH8iPGH0tIuxee5_2 → dg8VQ7Ejlo0Qsh_x92oIt}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{DNhaAH8iPGH0tIuxee5_2 → dg8VQ7Ejlo0Qsh_x92oIt}/_ssgManifest.js +0 -0
package/src/audit/leak-scan.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulling credential VALUES out of a tool event.
|
|
3
|
-
*
|
|
4
|
-
* The audit already replays every event through the policy engine, but
|
|
5
|
-
* `ReplayHit` carries `{policyName, decision, reason}` and no matched text —
|
|
6
|
-
* the engine answers "did a rule fire", not "on what". That is the right answer
|
|
7
|
-
* for enforcement and the wrong one for a leak report, which has to say *which*
|
|
8
|
-
* key, so this walks the event itself.
|
|
9
|
-
*
|
|
10
|
-
* Running beside the replay rather than inside it is also what the pattern
|
|
11
|
-
* census concluded independently: detection wants a vocabulary tuned for
|
|
12
|
-
* PRECISION (a false positive becomes an email to a customer about a key that
|
|
13
|
-
* never leaked), while the redactor's vocabulary is tuned for RECALL (a false
|
|
14
|
-
* positive costs a few characters in a digest). Sharing one list would drag one
|
|
15
|
-
* of them to the other's error costs — measured, importing the detector's names
|
|
16
|
-
* into the redactor stopped 305 of 646 currently-masked names from being masked.
|
|
17
|
-
*
|
|
18
|
-
* ## Where it looks, and why both directions matter
|
|
19
|
-
*
|
|
20
|
-
* Tool INPUT and tool RESULT are separate exposures with separate remedies. An
|
|
21
|
-
* input is what the agent SENT — deniable at PreToolUse on all 12 CLIs, and
|
|
22
|
-
* 39.5% of measured credential appearances live there. A result is what the
|
|
23
|
-
* agent RECEIVED, blockable on 2 of 12 and only after the fact. The record keeps
|
|
24
|
-
* them apart so the report can tell a user which of the two happened.
|
|
25
|
-
*/
|
|
26
|
-
import { SECRET_PATTERNS } from "../hooks/builtin-policies";
|
|
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;
|
|
37
|
-
|
|
38
|
-
/** One credential value found in one event. */
|
|
39
|
-
export interface SecretMatch {
|
|
40
|
-
/** The raw value. Held in memory only — the caller fingerprints it and must
|
|
41
|
-
* never persist or transmit this. */
|
|
42
|
-
value: string;
|
|
43
|
-
/** The identifier it was assigned to, when a recognized shaped value also
|
|
44
|
-
* appeared in an assignment. Used only as an actionable label. */
|
|
45
|
-
name: string | null;
|
|
46
|
-
/** Which recognizable credential shape found it. */
|
|
47
|
-
rule: string;
|
|
48
|
-
/** Whether the value carried a recognised vendor shape. Drives the record's
|
|
49
|
-
* confidence, and therefore whether an alert may name a console. */
|
|
50
|
-
shaped: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* `NAME=value` in its four real spellings.
|
|
55
|
-
*
|
|
56
|
-
* Kept in step with `redact-example.ts`'s ASSIGNMENT_RE — same four shapes, same
|
|
57
|
-
* scheme guard — because a value the DETECTOR can report but the REDACTOR
|
|
58
|
-
* cannot mask is a leak in the report itself. The redactor may be broader; it
|
|
59
|
-
* must never be narrower. `__tests__/audit/leak-scan.test.ts` asserts that
|
|
60
|
-
* direction explicitly.
|
|
61
|
-
*/
|
|
62
|
-
/**
|
|
63
|
-
* The identifier is BOUNDED, and the bound is load-bearing rather than tidy.
|
|
64
|
-
*
|
|
65
|
-
* Unbounded (`[A-Za-z0-9_]*`), this is quadratic on a long unbroken token: the
|
|
66
|
-
* engine starts at every one of n positions, consumes the whole run, then
|
|
67
|
-
* backtracks a character at a time looking for the `=` or `:` that never comes.
|
|
68
|
-
* Measured on a plain 300 KB base64 blob — `"A".repeat(300000)` — it ran for
|
|
69
|
-
* over 20 SECONDS on one string. That is not an exotic input: agent transcripts
|
|
70
|
-
* carry base64 images, minified bundles and whole file contents as single
|
|
71
|
-
* lines, and the audit reads every one of them. A scheduled scan hitting a few
|
|
72
|
-
* of these would stall for minutes with nobody watching.
|
|
73
|
-
*
|
|
74
|
-
* 128 characters is far past any real environment-variable or field name, so
|
|
75
|
-
* bounding it costs nothing that exists and makes the scan linear in n.
|
|
76
|
-
*/
|
|
77
|
-
const ASSIGNMENT_RE =
|
|
78
|
-
/(["']?)\b([A-Za-z_][A-Za-z0-9_]{0,127})\1([ \t]*(?::(?!\/\/)|=)[ \t]*)("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
|
|
79
|
-
|
|
80
|
-
/** Strip one layer of surrounding quotes. */
|
|
81
|
-
function unquote(v: string): string {
|
|
82
|
-
return (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))
|
|
83
|
-
? v.slice(1, -1)
|
|
84
|
-
: v;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* SHA-256 of credentials the vendors themselves publish in their documentation.
|
|
89
|
-
*
|
|
90
|
-
* The single highest-value refuter in the whole detector, and the only one that
|
|
91
|
-
* is decisive rather than heuristic. Measured on the real corpus: **456 of 457
|
|
92
|
-
* `AKIA` matches were AWS's own documentation literal** — 99.8% of that
|
|
93
|
-
* pattern's entire output, and 232 findings collapsing to zero. Across the
|
|
94
|
-
* pattern census, 44 of 113 patterns with five or more hits were at least 85%
|
|
95
|
-
* one literal, and 30 fired on nothing else at all.
|
|
96
|
-
*
|
|
97
|
-
* Stored as hashes so this file does not itself contain the values — a
|
|
98
|
-
* denylist written in plaintext is a file our own scanner flags, and one that
|
|
99
|
-
* lands in the corpus of every machine that installs us.
|
|
100
|
-
*
|
|
101
|
-
* A hash comparison cannot false-positive: either the value is byte-identical
|
|
102
|
-
* to a published sample or it is not. That is what makes this worth more than
|
|
103
|
-
* any amount of pattern tuning.
|
|
104
|
-
*/
|
|
105
|
-
const DOCS_LITERALS = new Set([
|
|
106
|
-
"1a5d44a2dca19669d72edf4c4f1c27c4c1ca4b4408fbb17f6ce4ad452d78ddb3", // AWS access key ID
|
|
107
|
-
"78314b11be2e581549ac1c4f616563fad3fdf0c3b71678f6e2299182080e0598", // AWS secret access key
|
|
108
|
-
"cb20d34a7adda2f5d2043836051e242fd4c2b233e02c80c91fe9d5af9b100574", // AWS temporary key ID
|
|
109
|
-
"3543c50b0cb9cea8e55eb1f529b79f043cb250d485fde7b66cabbb5183add7da", // GitHub PAT sample
|
|
110
|
-
"1fae28c1b317300acfd90078c973567c8159015822b0c8b81508a5ec40517d20", // Google API key sample
|
|
111
|
-
"23219833092a3136d7ad178ccb62ebd6ac8cba54ff24c2cfcd37b84aba9e95cf", // SendGrid key sample
|
|
112
|
-
"872db7f4419592e397ef45230f910e06b48c36ed9970cf1f9e4fb896e69f129c", // Slack bot token sample
|
|
113
|
-
"78a08441f4314f0a2833cfe58c62e555a162264206982cda39f6804f5048f570", // Stripe live key sample
|
|
114
|
-
"2cafc0970149a84f3b9e62eaf169f36f59907a3b3e31f7b82e68c69cd27f7326", // Stripe test key sample
|
|
115
|
-
"b363ade84d56c43bebbc9339b4971e340e3f13f7f8d45580b53373d2763e9e33", // Twilio account SID sample
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
/** True when the value is a credential a vendor published on purpose. */
|
|
119
|
-
export function isDocsLiteral(value: string): boolean {
|
|
120
|
-
// Required lazily so this module stays importable from the browser bundle.
|
|
121
|
-
const { createHash } = require("node:crypto") as typeof import("node:crypto");
|
|
122
|
-
return DOCS_LITERALS.has(createHash("sha256").update(value).digest("hex"));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Text that is failproofai's OWN output being replayed back at us.
|
|
127
|
-
*
|
|
128
|
-
* Auditing for secrets writes secrets into the corpus the next audit reads.
|
|
129
|
-
* That is measured, not theoretical: the single largest organic cluster in
|
|
130
|
-
* 1.6 GB of transcripts was one file — a previous secret-audit run quoting back
|
|
131
|
-
* the credentials it had found (123 Atlassian tokens, 26 Telegram, 25 Slack,
|
|
132
|
-
* 18 GitHub) — and 108 of the 161 patterns that matched anything matched ONLY
|
|
133
|
-
* inside the investigation's own transcripts.
|
|
134
|
-
*
|
|
135
|
-
* Left unhandled the count climbs every scan, each report inflating the next.
|
|
136
|
-
* These markers appear in our own rendered output and essentially nowhere else.
|
|
137
|
-
*/
|
|
138
|
-
const SELF_OUTPUT_MARKERS = [
|
|
139
|
-
"[REDACTED:",
|
|
140
|
-
"[redacted:",
|
|
141
|
-
"failproofai audit",
|
|
142
|
-
"assigned secret]",
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
/** True when this text is our own report, not a transcript of real work. */
|
|
146
|
-
export function isSelfOutput(text: string): boolean {
|
|
147
|
-
return SELF_OUTPUT_MARKERS.some((m) => text.includes(m));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Literal prefixes that must appear before any vendor regex is worth running.
|
|
152
|
-
*
|
|
153
|
-
* A prefilter, and it is not an optimisation — it is what makes the pattern set
|
|
154
|
-
* shippable at all. The census measured the cost as LINEAR in pattern count:
|
|
155
|
-
* ~0.095s per pattern per 6 MB, so 288 patterns over a 1.6 GB corpus is roughly
|
|
156
|
-
* two hours, while the handful that ever match take five seconds. Scanning is
|
|
157
|
-
* already the audit's slowest phase and the whole point of adding vendors is
|
|
158
|
-
* that most of them never fire on any given machine.
|
|
159
|
-
*
|
|
160
|
-
* So: one cheap pass over the text asking "does any prefix appear at all", and
|
|
161
|
-
* the regexes only run when one does. Derived from `SECRET_PATTERNS` at module
|
|
162
|
-
* load rather than hand-maintained, because a hand-copied list that drifts from
|
|
163
|
-
* its source silently stops matching — the exact failure class this repo already
|
|
164
|
-
* has a tripwire test for elsewhere.
|
|
165
|
-
*
|
|
166
|
-
* Patterns with no literal prefix (a bare JWT, a connection string, the Telegram
|
|
167
|
-
* `\d{8,10}:` shape) yield no gate token and are always run; they are few, and
|
|
168
|
-
* a pattern that cannot be prefiltered is one whose cost has to be paid.
|
|
169
|
-
*/
|
|
170
|
-
function literalPrefixOf(source: string): string | null {
|
|
171
|
-
// Walk the regex source until the first construct that is not a plain
|
|
172
|
-
// literal. Anything before that is text the input must contain verbatim.
|
|
173
|
-
let out = "";
|
|
174
|
-
for (let i = 0; i < source.length; i++) {
|
|
175
|
-
const c = source[i];
|
|
176
|
-
if (c === "\\") {
|
|
177
|
-
// An escape: only a handful are literal characters we can rely on.
|
|
178
|
-
const next = source[i + 1];
|
|
179
|
-
if (next === undefined) break;
|
|
180
|
-
if (/[dwsSDWbB]/.test(next)) break; // a class, not a literal
|
|
181
|
-
out += next;
|
|
182
|
-
i++;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if ("[](){}|?*+^$.".includes(c)) break;
|
|
186
|
-
out += c;
|
|
187
|
-
}
|
|
188
|
-
// Two characters is the shortest gate worth having: shorter than that and the
|
|
189
|
-
// prefilter matches nearly every input and buys nothing.
|
|
190
|
-
return out.length >= 2 ? out : null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Built once. Each entry is `[pattern, label, gate]`; a null gate always runs. */
|
|
194
|
-
const GATED_PATTERNS: ReadonlyArray<readonly [RegExp, string, string | null]> =
|
|
195
|
-
SECRET_PATTERNS.map(([re, label]) => [re, label, literalPrefixOf(re.source)] as const);
|
|
196
|
-
|
|
197
|
-
/** Patterns worth running against this text. */
|
|
198
|
-
function applicablePatterns(text: string): ReadonlyArray<readonly [RegExp, string, string | null]> {
|
|
199
|
-
return GATED_PATTERNS.filter(([, , gate]) => gate === null || text.includes(gate));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Every credential value in one blob of text.
|
|
204
|
-
*
|
|
205
|
-
* Vendor SHAPES first, then secret-NAMED assignments, deduped by value so one
|
|
206
|
-
* key matched by both layers is reported once — under its vendor label, which
|
|
207
|
-
* is the one that can carry a revocation link. The two layers are near-disjoint
|
|
208
|
-
* in practice: 55.9% of real vendor-shaped credentials in the corpus have no
|
|
209
|
-
* secret-ish word within 60 characters, and 230 of 237 secret-named assignments
|
|
210
|
-
* match no vendor pattern at all.
|
|
211
|
-
*
|
|
212
|
-
* Both refuters run before anything is reported: our own replayed output is
|
|
213
|
-
* dropped whole, and every published documentation sample is dropped by hash.
|
|
214
|
-
*/
|
|
215
|
-
export function findSecrets(text: string): SecretMatch[] {
|
|
216
|
-
if (!text) return [];
|
|
217
|
-
// Our own report replayed into a transcript is not a leak — see isSelfOutput.
|
|
218
|
-
if (isSelfOutput(text)) return [];
|
|
219
|
-
const byValue = new Map<string, SecretMatch>();
|
|
220
|
-
|
|
221
|
-
for (const [pattern, label] of applicablePatterns(text)) {
|
|
222
|
-
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
|
|
223
|
-
for (const m of text.matchAll(re)) {
|
|
224
|
-
const value = m[0];
|
|
225
|
-
if (isDocsLiteral(value)) continue;
|
|
226
|
-
if (!byValue.has(value)) byValue.set(value, { value, name: null, rule: label, shaped: true });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
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.
|
|
236
|
-
for (const m of text.matchAll(ASSIGNMENT_RE)) {
|
|
237
|
-
const name = m[2];
|
|
238
|
-
const value = unquote(m[4]);
|
|
239
|
-
const existing = byValue.get(value);
|
|
240
|
-
if (existing) existing.name ??= name;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return [...byValue.values()];
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Serialize a tool input for scanning.
|
|
248
|
-
*
|
|
249
|
-
* Every string leaf, not just `command`: the measured surface includes file
|
|
250
|
-
* bodies on Write, replacement text on Edit, headers on WebFetch and arbitrary
|
|
251
|
-
* MCP payloads. `JSON.stringify` would work but escapes quotes, which breaks
|
|
252
|
-
* the assignment grammar on exactly the JSON-config shape that carries most
|
|
253
|
-
* first-party keys — so the leaves are joined raw.
|
|
254
|
-
*/
|
|
255
|
-
export function flattenToolInput(input: unknown, depth = 0): string {
|
|
256
|
-
if (depth > 6 || input == null) return "";
|
|
257
|
-
if (typeof input === "string") return input;
|
|
258
|
-
if (typeof input === "number" || typeof input === "boolean") return String(input);
|
|
259
|
-
if (Array.isArray(input)) return input.map((v) => flattenToolInput(v, depth + 1)).join("\n");
|
|
260
|
-
if (typeof input === "object") {
|
|
261
|
-
return Object.entries(input as Record<string, unknown>)
|
|
262
|
-
// Keep the key adjacent to the value so `{"api_key": "…"}` still reads as
|
|
263
|
-
// an assignment to the grammar above.
|
|
264
|
-
.map(([k, v]) => `${k}=${flattenToolInput(v, depth + 1)}`)
|
|
265
|
-
.join("\n");
|
|
266
|
-
}
|
|
267
|
-
return "";
|
|
268
|
-
}
|
package/src/audit/leak-store.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistence for the leak record.
|
|
3
|
-
*
|
|
4
|
-
* Two files, because the data has two different reset semantics and one class
|
|
5
|
-
* cannot be right for both — the same lesson `fp-home.ts` learned when
|
|
6
|
-
* `session.json` moved into a directory classified wholesale as `derived`:
|
|
7
|
-
*
|
|
8
|
-
* audit/leaks.json derived — every finding is reproducible by
|
|
9
|
-
* rescanning, so losing it costs a scan
|
|
10
|
-
* audit/leak-identity.json identity — the HMAC salt and the user's
|
|
11
|
-
* dismissals, neither of which any
|
|
12
|
-
* rescan can recreate
|
|
13
|
-
*
|
|
14
|
-
* Regenerate the salt and every finding id changes, so a machine that has been
|
|
15
|
-
* quiet for months re-alerts for every credential at once. Drop the dismissals
|
|
16
|
-
* and findings a human already judged not-a-secret come back, which is worse
|
|
17
|
-
* than a missed alert: it teaches them the tool does not listen.
|
|
18
|
-
*
|
|
19
|
-
* ## Never throws
|
|
20
|
-
*
|
|
21
|
-
* A scan must not fail because a cache file is corrupt, unreadable, or written
|
|
22
|
-
* by a newer version. Every read degrades to "no record yet" and every write
|
|
23
|
-
* degrades to a no-op with a logged reason. The audit's job is to find leaks,
|
|
24
|
-
* and refusing to run because its own bookkeeping is damaged would be the
|
|
25
|
-
* failure mode that matters least causing the one that matters most.
|
|
26
|
-
*/
|
|
27
|
-
import { randomBytes } from "node:crypto";
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
29
|
-
import { dirname } from "node:path";
|
|
30
|
-
|
|
31
|
-
import { auditLeaksFile, auditLeakIdentityFile } from "../hooks/fp-home";
|
|
32
|
-
import { writeJsonAtomically } from "../../lib/atomic-write";
|
|
33
|
-
import {
|
|
34
|
-
emptyRecord,
|
|
35
|
-
pruneRecord,
|
|
36
|
-
LEAK_RECORD_SCHEMA_VERSION,
|
|
37
|
-
type LeakRecord,
|
|
38
|
-
type LeakFinding,
|
|
39
|
-
type LeakSighting,
|
|
40
|
-
} from "./leak-record";
|
|
41
|
-
import { isFindingId } from "./leak-fingerprint";
|
|
42
|
-
|
|
43
|
-
interface LeakIdentity {
|
|
44
|
-
/** Hex HMAC salt. Generated once, never transmitted, never derived from
|
|
45
|
-
* anything guessable — see `fingerprintId`. */
|
|
46
|
-
salt: string;
|
|
47
|
-
/** Finding ids the user has said are not secrets. */
|
|
48
|
-
dismissed: string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Mode for both files. They carry no credential, but they do carry a machine
|
|
52
|
-
* salt and a map of which projects leaked what — not world-readable. */
|
|
53
|
-
const FILE_MODE = 0o600;
|
|
54
|
-
|
|
55
|
-
function readJson<T>(path: string): T | null {
|
|
56
|
-
try {
|
|
57
|
-
if (!existsSync(path)) return null;
|
|
58
|
-
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
59
|
-
} catch {
|
|
60
|
-
// Corrupt, truncated by a crash, or written by a version we cannot read.
|
|
61
|
-
// Treated as absent: the next write replaces it.
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The machine's leak identity, minting a salt on first use.
|
|
68
|
-
*
|
|
69
|
-
* `randomBytes(32)`, not a hash of anything about the machine. A salt derived
|
|
70
|
-
* from a hostname or a uid would be reproducible by anyone who knows those, and
|
|
71
|
-
* the whole point of salting the fingerprint is that a third party holding a
|
|
72
|
-
* guessable candidate cannot confirm it by recomputing the digest.
|
|
73
|
-
*/
|
|
74
|
-
export function readLeakIdentity(home?: string): LeakIdentity {
|
|
75
|
-
const existing = readJson<Partial<LeakIdentity>>(auditLeakIdentityFile(home));
|
|
76
|
-
if (existing && typeof existing.salt === "string" && existing.salt.length >= 32) {
|
|
77
|
-
return {
|
|
78
|
-
salt: existing.salt,
|
|
79
|
-
dismissed: existing.dismissed ?? [],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
const minted: LeakIdentity = { salt: randomBytes(32).toString("hex"), dismissed: [] };
|
|
83
|
-
writeLeakIdentity(minted, home);
|
|
84
|
-
return minted;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function writeLeakIdentity(identity: LeakIdentity, home?: string): boolean {
|
|
88
|
-
const path = auditLeakIdentityFile(home);
|
|
89
|
-
try {
|
|
90
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
91
|
-
writeJsonAtomically(path, identity, { mode: FILE_MODE });
|
|
92
|
-
return true;
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Load the record, or an empty one.
|
|
100
|
-
*
|
|
101
|
-
* A record written by a NEWER schema is discarded rather than migrated or
|
|
102
|
-
* partially read: this file is a cache of derived findings, so throwing it away
|
|
103
|
-
* costs one rescan, while guessing at an unknown shape risks reporting a
|
|
104
|
-
* fingerprint built by rules we no longer understand.
|
|
105
|
-
*/
|
|
106
|
-
/**
|
|
107
|
-
* Drop a finding this build cannot render, and repair one it can.
|
|
108
|
-
*
|
|
109
|
-
* `leaks.json` is a file on disk. It can be truncated by a full disk mid-write,
|
|
110
|
-
* edited by hand, or written by a NEWER build whose findings carry a shape this
|
|
111
|
-
* one does not expect — and the envelope check below cannot see any of that,
|
|
112
|
-
* because it only looks at `schemaVersion` and `findings` being an array.
|
|
113
|
-
*
|
|
114
|
-
* The failure that motivated this was not hypothetical in its consequences: one
|
|
115
|
-
* malformed entry made `selectLeaks` dereference `undefined`, which threw out
|
|
116
|
-
* of `buildHarmReport`, which sits OUTSIDE `reportHarm`'s try — so a scheduled
|
|
117
|
-
* run that scanned correctly and wrote its cache correctly still exited 1, and
|
|
118
|
-
* would keep doing so on every run until somebody opened the file.
|
|
119
|
-
*
|
|
120
|
-
* A bad SIGHTING loses the sighting, not the finding: the credential is the
|
|
121
|
-
* thing that needs rotating, and "seen in a transcript" with no detail beats
|
|
122
|
-
* silence about a leaked key.
|
|
123
|
-
*/
|
|
124
|
-
function sanitizeFinding(raw: unknown): LeakFinding | null {
|
|
125
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
126
|
-
const f = raw as Record<string, unknown>;
|
|
127
|
-
// The id becomes a filename in the macOS notification queue, so anything
|
|
128
|
-
// that is not the exact minted shape disqualifies the whole entry.
|
|
129
|
-
if (typeof f.id !== "string" || !isFindingId(f.id)) return null;
|
|
130
|
-
|
|
131
|
-
const fp = f.fingerprint as Record<string, unknown> | undefined;
|
|
132
|
-
if (!fp || typeof fp !== "object" || typeof fp.display !== "string" || typeof fp.label !== "string") {
|
|
133
|
-
return null; // with no way to name it, a row would say nothing at all
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const sightings = Array.isArray(f.sightings)
|
|
137
|
-
? (f.sightings as unknown[]).filter((s): s is LeakSighting => {
|
|
138
|
-
if (!s || typeof s !== "object") return false;
|
|
139
|
-
const m = (s as Record<string, unknown>).mechanism as Record<string, unknown> | undefined;
|
|
140
|
-
return !!m && typeof m === "object" && typeof m.summary === "string";
|
|
141
|
-
})
|
|
142
|
-
: [];
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
...(f as unknown as LeakFinding),
|
|
146
|
-
fingerprint: {
|
|
147
|
-
display: fp.display,
|
|
148
|
-
label: fp.label,
|
|
149
|
-
length: typeof fp.length === "number" && Number.isFinite(fp.length) ? fp.length : 0,
|
|
150
|
-
attributed: fp.attributed === true,
|
|
151
|
-
},
|
|
152
|
-
name: typeof f.name === "string" ? f.name : null,
|
|
153
|
-
firstSeen: typeof f.firstSeen === "string" ? f.firstSeen : "",
|
|
154
|
-
lastSeen: typeof f.lastSeen === "string" ? f.lastSeen : "",
|
|
155
|
-
occurrences:
|
|
156
|
-
typeof f.occurrences === "number" && Number.isFinite(f.occurrences) ? f.occurrences : sightings.length,
|
|
157
|
-
sightings,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function readLeakRecord(home?: string): LeakRecord {
|
|
162
|
-
const identity = readLeakIdentity(home);
|
|
163
|
-
const raw = readJson<LeakRecord>(auditLeaksFile(home));
|
|
164
|
-
if (!raw || raw.schemaVersion !== LEAK_RECORD_SCHEMA_VERSION || !Array.isArray(raw.findings)) {
|
|
165
|
-
return emptyRecord(identity.salt, new Date().toISOString());
|
|
166
|
-
}
|
|
167
|
-
// Every consumer — the digest, the dashboard, the notice guard — reads through
|
|
168
|
-
// here, so validating once at the gate is what keeps them all from each
|
|
169
|
-
// needing to defend against the same corrupt file.
|
|
170
|
-
const findings = (raw.findings as unknown[])
|
|
171
|
-
.map(sanitizeFinding)
|
|
172
|
-
.filter((f): f is LeakFinding => f !== null);
|
|
173
|
-
// The salt always comes from the identity file, never from the record — if
|
|
174
|
-
// the two ever disagree the identity file is authoritative, because it is the
|
|
175
|
-
// one classified to survive a reset.
|
|
176
|
-
return { ...raw, findings, salt: identity.salt };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Persist the record, pruned.
|
|
181
|
-
*
|
|
182
|
-
* Pruning on WRITE rather than on read, so the bound is enforced exactly once
|
|
183
|
-
* per scan and a reader never pays for it. Returns false on failure rather than
|
|
184
|
-
* throwing — see the module header.
|
|
185
|
-
*/
|
|
186
|
-
export function writeLeakRecord(record: LeakRecord, home?: string, nowMs = Date.now()): boolean {
|
|
187
|
-
const path = auditLeaksFile(home);
|
|
188
|
-
try {
|
|
189
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
190
|
-
const pruned = pruneRecord(record, nowMs);
|
|
191
|
-
pruned.updatedAt = new Date(nowMs).toISOString();
|
|
192
|
-
writeJsonAtomically(path, pruned, { mode: FILE_MODE });
|
|
193
|
-
return true;
|
|
194
|
-
} catch {
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Mark a finding as not-a-secret. Survives a reset; see the module header. */
|
|
200
|
-
export function dismissFinding(id: string, home?: string): boolean {
|
|
201
|
-
const identity = readLeakIdentity(home);
|
|
202
|
-
if (identity.dismissed.includes(id)) return true;
|
|
203
|
-
identity.dismissed.push(id);
|
|
204
|
-
return writeLeakIdentity(identity, home);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Findings worth showing a human: undismissed, newest first.
|
|
209
|
-
*
|
|
210
|
-
* The dismissal list is applied HERE rather than at write time, so a dismissal
|
|
211
|
-
* never destroys evidence — the finding stays in the record and simply stops
|
|
212
|
-
* being surfaced. A user who dismisses by mistake has lost a row in a list, not
|
|
213
|
-
* the fact that a credential leaked.
|
|
214
|
-
*/
|
|
215
|
-
export function activeFindings(record: LeakRecord, home?: string): LeakRecord["findings"] {
|
|
216
|
-
const dismissed = new Set(readLeakIdentity(home).dismissed);
|
|
217
|
-
return record.findings
|
|
218
|
-
.filter((f) => !dismissed.has(f.id))
|
|
219
|
-
.sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : a.lastSeen > b.lastSeen ? -1 : 0));
|
|
220
|
-
}
|