failproofai 1.0.3 → 1.0.4-beta.2
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 +5 -5
- 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.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.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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +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/run/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +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-request/route_client-reference-manifest.js +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/login-verify/route_client-reference-manifest.js +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/logout/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.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/api/download/[project]/[session]/route_client-reference-manifest.js +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 +37 -4
- 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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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.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.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.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.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.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 -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]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.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]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- 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]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
- 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/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- 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 +54 -21
- package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
- package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
- package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
- package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
- package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
- package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
- package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
- package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
- package/.next/standalone/app/actions/get-leaks.ts +100 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
- package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
- package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
- package/.next/standalone/app/audit/audit-styles.css +114 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
- package/.next/standalone/lib/auth/api-server-client.ts +28 -0
- package/.next/standalone/lib/claude-sessions.ts +101 -27
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
- package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
- package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +11 -11
- package/.next/standalone/package.json +14 -13
- package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
- package/.next/standalone/sdk/python/README.md +8 -0
- package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
- package/.next/standalone/sdk/python/uv.lock +29 -29
- package/.next/standalone/server.js +1 -1
- package/README.md +13 -58
- package/dist/cli.mjs +5368 -8756
- package/dist/worker.mjs +539 -312
- package/lib/auth/api-server-client.ts +28 -0
- package/lib/claude-sessions.ts +101 -27
- package/package.json +14 -13
- package/pi-extension/index.ts +42 -4
- package/scripts/changelog-open.py +115 -0
- package/src/audit/cli.ts +134 -0
- package/src/audit/desktop-notify.ts +420 -0
- package/src/audit/harm-report.ts +101 -0
- package/src/audit/index.ts +141 -13
- package/src/audit/leak-fingerprint.ts +200 -0
- package/src/audit/leak-notice.ts +161 -0
- package/src/audit/leak-record.ts +232 -0
- package/src/audit/leak-scan.ts +292 -0
- package/src/audit/leak-store.ts +217 -0
- package/src/audit/macos-notifier.ts +310 -0
- package/src/audit/redact-example.ts +266 -14
- package/src/audit/report-harm.ts +12 -1
- package/src/audit/report.ts +6 -3
- package/src/audit/schedule-cli.ts +37 -0
- package/src/audit/scoring.ts +49 -0
- package/src/audit/types.ts +41 -0
- package/src/hooks/builtin-policies.ts +67 -2
- package/src/hooks/configure-wizard.ts +20 -0
- package/src/hooks/fp-config.ts +71 -11
- package/src/hooks/fp-home.ts +27 -0
- package/src/hooks/handler.ts +59 -1
- package/src/hooks/integrations.ts +47 -13
- package/src/hooks/notice.ts +155 -0
- package/src/hooks/uninstall-cli.ts +15 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
- package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
- package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
- package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
- package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
- package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
- package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { homedir } from "node:os";
|
|
44
44
|
|
|
45
|
+
import type { AuditResult } from "./types";
|
|
46
|
+
|
|
45
47
|
import { SECRET_PATTERNS } from "../hooks/builtin-policies";
|
|
46
48
|
|
|
47
49
|
/** Longest example we let through, after redaction. */
|
|
@@ -132,39 +134,181 @@ export function maskTruncatedSecret(input: string): string {
|
|
|
132
134
|
* identifier, including the camelCase `_authToken` that `npm config set`
|
|
133
135
|
* writes. `KEY`, `PASS`, `AUTH`, `PAT` and `SIG` are also fragments of ordinary
|
|
134
136
|
* words — `MONKEY_COUNT`, `PASSENGERS`, `AUTHOR`, `PATH`, `SIGNAL` — so they
|
|
135
|
-
* only count as a whole
|
|
137
|
+
* only count as a whole component, where the components come from
|
|
138
|
+
* `identifierComponents` and include camelCase humps as well as `_`.
|
|
136
139
|
*/
|
|
137
140
|
const SECRET_NAME_SUBSTRINGS = [
|
|
138
141
|
"TOKEN",
|
|
139
142
|
"SECRET",
|
|
140
143
|
"PASSWORD",
|
|
141
144
|
"PASSWD",
|
|
145
|
+
"PASSPHRASE",
|
|
142
146
|
"CREDENTIAL",
|
|
143
147
|
"APIKEY",
|
|
144
148
|
"PRIVATEKEY",
|
|
145
149
|
];
|
|
146
150
|
const SECRET_NAME_COMPONENTS = ["KEY", "PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "SESSION", "COOKIE"];
|
|
147
151
|
|
|
152
|
+
/**
|
|
153
|
+
* `PWD` names a credential only in a COMPOUND identifier. `MYSQL_PWD` is
|
|
154
|
+
* MySQL's documented password variable; a bare `PWD` is the shell's working
|
|
155
|
+
* directory, which is on every second line of a captured session.
|
|
156
|
+
*/
|
|
157
|
+
const COMPOUND_ONLY_COMPONENTS = ["PWD"];
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Split an identifier into its uppercased components, on `_`, `-`, `.` AND
|
|
161
|
+
* camelCase humps.
|
|
162
|
+
*
|
|
163
|
+
* The humps are the point. Splitting on `_` alone means `sessionKey`, `dbPass`,
|
|
164
|
+
* `basicAuth` and `authCookie` decompose to a single component that matches
|
|
165
|
+
* nothing, so their values shipped to the digest verbatim — 203 such names on
|
|
166
|
+
* this machine's corpus. They are not exotic: camelCase is what an identifier
|
|
167
|
+
* looks like everywhere except a shell environment, and the redactor is applied
|
|
168
|
+
* to transcripts full of TypeScript, JSON and Python.
|
|
169
|
+
*
|
|
170
|
+
* The second replace splits an acronym from the word that follows it, so
|
|
171
|
+
* `APIKey` yields `API` + `KEY` rather than one run of capitals that matches
|
|
172
|
+
* neither.
|
|
173
|
+
*/
|
|
174
|
+
function identifierComponents(name: string): string[] {
|
|
175
|
+
return name
|
|
176
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
177
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
178
|
+
.toUpperCase()
|
|
179
|
+
.split(/[_\-.]+/)
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
}
|
|
182
|
+
|
|
148
183
|
/**
|
|
149
184
|
* `NAME=value`, with the value quoted or running to the next shell separator.
|
|
150
185
|
*
|
|
151
186
|
* The unquoted alternative excludes quotes as well as separators: a value that
|
|
152
187
|
* runs to end-of-token inside an already-quoted string (`"…?sig=deadbeef"`)
|
|
153
188
|
* would otherwise swallow the closing quote and leave the line unbalanced.
|
|
189
|
+
*
|
|
190
|
+
* Four shapes, because the first spelling of this pattern was `NAME=value` and
|
|
191
|
+
* only that — no whitespace around the `=`, no `:`, no quoted name — and the
|
|
192
|
+
* other three all reached the emailed digest UNREDACTED. `NAME = "value"` and
|
|
193
|
+
* `NAME: "value"` are what a config file, a YAML key and a JSON body look like,
|
|
194
|
+
* which is most of where credentials are actually written down; `"name": val`
|
|
195
|
+
* is the same line once an agent pretty-prints it. A value only survived that
|
|
196
|
+
* gap if it independently matched one of the vendor patterns in
|
|
197
|
+
* `SECRET_PATTERNS` — and 230 of 237 secret-named assignments measured across
|
|
198
|
+
* this machine's transcripts match none of them, so the assignment rule was the
|
|
199
|
+
* only thing standing between them and the digest.
|
|
200
|
+
*
|
|
201
|
+
* The separator is captured and re-emitted verbatim rather than normalised to
|
|
202
|
+
* `=`, so a redacted YAML line is still YAML and a redacted JSON line is still
|
|
203
|
+
* recognisable as the field it came from. Horizontal whitespace only: `\s`
|
|
204
|
+
* would let a `KEY:` at end-of-line swallow the newline and glue the next line
|
|
205
|
+
* onto the match.
|
|
206
|
+
*
|
|
207
|
+
* The `:` alternative refuses a `//` after it, because a URL scheme is the one
|
|
208
|
+
* `name:value` shape that is not an assignment. Without the guard `https` reads
|
|
209
|
+
* as the name and the rest of the URL as its value — and since a non-secret
|
|
210
|
+
* name returns the match UNCHANGED but still CONSUMES it, a `?token=…` sitting
|
|
211
|
+
* inside that URL was swallowed by the `https:` match and never examined. The
|
|
212
|
+
* failure mode is silent and inverted: adding a separator to catch more secrets
|
|
213
|
+
* stopped one already being caught.
|
|
154
214
|
*/
|
|
155
|
-
|
|
215
|
+
/**
|
|
216
|
+
* The identifier is BOUNDED, and the bound is load-bearing rather than tidy.
|
|
217
|
+
*
|
|
218
|
+
* Unbounded (`[A-Za-z0-9_]*`), this is quadratic on a long unbroken token: the
|
|
219
|
+
* engine starts at every one of n positions, consumes the whole run, then
|
|
220
|
+
* backtracks a character at a time looking for the `=` or `:` that never comes.
|
|
221
|
+
* Measured on a plain 300 KB base64 blob — `"A".repeat(300000)` — it ran for
|
|
222
|
+
* over 20 SECONDS on one string. That is not an exotic input: agent transcripts
|
|
223
|
+
* carry base64 images, minified bundles and whole file contents as single
|
|
224
|
+
* lines, and the audit reads every one of them. A scheduled scan hitting a few
|
|
225
|
+
* of these would stall for minutes with nobody watching.
|
|
226
|
+
*
|
|
227
|
+
* 128 characters is far past any real environment-variable or field name, so
|
|
228
|
+
* bounding it costs nothing that exists and makes the scan linear in n.
|
|
229
|
+
*/
|
|
230
|
+
const ASSIGNMENT_RE =
|
|
231
|
+
/(["']?)\b([A-Za-z_][A-Za-z0-9_]{0,127})\1([ \t]*(?::(?!\/\/)|=)[ \t]*)("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
|
|
156
232
|
|
|
157
233
|
/** Credentials inline in a URL, on ANY scheme — `https://user:pass@host`. */
|
|
158
|
-
|
|
234
|
+
// Every quantifier here is BOUNDED, for the same reason ASSIGNMENT_RE's is.
|
|
235
|
+
// Unbounded, `[a-z0-9+.\-]*` is quadratic on any long lowercase run: at each of
|
|
236
|
+
// n positions it consumes to the end hunting for a `://` that is not there,
|
|
237
|
+
// then backtracks a character at a time. Measured at 18 SECONDS on 100 KB of a
|
|
238
|
+
// single repeated letter — and this one runs over the WHOLE text, on every
|
|
239
|
+
// example the audit redacts. A URI scheme is at most a few characters (RFC 3986
|
|
240
|
+
// allows more, but nothing real uses it), and userinfo that runs past 256 bytes
|
|
241
|
+
// is not a credential anyone typed.
|
|
242
|
+
const URL_CREDENTIALS_RE = /([a-z][a-z0-9+.\-]{0,31}:\/\/)[^\s/:@]{1,256}:[^\s/@]{1,256}@/gi;
|
|
159
243
|
|
|
160
244
|
/** `curl -u user:pass`, in both spellings and both separators. */
|
|
161
245
|
const BASIC_AUTH_FLAG_RE = /((?:^|\s)(?:-u|--user)[\s=])\S+:\S+/g;
|
|
162
246
|
|
|
163
247
|
/** True when an identifier's name says its value is a credential. */
|
|
248
|
+
/**
|
|
249
|
+
* Prefixes whose values a build tool INLINES INTO THE BROWSER BUNDLE.
|
|
250
|
+
*
|
|
251
|
+
* These are not conventionally public, they are mechanically public: Next.js,
|
|
252
|
+
* Vite, CRA, Expo, SvelteKit, Astro, Nuxt and Gatsby each substitute the value
|
|
253
|
+
* into the JavaScript they ship to every visitor. A credential named this way
|
|
254
|
+
* has already been published by the framework, to everyone, before failproofai
|
|
255
|
+
* ever saw it.
|
|
256
|
+
*/
|
|
257
|
+
const CLIENT_BUNDLE_PREFIXES = [
|
|
258
|
+
"NEXT_PUBLIC",
|
|
259
|
+
"VITE",
|
|
260
|
+
"REACT_APP",
|
|
261
|
+
"EXPO_PUBLIC",
|
|
262
|
+
"NUXT_PUBLIC",
|
|
263
|
+
"GATSBY",
|
|
264
|
+
"STORYBOOK",
|
|
265
|
+
"PUBLIC",
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
/** Markers that name a key as the publishable half of a pair. */
|
|
269
|
+
const PUBLISHED_MARKERS = /PUBLISHABLE|(^|_)PUBLIC_KEY$|(^|_)ANON_KEY$/;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* True when the identifier says the value is meant to be public.
|
|
273
|
+
*
|
|
274
|
+
* `isSecretName` matches any component in `KEY`/`PASS`/`AUTH`/… — and EVERY
|
|
275
|
+
* publishable key on earth is named `*_KEY`. Measured before this existed, it
|
|
276
|
+
* returned true for all twelve of `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`,
|
|
277
|
+
* `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `VAPID_PUBLIC_KEY`, `PUBLISHABLE_KEY` and
|
|
278
|
+
* friends — including the ones carrying the literal word PUBLIC.
|
|
279
|
+
*
|
|
280
|
+
* Over-redaction is usually cheap: masking something that might be secret costs
|
|
281
|
+
* readability. This case is different, because we can PROVE the value is not a
|
|
282
|
+
* secret — so masking it reports a credential exposure that did not happen, and
|
|
283
|
+
* a security tool crying wolf about a browser-bundled config value is not a
|
|
284
|
+
* cheap error. This repo's own committed PostHog `phc_` key, documented in
|
|
285
|
+
* source as "Write-only (safe to commit)", is masked as an "assigned secret" by
|
|
286
|
+
* the rule this vetoes.
|
|
287
|
+
*
|
|
288
|
+
* Deliberately narrow. The marker must be a PREFIX (so a mid-name `PUBLIC` in
|
|
289
|
+
* `MY_PUBLIC_FACING_SECRET` does not disarm it) or an explicit publishable
|
|
290
|
+
* suffix. Words like ANON, BROWSER and SITE are excluded on their own — only
|
|
291
|
+
* `ANON_KEY`, which is Supabase's documented browser key, qualifies.
|
|
292
|
+
*/
|
|
293
|
+
export function isPublishedByDesign(name: string): boolean {
|
|
294
|
+
const components = identifierComponents(name);
|
|
295
|
+
if (components.length === 0) return false;
|
|
296
|
+
const upper = components.join("_");
|
|
297
|
+
for (const prefix of CLIENT_BUNDLE_PREFIXES) {
|
|
298
|
+
if (upper === prefix || upper.startsWith(prefix + "_")) return true;
|
|
299
|
+
}
|
|
300
|
+
return PUBLISHED_MARKERS.test(upper);
|
|
301
|
+
}
|
|
302
|
+
|
|
164
303
|
export function isSecretName(name: string): boolean {
|
|
304
|
+
// Runs first and beats every other signal: a value the framework compiles
|
|
305
|
+
// into the browser bundle is public no matter what the rest of the name says.
|
|
306
|
+
if (isPublishedByDesign(name)) return false;
|
|
165
307
|
const upper = name.toUpperCase();
|
|
166
308
|
if (SECRET_NAME_SUBSTRINGS.some((word) => upper.includes(word))) return true;
|
|
167
|
-
|
|
309
|
+
const components = identifierComponents(name);
|
|
310
|
+
if (components.some((part) => SECRET_NAME_COMPONENTS.includes(part))) return true;
|
|
311
|
+
return components.length > 1 && components.some((part) => COMPOUND_ONLY_COMPONENTS.includes(part));
|
|
168
312
|
}
|
|
169
313
|
|
|
170
314
|
/**
|
|
@@ -193,15 +337,18 @@ export function isSecretName(name: string): boolean {
|
|
|
193
337
|
* is the actionable half of the finding.
|
|
194
338
|
*/
|
|
195
339
|
export function maskAssignedSecrets(input: string): string {
|
|
196
|
-
let out = input.replace(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
340
|
+
let out = input.replace(
|
|
341
|
+
ASSIGNMENT_RE,
|
|
342
|
+
(match, quote: string, name: string, separator: string, value: string) => {
|
|
343
|
+
if (!isSecretName(name)) return match;
|
|
344
|
+
// An earlier pass already named this one, and it named it better.
|
|
345
|
+
// `export ANTHROPIC_API_KEY=sk-ant-…` is masked by the vendor pattern as
|
|
346
|
+
// "Anthropic API key"; re-masking it here would downgrade that to the
|
|
347
|
+
// generic label and strip the marker's own tail as it went.
|
|
348
|
+
if (value.startsWith("[REDACTED")) return match;
|
|
349
|
+
return `${quote}${name}${quote}${separator}[REDACTED: assigned secret]`;
|
|
350
|
+
},
|
|
351
|
+
);
|
|
205
352
|
out = out.replace(URL_CREDENTIALS_RE, "$1[REDACTED: URL credentials]@");
|
|
206
353
|
out = out.replace(BASIC_AUTH_FLAG_RE, "$1[REDACTED: basic auth]");
|
|
207
354
|
return out;
|
|
@@ -298,15 +445,120 @@ export function shortenPaths(input: string, home = homedir()): string {
|
|
|
298
445
|
* digest as one row, and a raw newline there breaks the plain-text layout while
|
|
299
446
|
* saying nothing the single line does not.
|
|
300
447
|
*/
|
|
448
|
+
/**
|
|
449
|
+
* Mask every secret, and nothing else.
|
|
450
|
+
*
|
|
451
|
+
* The three masking passes of `redactExample` without the path shortening, the
|
|
452
|
+
* whitespace collapse or the length cap. This is what a LOCAL renderer wants:
|
|
453
|
+
* on your own machine your own paths are the useful half of an example, and
|
|
454
|
+
* `~/…/db.ts` costs readability for no gain — nobody is protected from their own
|
|
455
|
+
* directory names. A credential on screen is the other half of that trade: it is
|
|
456
|
+
* one screenshot, one pasted issue or one screen-share away from being published,
|
|
457
|
+
* and unlike the path it can never be un-leaked.
|
|
458
|
+
*
|
|
459
|
+
* So the split is deliberate: everything that LEAVES the machine goes through
|
|
460
|
+
* `redactExample`; the terminal gets this.
|
|
461
|
+
*/
|
|
462
|
+
export function maskSecretsOnly(input: string): string {
|
|
463
|
+
return maskAssignedSecrets(maskTruncatedSecret(maskSecrets(input)));
|
|
464
|
+
}
|
|
465
|
+
|
|
301
466
|
export function redactExample(input: string, home = homedir()): string {
|
|
302
467
|
// Assignment masking runs LAST of the three, so the two pattern-based passes
|
|
303
468
|
// get first refusal on anything they can name precisely. A vendor prefix
|
|
304
469
|
// yields "[REDACTED: Anthropic API key]"; falling through to this one would
|
|
305
470
|
// have said only "assigned secret", which is true but less useful to read.
|
|
306
|
-
const masked =
|
|
471
|
+
const masked = maskSecretsOnly(input);
|
|
307
472
|
const shortened = shortenPaths(masked, home);
|
|
308
473
|
const collapsed = shortened.replace(/\s+/g, " ").trim();
|
|
309
474
|
return collapsed.length > REDACTED_EXAMPLE_MAX_CHARS
|
|
310
475
|
? `${collapsed.slice(0, REDACTED_EXAMPLE_MAX_CHARS - 1)}…`
|
|
311
476
|
: collapsed;
|
|
312
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Redact every free-text field of an `AuditResult` that could carry a secret or
|
|
481
|
+
* name someone's disk.
|
|
482
|
+
*
|
|
483
|
+
* The counts, titles, timestamps and policy names are the substance of a report
|
|
484
|
+
* and none of them come from user data. What DOES come from user data is the
|
|
485
|
+
* example strings (slices of real commands), the per-example `cwd`, and the
|
|
486
|
+
* project lists — which are the same three things `redactExample` was written
|
|
487
|
+
* for, applied to the whole structure instead of one row.
|
|
488
|
+
*
|
|
489
|
+
* Used by every renderer that produces an artifact which can travel:
|
|
490
|
+
* `formatMarkdown` (the file the CLI calls a "Shareable report") and
|
|
491
|
+
* `formatJson` (whatever the caller pipes it into). The emailed digest reaches
|
|
492
|
+
* the same guarantee by a different route — `harm-report.ts` redacts each
|
|
493
|
+
* example as it selects it, because it also has to apply the reporting window.
|
|
494
|
+
*
|
|
495
|
+
* Returns a NEW object. The caller's result is left alone, so the dashboard and
|
|
496
|
+
* the cache keep the unredacted values they need to render a local view.
|
|
497
|
+
*/
|
|
498
|
+
export function redactAuditResult(result: AuditResult, home = homedir()): AuditResult {
|
|
499
|
+
// Built field by field ON PURPOSE. This was `{...result}` plus three named
|
|
500
|
+
// rewrites, which meant it was a FIELD ALLOWLIST pretending to be a redactor:
|
|
501
|
+
// every field it did not name — including any field added later — passed
|
|
502
|
+
// through byte-identical, absolute home paths and all. It guards
|
|
503
|
+
// `formatJson`, i.e. an artifact the product invites the user to share.
|
|
504
|
+
//
|
|
505
|
+
// Enumerating every key means TypeScript rejects the build when `AuditResult`
|
|
506
|
+
// grows a field nobody decided about here, which is the only mechanism that
|
|
507
|
+
// survives a future contributor. `redactAuditResultKeys` in the tests reflects
|
|
508
|
+
// over the real object and fails if this list drifts, covering the case where
|
|
509
|
+
// a field is added as optional and the compiler stays quiet.
|
|
510
|
+
const redacted: AuditResult = {
|
|
511
|
+
// Scalars and counters: no paths, no user content, nothing to redact.
|
|
512
|
+
version: result.version,
|
|
513
|
+
scannedAt: result.scannedAt,
|
|
514
|
+
transcripts: result.transcripts,
|
|
515
|
+
totals: result.totals,
|
|
516
|
+
eventsScanned: result.eventsScanned,
|
|
517
|
+
enabledBuiltinNames: result.enabledBuiltinNames,
|
|
518
|
+
// Salted, machine-local ids. They carry no path and cannot be reversed to
|
|
519
|
+
// a credential, so they pass through — but the decision is recorded here
|
|
520
|
+
// rather than inherited from a spread.
|
|
521
|
+
newLeakIds: result.newLeakIds,
|
|
522
|
+
|
|
523
|
+
// Everything below carries a path or user content and is rewritten.
|
|
524
|
+
scope: {
|
|
525
|
+
cli: result.scope.cli,
|
|
526
|
+
since: result.scope.since,
|
|
527
|
+
projects: result.scope.projects === "all"
|
|
528
|
+
? "all"
|
|
529
|
+
: result.scope.projects.map((p) => shortenPaths(p, home)),
|
|
530
|
+
},
|
|
531
|
+
results: result.results.map((row) => ({
|
|
532
|
+
...row,
|
|
533
|
+
examples: row.examples.map((e) => ({
|
|
534
|
+
...e,
|
|
535
|
+
example: redactExample(e.example, home),
|
|
536
|
+
cwd: shortenPaths(e.cwd, home),
|
|
537
|
+
})),
|
|
538
|
+
})),
|
|
539
|
+
projectsScanned: result.projectsScanned.map((p) => shortenPaths(p, home)),
|
|
540
|
+
};
|
|
541
|
+
return redacted;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Every top-level key `redactAuditResult` has consciously handled.
|
|
546
|
+
*
|
|
547
|
+
* The compiler catches a REQUIRED field added to `AuditResult`, because the
|
|
548
|
+
* object literal above would stop satisfying the type. It does not catch an
|
|
549
|
+
* OPTIONAL one — and the leak record is arriving as optional fields. So the
|
|
550
|
+
* test reflects over a real result and fails when a key appears here that is
|
|
551
|
+
* not in this list, which forces the decision to be made rather than defaulted.
|
|
552
|
+
*/
|
|
553
|
+
export const REDACTED_AUDIT_RESULT_KEYS: ReadonlyArray<keyof AuditResult> = [
|
|
554
|
+
"version",
|
|
555
|
+
"scannedAt",
|
|
556
|
+
"scope",
|
|
557
|
+
"transcripts",
|
|
558
|
+
"results",
|
|
559
|
+
"totals",
|
|
560
|
+
"projectsScanned",
|
|
561
|
+
"eventsScanned",
|
|
562
|
+
"enabledBuiltinNames",
|
|
563
|
+
"newLeakIds",
|
|
564
|
+
];
|
package/src/audit/report-harm.ts
CHANGED
|
@@ -31,6 +31,7 @@ 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";
|
|
34
35
|
import {
|
|
35
36
|
ensureMachineIdentity,
|
|
36
37
|
machineLabel,
|
|
@@ -101,7 +102,16 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
101
102
|
return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
// The leak record is read here rather than carried on the AuditResult: the
|
|
106
|
+
// digest wants every credential still live in this window, not only the ones
|
|
107
|
+
// this particular scan happened to re-encounter. A key that leaked last month
|
|
108
|
+
// and was not touched by today's transcripts is still a key to rotate.
|
|
109
|
+
const report = buildHarmReport(
|
|
110
|
+
result,
|
|
111
|
+
identity.last_reported_at,
|
|
112
|
+
intervalDays,
|
|
113
|
+
activeFindings(readLeakRecord()),
|
|
114
|
+
);
|
|
105
115
|
const hits = report.harmful.reduce((n, p) => n + p.hits, 0);
|
|
106
116
|
|
|
107
117
|
try {
|
|
@@ -112,6 +122,7 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
|
|
|
112
122
|
window_from: report.window_from,
|
|
113
123
|
window_to: report.window_to,
|
|
114
124
|
harmful: report.harmful,
|
|
125
|
+
leaks: report.leaks,
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
// Persist whatever the server says the next window starts at, INCLUDING when
|
package/src/audit/report.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
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";
|
|
13
14
|
|
|
14
15
|
const ANSI = {
|
|
15
16
|
reset: "\x1B[0m",
|
|
@@ -105,7 +106,7 @@ function renderRow(r: AuditCount, opts: { showExamples?: boolean }): string[] {
|
|
|
105
106
|
` ${ANSI.dim}Last seen ${formatTimeAgo(r.lastSeen)} · ${r.projects} project${r.projects === 1 ? "" : "s"}${ANSI.reset}`,
|
|
106
107
|
);
|
|
107
108
|
if (opts.showExamples && r.examples[0]) {
|
|
108
|
-
out.push(` ${ANSI.dim}Example: ${r.examples[0].example}${ANSI.reset}`);
|
|
109
|
+
out.push(` ${ANSI.dim}Example: ${maskSecretsOnly(r.examples[0].example)}${ANSI.reset}`);
|
|
109
110
|
}
|
|
110
111
|
if (r.installHint) {
|
|
111
112
|
const arrowColor = r.enabledInConfig ? ANSI.green : ANSI.cyan;
|
|
@@ -226,7 +227,7 @@ export function formatText(result: AuditResult, opts: RunAuditOptions = {}): str
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
export function formatJson(result: AuditResult): string {
|
|
229
|
-
return JSON.stringify(result, null, 2);
|
|
230
|
+
return JSON.stringify(redactAuditResult(result), null, 2);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
/** Escape characters that would break a markdown table row. Pipes split
|
|
@@ -334,7 +335,9 @@ export function formatMarkdown(result: AuditResult): string {
|
|
|
334
335
|
out.push("");
|
|
335
336
|
}
|
|
336
337
|
for (const e of r.examples) {
|
|
337
|
-
out.push(
|
|
338
|
+
out.push(
|
|
339
|
+
`- \`${escapeBackticks(redactExample(e.example))}\` _(${shortenPaths(e.cwd) || "?"}, ${formatTimeAgo(e.timestamp)})_`,
|
|
340
|
+
);
|
|
338
341
|
}
|
|
339
342
|
out.push("");
|
|
340
343
|
}
|
|
@@ -219,6 +219,14 @@ export function runScheduleStatus(): void {
|
|
|
219
219
|
// so it is a destination in name only. Showing the address for one would tell
|
|
220
220
|
// somebody their digests are going somewhere they are not.
|
|
221
221
|
const live = auth && auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
|
|
222
|
+
// Beside the schedule, because they are the two halves of "what does this
|
|
223
|
+
// machine do on its own" — one decides whether it scans, the other whether it
|
|
224
|
+
// is allowed to interrupt you about what it found.
|
|
225
|
+
detail.push([
|
|
226
|
+
"leak notifications",
|
|
227
|
+
config.audit.notify ? green("on") : dim("off (failproofai audit --notify)"),
|
|
228
|
+
]);
|
|
229
|
+
|
|
222
230
|
detail.push(["reports to", live ? live.user.email : dim("— signed out")]);
|
|
223
231
|
if (on && !live) {
|
|
224
232
|
// The state the reporter surfaces as "signed-out". Named here for the same
|
|
@@ -346,3 +354,32 @@ export function asScheduleError(err: unknown): never {
|
|
|
346
354
|
}
|
|
347
355
|
throw err;
|
|
348
356
|
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Turn the desktop banner on or off.
|
|
360
|
+
*
|
|
361
|
+
* A separate command from `--schedule` rather than a flag on it, because they
|
|
362
|
+
* are separate decisions with different lifetimes: somebody who wants the
|
|
363
|
+
* weekly scan and not the banner should not have to re-state their schedule to
|
|
364
|
+
* say so, and somebody silencing the banner in an annoyed moment should not
|
|
365
|
+
* discover they also turned off the scan.
|
|
366
|
+
*
|
|
367
|
+
* Writes the same `audit.notify` key the dashboard toggle writes and the audit
|
|
368
|
+
* child reads, so the two surfaces cannot disagree.
|
|
369
|
+
*/
|
|
370
|
+
export function runNotifyToggle(enable: boolean): void {
|
|
371
|
+
try {
|
|
372
|
+
updateConfig({ audit: { notify: enable } });
|
|
373
|
+
} catch (err) {
|
|
374
|
+
process.stderr.write(
|
|
375
|
+
`Could not update the setting: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
376
|
+
);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
process.stdout.write(
|
|
380
|
+
enable
|
|
381
|
+
? `\n${green("on")} leak notifications — this machine will tell you when a scan finds a credential.\n\n`
|
|
382
|
+
: `\n${dim("off")} leak notifications — scans continue, and findings still appear in ` +
|
|
383
|
+
`\`failproofai audit\` and the emailed digest.\n\n`,
|
|
384
|
+
);
|
|
385
|
+
}
|
package/src/audit/scoring.ts
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Score derivation for the audit dashboard.
|
|
3
3
|
*
|
|
4
|
+
* ## SWITCHED OFF — nothing in the product calls any of this (2026-09-07)
|
|
5
|
+
*
|
|
6
|
+
* Every function below is intact and still covered by
|
|
7
|
+
* `__tests__/audit/scoring.test.ts`, deliberately: the score is commented out at
|
|
8
|
+
* its call sites, not deleted, so restoring it is un-commenting rather than
|
|
9
|
+
* rewriting. The call sites, all commented and cross-referenced back to here:
|
|
10
|
+
* `audit-dashboard.tsx` (the four `useMemo`s, the poster/how-to-improve/
|
|
11
|
+
* come-back-better props, and the `score`/`grade` PostHog properties),
|
|
12
|
+
* `audit-poster.tsx` (the `.poster-score` block, the PNG filename, the share
|
|
13
|
+
* payload), `how-to-improve-section.tsx` (the projected sub-heading),
|
|
14
|
+
* `share-templates.ts` (19 of 20 templates interpolated the number).
|
|
15
|
+
*
|
|
16
|
+
* ### Why
|
|
17
|
+
*
|
|
18
|
+
* Two reasons, one product and one correctness.
|
|
19
|
+
*
|
|
20
|
+
* The product reason is the owner's: the audit tried to do too many things
|
|
21
|
+
* shallowly, and grading a machine 0-100 with a letter tier was not the half
|
|
22
|
+
* worth keeping.
|
|
23
|
+
*
|
|
24
|
+
* The correctness reason is worse and is the one to read before switching it
|
|
25
|
+
* back on. **`deriveScore` never reads `enabledInConfig`** — only
|
|
26
|
+
* `projectedScore` does, at the filter below. So the number the dashboard
|
|
27
|
+
* rendered as "enable all N → projected {score}" was undeliverable by
|
|
28
|
+
* construction: a user who took the prescription, enabled the policies and
|
|
29
|
+
* re-ran got the SAME score with the projection gone. Advertised up to +22,
|
|
30
|
+
* delivered 0. There is also no time window, so a hit denied today is counted
|
|
31
|
+
* against the machine forever.
|
|
32
|
+
*
|
|
33
|
+
* Two further things measured at the same time, neither of them the score's
|
|
34
|
+
* fault but both worth knowing if this comes back:
|
|
35
|
+
*
|
|
36
|
+
* - `penaltyFor` consumes `row.hits`, i.e. OCCURRENCES. Occurrences amplify
|
|
37
|
+
* against distinct values by 10.7x-41x and non-uniformly by class, so the
|
|
38
|
+
* sum does not preserve rank order between classes. Fixing that needs a
|
|
39
|
+
* per-distinct-value carrier, which `AuditCount` does not have.
|
|
40
|
+
* - `syntheticRank`, `scoreToPercentile` and `COHORT_SIZE` had already lost
|
|
41
|
+
* their last caller before this change — the cohort they describe does not
|
|
42
|
+
* exist. They are dead either way.
|
|
43
|
+
*
|
|
44
|
+
* ### To restore
|
|
45
|
+
*
|
|
46
|
+
* Un-comment the call sites listed above (each carries its original inline) and
|
|
47
|
+
* put the score-bearing share copy back from the block at the bottom of
|
|
48
|
+
* `share-templates.ts`. Make `score`/`grade` required again in `ShareCtx` and in
|
|
49
|
+
* `audit-poster.tsx`'s `Props`, and the compiler will walk you through the rest.
|
|
50
|
+
* Fix the `enabledInConfig` defect first, or it will be advertising the same
|
|
51
|
+
* undeliverable number.
|
|
52
|
+
*
|
|
4
53
|
* Score is on 0-100, mapped to letter grades that anchor the leaderboard
|
|
5
54
|
* + tier prose. The thresholds match the reference design (assets/audit):
|
|
6
55
|
*
|
package/src/audit/types.ts
CHANGED
|
@@ -119,6 +119,27 @@ export interface AuditCount {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/** Per-transcript scan result (also the per-transcript cache value). */
|
|
122
|
+
/** One credential sighting, already fingerprinted. */
|
|
123
|
+
export interface TranscriptLeak {
|
|
124
|
+
/** Salted, machine-local id for the DISTINCT value. */
|
|
125
|
+
id: string;
|
|
126
|
+
/** Masked rendering + class + length. Never the value. */
|
|
127
|
+
fingerprint: import("./leak-fingerprint").LeakFingerprint;
|
|
128
|
+
/** Identifier it was assigned to, when it came from an assignment. */
|
|
129
|
+
name: string | null;
|
|
130
|
+
/** Rule that found it. */
|
|
131
|
+
rule: string;
|
|
132
|
+
/** Whether a vendor shape matched — gates whether an alert may name a console. */
|
|
133
|
+
shaped: boolean;
|
|
134
|
+
timestamp: string;
|
|
135
|
+
cwd: string;
|
|
136
|
+
toolName: string;
|
|
137
|
+
/** Sent by the agent, or received by it. Different exposures, different fixes. */
|
|
138
|
+
direction: "input" | "result";
|
|
139
|
+
/** Home-shortened path the value was read from or written to, when there was one. */
|
|
140
|
+
path: string | null;
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
export interface TranscriptAuditResult {
|
|
123
144
|
/** Cache key: matches TranscriptMetadata.transcriptPath. */
|
|
124
145
|
transcriptPath: string;
|
|
@@ -140,6 +161,17 @@ export interface TranscriptAuditResult {
|
|
|
140
161
|
hitsByName: Record<string, number>;
|
|
141
162
|
/** Up to 3 example commands per policy/detector (later coalesced upstream). */
|
|
142
163
|
examplesByName: Record<string, { timestamp: string; cwd: string; example: string }[]>;
|
|
164
|
+
/**
|
|
165
|
+
* Credentials found in this transcript, one entry per SIGHTING.
|
|
166
|
+
*
|
|
167
|
+
* Deliberately separate from `hitsByName`, which counts by RULE and cannot
|
|
168
|
+
* enumerate distinct values. These carry the fingerprint rather than the
|
|
169
|
+
* credential — the raw value never leaves `scanOneTranscript`.
|
|
170
|
+
*
|
|
171
|
+
* Optional so a cached result written before this field existed still parses;
|
|
172
|
+
* absent reads as "this transcript predates leak scanning", not "clean".
|
|
173
|
+
*/
|
|
174
|
+
leaks?: TranscriptLeak[];
|
|
143
175
|
/** First/last timestamp per name. */
|
|
144
176
|
rangeByName: Record<string, { first: string; last: string }>;
|
|
145
177
|
}
|
|
@@ -179,6 +211,15 @@ export interface AuditResult {
|
|
|
179
211
|
* Lets the dashboard answer "is this policy already on?" for
|
|
180
212
|
* detector-mapped policies that may not have hit during this audit. */
|
|
181
213
|
enabledBuiltinNames: string[];
|
|
214
|
+
/**
|
|
215
|
+
* Ids of credentials seen for the FIRST time in this scan.
|
|
216
|
+
*
|
|
217
|
+
* What a notice keys on. Not "findings this scan saw" — a key already in the
|
|
218
|
+
* record must not re-alert however many fresh sightings it picks up, and one
|
|
219
|
+
* seen for the first time must alert even though its rule has fired before.
|
|
220
|
+
* Optional so a cached result predating leak scanning still parses.
|
|
221
|
+
*/
|
|
222
|
+
newLeakIds?: string[];
|
|
182
223
|
}
|
|
183
224
|
|
|
184
225
|
/** CLI-supplied options for `runAudit()`. Set by `bin/failproofai.mjs`. */
|
|
@@ -126,15 +126,80 @@ const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
|
|
|
126
126
|
|
|
127
127
|
// sanitizeApiKeys
|
|
128
128
|
const API_KEY_PATTERNS: Array<[RegExp, string]> = [
|
|
129
|
+
// ── Anthropic ── prefix doc-verified: platform.claude.com says a key
|
|
130
|
+
// "starts with `sk-ant-`" and its own example is `sk-ant-api03-...`.
|
|
131
|
+
[/sk-ant-api03-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
132
|
+
// Minted by the Claude CLI's OAuth flow and billed against a Pro/Max
|
|
133
|
+
// subscription rather than the API account — a different thing to revoke.
|
|
134
|
+
[/sk-ant-oat01-[A-Za-z0-9\-_]{20,}/, "Anthropic OAuth token"],
|
|
129
135
|
[/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
136
|
+
|
|
137
|
+
// ── OpenAI ── the body charset EXCLUDES `-` and `_` on purpose: with them,
|
|
138
|
+
// this rule's run would bridge the hyphen in `sk-proj-` and report every
|
|
139
|
+
// project key a second time under the wrong label.
|
|
130
140
|
[/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
|
|
131
|
-
[/sk-[A-Za-z0-9]{20,}/, "OpenAI
|
|
132
|
-
|
|
141
|
+
[/sk-svcacct-[A-Za-z0-9\-_]{20,}/, "OpenAI service-account key"],
|
|
142
|
+
// Highest blast radius in the family: it authenticates the organisation
|
|
143
|
+
// admin surface, and lives at a DIFFERENT console from the other keys.
|
|
144
|
+
[/sk-admin-[A-Za-z0-9\-_]{20,}/, "OpenAI admin key"],
|
|
145
|
+
|
|
146
|
+
// ── OpenAI-compatible gateways ──
|
|
147
|
+
[/sk-or-v1-[A-Za-z0-9\-_]{20,}/, "OpenRouter API key"],
|
|
148
|
+
// A BARE `sk-` CANNOT BE ATTRIBUTED, and calling it OpenAI is what this rule
|
|
149
|
+
// used to do. LiteLLM's own docs say its virtual keys "must start with sk-",
|
|
150
|
+
// and DeepSeek plus every other OpenAI-compatible proxy mints the same shape
|
|
151
|
+
// — so on a machine running a gateway (this one does) the old label sent the
|
|
152
|
+
// user to the wrong console to revoke a key that was never OpenAI's. The
|
|
153
|
+
// honest label names the family, not a vendor.
|
|
154
|
+
[/sk-[A-Za-z0-9]{20,}/, "OpenAI-compatible key (issuer unknown)"],
|
|
155
|
+
|
|
156
|
+
// ── GitHub ── all five documented types. github.blog's token-format post
|
|
157
|
+
// names ghp/gho/ghu/ghs/ghr; only two of them shipped before.
|
|
133
158
|
[/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
|
|
159
|
+
[/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
|
|
160
|
+
[/gho_[A-Za-z0-9]{36}/, "GitHub OAuth token"],
|
|
161
|
+
[/ghu_[A-Za-z0-9]{36}/, "GitHub user-to-server token"],
|
|
162
|
+
[/ghs_[A-Za-z0-9]{36}/, "GitHub server-to-server token"],
|
|
163
|
+
[/ghr_[A-Za-z0-9]{36}/, "GitHub refresh token"],
|
|
164
|
+
[/glpat-[A-Za-z0-9\-_]{20,}/, "GitLab personal access token"],
|
|
165
|
+
|
|
166
|
+
// ── AWS ── the ID is the PUBLIC half; the paired 40-char secret is the
|
|
167
|
+
// credential. Reported anyway because an exposed ID names the account and is
|
|
168
|
+
// what a user searches their console for. `ASIA` (temporary STS) was missing.
|
|
134
169
|
[/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
|
|
170
|
+
[/ASIA[A-Z0-9]{16}/, "AWS temporary access key ID"],
|
|
171
|
+
|
|
172
|
+
// ── Payments ── Stripe documents `pk_*` as "Safe to expose: Yes", so only
|
|
173
|
+
// the secret and restricted halves appear here.
|
|
135
174
|
[/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
|
|
136
175
|
[/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
|
|
176
|
+
[/rk_live_[A-Za-z0-9]{24,}/, "Stripe restricted key"],
|
|
177
|
+
|
|
178
|
+
// ── Google ── `AIza` also covers Firebase Web and Maps browser keys, which
|
|
179
|
+
// are public by design, so the label stays non-committal about severity.
|
|
137
180
|
[/AIza[0-9A-Za-z\-_]{35}/, "Google API key"],
|
|
181
|
+
|
|
182
|
+
// ── Messaging ── the Telegram bot token was matched by ZERO of the shipped
|
|
183
|
+
// patterns and was found only because a phone-number regex tripped over it;
|
|
184
|
+
// two live-looking ones sit in this machine's corpus right now. Slack's
|
|
185
|
+
// prefixes were in the Rust collector but not here — one of nine such
|
|
186
|
+
// divergences between the two engines.
|
|
187
|
+
[/\d{8,10}:[A-Za-z0-9_-]{35}/, "Telegram bot token"],
|
|
188
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
|
|
189
|
+
[/xapp-[0-9]-[A-Za-z0-9-]{10,}/, "Slack app-level token"],
|
|
190
|
+
|
|
191
|
+
// ── Model hosts ──
|
|
192
|
+
[/hf_[A-Za-z0-9]{30,}/, "Hugging Face token"],
|
|
193
|
+
[/gsk_[A-Za-z0-9]{40,}/, "Groq API key"],
|
|
194
|
+
[/r8_[A-Za-z0-9]{30,}/, "Replicate token"],
|
|
195
|
+
[/xai-[A-Za-z0-9]{40,}/, "xAI API key"],
|
|
196
|
+
|
|
197
|
+
// ── Registries ──
|
|
198
|
+
[/npm_[A-Za-z0-9]{36}/, "npm access token"],
|
|
199
|
+
[/dop_v1_[a-f0-9]{64}/, "DigitalOcean token"],
|
|
200
|
+
[/shpat_[a-fA-F0-9]{32}/, "Shopify access token"],
|
|
201
|
+
[/sb_secret_[A-Za-z0-9\-_]{20,}/, "Supabase secret key"],
|
|
202
|
+
[/sbp_[a-f0-9]{40}/, "Supabase personal token"],
|
|
138
203
|
];
|
|
139
204
|
|
|
140
205
|
// sanitizeConnectionStrings
|