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
|
@@ -42,8 +42,6 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { homedir } from "node:os";
|
|
44
44
|
|
|
45
|
-
import type { AuditResult } from "./types";
|
|
46
|
-
|
|
47
45
|
import { SECRET_PATTERNS } from "../hooks/builtin-policies";
|
|
48
46
|
|
|
49
47
|
/** Longest example we let through, after redaction. */
|
|
@@ -134,77 +132,18 @@ export function maskTruncatedSecret(input: string): string {
|
|
|
134
132
|
* identifier, including the camelCase `_authToken` that `npm config set`
|
|
135
133
|
* writes. `KEY`, `PASS`, `AUTH`, `PAT` and `SIG` are also fragments of ordinary
|
|
136
134
|
* words — `MONKEY_COUNT`, `PASSENGERS`, `AUTHOR`, `PATH`, `SIGNAL` — so they
|
|
137
|
-
* only count as a whole component
|
|
138
|
-
* `identifierComponents` and include camelCase humps as well as `_`.
|
|
135
|
+
* only count as a whole `_`-delimited component.
|
|
139
136
|
*/
|
|
140
137
|
const SECRET_NAME_SUBSTRINGS = [
|
|
141
138
|
"TOKEN",
|
|
142
139
|
"SECRET",
|
|
143
140
|
"PASSWORD",
|
|
144
141
|
"PASSWD",
|
|
145
|
-
"PASSPHRASE",
|
|
146
142
|
"CREDENTIAL",
|
|
147
143
|
"APIKEY",
|
|
148
144
|
"PRIVATEKEY",
|
|
149
145
|
];
|
|
150
|
-
const SECRET_NAME_COMPONENTS = ["PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "COOKIE"];
|
|
151
|
-
|
|
152
|
-
/**
|
|
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`.
|
|
182
|
-
*/
|
|
183
|
-
const COMPOUND_ONLY_COMPONENTS = ["PWD", "KEY"];
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Split an identifier into its uppercased components, on `_`, `-`, `.` AND
|
|
187
|
-
* camelCase humps.
|
|
188
|
-
*
|
|
189
|
-
* The humps are the point. Splitting on `_` alone means `sessionKey`, `dbPass`,
|
|
190
|
-
* `basicAuth` and `authCookie` decompose to a single component that matches
|
|
191
|
-
* nothing, so their values shipped to the digest verbatim — 203 such names on
|
|
192
|
-
* this machine's corpus. They are not exotic: camelCase is what an identifier
|
|
193
|
-
* looks like everywhere except a shell environment, and the redactor is applied
|
|
194
|
-
* to transcripts full of TypeScript, JSON and Python.
|
|
195
|
-
*
|
|
196
|
-
* The second replace splits an acronym from the word that follows it, so
|
|
197
|
-
* `APIKey` yields `API` + `KEY` rather than one run of capitals that matches
|
|
198
|
-
* neither.
|
|
199
|
-
*/
|
|
200
|
-
function identifierComponents(name: string): string[] {
|
|
201
|
-
return name
|
|
202
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
203
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
204
|
-
.toUpperCase()
|
|
205
|
-
.split(/[_\-.]+/)
|
|
206
|
-
.filter(Boolean);
|
|
207
|
-
}
|
|
146
|
+
const SECRET_NAME_COMPONENTS = ["KEY", "PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "SESSION", "COOKIE"];
|
|
208
147
|
|
|
209
148
|
/**
|
|
210
149
|
* `NAME=value`, with the value quoted or running to the next shell separator.
|
|
@@ -212,158 +151,20 @@ function identifierComponents(name: string): string[] {
|
|
|
212
151
|
* The unquoted alternative excludes quotes as well as separators: a value that
|
|
213
152
|
* runs to end-of-token inside an already-quoted string (`"…?sig=deadbeef"`)
|
|
214
153
|
* would otherwise swallow the closing quote and leave the line unbalanced.
|
|
215
|
-
*
|
|
216
|
-
* Four shapes, because the first spelling of this pattern was `NAME=value` and
|
|
217
|
-
* only that — no whitespace around the `=`, no `:`, no quoted name — and the
|
|
218
|
-
* other three all reached the emailed digest UNREDACTED. `NAME = "value"` and
|
|
219
|
-
* `NAME: "value"` are what a config file, a YAML key and a JSON body look like,
|
|
220
|
-
* which is most of where credentials are actually written down; `"name": val`
|
|
221
|
-
* is the same line once an agent pretty-prints it. A value only survived that
|
|
222
|
-
* gap if it independently matched one of the vendor patterns in
|
|
223
|
-
* `SECRET_PATTERNS` — and 230 of 237 secret-named assignments measured across
|
|
224
|
-
* this machine's transcripts match none of them, so the assignment rule was the
|
|
225
|
-
* only thing standing between them and the digest.
|
|
226
|
-
*
|
|
227
|
-
* The separator is captured and re-emitted verbatim rather than normalised to
|
|
228
|
-
* `=`, so a redacted YAML line is still YAML and a redacted JSON line is still
|
|
229
|
-
* recognisable as the field it came from. Horizontal whitespace only: `\s`
|
|
230
|
-
* would let a `KEY:` at end-of-line swallow the newline and glue the next line
|
|
231
|
-
* onto the match.
|
|
232
|
-
*
|
|
233
|
-
* The `:` alternative refuses a `//` after it, because a URL scheme is the one
|
|
234
|
-
* `name:value` shape that is not an assignment. Without the guard `https` reads
|
|
235
|
-
* as the name and the rest of the URL as its value — and since a non-secret
|
|
236
|
-
* name returns the match UNCHANGED but still CONSUMES it, a `?token=…` sitting
|
|
237
|
-
* inside that URL was swallowed by the `https:` match and never examined. The
|
|
238
|
-
* failure mode is silent and inverted: adding a separator to catch more secrets
|
|
239
|
-
* stopped one already being caught.
|
|
240
|
-
*/
|
|
241
|
-
/**
|
|
242
|
-
* The identifier is BOUNDED, and the bound is load-bearing rather than tidy.
|
|
243
|
-
*
|
|
244
|
-
* Unbounded (`[A-Za-z0-9_]*`), this is quadratic on a long unbroken token: the
|
|
245
|
-
* engine starts at every one of n positions, consumes the whole run, then
|
|
246
|
-
* backtracks a character at a time looking for the `=` or `:` that never comes.
|
|
247
|
-
* Measured on a plain 300 KB base64 blob — `"A".repeat(300000)` — it ran for
|
|
248
|
-
* over 20 SECONDS on one string. That is not an exotic input: agent transcripts
|
|
249
|
-
* carry base64 images, minified bundles and whole file contents as single
|
|
250
|
-
* lines, and the audit reads every one of them. A scheduled scan hitting a few
|
|
251
|
-
* of these would stall for minutes with nobody watching.
|
|
252
|
-
*
|
|
253
|
-
* 128 characters is far past any real environment-variable or field name, so
|
|
254
|
-
* bounding it costs nothing that exists and makes the scan linear in n.
|
|
255
154
|
*/
|
|
256
|
-
const ASSIGNMENT_RE =
|
|
257
|
-
/(["']?)\b([A-Za-z_][A-Za-z0-9_]{0,127})\1([ \t]*(?::(?!\/\/)|=)[ \t]*)("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
|
|
155
|
+
const ASSIGNMENT_RE = /\b([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
|
|
258
156
|
|
|
259
157
|
/** Credentials inline in a URL, on ANY scheme — `https://user:pass@host`. */
|
|
260
|
-
|
|
261
|
-
// Unbounded, `[a-z0-9+.\-]*` is quadratic on any long lowercase run: at each of
|
|
262
|
-
// n positions it consumes to the end hunting for a `://` that is not there,
|
|
263
|
-
// then backtracks a character at a time. Measured at 18 SECONDS on 100 KB of a
|
|
264
|
-
// single repeated letter — and this one runs over the WHOLE text, on every
|
|
265
|
-
// example the audit redacts. A URI scheme is at most a few characters (RFC 3986
|
|
266
|
-
// allows more, but nothing real uses it), and userinfo that runs past 256 bytes
|
|
267
|
-
// is not a credential anyone typed.
|
|
268
|
-
const URL_CREDENTIALS_RE = /([a-z][a-z0-9+.\-]{0,31}:\/\/)[^\s/:@]{1,256}:[^\s/@]{1,256}@/gi;
|
|
158
|
+
const URL_CREDENTIALS_RE = /([a-z][a-z0-9+.\-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
|
|
269
159
|
|
|
270
160
|
/** `curl -u user:pass`, in both spellings and both separators. */
|
|
271
161
|
const BASIC_AUTH_FLAG_RE = /((?:^|\s)(?:-u|--user)[\s=])\S+:\S+/g;
|
|
272
162
|
|
|
273
163
|
/** True when an identifier's name says its value is a credential. */
|
|
274
|
-
/**
|
|
275
|
-
* Prefixes whose values a build tool INLINES INTO THE BROWSER BUNDLE.
|
|
276
|
-
*
|
|
277
|
-
* These are not conventionally public, they are mechanically public: Next.js,
|
|
278
|
-
* Vite, CRA, Expo, SvelteKit, Astro, Nuxt and Gatsby each substitute the value
|
|
279
|
-
* into the JavaScript they ship to every visitor. A credential named this way
|
|
280
|
-
* has already been published by the framework, to everyone, before failproofai
|
|
281
|
-
* ever saw it.
|
|
282
|
-
*/
|
|
283
|
-
const CLIENT_BUNDLE_PREFIXES = [
|
|
284
|
-
"NEXT_PUBLIC",
|
|
285
|
-
"VITE",
|
|
286
|
-
"REACT_APP",
|
|
287
|
-
"EXPO_PUBLIC",
|
|
288
|
-
"NUXT_PUBLIC",
|
|
289
|
-
"GATSBY",
|
|
290
|
-
"STORYBOOK",
|
|
291
|
-
"PUBLIC",
|
|
292
|
-
];
|
|
293
|
-
|
|
294
|
-
/** Markers that name a key as the publishable half of a pair. */
|
|
295
|
-
const PUBLISHED_MARKERS = /PUBLISHABLE|(^|_)PUBLIC_KEY$|(^|_)ANON_KEY$/;
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* True when the identifier says the value is meant to be public.
|
|
299
|
-
*
|
|
300
|
-
* `isSecretName` matches any component in `KEY`/`PASS`/`AUTH`/… — and EVERY
|
|
301
|
-
* publishable key on earth is named `*_KEY`. Measured before this existed, it
|
|
302
|
-
* returned true for all twelve of `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`,
|
|
303
|
-
* `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `VAPID_PUBLIC_KEY`, `PUBLISHABLE_KEY` and
|
|
304
|
-
* friends — including the ones carrying the literal word PUBLIC.
|
|
305
|
-
*
|
|
306
|
-
* Over-redaction is usually cheap: masking something that might be secret costs
|
|
307
|
-
* readability. This case is different, because we can PROVE the value is not a
|
|
308
|
-
* secret — so masking it reports a credential exposure that did not happen, and
|
|
309
|
-
* a security tool crying wolf about a browser-bundled config value is not a
|
|
310
|
-
* cheap error. This repo's own committed PostHog `phc_` key, documented in
|
|
311
|
-
* source as "Write-only (safe to commit)", is masked as an "assigned secret" by
|
|
312
|
-
* the rule this vetoes.
|
|
313
|
-
*
|
|
314
|
-
* Deliberately narrow. The marker must be a PREFIX (so a mid-name `PUBLIC` in
|
|
315
|
-
* `MY_PUBLIC_FACING_SECRET` does not disarm it) or an explicit publishable
|
|
316
|
-
* suffix. Words like ANON, BROWSER and SITE are excluded on their own — only
|
|
317
|
-
* `ANON_KEY`, which is Supabase's documented browser key, qualifies.
|
|
318
|
-
*/
|
|
319
|
-
export function isPublishedByDesign(name: string): boolean {
|
|
320
|
-
const components = identifierComponents(name);
|
|
321
|
-
if (components.length === 0) return false;
|
|
322
|
-
const upper = components.join("_");
|
|
323
|
-
for (const prefix of CLIENT_BUNDLE_PREFIXES) {
|
|
324
|
-
if (upper === prefix || upper.startsWith(prefix + "_")) return true;
|
|
325
|
-
}
|
|
326
|
-
return PUBLISHED_MARKERS.test(upper);
|
|
327
|
-
}
|
|
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
|
-
|
|
358
164
|
export function isSecretName(name: string): boolean {
|
|
359
|
-
// Runs first and beats every other signal: a value the framework compiles
|
|
360
|
-
// into the browser bundle is public no matter what the rest of the name says.
|
|
361
|
-
if (isPublishedByDesign(name)) return false;
|
|
362
165
|
const upper = name.toUpperCase();
|
|
363
166
|
if (SECRET_NAME_SUBSTRINGS.some((word) => upper.includes(word))) return true;
|
|
364
|
-
|
|
365
|
-
if (components.some((part) => SECRET_NAME_COMPONENTS.includes(part))) return true;
|
|
366
|
-
return components.length > 1 && components.some((part) => COMPOUND_ONLY_COMPONENTS.includes(part));
|
|
167
|
+
return upper.split("_").some((part) => SECRET_NAME_COMPONENTS.includes(part));
|
|
367
168
|
}
|
|
368
169
|
|
|
369
170
|
/**
|
|
@@ -392,18 +193,15 @@ export function isSecretName(name: string): boolean {
|
|
|
392
193
|
* is the actionable half of the finding.
|
|
393
194
|
*/
|
|
394
195
|
export function maskAssignedSecrets(input: string): string {
|
|
395
|
-
let out = input.replace(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return `${quote}${name}${quote}${separator}[REDACTED: assigned secret]`;
|
|
405
|
-
},
|
|
406
|
-
);
|
|
196
|
+
let out = input.replace(ASSIGNMENT_RE, (match, name: string, value: string) => {
|
|
197
|
+
if (!isSecretName(name)) return match;
|
|
198
|
+
// An earlier pass already named this one, and it named it better.
|
|
199
|
+
// `export ANTHROPIC_API_KEY=sk-ant-…` is masked by the vendor pattern as
|
|
200
|
+
// "Anthropic API key"; re-masking it here would downgrade that to the
|
|
201
|
+
// generic label and strip the marker's own tail as it went.
|
|
202
|
+
if (value.startsWith("[REDACTED")) return match;
|
|
203
|
+
return `${name}=[REDACTED: assigned secret]`;
|
|
204
|
+
});
|
|
407
205
|
out = out.replace(URL_CREDENTIALS_RE, "$1[REDACTED: URL credentials]@");
|
|
408
206
|
out = out.replace(BASIC_AUTH_FLAG_RE, "$1[REDACTED: basic auth]");
|
|
409
207
|
return out;
|
|
@@ -500,122 +298,15 @@ export function shortenPaths(input: string, home = homedir()): string {
|
|
|
500
298
|
* digest as one row, and a raw newline there breaks the plain-text layout while
|
|
501
299
|
* saying nothing the single line does not.
|
|
502
300
|
*/
|
|
503
|
-
/**
|
|
504
|
-
* Mask every secret, and nothing else.
|
|
505
|
-
*
|
|
506
|
-
* The three masking passes of `redactExample` without the path shortening, the
|
|
507
|
-
* whitespace collapse or the length cap. This is what a LOCAL renderer wants:
|
|
508
|
-
* on your own machine your own paths are the useful half of an example, and
|
|
509
|
-
* `~/…/db.ts` costs readability for no gain — nobody is protected from their own
|
|
510
|
-
* directory names. A credential on screen is the other half of that trade: it is
|
|
511
|
-
* one screenshot, one pasted issue or one screen-share away from being published,
|
|
512
|
-
* and unlike the path it can never be un-leaked.
|
|
513
|
-
*
|
|
514
|
-
* So the split is deliberate: everything that LEAVES the machine goes through
|
|
515
|
-
* `redactExample`; the terminal gets this.
|
|
516
|
-
*/
|
|
517
|
-
export function maskSecretsOnly(input: string): string {
|
|
518
|
-
return maskAssignedSecrets(maskTruncatedSecret(maskSecrets(input)));
|
|
519
|
-
}
|
|
520
|
-
|
|
521
301
|
export function redactExample(input: string, home = homedir()): string {
|
|
522
302
|
// Assignment masking runs LAST of the three, so the two pattern-based passes
|
|
523
303
|
// get first refusal on anything they can name precisely. A vendor prefix
|
|
524
304
|
// yields "[REDACTED: Anthropic API key]"; falling through to this one would
|
|
525
305
|
// have said only "assigned secret", which is true but less useful to read.
|
|
526
|
-
const masked =
|
|
306
|
+
const masked = maskAssignedSecrets(maskTruncatedSecret(maskSecrets(input)));
|
|
527
307
|
const shortened = shortenPaths(masked, home);
|
|
528
308
|
const collapsed = shortened.replace(/\s+/g, " ").trim();
|
|
529
309
|
return collapsed.length > REDACTED_EXAMPLE_MAX_CHARS
|
|
530
310
|
? `${collapsed.slice(0, REDACTED_EXAMPLE_MAX_CHARS - 1)}…`
|
|
531
311
|
: collapsed;
|
|
532
312
|
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Redact every free-text field of an `AuditResult` that could carry a secret or
|
|
536
|
-
* name someone's disk.
|
|
537
|
-
*
|
|
538
|
-
* The counts, titles, timestamps and policy names are the substance of a report
|
|
539
|
-
* and none of them come from user data. What DOES come from user data is the
|
|
540
|
-
* example strings (slices of real commands), the per-example `cwd`, and the
|
|
541
|
-
* project lists — which are the same three things `redactExample` was written
|
|
542
|
-
* for, applied to the whole structure instead of one row.
|
|
543
|
-
*
|
|
544
|
-
* Used by every renderer that produces an artifact which can travel:
|
|
545
|
-
* `formatMarkdown` (the file the CLI calls a "Shareable report") and
|
|
546
|
-
* `formatJson` (whatever the caller pipes it into). The emailed digest reaches
|
|
547
|
-
* the same guarantee by a different route — `harm-report.ts` redacts each
|
|
548
|
-
* example as it selects it, because it also has to apply the reporting window.
|
|
549
|
-
*
|
|
550
|
-
* Returns a NEW object. The caller's result is left alone, so the dashboard and
|
|
551
|
-
* the cache keep the unredacted values they need to render a local view.
|
|
552
|
-
*/
|
|
553
|
-
export function redactAuditResult(result: AuditResult, home = homedir()): AuditResult {
|
|
554
|
-
// Built field by field ON PURPOSE. This was `{...result}` plus three named
|
|
555
|
-
// rewrites, which meant it was a FIELD ALLOWLIST pretending to be a redactor:
|
|
556
|
-
// every field it did not name — including any field added later — passed
|
|
557
|
-
// through byte-identical, absolute home paths and all. It guards
|
|
558
|
-
// `formatJson`, i.e. an artifact the product invites the user to share.
|
|
559
|
-
//
|
|
560
|
-
// Enumerating every key means TypeScript rejects the build when `AuditResult`
|
|
561
|
-
// grows a field nobody decided about here, which is the only mechanism that
|
|
562
|
-
// survives a future contributor. `redactAuditResultKeys` in the tests reflects
|
|
563
|
-
// over the real object and fails if this list drifts, covering the case where
|
|
564
|
-
// a field is added as optional and the compiler stays quiet.
|
|
565
|
-
const redacted: AuditResult = {
|
|
566
|
-
// Scalars and counters: no paths, no user content, nothing to redact.
|
|
567
|
-
version: result.version,
|
|
568
|
-
scannedAt: result.scannedAt,
|
|
569
|
-
transcripts: result.transcripts,
|
|
570
|
-
totals: result.totals,
|
|
571
|
-
eventsScanned: result.eventsScanned,
|
|
572
|
-
enabledBuiltinNames: result.enabledBuiltinNames,
|
|
573
|
-
// Salted, machine-local ids. They carry no path and cannot be reversed to
|
|
574
|
-
// a credential, so they pass through — but the decision is recorded here
|
|
575
|
-
// rather than inherited from a spread.
|
|
576
|
-
leakIds: result.leakIds,
|
|
577
|
-
newLeakIds: result.newLeakIds,
|
|
578
|
-
|
|
579
|
-
// Everything below carries a path or user content and is rewritten.
|
|
580
|
-
scope: {
|
|
581
|
-
cli: result.scope.cli,
|
|
582
|
-
since: result.scope.since,
|
|
583
|
-
projects: result.scope.projects === "all"
|
|
584
|
-
? "all"
|
|
585
|
-
: result.scope.projects.map((p) => shortenPaths(p, home)),
|
|
586
|
-
},
|
|
587
|
-
results: result.results.map((row) => ({
|
|
588
|
-
...row,
|
|
589
|
-
examples: row.examples.map((e) => ({
|
|
590
|
-
...e,
|
|
591
|
-
example: redactExample(e.example, home),
|
|
592
|
-
cwd: shortenPaths(e.cwd, home),
|
|
593
|
-
})),
|
|
594
|
-
})),
|
|
595
|
-
projectsScanned: result.projectsScanned.map((p) => shortenPaths(p, home)),
|
|
596
|
-
};
|
|
597
|
-
return redacted;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Every top-level key `redactAuditResult` has consciously handled.
|
|
602
|
-
*
|
|
603
|
-
* The compiler catches a REQUIRED field added to `AuditResult`, because the
|
|
604
|
-
* object literal above would stop satisfying the type. It does not catch an
|
|
605
|
-
* OPTIONAL one — and the leak record is arriving as optional fields. So the
|
|
606
|
-
* test reflects over a real result and fails when a key appears here that is
|
|
607
|
-
* not in this list, which forces the decision to be made rather than defaulted.
|
|
608
|
-
*/
|
|
609
|
-
export const REDACTED_AUDIT_RESULT_KEYS: ReadonlyArray<keyof AuditResult> = [
|
|
610
|
-
"version",
|
|
611
|
-
"scannedAt",
|
|
612
|
-
"scope",
|
|
613
|
-
"transcripts",
|
|
614
|
-
"results",
|
|
615
|
-
"totals",
|
|
616
|
-
"projectsScanned",
|
|
617
|
-
"eventsScanned",
|
|
618
|
-
"enabledBuiltinNames",
|
|
619
|
-
"leakIds",
|
|
620
|
-
"newLeakIds",
|
|
621
|
-
];
|
package/src/audit/report-harm.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
2
|
+
* The side effect a scheduled audit has that no other audit does: telling the
|
|
3
|
+
* api-server what it found, so a harm digest can be mailed.
|
|
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 enabled emailed reports must never be able to tell this code exists.
|
|
19
19
|
*
|
|
20
20
|
* ## Why the CHILD does this and not the daemon
|
|
21
21
|
*
|
|
@@ -31,11 +31,9 @@ import { getValidAccessToken } from "../../lib/auth/auth-store";
|
|
|
31
31
|
import { AuthApiError, submitAuditReport } from "../../lib/auth/api-server-client";
|
|
32
32
|
import { readConfig } from "../hooks/fp-config";
|
|
33
33
|
import { buildHarmReport } from "./harm-report";
|
|
34
|
-
import { activeFindings, readLeakRecord } from "./leak-store";
|
|
35
34
|
import {
|
|
36
35
|
ensureMachineIdentity,
|
|
37
36
|
machineLabel,
|
|
38
|
-
readMachineIdentity,
|
|
39
37
|
recordReportWatermark,
|
|
40
38
|
} from "./machine-store";
|
|
41
39
|
import type { AuditResult } from "./types";
|
|
@@ -43,26 +41,31 @@ import type { AuditResult } from "./types";
|
|
|
43
41
|
/** What happened, for the one line the scheduled run prints. */
|
|
44
42
|
export type HarmReportOutcome =
|
|
45
43
|
| { kind: "disabled" }
|
|
46
|
-
| { kind: "
|
|
44
|
+
| { kind: "consent-required" }
|
|
47
45
|
| { kind: "signed-out" }
|
|
48
|
-
| { kind: "sent";
|
|
49
|
-
| { kind: "held";
|
|
46
|
+
| { kind: "sent"; hits: number }
|
|
47
|
+
| { kind: "held"; hits: number; reason: string }
|
|
50
48
|
| { kind: "failed"; error: string };
|
|
51
49
|
|
|
52
50
|
/**
|
|
53
|
-
* Report this scan's
|
|
51
|
+
* Report this scan's harmful findings, if the user asked for that.
|
|
54
52
|
*
|
|
55
53
|
* Returns an outcome rather than a boolean so the caller can say something
|
|
56
|
-
* truthful. "held" in particular is not a failure — a machine
|
|
57
|
-
* cooldown is working exactly as intended, and a line
|
|
58
|
-
* would train people to ignore the line.
|
|
54
|
+
* truthful. "held" in particular is not a failure — a machine below the
|
|
55
|
+
* threshold, or inside its cooldown, is working exactly as intended, and a line
|
|
56
|
+
* that called that an error would train people to ignore the line.
|
|
59
57
|
*/
|
|
60
58
|
export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome> {
|
|
59
|
+
// ONE switch. `auto` means "scan on a timer AND tell me", because the reason
|
|
60
|
+
// to put a scan on a timer is to be told — a machine scanning quietly with
|
|
61
|
+
// nothing to report to is a feature that looks on and does nothing visible.
|
|
61
62
|
let auto = false;
|
|
63
|
+
let consentedAt: number | undefined;
|
|
62
64
|
let intervalDays = 7;
|
|
63
65
|
try {
|
|
64
66
|
const config = readConfig();
|
|
65
67
|
auto = config.audit.auto;
|
|
68
|
+
consentedAt = config.audit.reportsConsentedAt;
|
|
66
69
|
// Also the width of a FIRST report's window, so a new machine's opening
|
|
67
70
|
// digest covers the same period every later one will.
|
|
68
71
|
intervalDays = config.audit.intervalDays;
|
|
@@ -72,18 +75,13 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
72
75
|
}
|
|
73
76
|
if (!auto) return { kind: "disabled" };
|
|
74
77
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
prior?.last_reported_at,
|
|
83
|
-
intervalDays,
|
|
84
|
-
activeFindings(readLeakRecord()),
|
|
85
|
-
);
|
|
86
|
-
if (report.leaks.length === 0) return { kind: "no-leak" };
|
|
78
|
+
// `auto` alone is not consent to SEND, on a machine that set it before this
|
|
79
|
+
// existed. Through 1.0.0 that key meant "scan locally on a timer" — no
|
|
80
|
+
// account, no network, and the toggle that wrote it said so. Sending is
|
|
81
|
+
// gated on the separate stamp every current opt-in path writes, so a
|
|
82
|
+
// machine upgrading into this release keeps scanning and mails nothing until
|
|
83
|
+
// a person opts in again and sees what that sends. See `audit.reportsConsentedAt`.
|
|
84
|
+
if (consentedAt === undefined) return { kind: "consent-required" };
|
|
87
85
|
|
|
88
86
|
const auth = await getValidAccessToken();
|
|
89
87
|
if (!auth) {
|
|
@@ -103,6 +101,9 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
103
101
|
return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
const report = buildHarmReport(result, identity.last_reported_at, intervalDays);
|
|
105
|
+
const hits = report.harmful.reduce((n, p) => n + p.hits, 0);
|
|
106
|
+
|
|
106
107
|
try {
|
|
107
108
|
const res = await submitAuditReport(auth.access_token, {
|
|
108
109
|
machine_id: identity.machine_id,
|
|
@@ -111,7 +112,6 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
111
112
|
window_from: report.window_from,
|
|
112
113
|
window_to: report.window_to,
|
|
113
114
|
harmful: report.harmful,
|
|
114
|
-
leaks: report.leaks,
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
// Persist whatever the server says the next window starts at, INCLUDING when
|
|
@@ -128,8 +128,8 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
return res.emailed
|
|
131
|
-
? { kind: "sent",
|
|
132
|
-
: { kind: "held",
|
|
131
|
+
? { kind: "sent", hits }
|
|
132
|
+
: { kind: "held", hits, reason: res.reason ?? "not_sent" };
|
|
133
133
|
} catch (err) {
|
|
134
134
|
// A 401 here means the session died between `getValidAccessToken` and this
|
|
135
135
|
// call — rare, and indistinguishable from any other failure as far as this
|
|
@@ -149,15 +149,19 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
149
149
|
export function describeOutcome(outcome: HarmReportOutcome): string | null {
|
|
150
150
|
switch (outcome.kind) {
|
|
151
151
|
case "disabled":
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
return null; // Say nothing at all to the majority who never opted in.
|
|
153
|
+
case "consent-required":
|
|
154
|
+
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";
|
|
154
155
|
case "signed-out":
|
|
155
|
-
|
|
156
|
+
// Names the two places that can actually fix it. It used to say "sign in
|
|
157
|
+
// from the audit page", which stopped being true when this release moved
|
|
158
|
+
// that dialog behind "invite a friend".
|
|
159
|
+
return "failproofai: emailed reports are on but this machine is signed out — run `failproofai audit --schedule` or visit /settings to resume them";
|
|
156
160
|
case "sent":
|
|
157
|
-
return
|
|
161
|
+
return `failproofai: emailed a harm digest (${outcome.hits} finding${outcome.hits === 1 ? "" : "s"})`;
|
|
158
162
|
case "held":
|
|
159
|
-
return `failproofai:
|
|
163
|
+
return `failproofai: ${outcome.hits} finding${outcome.hits === 1 ? "" : "s"} reported, no email (${outcome.reason})`;
|
|
160
164
|
case "failed":
|
|
161
|
-
return `failproofai: could not send the
|
|
165
|
+
return `failproofai: could not send the harm report: ${outcome.error}`;
|
|
162
166
|
}
|
|
163
167
|
}
|
package/src/audit/report.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* install command + report path + star link.
|
|
11
11
|
*/
|
|
12
12
|
import type { AuditCount, AuditResult, RunAuditOptions } from "./types";
|
|
13
|
-
import { maskSecretsOnly, redactAuditResult, redactExample, shortenPaths } from "./redact-example";
|
|
14
13
|
|
|
15
14
|
const ANSI = {
|
|
16
15
|
reset: "\x1B[0m",
|
|
@@ -106,7 +105,7 @@ function renderRow(r: AuditCount, opts: { showExamples?: boolean }): string[] {
|
|
|
106
105
|
` ${ANSI.dim}Last seen ${formatTimeAgo(r.lastSeen)} · ${r.projects} project${r.projects === 1 ? "" : "s"}${ANSI.reset}`,
|
|
107
106
|
);
|
|
108
107
|
if (opts.showExamples && r.examples[0]) {
|
|
109
|
-
out.push(` ${ANSI.dim}Example: ${
|
|
108
|
+
out.push(` ${ANSI.dim}Example: ${r.examples[0].example}${ANSI.reset}`);
|
|
110
109
|
}
|
|
111
110
|
if (r.installHint) {
|
|
112
111
|
const arrowColor = r.enabledInConfig ? ANSI.green : ANSI.cyan;
|
|
@@ -227,7 +226,7 @@ export function formatText(result: AuditResult, opts: RunAuditOptions = {}): str
|
|
|
227
226
|
}
|
|
228
227
|
|
|
229
228
|
export function formatJson(result: AuditResult): string {
|
|
230
|
-
return JSON.stringify(
|
|
229
|
+
return JSON.stringify(result, null, 2);
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
/** Escape characters that would break a markdown table row. Pipes split
|
|
@@ -335,9 +334,7 @@ export function formatMarkdown(result: AuditResult): string {
|
|
|
335
334
|
out.push("");
|
|
336
335
|
}
|
|
337
336
|
for (const e of r.examples) {
|
|
338
|
-
out.push(
|
|
339
|
-
`- \`${escapeBackticks(redactExample(e.example))}\` _(${shortenPaths(e.cwd) || "?"}, ${formatTimeAgo(e.timestamp)})_`,
|
|
340
|
-
);
|
|
337
|
+
out.push(`- \`${escapeBackticks(e.example)}\` _(${e.cwd || "?"}, ${formatTimeAgo(e.timestamp)})_`);
|
|
341
338
|
}
|
|
342
339
|
out.push("");
|
|
343
340
|
}
|