@akasecurity/cli 0.9.3 → 0.9.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/dist/cli.js +1776 -215
- package/package.json +5 -5
- package/web-ui/package.json +6 -2
- package/web-ui/packages/dashboard-ui/package.json +2 -1
- package/web-ui/packages/detections/package.json +1 -1
- package/web-ui/packages/local-ops/package.json +1 -1
- package/web-ui/packages/persistence/package.json +1 -1
- package/web-ui/packages/plugin-sdk/package.json +1 -1
- package/web-ui/packages/schema/package.json +1 -1
- package/web-ui/packages/ui-kit/package.json +1 -1
- package/web-ui/web-ui/.next/BUILD_ID +1 -1
- package/web-ui/web-ui/.next/app-build-manifest.json +135 -126
- package/web-ui/web-ui/.next/app-path-routes-manifest.json +7 -6
- package/web-ui/web-ui/.next/build-manifest.json +14 -14
- package/web-ui/web-ui/.next/prerender-manifest.json +10 -10
- package/web-ui/web-ui/.next/required-server-files.json +1 -2
- package/web-ui/web-ui/.next/routes-manifest.json +6 -0
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -0
- package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -0
- package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -0
- package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.rsc +10 -10
- package/web-ui/web-ui/.next/server/app/icon.svg/route.js +1 -1
- package/web-ui/web-ui/.next/server/app/icon.svg/route.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/index.html +1 -1
- package/web-ui/web-ui/.next/server/app/index.rsc +10 -10
- package/web-ui/web-ui/.next/server/app/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app-paths-manifest.json +7 -6
- package/web-ui/web-ui/.next/server/chunks/243.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/28.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/330.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/{924.js → 334.js} +1 -1
- package/web-ui/web-ui/.next/server/chunks/447.js +6 -0
- package/web-ui/web-ui/.next/server/chunks/514.js +755 -0
- package/web-ui/web-ui/.next/server/chunks/539.js +22 -0
- package/web-ui/web-ui/.next/server/chunks/628.js +84 -0
- package/web-ui/web-ui/.next/server/chunks/817.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/830.js +750 -123
- package/web-ui/web-ui/.next/server/chunks/935.js +11 -0
- package/web-ui/web-ui/.next/server/chunks/949.js +123 -0
- package/web-ui/web-ui/.next/server/functions-config-manifest.json +5 -4
- package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
- package/web-ui/web-ui/.next/server/middleware.js +2 -2
- package/web-ui/web-ui/.next/server/pages/404.html +1 -1
- package/web-ui/web-ui/.next/server/pages/500.html +1 -1
- package/web-ui/web-ui/.next/server/pages/_app.js +1 -1
- package/web-ui/web-ui/.next/server/pages/_app.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/pages/_document.js +1 -1
- package/web-ui/web-ui/.next/server/pages/_document.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/pages/_error.js +3 -3
- package/web-ui/web-ui/.next/server/pages/_error.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
- package/web-ui/web-ui/.next/static/cVKs25XbQsC1083sU5GOD/_buildManifest.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/321-46ce36c1ef95d231.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/501-939e9e83b25a6293.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/84-8205054e8901dbde.js +62 -0
- package/web-ui/web-ui/.next/static/chunks/89-d23f5e27ab3b468f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e3ce4be93d2f8ac5.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-8bb13b53c7bde504.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07307e6d8ddb5cbf.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-aecbc7b6fbd143c2.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-1d468a1a0cf12aea.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-863e759b7e26c759.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-7636a56ffa40d5c6.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-c80a692b9d05f321.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-01e6dceb36cb882f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-d98e303b17524dad.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-178c53b1f56b5e97.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-7f69b2579337c729.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-7019a1567cd9aadd.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-90ce0ad1b9403f48.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-7ef77f76ff07ea1f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-3cf97f2dd544e8cb.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/layout-f1727d7b45e0779b.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/page-3fbfddda391d9939.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/{dddf6c0a-f09ab3fc84be1c52.js → d043c60e-31360d0d5cb78983.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/framework-3444de8758288822.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/main-app-9e734a14592bc9c4.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/main-b13545f01f111855.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/pages/_app-7f0d920cc5af1c9f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/pages/_error-3d8b61361c4b2c6b.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/{webpack-ea80c9c368a8364e.js → webpack-61986b0a62ba55ed.js} +1 -1
- package/web-ui/web-ui/.next/static/css/10425160b1a85ff4.css +2 -0
- package/web-ui/web-ui/package.json +1 -1
- package/web-ui/web-ui/server.js +1 -1
- package/web-ui/web-ui/.next/server/chunks/100.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/142.js +0 -700
- package/web-ui/web-ui/.next/server/chunks/472.js +0 -84
- package/web-ui/web-ui/.next/server/chunks/723.js +0 -22
- package/web-ui/web-ui/.next/server/chunks/75.js +0 -6
- package/web-ui/web-ui/.next/server/chunks/850.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/853.js +0 -698
- package/web-ui/web-ui/.next/server/chunks/871.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/959.js +0 -9
- package/web-ui/web-ui/.next/static/X_Z1LZQpnGXTuZXKkv4Vb/_buildManifest.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/137-8f757e04d1877a6c.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/517-a4643cfd51efbca4.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/613-734dcad67e1fa08c.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/784-25fa9bc074877c93.js +0 -62
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-72558bcb945a0abc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-540aa3dbe1c0b3ad.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-8fac90d5b77b7f89.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-06ee1868466199df.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-8b2ea0bbbc4fc69c.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-2aa604a6c36786fc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-28f48db02051d88c.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-15bc43471843cf2e.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-f034effcd10a332b.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-de465eacbb359987.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-fa10c50b1436102d.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-a61b90eb3a575f85.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-be0284caa1e78227.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-eec4e54274dcbb4d.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-26662e9d7ee32abc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/layout-e531d303d8c8a313.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/page-3ffe075f223c0af9.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/framework-f8855380599951ba.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/main-app-7291de73392cac56.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/main-ff0082770df07ddc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/pages/_app-56d1709db70ee49f.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/pages/_error-63dd320a91a40541.js +0 -1
- package/web-ui/web-ui/.next/static/css/a30725ee12aca680.css +0 -2
- /package/web-ui/web-ui/.next/static/{X_Z1LZQpnGXTuZXKkv4Vb → cVKs25XbQsC1083sU5GOD}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -751,7 +751,7 @@ function installAgentPlugin(agentId, mode = "capture") {
|
|
|
751
751
|
}
|
|
752
752
|
|
|
753
753
|
// ../packages/local-ops/src/egress-record.ts
|
|
754
|
-
import { statSync as
|
|
754
|
+
import { statSync as statSync5 } from "fs";
|
|
755
755
|
import { dirname as dirname4, relative as relative2, resolve } from "path";
|
|
756
756
|
|
|
757
757
|
// ../packages/detections/src/egress/registry.ts
|
|
@@ -1855,9 +1855,9 @@ function extractGoMod(text) {
|
|
|
1855
1855
|
let blockKeyword = null;
|
|
1856
1856
|
eachLine(text, (rawLine, lineNumber) => {
|
|
1857
1857
|
if (blockKeyword === null) {
|
|
1858
|
-
const
|
|
1859
|
-
if (
|
|
1860
|
-
blockKeyword =
|
|
1858
|
+
const open2 = GO_BLOCK_OPEN.exec(rawLine)?.[1];
|
|
1859
|
+
if (open2 !== void 0) {
|
|
1860
|
+
blockKeyword = open2;
|
|
1861
1861
|
return;
|
|
1862
1862
|
}
|
|
1863
1863
|
const path = GO_REQUIRE_SINGLE_LINE.exec(rawLine)?.[1];
|
|
@@ -2375,12 +2375,12 @@ function redact(text, findings) {
|
|
|
2375
2375
|
const regions = [];
|
|
2376
2376
|
for (const f of sorted) {
|
|
2377
2377
|
const rank = SEVERITY_RANK[f.severity];
|
|
2378
|
-
const
|
|
2379
|
-
if (
|
|
2380
|
-
|
|
2381
|
-
if (rank >
|
|
2382
|
-
|
|
2383
|
-
|
|
2378
|
+
const open2 = regions[regions.length - 1];
|
|
2379
|
+
if (open2 && f.span.start < open2.end) {
|
|
2380
|
+
open2.end = Math.max(open2.end, f.span.end);
|
|
2381
|
+
if (rank > open2.rank) {
|
|
2382
|
+
open2.rank = rank;
|
|
2383
|
+
open2.category = f.category;
|
|
2384
2384
|
}
|
|
2385
2385
|
} else {
|
|
2386
2386
|
regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
|
|
@@ -2409,94 +2409,6 @@ function maskMatch(raw) {
|
|
|
2409
2409
|
return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
|
|
2410
2410
|
}
|
|
2411
2411
|
|
|
2412
|
-
// ../packages/detections/src/posture/config-posture.ts
|
|
2413
|
-
var RULE_VERSION = "1";
|
|
2414
|
-
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
2415
|
-
"prettier",
|
|
2416
|
-
"eslint",
|
|
2417
|
-
"biome",
|
|
2418
|
-
"black",
|
|
2419
|
-
"ruff",
|
|
2420
|
-
"gofmt",
|
|
2421
|
-
"rustfmt",
|
|
2422
|
-
"node",
|
|
2423
|
-
"npx",
|
|
2424
|
-
"npm",
|
|
2425
|
-
"pnpm",
|
|
2426
|
-
"yarn",
|
|
2427
|
-
"make",
|
|
2428
|
-
"git",
|
|
2429
|
-
"jq",
|
|
2430
|
-
"echo"
|
|
2431
|
-
]);
|
|
2432
|
-
var EGRESS_RE = /\b(curl|wget|nc|ncat|netcat|ssh|scp|rsync)\b|https?:\/\//;
|
|
2433
|
-
var MUTATION_RE = /(--write|--fix|\s-w\b|\s-i\b|(?<![\d&>])>\s?\S)/;
|
|
2434
|
-
var CONFIG_POSTURE_RULES = [
|
|
2435
|
-
{
|
|
2436
|
-
ruleId: "hook-conflict",
|
|
2437
|
-
version: RULE_VERSION,
|
|
2438
|
-
name: "Overlapping hooks \u2014 run order is undefined",
|
|
2439
|
-
category: "config",
|
|
2440
|
-
severity: "medium",
|
|
2441
|
-
definition: JSON.stringify({
|
|
2442
|
-
kind: "hook-conflict",
|
|
2443
|
-
mutation: MUTATION_RE.source,
|
|
2444
|
-
note: "two hooks on the same event with overlapping matchers, both mutating the tool target"
|
|
2445
|
-
})
|
|
2446
|
-
},
|
|
2447
|
-
{
|
|
2448
|
-
ruleId: "hook-unknown",
|
|
2449
|
-
version: RULE_VERSION,
|
|
2450
|
-
name: "Unknown hook \u2014 not attributable to a plugin or known tool",
|
|
2451
|
-
category: "config",
|
|
2452
|
-
// medium, not high: with a deliberately tiny allowlist this v1 heuristic
|
|
2453
|
-
// fires on ordinary interpreter hooks (python3 x.py) and compound commands
|
|
2454
|
-
// (cd … && tool); 'high' is reserved for egress, which is genuinely
|
|
2455
|
-
// dangerous. Revisit when the known-hooks registry replaces the allowlist.
|
|
2456
|
-
severity: "medium",
|
|
2457
|
-
definition: JSON.stringify({ kind: "hook-unknown", knownTools: [...KNOWN_TOOLS] })
|
|
2458
|
-
},
|
|
2459
|
-
{
|
|
2460
|
-
ruleId: "hook-external-egress",
|
|
2461
|
-
version: RULE_VERSION,
|
|
2462
|
-
name: "Hook command can send data externally",
|
|
2463
|
-
category: "config",
|
|
2464
|
-
severity: "high",
|
|
2465
|
-
definition: JSON.stringify({ kind: "hook-external-egress", pattern: EGRESS_RE.source })
|
|
2466
|
-
}
|
|
2467
|
-
];
|
|
2468
|
-
|
|
2469
|
-
// ../packages/detections/src/security/redos-probe.ts
|
|
2470
|
-
var EXPONENTIAL_UNITS = [
|
|
2471
|
-
"a",
|
|
2472
|
-
"0",
|
|
2473
|
-
" ",
|
|
2474
|
-
"x",
|
|
2475
|
-
"ab",
|
|
2476
|
-
"a.",
|
|
2477
|
-
"a-",
|
|
2478
|
-
"a_",
|
|
2479
|
-
"a@",
|
|
2480
|
-
"a/",
|
|
2481
|
-
"a:",
|
|
2482
|
-
"a=",
|
|
2483
|
-
"a;",
|
|
2484
|
-
"aA0",
|
|
2485
|
-
" "
|
|
2486
|
-
];
|
|
2487
|
-
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
2488
|
-
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
2489
|
-
);
|
|
2490
|
-
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
2491
|
-
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
2492
|
-
);
|
|
2493
|
-
|
|
2494
|
-
// ../packages/persistence/src/database.ts
|
|
2495
|
-
import { randomUUID as randomUUID8 } from "crypto";
|
|
2496
|
-
import { existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
2497
|
-
import { join as join2, sep } from "path";
|
|
2498
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2499
|
-
|
|
2500
2412
|
// ../packages/schema/src/drizzle/sqlite-ddl.ts
|
|
2501
2413
|
var SQLITE_MIGRATIONS = [
|
|
2502
2414
|
{
|
|
@@ -2558,6 +2470,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
2558
2470
|
{
|
|
2559
2471
|
tag: "0014_drop_legacy_events_findings",
|
|
2560
2472
|
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
tag: "0015_busy_vengeance",
|
|
2476
|
+
sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
|
|
2477
|
+
},
|
|
2478
|
+
{
|
|
2479
|
+
tag: "0016_breezy_zodiak",
|
|
2480
|
+
sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
tag: "0017_rainy_kat_farrell",
|
|
2484
|
+
sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
tag: "0018_serious_tana_nile",
|
|
2488
|
+
sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
|
|
2561
2489
|
}
|
|
2562
2490
|
];
|
|
2563
2491
|
|
|
@@ -18337,6 +18265,7 @@ var ExceptionConditions = external_exports.object({
|
|
|
18337
18265
|
sourceTool: external_exports.string().optional(),
|
|
18338
18266
|
provider: external_exports.string().optional()
|
|
18339
18267
|
}).strict();
|
|
18268
|
+
var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
|
|
18340
18269
|
var DetectionException = external_exports.object({
|
|
18341
18270
|
id: external_exports.guid(),
|
|
18342
18271
|
ruleId: external_exports.string(),
|
|
@@ -18353,6 +18282,7 @@ var DetectionException = external_exports.object({
|
|
|
18353
18282
|
keyVersion: external_exports.number().int().positive(),
|
|
18354
18283
|
// maskMatch() preview of the approved value — never the raw value.
|
|
18355
18284
|
maskedValue: external_exports.string(),
|
|
18285
|
+
capability: ExceptionCapability.default("suppress"),
|
|
18356
18286
|
scope: ExceptionScope,
|
|
18357
18287
|
expiresAt: external_exports.iso.datetime().nullable(),
|
|
18358
18288
|
maxUses: external_exports.number().int().positive().nullable(),
|
|
@@ -18376,6 +18306,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
18376
18306
|
ruleId: true,
|
|
18377
18307
|
valueFingerprint: true,
|
|
18378
18308
|
keyVersion: true,
|
|
18309
|
+
capability: true,
|
|
18379
18310
|
expiresAt: true,
|
|
18380
18311
|
maxUses: true,
|
|
18381
18312
|
useCount: true,
|
|
@@ -19483,8 +19414,128 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
19483
19414
|
message: "At least one field must be provided"
|
|
19484
19415
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
19485
19416
|
|
|
19417
|
+
// ../packages/schema/src/zod/vault.ts
|
|
19418
|
+
var POINTER_FORMAT_VERSION = 2;
|
|
19419
|
+
var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
|
|
19420
|
+
var POINTER_TOKEN_PATTERN = new RegExp(
|
|
19421
|
+
`\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
|
|
19422
|
+
);
|
|
19423
|
+
var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
|
|
19424
|
+
function pointerTokenScanner() {
|
|
19425
|
+
return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
|
|
19426
|
+
}
|
|
19427
|
+
var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
|
|
19428
|
+
var ParsedPointer = external_exports.object({
|
|
19429
|
+
category: DetectionCategory,
|
|
19430
|
+
keyVersion: external_exports.number().int().positive(),
|
|
19431
|
+
pointerId: external_exports.string(),
|
|
19432
|
+
tag: external_exports.string()
|
|
19433
|
+
});
|
|
19434
|
+
var VaultEntry = external_exports.object({
|
|
19435
|
+
pointerId: external_exports.string(),
|
|
19436
|
+
// The keyed HMAC of the raw value under `exception.key`, and the epoch it was
|
|
19437
|
+
// derived under. This is what a reveal-to-model grant matches on, and it rotates
|
|
19438
|
+
// independently of the vault encryption key below.
|
|
19439
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
19440
|
+
fingerprintKeyVersion: external_exports.number().int().positive(),
|
|
19441
|
+
// The vault-key epoch this row's ciphertext was sealed under.
|
|
19442
|
+
keyVersion: external_exports.number().int().positive(),
|
|
19443
|
+
// Fixed at first mint and never updated: the same value detected later under a
|
|
19444
|
+
// different rule's category keeps the category it was minted with, so one
|
|
19445
|
+
// value always produces exactly one wire token.
|
|
19446
|
+
category: DetectionCategory,
|
|
19447
|
+
ruleId: external_exports.string(),
|
|
19448
|
+
// Partial-reveal preview for badges and listings. Never the raw value.
|
|
19449
|
+
maskedMatch: external_exports.string(),
|
|
19450
|
+
provider: external_exports.string().optional(),
|
|
19451
|
+
ciphertext: external_exports.string(),
|
|
19452
|
+
nonce: external_exports.string(),
|
|
19453
|
+
authTag: external_exports.string(),
|
|
19454
|
+
// How many times this value has been detected on this machine — the reuse
|
|
19455
|
+
// signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
|
|
19456
|
+
occurrenceCount: external_exports.number().int().nonnegative(),
|
|
19457
|
+
firstSeen: external_exports.string(),
|
|
19458
|
+
lastSeen: external_exports.string()
|
|
19459
|
+
});
|
|
19460
|
+
var PointerDescriptor = external_exports.object({
|
|
19461
|
+
category: DetectionCategory,
|
|
19462
|
+
provider: external_exports.string().optional(),
|
|
19463
|
+
maskedMatch: external_exports.string(),
|
|
19464
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
19465
|
+
firstSeen: external_exports.string(),
|
|
19466
|
+
lastSeen: external_exports.string()
|
|
19467
|
+
});
|
|
19468
|
+
var PointerIdentity = external_exports.object({
|
|
19469
|
+
ruleId: external_exports.string(),
|
|
19470
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
19471
|
+
fingerprintKeyVersion: external_exports.number().int().positive()
|
|
19472
|
+
});
|
|
19473
|
+
var DetokenizeTarget = external_exports.enum(["human", "model"]);
|
|
19474
|
+
var VaultDerefReason = external_exports.enum([
|
|
19475
|
+
"display",
|
|
19476
|
+
"explicit-reveal",
|
|
19477
|
+
"view-render",
|
|
19478
|
+
"model-input",
|
|
19479
|
+
"remediation",
|
|
19480
|
+
"purge"
|
|
19481
|
+
]);
|
|
19482
|
+
var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
|
|
19483
|
+
var BATCHED_DEREF_REASONS = ["display", "view-render"];
|
|
19484
|
+
function isBatchedDerefReason(reason) {
|
|
19485
|
+
return BATCHED_DEREF_REASONS.includes(reason);
|
|
19486
|
+
}
|
|
19487
|
+
var VaultDeref = external_exports.object({
|
|
19488
|
+
id: external_exports.guid(),
|
|
19489
|
+
pointerId: external_exports.string(),
|
|
19490
|
+
at: external_exports.string(),
|
|
19491
|
+
target: DetokenizeTarget,
|
|
19492
|
+
reason: VaultDerefReason,
|
|
19493
|
+
outcome: VaultDerefOutcome,
|
|
19494
|
+
// Present only on a model-target crossing that a reveal grant authorized.
|
|
19495
|
+
grantId: external_exports.string().optional(),
|
|
19496
|
+
// How many pointers ONE batched render resolved. 1 for unbatched rows. Named
|
|
19497
|
+
// apart from VaultEntry.occurrenceCount, which counts detections of a value.
|
|
19498
|
+
pointerCount: external_exports.number().int().positive().default(1)
|
|
19499
|
+
});
|
|
19500
|
+
var VaultSightingKind = external_exports.enum([
|
|
19501
|
+
"prompt",
|
|
19502
|
+
"tool-input",
|
|
19503
|
+
"tool-output",
|
|
19504
|
+
"file",
|
|
19505
|
+
"transcript"
|
|
19506
|
+
]);
|
|
19507
|
+
var VaultSighting = external_exports.object({
|
|
19508
|
+
location: external_exports.string(),
|
|
19509
|
+
kind: VaultSightingKind,
|
|
19510
|
+
firstSeen: external_exports.string(),
|
|
19511
|
+
lastSeen: external_exports.string()
|
|
19512
|
+
});
|
|
19513
|
+
var VaultInventoryEntry = external_exports.object({
|
|
19514
|
+
pointerId: external_exports.string(),
|
|
19515
|
+
category: DetectionCategory,
|
|
19516
|
+
provider: external_exports.string().optional(),
|
|
19517
|
+
maskedMatch: external_exports.string(),
|
|
19518
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
19519
|
+
firstSeen: external_exports.string(),
|
|
19520
|
+
lastSeen: external_exports.string(),
|
|
19521
|
+
// The active reveal-to-model grant covering this value, when one exists —
|
|
19522
|
+
// the inventory badges it, the row links to revocation.
|
|
19523
|
+
revealGrantId: external_exports.string().nullable(),
|
|
19524
|
+
sightings: external_exports.array(VaultSighting)
|
|
19525
|
+
});
|
|
19526
|
+
var VaultKeyCustody = external_exports.string();
|
|
19527
|
+
var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
|
|
19528
|
+
var VAULT_CONSENT_VERSION = 1;
|
|
19529
|
+
var VaultConsent = external_exports.object({
|
|
19530
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
19531
|
+
version: external_exports.number().int().positive()
|
|
19532
|
+
});
|
|
19533
|
+
function isVaultConsentValid(consent) {
|
|
19534
|
+
return consent?.version === VAULT_CONSENT_VERSION;
|
|
19535
|
+
}
|
|
19536
|
+
|
|
19486
19537
|
// ../packages/schema/src/zod/local.ts
|
|
19487
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
19538
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
|
|
19488
19539
|
var RunMode = external_exports.enum(["standalone"]);
|
|
19489
19540
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
19490
19541
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
@@ -19506,6 +19557,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
19506
19557
|
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
19507
19558
|
// Shares writes.
|
|
19508
19559
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
19560
|
+
// Consent to keep a RECOVERABLE encrypted copy of detected values in the local
|
|
19561
|
+
// vault, instead of destroying them. Absent by default: this is a custody
|
|
19562
|
+
// change from one-way redaction, so it is never an assumed grant on upgrade.
|
|
19563
|
+
// Revoking stops future vaulting; it does not erase what is already stored —
|
|
19564
|
+
// purging the vault is the eraser.
|
|
19565
|
+
vaultConsent: VaultConsent.optional(),
|
|
19566
|
+
// Where the vault master key lives.
|
|
19567
|
+
vaultKeyCustody: VaultKeyCustody.default("file"),
|
|
19568
|
+
// How a pointer renders in assistant prose on screen (see VaultInlineReveal).
|
|
19569
|
+
vaultInlineReveal: VaultInlineReveal.default("masked"),
|
|
19509
19570
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
19510
19571
|
onboardedAt: external_exports.iso.datetime().optional(),
|
|
19511
19572
|
// Records that the user consented to sending findings to the model API for
|
|
@@ -20052,6 +20113,112 @@ function reviewSeverityRank(reasons) {
|
|
|
20052
20113
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
20053
20114
|
}
|
|
20054
20115
|
|
|
20116
|
+
// ../packages/detections/src/pointer-shield.ts
|
|
20117
|
+
function shieldPointers(text) {
|
|
20118
|
+
const spans = [];
|
|
20119
|
+
let out = null;
|
|
20120
|
+
for (const match of text.matchAll(pointerTokenScanner())) {
|
|
20121
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
20122
|
+
out ??= text;
|
|
20123
|
+
out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
|
|
20124
|
+
}
|
|
20125
|
+
return { text: out ?? text, spans };
|
|
20126
|
+
}
|
|
20127
|
+
function dropShieldedFindings(findings, spans) {
|
|
20128
|
+
if (spans.length === 0) return findings;
|
|
20129
|
+
return findings.filter(
|
|
20130
|
+
(finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
|
|
20131
|
+
);
|
|
20132
|
+
}
|
|
20133
|
+
|
|
20134
|
+
// ../packages/detections/src/posture/config-posture.ts
|
|
20135
|
+
var RULE_VERSION = "1";
|
|
20136
|
+
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
20137
|
+
"prettier",
|
|
20138
|
+
"eslint",
|
|
20139
|
+
"biome",
|
|
20140
|
+
"black",
|
|
20141
|
+
"ruff",
|
|
20142
|
+
"gofmt",
|
|
20143
|
+
"rustfmt",
|
|
20144
|
+
"node",
|
|
20145
|
+
"npx",
|
|
20146
|
+
"npm",
|
|
20147
|
+
"pnpm",
|
|
20148
|
+
"yarn",
|
|
20149
|
+
"make",
|
|
20150
|
+
"git",
|
|
20151
|
+
"jq",
|
|
20152
|
+
"echo"
|
|
20153
|
+
]);
|
|
20154
|
+
var EGRESS_RE = /\b(curl|wget|nc|ncat|netcat|ssh|scp|rsync)\b|https?:\/\//;
|
|
20155
|
+
var MUTATION_RE = /(--write|--fix|\s-w\b|\s-i\b|(?<![\d&>])>\s?\S)/;
|
|
20156
|
+
var CONFIG_POSTURE_RULES = [
|
|
20157
|
+
{
|
|
20158
|
+
ruleId: "hook-conflict",
|
|
20159
|
+
version: RULE_VERSION,
|
|
20160
|
+
name: "Overlapping hooks \u2014 run order is undefined",
|
|
20161
|
+
category: "config",
|
|
20162
|
+
severity: "medium",
|
|
20163
|
+
definition: JSON.stringify({
|
|
20164
|
+
kind: "hook-conflict",
|
|
20165
|
+
mutation: MUTATION_RE.source,
|
|
20166
|
+
note: "two hooks on the same event with overlapping matchers, both mutating the tool target"
|
|
20167
|
+
})
|
|
20168
|
+
},
|
|
20169
|
+
{
|
|
20170
|
+
ruleId: "hook-unknown",
|
|
20171
|
+
version: RULE_VERSION,
|
|
20172
|
+
name: "Unknown hook \u2014 not attributable to a plugin or known tool",
|
|
20173
|
+
category: "config",
|
|
20174
|
+
// medium, not high: with a deliberately tiny allowlist this v1 heuristic
|
|
20175
|
+
// fires on ordinary interpreter hooks (python3 x.py) and compound commands
|
|
20176
|
+
// (cd … && tool); 'high' is reserved for egress, which is genuinely
|
|
20177
|
+
// dangerous. Revisit when the known-hooks registry replaces the allowlist.
|
|
20178
|
+
severity: "medium",
|
|
20179
|
+
definition: JSON.stringify({ kind: "hook-unknown", knownTools: [...KNOWN_TOOLS] })
|
|
20180
|
+
},
|
|
20181
|
+
{
|
|
20182
|
+
ruleId: "hook-external-egress",
|
|
20183
|
+
version: RULE_VERSION,
|
|
20184
|
+
name: "Hook command can send data externally",
|
|
20185
|
+
category: "config",
|
|
20186
|
+
severity: "high",
|
|
20187
|
+
definition: JSON.stringify({ kind: "hook-external-egress", pattern: EGRESS_RE.source })
|
|
20188
|
+
}
|
|
20189
|
+
];
|
|
20190
|
+
|
|
20191
|
+
// ../packages/detections/src/security/redos-probe.ts
|
|
20192
|
+
var EXPONENTIAL_UNITS = [
|
|
20193
|
+
"a",
|
|
20194
|
+
"0",
|
|
20195
|
+
" ",
|
|
20196
|
+
"x",
|
|
20197
|
+
"ab",
|
|
20198
|
+
"a.",
|
|
20199
|
+
"a-",
|
|
20200
|
+
"a_",
|
|
20201
|
+
"a@",
|
|
20202
|
+
"a/",
|
|
20203
|
+
"a:",
|
|
20204
|
+
"a=",
|
|
20205
|
+
"a;",
|
|
20206
|
+
"aA0",
|
|
20207
|
+
" "
|
|
20208
|
+
];
|
|
20209
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
20210
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
20211
|
+
);
|
|
20212
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
20213
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
20214
|
+
);
|
|
20215
|
+
|
|
20216
|
+
// ../packages/persistence/src/database.ts
|
|
20217
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
20218
|
+
import { existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
20219
|
+
import { join as join2, sep } from "path";
|
|
20220
|
+
import { DatabaseSync } from "node:sqlite";
|
|
20221
|
+
|
|
20055
20222
|
// ../packages/persistence/src/ids.ts
|
|
20056
20223
|
import { createHash } from "crypto";
|
|
20057
20224
|
function sha256Hex(input) {
|
|
@@ -21912,6 +22079,9 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
21912
22079
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
21913
22080
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
21914
22081
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
22082
|
+
var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
|
|
22083
|
+
AND conditions IS NULL
|
|
22084
|
+
AND ${ACTIVE_PREDICATE}`;
|
|
21915
22085
|
var SqliteExceptionsRepository = class {
|
|
21916
22086
|
constructor(db) {
|
|
21917
22087
|
this.db = db;
|
|
@@ -22003,11 +22173,11 @@ var SqliteExceptionsRepository = class {
|
|
|
22003
22173
|
this.db.prepare(
|
|
22004
22174
|
`INSERT INTO exceptions (
|
|
22005
22175
|
id, rule_id, category, value_fingerprint, key_version, masked_value,
|
|
22006
|
-
scope, expires_at, max_uses, use_count, last_used_at,
|
|
22007
|
-
conditions, created_by, created_via, created_at, updated_at
|
|
22176
|
+
capability, scope, expires_at, max_uses, use_count, last_used_at,
|
|
22177
|
+
justification, conditions, created_by, created_via, created_at, updated_at
|
|
22008
22178
|
) VALUES (
|
|
22009
22179
|
:id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
|
|
22010
|
-
:scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
22180
|
+
:capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
22011
22181
|
:conditions, :createdBy, :createdVia, :now, :now
|
|
22012
22182
|
)`
|
|
22013
22183
|
).run({
|
|
@@ -22017,6 +22187,7 @@ var SqliteExceptionsRepository = class {
|
|
|
22017
22187
|
valueFingerprint: input.valueFingerprint,
|
|
22018
22188
|
keyVersion: input.keyVersion,
|
|
22019
22189
|
maskedValue: input.maskedValue,
|
|
22190
|
+
capability: input.capability ?? "suppress",
|
|
22020
22191
|
scope: input.scope,
|
|
22021
22192
|
expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
|
|
22022
22193
|
maxUses: input.maxUses,
|
|
@@ -22110,6 +22281,7 @@ var SqliteExceptionsRepository = class {
|
|
|
22110
22281
|
ruleId: row.rule_id,
|
|
22111
22282
|
valueFingerprint: row.value_fingerprint,
|
|
22112
22283
|
keyVersion: row.key_version,
|
|
22284
|
+
capability: row.capability,
|
|
22113
22285
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
22114
22286
|
maxUses: row.max_uses,
|
|
22115
22287
|
useCount: row.use_count,
|
|
@@ -22164,6 +22336,35 @@ var SqliteExceptionsRepository = class {
|
|
|
22164
22336
|
}))
|
|
22165
22337
|
);
|
|
22166
22338
|
}
|
|
22339
|
+
/**
|
|
22340
|
+
* The active reveal-to-model grant for a vaulted value's identity, or null.
|
|
22341
|
+
* Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
|
|
22342
|
+
* suppression uses — plus the capability: a suppression grant must never
|
|
22343
|
+
* authorize a reveal. Read-only: the caller does NOT consume here, because a
|
|
22344
|
+
* revealed value re-enters the detection scan immediately afterward and the
|
|
22345
|
+
* suppression match there claims the use — one crossing, one use.
|
|
22346
|
+
*
|
|
22347
|
+
* A grant with `conditions` NEVER matches here: the reveal path does not yet
|
|
22348
|
+
* evaluate conditions, and a narrowing clause that is ignored would WIDEN the
|
|
22349
|
+
* grant instead. Fail closed until reveal-side condition evaluation exists.
|
|
22350
|
+
*/
|
|
22351
|
+
activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
|
|
22352
|
+
try {
|
|
22353
|
+
const row = getRow(
|
|
22354
|
+
this.db.prepare(
|
|
22355
|
+
`SELECT id FROM exceptions
|
|
22356
|
+
WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
|
|
22357
|
+
AND key_version = :keyVersion
|
|
22358
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
22359
|
+
LIMIT 1`
|
|
22360
|
+
),
|
|
22361
|
+
{ ruleId, valueFingerprint, keyVersion, now }
|
|
22362
|
+
);
|
|
22363
|
+
return Promise.resolve(row ?? null);
|
|
22364
|
+
} catch (err) {
|
|
22365
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
22366
|
+
}
|
|
22367
|
+
}
|
|
22167
22368
|
/**
|
|
22168
22369
|
* Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
|
|
22169
22370
|
* exhausted) whose last transition is older than the retention window.
|
|
@@ -22191,6 +22392,7 @@ function parseExceptionRow(row) {
|
|
|
22191
22392
|
valueFingerprint: row.value_fingerprint,
|
|
22192
22393
|
keyVersion: row.key_version,
|
|
22193
22394
|
maskedValue: row.masked_value,
|
|
22395
|
+
capability: row.capability,
|
|
22194
22396
|
scope: row.scope,
|
|
22195
22397
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
22196
22398
|
maxUses: row.max_uses,
|
|
@@ -24356,29 +24558,310 @@ var SqliteScanLedgerRepository = class {
|
|
|
24356
24558
|
}
|
|
24357
24559
|
};
|
|
24358
24560
|
|
|
24359
|
-
// ../packages/persistence/src/repositories/
|
|
24360
|
-
|
|
24361
|
-
var
|
|
24362
|
-
|
|
24363
|
-
|
|
24364
|
-
|
|
24365
|
-
|
|
24366
|
-
|
|
24367
|
-
|
|
24368
|
-
|
|
24369
|
-
|
|
24370
|
-
|
|
24371
|
-
|
|
24372
|
-
|
|
24373
|
-
|
|
24374
|
-
|
|
24375
|
-
|
|
24376
|
-
|
|
24377
|
-
|
|
24378
|
-
|
|
24379
|
-
|
|
24380
|
-
|
|
24381
|
-
|
|
24561
|
+
// ../packages/persistence/src/repositories/secret-vault.ts
|
|
24562
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
24563
|
+
var SELECT_COLUMNS = `
|
|
24564
|
+
pointer_id AS pointerId,
|
|
24565
|
+
value_fingerprint AS valueFingerprint,
|
|
24566
|
+
fingerprint_key_version AS fingerprintKeyVersion,
|
|
24567
|
+
key_version AS keyVersion,
|
|
24568
|
+
format_version AS formatVersion,
|
|
24569
|
+
category,
|
|
24570
|
+
rule_id AS ruleId,
|
|
24571
|
+
masked_match AS maskedMatch,
|
|
24572
|
+
provider,
|
|
24573
|
+
ciphertext,
|
|
24574
|
+
nonce,
|
|
24575
|
+
auth_tag AS authTag,
|
|
24576
|
+
occurrence_count AS occurrenceCount,
|
|
24577
|
+
first_seen AS firstSeen,
|
|
24578
|
+
last_seen AS lastSeen`;
|
|
24579
|
+
function toRow(raw) {
|
|
24580
|
+
const { provider, ...rest } = raw;
|
|
24581
|
+
return provider === null ? rest : { ...rest, provider };
|
|
24582
|
+
}
|
|
24583
|
+
var SqliteSecretVaultRepository = class {
|
|
24584
|
+
constructor(db) {
|
|
24585
|
+
this.db = db;
|
|
24586
|
+
this.insertStmt = db.prepare(
|
|
24587
|
+
`INSERT INTO secret_vault (
|
|
24588
|
+
pointer_id, value_fingerprint, fingerprint_key_version, key_version,
|
|
24589
|
+
format_version, category, rule_id, masked_match, provider,
|
|
24590
|
+
ciphertext, nonce, auth_tag,
|
|
24591
|
+
occurrence_count, first_seen, last_seen
|
|
24592
|
+
) VALUES (
|
|
24593
|
+
:pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
|
|
24594
|
+
:formatVersion, :category, :ruleId, :maskedMatch, :provider,
|
|
24595
|
+
:ciphertext, :nonce, :authTag,
|
|
24596
|
+
1, :now, :now
|
|
24597
|
+
)`
|
|
24598
|
+
);
|
|
24599
|
+
this.bumpStmt = db.prepare(
|
|
24600
|
+
`UPDATE secret_vault
|
|
24601
|
+
SET occurrence_count = occurrence_count + 1, last_seen = :now
|
|
24602
|
+
WHERE value_fingerprint = :valueFingerprint`
|
|
24603
|
+
);
|
|
24604
|
+
this.byPointerStmt = db.prepare(
|
|
24605
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
|
|
24606
|
+
);
|
|
24607
|
+
this.byFingerprintStmt = db.prepare(
|
|
24608
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
|
|
24609
|
+
);
|
|
24610
|
+
this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
|
|
24611
|
+
this.replaceCiphertextStmt = db.prepare(
|
|
24612
|
+
`UPDATE secret_vault
|
|
24613
|
+
SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
|
|
24614
|
+
WHERE pointer_id = :pointerId`
|
|
24615
|
+
);
|
|
24616
|
+
this.refreshFingerprintStmt = db.prepare(
|
|
24617
|
+
`UPDATE secret_vault
|
|
24618
|
+
SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
|
|
24619
|
+
WHERE pointer_id = :pointerId`
|
|
24620
|
+
);
|
|
24621
|
+
this.derefStmt = db.prepare(
|
|
24622
|
+
`INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
|
|
24623
|
+
VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
|
|
24624
|
+
);
|
|
24625
|
+
}
|
|
24626
|
+
db;
|
|
24627
|
+
insertStmt;
|
|
24628
|
+
bumpStmt;
|
|
24629
|
+
byPointerStmt;
|
|
24630
|
+
byFingerprintStmt;
|
|
24631
|
+
listStmt;
|
|
24632
|
+
replaceCiphertextStmt;
|
|
24633
|
+
refreshFingerprintStmt;
|
|
24634
|
+
derefStmt;
|
|
24635
|
+
/**
|
|
24636
|
+
* Vault a value, or record another sighting of one already vaulted. Keyed on
|
|
24637
|
+
* `valueFingerprint`, never on the caller's pointer id: a value seen again
|
|
24638
|
+
* bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
|
|
24639
|
+
* pointer, category and ciphertext, so the same secret always resolves to one
|
|
24640
|
+
* wire token. `minted` is true only when this call created the row.
|
|
24641
|
+
*
|
|
24642
|
+
* The read-then-write runs in one IMMEDIATE transaction so two concurrent
|
|
24643
|
+
* writers cannot both decide they are minting.
|
|
24644
|
+
*/
|
|
24645
|
+
upsert(input, now) {
|
|
24646
|
+
let minted = false;
|
|
24647
|
+
withTransaction(
|
|
24648
|
+
this.db,
|
|
24649
|
+
() => {
|
|
24650
|
+
const existing = getRow(this.byFingerprintStmt, {
|
|
24651
|
+
valueFingerprint: input.valueFingerprint
|
|
24652
|
+
});
|
|
24653
|
+
if (existing === void 0) {
|
|
24654
|
+
this.insertStmt.run(
|
|
24655
|
+
bindParams({
|
|
24656
|
+
pointerId: input.pointerId,
|
|
24657
|
+
valueFingerprint: input.valueFingerprint,
|
|
24658
|
+
fingerprintKeyVersion: input.fingerprintKeyVersion,
|
|
24659
|
+
keyVersion: input.keyVersion,
|
|
24660
|
+
formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
|
|
24661
|
+
category: input.category,
|
|
24662
|
+
ruleId: input.ruleId,
|
|
24663
|
+
maskedMatch: input.maskedMatch,
|
|
24664
|
+
provider: input.provider,
|
|
24665
|
+
ciphertext: input.ciphertext,
|
|
24666
|
+
nonce: input.nonce,
|
|
24667
|
+
authTag: input.authTag,
|
|
24668
|
+
now
|
|
24669
|
+
})
|
|
24670
|
+
);
|
|
24671
|
+
minted = true;
|
|
24672
|
+
return;
|
|
24673
|
+
}
|
|
24674
|
+
this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
|
|
24675
|
+
},
|
|
24676
|
+
"IMMEDIATE"
|
|
24677
|
+
);
|
|
24678
|
+
const row = getRow(this.byFingerprintStmt, {
|
|
24679
|
+
valueFingerprint: input.valueFingerprint
|
|
24680
|
+
});
|
|
24681
|
+
if (row === void 0) throw new Error("vault: row vanished immediately after write");
|
|
24682
|
+
return { row: toRow(row), minted };
|
|
24683
|
+
}
|
|
24684
|
+
byPointerId(pointerId) {
|
|
24685
|
+
const raw = getRow(this.byPointerStmt, { pointerId });
|
|
24686
|
+
return raw === void 0 ? null : toRow(raw);
|
|
24687
|
+
}
|
|
24688
|
+
byValueFingerprint(fingerprint) {
|
|
24689
|
+
const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
|
|
24690
|
+
return raw === void 0 ? null : toRow(raw);
|
|
24691
|
+
}
|
|
24692
|
+
/** Append one audit row. Carries no raw value and no ciphertext, by shape. */
|
|
24693
|
+
recordDeref(entry) {
|
|
24694
|
+
this.derefStmt.run(
|
|
24695
|
+
bindParams({
|
|
24696
|
+
id: entry.id,
|
|
24697
|
+
pointerId: entry.pointerId,
|
|
24698
|
+
at: entry.at,
|
|
24699
|
+
target: entry.target,
|
|
24700
|
+
reason: entry.reason,
|
|
24701
|
+
outcome: entry.outcome,
|
|
24702
|
+
grantId: entry.grantId,
|
|
24703
|
+
pointerCount: entry.pointerCount ?? 1
|
|
24704
|
+
})
|
|
24705
|
+
);
|
|
24706
|
+
}
|
|
24707
|
+
listAll() {
|
|
24708
|
+
return allRows(this.listStmt).map(toRow);
|
|
24709
|
+
}
|
|
24710
|
+
/** Re-seal an entry under a new key epoch, leaving its identity untouched. */
|
|
24711
|
+
replaceCiphertext(pointerId, next) {
|
|
24712
|
+
this.replaceCiphertextStmt.run({ pointerId, ...next });
|
|
24713
|
+
}
|
|
24714
|
+
/** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
|
|
24715
|
+
refreshFingerprint(pointerId, next) {
|
|
24716
|
+
this.refreshFingerprintStmt.run({ pointerId, ...next });
|
|
24717
|
+
}
|
|
24718
|
+
/**
|
|
24719
|
+
* Destroy every vaulted value and report how many were destroyed. The deref
|
|
24720
|
+
* audit is left alone on purpose — see the table note above.
|
|
24721
|
+
*/
|
|
24722
|
+
purgeAll() {
|
|
24723
|
+
let destroyed = 0;
|
|
24724
|
+
withTransaction(
|
|
24725
|
+
this.db,
|
|
24726
|
+
() => {
|
|
24727
|
+
destroyed = this.countEntries();
|
|
24728
|
+
this.db.exec("DELETE FROM secret_vault");
|
|
24729
|
+
},
|
|
24730
|
+
"IMMEDIATE"
|
|
24731
|
+
);
|
|
24732
|
+
return destroyed;
|
|
24733
|
+
}
|
|
24734
|
+
/**
|
|
24735
|
+
* Record (or re-stamp) one place a pointer has been written. One row per
|
|
24736
|
+
* (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
|
|
24737
|
+
* on hook paths — a failure must never affect the rewrite that triggered it,
|
|
24738
|
+
* so callers wrap this, not the other way around.
|
|
24739
|
+
*/
|
|
24740
|
+
recordSighting(entry, now) {
|
|
24741
|
+
this.db.prepare(
|
|
24742
|
+
`INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
|
|
24743
|
+
VALUES (:id, :pointerId, :location, :kind, :now, :now)
|
|
24744
|
+
ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
|
|
24745
|
+
).run({
|
|
24746
|
+
id: randomUUID7(),
|
|
24747
|
+
pointerId: entry.pointerId,
|
|
24748
|
+
location: entry.location,
|
|
24749
|
+
kind: entry.kind,
|
|
24750
|
+
now
|
|
24751
|
+
});
|
|
24752
|
+
}
|
|
24753
|
+
listSightings(pointerId) {
|
|
24754
|
+
const rows = allRows(
|
|
24755
|
+
this.db.prepare(
|
|
24756
|
+
`SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
|
|
24757
|
+
WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
|
|
24758
|
+
),
|
|
24759
|
+
{ pointerId }
|
|
24760
|
+
);
|
|
24761
|
+
return rows.map((r) => ({
|
|
24762
|
+
location: r.location,
|
|
24763
|
+
kind: r.kind,
|
|
24764
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
24765
|
+
lastSeen: new Date(r.last_seen).toISOString()
|
|
24766
|
+
}));
|
|
24767
|
+
}
|
|
24768
|
+
/**
|
|
24769
|
+
* The dashboard inventory: every vaulted value's descriptor data joined with
|
|
24770
|
+
* its sightings and the active reveal-to-model grant when one exists.
|
|
24771
|
+
* Raw-free by construction — neither the fingerprint nor the ciphertext
|
|
24772
|
+
* columns are selected.
|
|
24773
|
+
*/
|
|
24774
|
+
listInventory(now = Date.now()) {
|
|
24775
|
+
const rows = allRows(
|
|
24776
|
+
this.db.prepare(
|
|
24777
|
+
`SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
|
|
24778
|
+
v.occurrence_count, v.first_seen, v.last_seen,
|
|
24779
|
+
(SELECT e.id FROM exceptions e
|
|
24780
|
+
WHERE e.rule_id = v.rule_id
|
|
24781
|
+
AND e.value_fingerprint = v.value_fingerprint
|
|
24782
|
+
AND e.key_version = v.fingerprint_key_version
|
|
24783
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
24784
|
+
LIMIT 1) AS grant_id
|
|
24785
|
+
FROM secret_vault v
|
|
24786
|
+
ORDER BY v.last_seen DESC`
|
|
24787
|
+
),
|
|
24788
|
+
{ now }
|
|
24789
|
+
);
|
|
24790
|
+
return rows.map((r) => ({
|
|
24791
|
+
pointerId: r.pointer_id,
|
|
24792
|
+
category: r.category,
|
|
24793
|
+
...r.provider === null ? {} : { provider: r.provider },
|
|
24794
|
+
maskedMatch: r.masked_match,
|
|
24795
|
+
occurrences: r.occurrence_count,
|
|
24796
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
24797
|
+
lastSeen: new Date(r.last_seen).toISOString(),
|
|
24798
|
+
revealGrantId: r.grant_id,
|
|
24799
|
+
sightings: this.listSightings(r.pointer_id)
|
|
24800
|
+
}));
|
|
24801
|
+
}
|
|
24802
|
+
/**
|
|
24803
|
+
* The de-reference trail, newest first. By default the batched, high-volume
|
|
24804
|
+
* reasons (display, view-render) are hidden and counted instead — the rows
|
|
24805
|
+
* that matter as a signal are the model crossings, and burying them under
|
|
24806
|
+
* render noise would defeat the audit's purpose.
|
|
24807
|
+
*/
|
|
24808
|
+
listDerefs(opts) {
|
|
24809
|
+
const limit = opts?.limit ?? 200;
|
|
24810
|
+
const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
|
|
24811
|
+
const rows = allRows(
|
|
24812
|
+
this.db.prepare(
|
|
24813
|
+
`SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
|
|
24814
|
+
FROM secret_vault_deref ${where}
|
|
24815
|
+
ORDER BY at DESC, rowid DESC LIMIT :limit`
|
|
24816
|
+
),
|
|
24817
|
+
{ limit }
|
|
24818
|
+
);
|
|
24819
|
+
const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
|
|
24820
|
+
this.db,
|
|
24821
|
+
`SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
|
|
24822
|
+
);
|
|
24823
|
+
return {
|
|
24824
|
+
rows: rows.map((r) => ({
|
|
24825
|
+
id: r.id,
|
|
24826
|
+
pointerId: r.pointer_id,
|
|
24827
|
+
at: new Date(r.at).toISOString(),
|
|
24828
|
+
target: r.target,
|
|
24829
|
+
reason: r.reason,
|
|
24830
|
+
outcome: r.outcome,
|
|
24831
|
+
...r.grant_id === null ? {} : { grantId: r.grant_id },
|
|
24832
|
+
pointerCount: r.pointer_count
|
|
24833
|
+
})),
|
|
24834
|
+
hiddenBatched
|
|
24835
|
+
};
|
|
24836
|
+
}
|
|
24837
|
+
countEntries() {
|
|
24838
|
+
return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
|
|
24839
|
+
}
|
|
24840
|
+
};
|
|
24841
|
+
|
|
24842
|
+
// ../packages/persistence/src/repositories/security.ts
|
|
24843
|
+
var DAY_MS4 = 864e5;
|
|
24844
|
+
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
24845
|
+
var ACTION_TO_KIND = {
|
|
24846
|
+
block: "blocked",
|
|
24847
|
+
redact: "redacted",
|
|
24848
|
+
warn: "warned"
|
|
24849
|
+
};
|
|
24850
|
+
var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
|
|
24851
|
+
var SCAN_COVERAGE = [
|
|
24852
|
+
{ provider: "claudecode", coverage: 100, supported: true },
|
|
24853
|
+
{ provider: "cursor", coverage: 0, supported: false },
|
|
24854
|
+
{ provider: "codex", coverage: 0, supported: false },
|
|
24855
|
+
{ provider: "chatgpt", coverage: 0, supported: false },
|
|
24856
|
+
{ provider: "copilot", coverage: 0, supported: false },
|
|
24857
|
+
{ provider: "api", coverage: 0, supported: false }
|
|
24858
|
+
];
|
|
24859
|
+
var GRANULARITY = {
|
|
24860
|
+
"7d": "day",
|
|
24861
|
+
"30d": "day",
|
|
24862
|
+
"3m": "week",
|
|
24863
|
+
"6m": "week"
|
|
24864
|
+
};
|
|
24382
24865
|
function granularityFor(range) {
|
|
24383
24866
|
return GRANULARITY[range];
|
|
24384
24867
|
}
|
|
@@ -24701,7 +25184,7 @@ var SqliteSecurityRepository = class {
|
|
|
24701
25184
|
};
|
|
24702
25185
|
|
|
24703
25186
|
// ../packages/persistence/src/repositories/shares.ts
|
|
24704
|
-
import { randomUUID as
|
|
25187
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
24705
25188
|
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
24706
25189
|
var IN_CHUNK = 500;
|
|
24707
25190
|
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
@@ -24957,7 +25440,7 @@ var SqliteSharesRepository = class {
|
|
|
24957
25440
|
(id, destination_id, host, decision, created_at, updated_at)
|
|
24958
25441
|
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
24959
25442
|
).run({
|
|
24960
|
-
id:
|
|
25443
|
+
id: randomUUID8(),
|
|
24961
25444
|
destinationId,
|
|
24962
25445
|
host: dest.host,
|
|
24963
25446
|
decision,
|
|
@@ -25106,7 +25589,7 @@ var SqliteSharesRepository = class {
|
|
|
25106
25589
|
let destinationId = destIds.get(hit.host);
|
|
25107
25590
|
if (destinationId === void 0) {
|
|
25108
25591
|
destStmt.run({
|
|
25109
|
-
id:
|
|
25592
|
+
id: randomUUID8(),
|
|
25110
25593
|
kind: hit.kind,
|
|
25111
25594
|
name: hit.name,
|
|
25112
25595
|
host: hit.host,
|
|
@@ -25122,7 +25605,7 @@ var SqliteSharesRepository = class {
|
|
|
25122
25605
|
let endpointId = endpointIds.get(endpointKey);
|
|
25123
25606
|
if (endpointId === void 0) {
|
|
25124
25607
|
endpointStmt.run({
|
|
25125
|
-
id:
|
|
25608
|
+
id: randomUUID8(),
|
|
25126
25609
|
destinationId,
|
|
25127
25610
|
method: hit.method,
|
|
25128
25611
|
transport: hit.transport,
|
|
@@ -25135,7 +25618,7 @@ var SqliteSharesRepository = class {
|
|
|
25135
25618
|
endpointIds.set(endpointKey, endpointId);
|
|
25136
25619
|
}
|
|
25137
25620
|
siteStmt.run({
|
|
25138
|
-
id:
|
|
25621
|
+
id: randomUUID8(),
|
|
25139
25622
|
endpointId,
|
|
25140
25623
|
project: input.project,
|
|
25141
25624
|
projectKey: input.projectKey,
|
|
@@ -25551,6 +26034,7 @@ function openAndInitialize(file2) {
|
|
|
25551
26034
|
policies,
|
|
25552
26035
|
installedPacks,
|
|
25553
26036
|
scanLedger: new SqliteScanLedgerRepository(db),
|
|
26037
|
+
secretVault: new SqliteSecretVaultRepository(db),
|
|
25554
26038
|
exceptions: new SqliteExceptionsRepository(db),
|
|
25555
26039
|
resolutions: new SqliteResolutionsRepository(db),
|
|
25556
26040
|
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
@@ -25586,6 +26070,7 @@ function openLocalDatabase(dir) {
|
|
|
25586
26070
|
policies,
|
|
25587
26071
|
installedPacks,
|
|
25588
26072
|
scanLedger,
|
|
26073
|
+
secretVault,
|
|
25589
26074
|
exceptions,
|
|
25590
26075
|
resolutions,
|
|
25591
26076
|
ruleProbeCache,
|
|
@@ -25694,7 +26179,7 @@ function openLocalDatabase(dir) {
|
|
|
25694
26179
|
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
25695
26180
|
if (!definitionId) continue;
|
|
25696
26181
|
inspectionFindings.insertFinding({
|
|
25697
|
-
id:
|
|
26182
|
+
id: randomUUID9(),
|
|
25698
26183
|
auditEventId: record2.scanEvent.id,
|
|
25699
26184
|
inspectionDefinitionId: definitionId,
|
|
25700
26185
|
span: finding.span,
|
|
@@ -25771,6 +26256,7 @@ function openLocalDatabase(dir) {
|
|
|
25771
26256
|
policies,
|
|
25772
26257
|
installedPacks,
|
|
25773
26258
|
scanLedger,
|
|
26259
|
+
secretVault,
|
|
25774
26260
|
exceptions,
|
|
25775
26261
|
resolutions,
|
|
25776
26262
|
ruleProbeCache,
|
|
@@ -25841,7 +26327,11 @@ function parseKeyFile(raw) {
|
|
|
25841
26327
|
}
|
|
25842
26328
|
return { version: version2, material: bytes };
|
|
25843
26329
|
}
|
|
25844
|
-
var
|
|
26330
|
+
var KEY_VERSION_COLUMNS = {
|
|
26331
|
+
exceptions: "key_version",
|
|
26332
|
+
blocked_detections: "key_version",
|
|
26333
|
+
secret_vault: "fingerprint_key_version"
|
|
26334
|
+
};
|
|
25845
26335
|
var SQLITE_ERROR = 1;
|
|
25846
26336
|
var FLOOR_BUSY_TIMEOUT_MS = 250;
|
|
25847
26337
|
var FloorUnreadableError = class extends Error {
|
|
@@ -25862,10 +26352,10 @@ function storedKeyVersionFloor(dataDir2) {
|
|
|
25862
26352
|
db = new DatabaseSync2(file2, { readOnly: true });
|
|
25863
26353
|
db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
|
|
25864
26354
|
let floor = 0;
|
|
25865
|
-
for (const table of
|
|
26355
|
+
for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
|
|
25866
26356
|
try {
|
|
25867
26357
|
const row = getRow(
|
|
25868
|
-
db.prepare(`SELECT MAX(
|
|
26358
|
+
db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
|
|
25869
26359
|
);
|
|
25870
26360
|
floor = Math.max(floor, row?.v ?? 0);
|
|
25871
26361
|
} catch (err) {
|
|
@@ -25944,6 +26434,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
25944
26434
|
function dbPath(base = defaultDataDir()) {
|
|
25945
26435
|
return join4(dataDir(base), "aka.db");
|
|
25946
26436
|
}
|
|
26437
|
+
function keysDir(base = defaultDataDir()) {
|
|
26438
|
+
return join4(base, "keys");
|
|
26439
|
+
}
|
|
25947
26440
|
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
25948
26441
|
ensureDataDirSync(dir);
|
|
25949
26442
|
}
|
|
@@ -25970,13 +26463,874 @@ function readJson(file2) {
|
|
|
25970
26463
|
return parseJsonObject(text) ?? null;
|
|
25971
26464
|
}
|
|
25972
26465
|
|
|
25973
|
-
// ../packages/persistence/src/
|
|
25974
|
-
import {
|
|
26466
|
+
// ../packages/persistence/src/vault/crypto.ts
|
|
26467
|
+
import {
|
|
26468
|
+
createCipheriv,
|
|
26469
|
+
createDecipheriv,
|
|
26470
|
+
createHmac as createHmac2,
|
|
26471
|
+
hkdfSync,
|
|
26472
|
+
timingSafeEqual
|
|
26473
|
+
} from "crypto";
|
|
26474
|
+
var POINTER_ID_BYTES = 16;
|
|
26475
|
+
var NONCE_BYTES = 12;
|
|
26476
|
+
var TAG_BYTES = 10;
|
|
26477
|
+
var SUBKEY_BYTES = 32;
|
|
26478
|
+
var HKDF_INFO_ENC = "aka:vault:enc:v1";
|
|
26479
|
+
var HKDF_INFO_SIGN = "aka:vault:sign:v1";
|
|
26480
|
+
var HKDF_SALT = "aka:vault:v1";
|
|
26481
|
+
var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
26482
|
+
function base32Encode(bytes) {
|
|
26483
|
+
let out = "";
|
|
26484
|
+
let buffer = 0;
|
|
26485
|
+
let bits = 0;
|
|
26486
|
+
for (const byte of bytes) {
|
|
26487
|
+
buffer = buffer << 8 | byte;
|
|
26488
|
+
bits += 8;
|
|
26489
|
+
while (bits >= 5) {
|
|
26490
|
+
out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
|
|
26491
|
+
bits -= 5;
|
|
26492
|
+
}
|
|
26493
|
+
}
|
|
26494
|
+
if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
|
|
26495
|
+
return out;
|
|
26496
|
+
}
|
|
26497
|
+
function base32Decode(text) {
|
|
26498
|
+
const out = [];
|
|
26499
|
+
let buffer = 0;
|
|
26500
|
+
let bits = 0;
|
|
26501
|
+
for (const char of text) {
|
|
26502
|
+
const value = B32_ALPHABET.indexOf(char);
|
|
26503
|
+
if (value < 0) throw new Error("base32: character outside the alphabet");
|
|
26504
|
+
buffer = buffer << 5 | value;
|
|
26505
|
+
bits += 5;
|
|
26506
|
+
if (bits >= 8) {
|
|
26507
|
+
out.push(buffer >>> bits - 8 & 255);
|
|
26508
|
+
bits -= 8;
|
|
26509
|
+
}
|
|
26510
|
+
}
|
|
26511
|
+
return Buffer.from(out);
|
|
26512
|
+
}
|
|
26513
|
+
function encodeKeyVersion(version2) {
|
|
26514
|
+
if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
|
|
26515
|
+
throw new Error("vault: key version out of range");
|
|
26516
|
+
}
|
|
26517
|
+
const bytes = [];
|
|
26518
|
+
let remaining = version2;
|
|
26519
|
+
while (remaining > 0) {
|
|
26520
|
+
bytes.unshift(remaining & 255);
|
|
26521
|
+
remaining = Math.floor(remaining / 256);
|
|
26522
|
+
}
|
|
26523
|
+
return base32Encode(Uint8Array.from(bytes));
|
|
26524
|
+
}
|
|
26525
|
+
function decodeKeyVersion(encoded) {
|
|
26526
|
+
const bytes = base32Decode(encoded);
|
|
26527
|
+
if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
|
|
26528
|
+
let version2 = 0;
|
|
26529
|
+
for (const byte of bytes) version2 = version2 * 256 + byte;
|
|
26530
|
+
if (version2 < 1) throw new Error("vault: bad key version");
|
|
26531
|
+
return version2;
|
|
26532
|
+
}
|
|
26533
|
+
function deriveSubkeys(master) {
|
|
26534
|
+
const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
|
|
26535
|
+
return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
|
|
26536
|
+
}
|
|
26537
|
+
function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
|
|
26538
|
+
if (pointerId.length !== POINTER_ID_BYTES) {
|
|
26539
|
+
throw new Error("vault: pointer id must be 16 bytes");
|
|
26540
|
+
}
|
|
26541
|
+
const head = Buffer.alloc(6);
|
|
26542
|
+
head.writeUInt16BE(formatVersion, 0);
|
|
26543
|
+
head.writeUInt32BE(keyVersion, 2);
|
|
26544
|
+
return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
|
|
26545
|
+
}
|
|
26546
|
+
function seal(encKey, plaintext, aad, nonce) {
|
|
26547
|
+
const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
|
|
26548
|
+
cipher.setAAD(aad);
|
|
26549
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
26550
|
+
return { ciphertext, nonce, authTag: cipher.getAuthTag() };
|
|
26551
|
+
}
|
|
26552
|
+
function open(encKey, sealed, aad) {
|
|
26553
|
+
try {
|
|
26554
|
+
const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
|
|
26555
|
+
decipher.setAAD(aad);
|
|
26556
|
+
decipher.setAuthTag(sealed.authTag);
|
|
26557
|
+
return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
|
|
26558
|
+
} catch {
|
|
26559
|
+
return null;
|
|
26560
|
+
}
|
|
26561
|
+
}
|
|
26562
|
+
function signPointer(signKey, keyVersion, pointerId, category) {
|
|
26563
|
+
return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
|
|
26564
|
+
}
|
|
26565
|
+
function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
|
|
26566
|
+
if (tag.length !== TAG_BYTES) return false;
|
|
26567
|
+
const expected = signPointer(signKey, keyVersion, pointerId, category);
|
|
26568
|
+
return timingSafeEqual(expected, Buffer.from(tag));
|
|
26569
|
+
}
|
|
26570
|
+
function formatPointer(category, keyVersion, pointerId, tag) {
|
|
26571
|
+
return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
|
|
26572
|
+
}
|
|
26573
|
+
|
|
26574
|
+
// ../packages/persistence/src/vault/key-provider.ts
|
|
26575
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
26576
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
26577
|
+
import {
|
|
26578
|
+
chmodSync as chmodSync2,
|
|
26579
|
+
mkdirSync as mkdirSync2,
|
|
26580
|
+
readFileSync as readFileSync4,
|
|
26581
|
+
renameSync as renameSync4,
|
|
26582
|
+
rmSync as rmSync3,
|
|
26583
|
+
statSync,
|
|
26584
|
+
writeFileSync as writeFileSync2
|
|
26585
|
+
} from "fs";
|
|
25975
26586
|
import { join as join6 } from "path";
|
|
26587
|
+
var VaultKeyEpochMissingError = class extends Error {
|
|
26588
|
+
version;
|
|
26589
|
+
constructor(version2) {
|
|
26590
|
+
super(`vault: key epoch ${String(version2)} is not present in the keyring`);
|
|
26591
|
+
this.name = "VaultKeyEpochMissingError";
|
|
26592
|
+
this.version = version2;
|
|
26593
|
+
}
|
|
26594
|
+
};
|
|
26595
|
+
var VAULT_KEY_FILENAME = "vault.key";
|
|
26596
|
+
var KEY_MATERIAL_BYTES2 = 32;
|
|
26597
|
+
var KEYCHAIN_SERVICE = "aka-vault";
|
|
26598
|
+
var KEYCHAIN_ACCOUNT = "keyring";
|
|
26599
|
+
function parseKeyring(raw) {
|
|
26600
|
+
const parsed = JSON.parse(raw);
|
|
26601
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
26602
|
+
throw new Error("vault key file is corrupt: not a JSON object");
|
|
26603
|
+
}
|
|
26604
|
+
const { current, keys } = parsed;
|
|
26605
|
+
if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
|
|
26606
|
+
throw new Error("vault key file is corrupt: bad current version");
|
|
26607
|
+
}
|
|
26608
|
+
if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
|
|
26609
|
+
throw new Error("vault key file is corrupt: bad keys map");
|
|
26610
|
+
}
|
|
26611
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
26612
|
+
for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
|
|
26613
|
+
const version2 = Number(rawVersion);
|
|
26614
|
+
if (!Number.isInteger(version2) || version2 < 1) {
|
|
26615
|
+
throw new Error("vault key file is corrupt: bad key version");
|
|
26616
|
+
}
|
|
26617
|
+
if (typeof rawMaterial !== "string") {
|
|
26618
|
+
throw new Error("vault key file is corrupt: bad key material");
|
|
26619
|
+
}
|
|
26620
|
+
const bytes = Buffer.from(rawMaterial, "base64");
|
|
26621
|
+
if (bytes.length !== KEY_MATERIAL_BYTES2) {
|
|
26622
|
+
throw new Error("vault key file is corrupt: bad key material length");
|
|
26623
|
+
}
|
|
26624
|
+
map2.set(version2, bytes);
|
|
26625
|
+
}
|
|
26626
|
+
if (!map2.has(current)) {
|
|
26627
|
+
throw new Error("vault key file is corrupt: current version has no material");
|
|
26628
|
+
}
|
|
26629
|
+
return { current, keys: map2 };
|
|
26630
|
+
}
|
|
26631
|
+
function serializeKeyring(keyring) {
|
|
26632
|
+
const keys = {};
|
|
26633
|
+
for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
|
|
26634
|
+
const material = keyring.keys.get(version2);
|
|
26635
|
+
if (material) keys[String(version2)] = material.toString("base64");
|
|
26636
|
+
}
|
|
26637
|
+
return JSON.stringify({ current: keyring.current, keys });
|
|
26638
|
+
}
|
|
26639
|
+
function mintKeyring() {
|
|
26640
|
+
return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
|
|
26641
|
+
}
|
|
26642
|
+
function withNextEpoch(keyring) {
|
|
26643
|
+
const next = Math.max(...keyring.keys.keys()) + 1;
|
|
26644
|
+
const keys = new Map(keyring.keys);
|
|
26645
|
+
keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
|
|
26646
|
+
return { current: next, keys };
|
|
26647
|
+
}
|
|
26648
|
+
function currentOf(keyring) {
|
|
26649
|
+
const material = keyring.keys.get(keyring.current);
|
|
26650
|
+
if (!material) throw new VaultKeyEpochMissingError(keyring.current);
|
|
26651
|
+
return { material, version: keyring.current };
|
|
26652
|
+
}
|
|
26653
|
+
function epochOf(keyring, version2) {
|
|
26654
|
+
const material = keyring.keys.get(version2);
|
|
26655
|
+
if (!material) throw new VaultKeyEpochMissingError(version2);
|
|
26656
|
+
return { material, version: version2 };
|
|
26657
|
+
}
|
|
26658
|
+
function asAsync(work) {
|
|
26659
|
+
try {
|
|
26660
|
+
return Promise.resolve(work());
|
|
26661
|
+
} catch (err) {
|
|
26662
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
26663
|
+
}
|
|
26664
|
+
}
|
|
26665
|
+
function asError(err) {
|
|
26666
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
26667
|
+
}
|
|
26668
|
+
var ROTATION_LOCK_STALE_MS = 6e4;
|
|
26669
|
+
var LOCK_OWNER_FILE = "owner";
|
|
26670
|
+
var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
|
|
26671
|
+
function claimRotationLock(lock, owner) {
|
|
26672
|
+
try {
|
|
26673
|
+
mkdirSync2(lock);
|
|
26674
|
+
} catch (err) {
|
|
26675
|
+
if (err.code === "EEXIST") return false;
|
|
26676
|
+
throw asError(err);
|
|
26677
|
+
}
|
|
26678
|
+
try {
|
|
26679
|
+
writeFileSync2(join6(lock, LOCK_OWNER_FILE), `${owner}
|
|
26680
|
+
`, { mode: DATA_FILE_MODE });
|
|
26681
|
+
return true;
|
|
26682
|
+
} catch (err) {
|
|
26683
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
26684
|
+
throw asError(err);
|
|
26685
|
+
}
|
|
26686
|
+
}
|
|
26687
|
+
function acquireRotationLock(keysDir2) {
|
|
26688
|
+
const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
|
|
26689
|
+
const owner = randomBytes2(16).toString("hex");
|
|
26690
|
+
if (claimRotationLock(lock, owner)) return { lock, owner };
|
|
26691
|
+
let held;
|
|
26692
|
+
try {
|
|
26693
|
+
held = statSync(lock);
|
|
26694
|
+
} catch {
|
|
26695
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
26696
|
+
}
|
|
26697
|
+
if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
|
|
26698
|
+
const aside = `${lock}.stale.${owner}`;
|
|
26699
|
+
try {
|
|
26700
|
+
const now = statSync(lock);
|
|
26701
|
+
if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
|
|
26702
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
26703
|
+
}
|
|
26704
|
+
renameSync4(lock, aside);
|
|
26705
|
+
} catch (err) {
|
|
26706
|
+
if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
|
|
26707
|
+
throw new Error(ROTATION_IN_PROGRESS, { cause: err });
|
|
26708
|
+
}
|
|
26709
|
+
rmSync3(aside, { recursive: true, force: true });
|
|
26710
|
+
if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
|
|
26711
|
+
return { lock, owner };
|
|
26712
|
+
}
|
|
26713
|
+
function releaseRotationLock(lease) {
|
|
26714
|
+
try {
|
|
26715
|
+
if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
|
|
26716
|
+
} catch {
|
|
26717
|
+
return;
|
|
26718
|
+
}
|
|
26719
|
+
rmSync3(lease.lock, { recursive: true, force: true });
|
|
26720
|
+
}
|
|
26721
|
+
function withRotationLock(keysDir2, work) {
|
|
26722
|
+
ensureDataDirSync(keysDir2);
|
|
26723
|
+
const lease = acquireRotationLock(keysDir2);
|
|
26724
|
+
try {
|
|
26725
|
+
return work();
|
|
26726
|
+
} finally {
|
|
26727
|
+
releaseRotationLock(lease);
|
|
26728
|
+
}
|
|
26729
|
+
}
|
|
26730
|
+
var FileKeyProvider = class {
|
|
26731
|
+
#keysDir;
|
|
26732
|
+
constructor(keysDir2) {
|
|
26733
|
+
this.#keysDir = keysDir2;
|
|
26734
|
+
}
|
|
26735
|
+
get filePath() {
|
|
26736
|
+
return join6(this.#keysDir, VAULT_KEY_FILENAME);
|
|
26737
|
+
}
|
|
26738
|
+
loadOrCreate() {
|
|
26739
|
+
return asAsync(() => {
|
|
26740
|
+
const existing = this.#read();
|
|
26741
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
26742
|
+
tightenFileMode(this.filePath);
|
|
26743
|
+
return currentOf(existing);
|
|
26744
|
+
});
|
|
26745
|
+
}
|
|
26746
|
+
rotate() {
|
|
26747
|
+
return asAsync(
|
|
26748
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
26749
|
+
const existing = this.#read();
|
|
26750
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
26751
|
+
return currentOf(this.#write(withNextEpoch(existing)));
|
|
26752
|
+
})
|
|
26753
|
+
);
|
|
26754
|
+
}
|
|
26755
|
+
materialFor(version2) {
|
|
26756
|
+
return asAsync(() => {
|
|
26757
|
+
const existing = this.#read();
|
|
26758
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
26759
|
+
return epochOf(existing, version2);
|
|
26760
|
+
});
|
|
26761
|
+
}
|
|
26762
|
+
/** The keyring, or null when the file is ABSENT. A corrupt file throws. */
|
|
26763
|
+
#read() {
|
|
26764
|
+
let raw;
|
|
26765
|
+
try {
|
|
26766
|
+
raw = readFileSync4(this.filePath, "utf8");
|
|
26767
|
+
} catch (err) {
|
|
26768
|
+
if (err.code === "ENOENT") return null;
|
|
26769
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
26770
|
+
}
|
|
26771
|
+
return parseKeyring(raw);
|
|
26772
|
+
}
|
|
26773
|
+
/**
|
|
26774
|
+
* First mint: the keyring is created at its FINAL path with a
|
|
26775
|
+
* creation-exclusive write, so two processes racing a fresh machine cannot
|
|
26776
|
+
* each mint a different epoch 1 — with tmp + rename the loser's replace
|
|
26777
|
+
* would orphan everything the winner had already sealed. On EEXIST the
|
|
26778
|
+
* loser re-reads and adopts the winner's keyring; it minted nothing.
|
|
26779
|
+
* Atomic replace is unnecessary here: nothing can be mid-read of a file
|
|
26780
|
+
* that did not exist, and a torn exclusive write parses as corrupt on the
|
|
26781
|
+
* next read and fails secure rather than being re-minted over.
|
|
26782
|
+
*/
|
|
26783
|
+
#createExclusive() {
|
|
26784
|
+
ensureDataDirSync(this.#keysDir);
|
|
26785
|
+
const keyring = mintKeyring();
|
|
26786
|
+
try {
|
|
26787
|
+
writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
|
|
26788
|
+
`, {
|
|
26789
|
+
flag: "wx",
|
|
26790
|
+
mode: DATA_FILE_MODE
|
|
26791
|
+
});
|
|
26792
|
+
} catch (err) {
|
|
26793
|
+
if (err.code !== "EEXIST") throw asError(err);
|
|
26794
|
+
const winner = this.#read();
|
|
26795
|
+
if (!winner) {
|
|
26796
|
+
throw new Error("vault: key file vanished during first mint", { cause: err });
|
|
26797
|
+
}
|
|
26798
|
+
return winner;
|
|
26799
|
+
}
|
|
26800
|
+
tightenFileMode(this.filePath);
|
|
26801
|
+
return keyring;
|
|
26802
|
+
}
|
|
26803
|
+
/**
|
|
26804
|
+
* Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
|
|
26805
|
+
* Used only for rotation, under the rotation lock — first creation goes
|
|
26806
|
+
* through the creation-exclusive path instead.
|
|
26807
|
+
*/
|
|
26808
|
+
#write(keyring) {
|
|
26809
|
+
ensureDataDirSync(this.#keysDir);
|
|
26810
|
+
const file2 = this.filePath;
|
|
26811
|
+
const tmp = `${file2}.tmp`;
|
|
26812
|
+
writeFileSync2(tmp, `${serializeKeyring(keyring)}
|
|
26813
|
+
`, { mode: DATA_FILE_MODE });
|
|
26814
|
+
renameSync4(tmp, file2);
|
|
26815
|
+
tightenFileMode(file2);
|
|
26816
|
+
return keyring;
|
|
26817
|
+
}
|
|
26818
|
+
};
|
|
26819
|
+
function tightenFileMode(file2) {
|
|
26820
|
+
try {
|
|
26821
|
+
chmodSync2(file2, DATA_FILE_MODE);
|
|
26822
|
+
} catch {
|
|
26823
|
+
}
|
|
26824
|
+
}
|
|
26825
|
+
var runSecurity = (args) => execFileSync2("/usr/bin/security", args, {
|
|
26826
|
+
encoding: "utf8",
|
|
26827
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
26828
|
+
});
|
|
26829
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
26830
|
+
var KeychainKeyProvider = class {
|
|
26831
|
+
#keysDir;
|
|
26832
|
+
#exec;
|
|
26833
|
+
constructor(keysDir2, exec = runSecurity) {
|
|
26834
|
+
if (exec === runSecurity && process.platform !== "darwin") {
|
|
26835
|
+
throw new Error(
|
|
26836
|
+
`keychain custody is not available on this platform (${process.platform}); use file custody`
|
|
26837
|
+
);
|
|
26838
|
+
}
|
|
26839
|
+
this.#keysDir = keysDir2;
|
|
26840
|
+
this.#exec = exec;
|
|
26841
|
+
}
|
|
26842
|
+
/** Where a fallback file provider for the same vault would keep its keyring. */
|
|
26843
|
+
get keysDir() {
|
|
26844
|
+
return this.#keysDir;
|
|
26845
|
+
}
|
|
26846
|
+
loadOrCreate() {
|
|
26847
|
+
return asAsync(() => {
|
|
26848
|
+
const existing = this.#read();
|
|
26849
|
+
if (existing) return currentOf(existing);
|
|
26850
|
+
return currentOf(this.#create(mintKeyring()));
|
|
26851
|
+
});
|
|
26852
|
+
}
|
|
26853
|
+
rotate() {
|
|
26854
|
+
return asAsync(
|
|
26855
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
26856
|
+
const existing = this.#read();
|
|
26857
|
+
if (!existing) return currentOf(this.#create(mintKeyring()));
|
|
26858
|
+
return currentOf(this.#replace(withNextEpoch(existing)));
|
|
26859
|
+
})
|
|
26860
|
+
);
|
|
26861
|
+
}
|
|
26862
|
+
materialFor(version2) {
|
|
26863
|
+
return asAsync(() => {
|
|
26864
|
+
const existing = this.#read();
|
|
26865
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
26866
|
+
return epochOf(existing, version2);
|
|
26867
|
+
});
|
|
26868
|
+
}
|
|
26869
|
+
/** The keyring, or null when no item exists yet. A corrupt item throws. */
|
|
26870
|
+
#read() {
|
|
26871
|
+
let raw;
|
|
26872
|
+
try {
|
|
26873
|
+
raw = this.#exec([
|
|
26874
|
+
"find-generic-password",
|
|
26875
|
+
"-s",
|
|
26876
|
+
KEYCHAIN_SERVICE,
|
|
26877
|
+
"-a",
|
|
26878
|
+
KEYCHAIN_ACCOUNT,
|
|
26879
|
+
"-w"
|
|
26880
|
+
]);
|
|
26881
|
+
} catch (err) {
|
|
26882
|
+
if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
|
|
26883
|
+
throw new Error(
|
|
26884
|
+
`vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
|
|
26885
|
+
{ cause: err }
|
|
26886
|
+
);
|
|
26887
|
+
}
|
|
26888
|
+
const body = raw.trim();
|
|
26889
|
+
if (body.length === 0) return null;
|
|
26890
|
+
return parseKeyring(body);
|
|
26891
|
+
}
|
|
26892
|
+
/**
|
|
26893
|
+
* First mint: a plain `add-generic-password` (no `-U`) fails when an item
|
|
26894
|
+
* already exists, so a concurrent first mint cannot overwrite the winner's
|
|
26895
|
+
* keyring — the loser re-reads and adopts it instead.
|
|
26896
|
+
*/
|
|
26897
|
+
#create(keyring) {
|
|
26898
|
+
const args = [
|
|
26899
|
+
"add-generic-password",
|
|
26900
|
+
"-s",
|
|
26901
|
+
KEYCHAIN_SERVICE,
|
|
26902
|
+
"-a",
|
|
26903
|
+
KEYCHAIN_ACCOUNT,
|
|
26904
|
+
"-w",
|
|
26905
|
+
serializeKeyring(keyring)
|
|
26906
|
+
];
|
|
26907
|
+
try {
|
|
26908
|
+
this.#exec(args);
|
|
26909
|
+
} catch (err) {
|
|
26910
|
+
const winner = this.#read();
|
|
26911
|
+
if (winner) return winner;
|
|
26912
|
+
throw asError(err);
|
|
26913
|
+
}
|
|
26914
|
+
return keyring;
|
|
26915
|
+
}
|
|
26916
|
+
// `-U` updates the item in place, deliberately replacing the stored map with
|
|
26917
|
+
// one that contains it — used only for rotation, under the rotation lock.
|
|
26918
|
+
#replace(keyring) {
|
|
26919
|
+
this.#exec([
|
|
26920
|
+
"add-generic-password",
|
|
26921
|
+
"-U",
|
|
26922
|
+
"-s",
|
|
26923
|
+
KEYCHAIN_SERVICE,
|
|
26924
|
+
"-a",
|
|
26925
|
+
KEYCHAIN_ACCOUNT,
|
|
26926
|
+
"-w",
|
|
26927
|
+
serializeKeyring(keyring)
|
|
26928
|
+
]);
|
|
26929
|
+
return keyring;
|
|
26930
|
+
}
|
|
26931
|
+
};
|
|
26932
|
+
function createKeyProvider(custody, keysDir2) {
|
|
26933
|
+
if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
|
|
26934
|
+
return new FileKeyProvider(keysDir2);
|
|
26935
|
+
}
|
|
26936
|
+
|
|
26937
|
+
// ../packages/persistence/src/vault/vault.ts
|
|
26938
|
+
import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
|
|
26939
|
+
var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
|
|
26940
|
+
var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
|
|
26941
|
+
var VAULT_PURGE_POINTER_ID = "*";
|
|
26942
|
+
function parsePointer(token) {
|
|
26943
|
+
if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
|
|
26944
|
+
const body = token.slice("[[aka:".length, -"]]".length);
|
|
26945
|
+
const colon = body.indexOf(":");
|
|
26946
|
+
if (colon < 0) return null;
|
|
26947
|
+
const category = body.slice(0, colon);
|
|
26948
|
+
const [kv, id, tag] = body.slice(colon + 1).split(".");
|
|
26949
|
+
if (kv === void 0 || id === void 0 || tag === void 0) return null;
|
|
26950
|
+
try {
|
|
26951
|
+
const keyVersion = decodeKeyVersion(kv);
|
|
26952
|
+
const pointerId = base32Decode(id);
|
|
26953
|
+
const tagBytes = base32Decode(tag);
|
|
26954
|
+
if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
|
|
26955
|
+
return null;
|
|
26956
|
+
}
|
|
26957
|
+
return { category, keyVersion, pointerId, tag: tagBytes };
|
|
26958
|
+
} catch {
|
|
26959
|
+
return null;
|
|
26960
|
+
}
|
|
26961
|
+
}
|
|
26962
|
+
var SecretVault = class {
|
|
26963
|
+
#repo;
|
|
26964
|
+
#keys;
|
|
26965
|
+
#fingerprintKey;
|
|
26966
|
+
#isConsented;
|
|
26967
|
+
#verifyGrant;
|
|
26968
|
+
#now;
|
|
26969
|
+
constructor(deps) {
|
|
26970
|
+
this.#repo = deps.repo;
|
|
26971
|
+
this.#keys = deps.keys;
|
|
26972
|
+
this.#fingerprintKey = deps.fingerprintKey;
|
|
26973
|
+
this.#isConsented = deps.isConsented;
|
|
26974
|
+
this.#verifyGrant = deps.verifyGrant;
|
|
26975
|
+
this.#now = deps.now ?? (() => Date.now());
|
|
26976
|
+
}
|
|
26977
|
+
/**
|
|
26978
|
+
* Store a value and return the pointer that stands for it. The same value
|
|
26979
|
+
* always yields the same pointer on this machine — one row, one pointer id,
|
|
26980
|
+
* one category — which is what makes dedup and reuse counting work.
|
|
26981
|
+
*/
|
|
26982
|
+
async tokenize(raw, meta3) {
|
|
26983
|
+
if (!this.#isConsented()) return CONSENT_ABSENT;
|
|
26984
|
+
const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
|
|
26985
|
+
const existing = this.#repo.byValueFingerprint(valueFingerprint);
|
|
26986
|
+
const now = this.#now();
|
|
26987
|
+
if (existing) {
|
|
26988
|
+
this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
|
|
26989
|
+
return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
|
|
26990
|
+
}
|
|
26991
|
+
const { material, version: version2 } = await this.#keys.loadOrCreate();
|
|
26992
|
+
const subkeys = deriveSubkeys(material);
|
|
26993
|
+
const pointerId = randomBytes3(POINTER_ID_BYTES);
|
|
26994
|
+
const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
|
|
26995
|
+
const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
|
|
26996
|
+
const { row } = this.#repo.upsert(
|
|
26997
|
+
{
|
|
26998
|
+
pointerId: base32Encode(pointerId),
|
|
26999
|
+
valueFingerprint,
|
|
27000
|
+
fingerprintKeyVersion: this.#fingerprintKey.version,
|
|
27001
|
+
keyVersion: version2,
|
|
27002
|
+
// Recorded so the row stays OPENABLE if the wire-format constant ever
|
|
27003
|
+
// moves: it is part of this row's AEAD AAD. It is not a tag input —
|
|
27004
|
+
// tags are pinned to the constant on both sides.
|
|
27005
|
+
formatVersion: POINTER_FORMAT_VERSION,
|
|
27006
|
+
category: meta3.category,
|
|
27007
|
+
ruleId: meta3.ruleId,
|
|
27008
|
+
maskedMatch: meta3.maskedMatch,
|
|
27009
|
+
provider: meta3.provider,
|
|
27010
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
27011
|
+
nonce: sealed.nonce.toString("base64"),
|
|
27012
|
+
authTag: sealed.authTag.toString("base64")
|
|
27013
|
+
},
|
|
27014
|
+
now
|
|
27015
|
+
);
|
|
27016
|
+
return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
|
|
27017
|
+
}
|
|
27018
|
+
/**
|
|
27019
|
+
* Resolve a pointer back to its value, for a human or (with a grant) for the
|
|
27020
|
+
* model. Every call that gets as far as an identified row writes an audit row.
|
|
27021
|
+
*/
|
|
27022
|
+
async detokenize(token, opts) {
|
|
27023
|
+
const parsed = parsePointer(token);
|
|
27024
|
+
if (!parsed) return UNAVAILABLE;
|
|
27025
|
+
let signKey;
|
|
27026
|
+
try {
|
|
27027
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
27028
|
+
signKey = deriveSubkeys(epoch.material).sign;
|
|
27029
|
+
} catch {
|
|
27030
|
+
return UNAVAILABLE;
|
|
27031
|
+
}
|
|
27032
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
27033
|
+
return UNAVAILABLE;
|
|
27034
|
+
}
|
|
27035
|
+
const pointerId = base32Encode(parsed.pointerId);
|
|
27036
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
27037
|
+
if (!row) {
|
|
27038
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
27039
|
+
return UNAVAILABLE;
|
|
27040
|
+
}
|
|
27041
|
+
if (row.category !== parsed.category) return UNAVAILABLE;
|
|
27042
|
+
if (opts.target === "model") {
|
|
27043
|
+
const grantId = opts.grantId;
|
|
27044
|
+
const verify = this.#verifyGrant;
|
|
27045
|
+
if (verify === void 0 || grantId === void 0 || grantId === "") {
|
|
27046
|
+
this.#audit(pointerId, opts, "refused");
|
|
27047
|
+
return UNAVAILABLE;
|
|
27048
|
+
}
|
|
27049
|
+
let covered;
|
|
27050
|
+
try {
|
|
27051
|
+
covered = await verify(grantId, {
|
|
27052
|
+
ruleId: row.ruleId,
|
|
27053
|
+
valueFingerprint: row.valueFingerprint,
|
|
27054
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
27055
|
+
});
|
|
27056
|
+
} catch {
|
|
27057
|
+
covered = false;
|
|
27058
|
+
}
|
|
27059
|
+
if (!covered) {
|
|
27060
|
+
this.#audit(pointerId, opts, "refused");
|
|
27061
|
+
return UNAVAILABLE;
|
|
27062
|
+
}
|
|
27063
|
+
}
|
|
27064
|
+
let raw;
|
|
27065
|
+
try {
|
|
27066
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
27067
|
+
raw = open(
|
|
27068
|
+
deriveSubkeys(epoch.material).enc,
|
|
27069
|
+
{
|
|
27070
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
27071
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
27072
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
27073
|
+
},
|
|
27074
|
+
// Sealed under the ROW's epoch and format version. Rotation may have
|
|
27075
|
+
// moved the epoch past the one this token names, and a format bump may
|
|
27076
|
+
// have moved the constant past the generation this row was sealed
|
|
27077
|
+
// under — the AAD follows the row in both cases, never the token.
|
|
27078
|
+
bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
|
|
27079
|
+
);
|
|
27080
|
+
} catch {
|
|
27081
|
+
raw = null;
|
|
27082
|
+
}
|
|
27083
|
+
if (raw === null) {
|
|
27084
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
27085
|
+
return UNAVAILABLE;
|
|
27086
|
+
}
|
|
27087
|
+
this.#audit(pointerId, opts, "revealed");
|
|
27088
|
+
return raw;
|
|
27089
|
+
}
|
|
27090
|
+
/**
|
|
27091
|
+
* Owner-surface reveal by row id: the dashboard shows a row the owner can
|
|
27092
|
+
* already see and asks for its value. There is no wire token here to verify —
|
|
27093
|
+
* the tag exists to stop FORGED tokens arriving in untrusted text, and a row
|
|
27094
|
+
* id selected server-side from the owner's own store is not that — so this
|
|
27095
|
+
* loads the row directly, opens its ciphertext under the row's epoch, and
|
|
27096
|
+
* audits exactly like a human-target de-reference. Never callable with
|
|
27097
|
+
* target 'model': the wire-token path with its grant gate is the only road
|
|
27098
|
+
* raw travels toward the model.
|
|
27099
|
+
*/
|
|
27100
|
+
async revealEntry(pointerId, opts) {
|
|
27101
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
27102
|
+
if (!row) {
|
|
27103
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
27104
|
+
return UNAVAILABLE;
|
|
27105
|
+
}
|
|
27106
|
+
const raw = await this.#openRow(row);
|
|
27107
|
+
if (raw === null) {
|
|
27108
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
27109
|
+
return UNAVAILABLE;
|
|
27110
|
+
}
|
|
27111
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
|
|
27112
|
+
return raw;
|
|
27113
|
+
}
|
|
27114
|
+
/** Badge and listing data. No raw value, no fingerprint, and no audit row. */
|
|
27115
|
+
async describePointer(token) {
|
|
27116
|
+
const row = await this.#rowFor(token);
|
|
27117
|
+
if (!row) return null;
|
|
27118
|
+
return {
|
|
27119
|
+
category: row.category,
|
|
27120
|
+
...row.provider === void 0 ? {} : { provider: row.provider },
|
|
27121
|
+
maskedMatch: row.maskedMatch,
|
|
27122
|
+
occurrences: row.occurrenceCount,
|
|
27123
|
+
firstSeen: new Date(row.firstSeen).toISOString(),
|
|
27124
|
+
lastSeen: new Date(row.lastSeen).toISOString()
|
|
27125
|
+
};
|
|
27126
|
+
}
|
|
27127
|
+
/**
|
|
27128
|
+
* The raw-free row identity a reveal grant matches on. Deliberately not fed to
|
|
27129
|
+
* view surfaces: the keyed fingerprint is a correlation key and must not reach
|
|
27130
|
+
* a presentation layer.
|
|
27131
|
+
*/
|
|
27132
|
+
async resolvePointerIdentity(token) {
|
|
27133
|
+
const row = await this.#rowFor(token);
|
|
27134
|
+
if (!row) return null;
|
|
27135
|
+
return {
|
|
27136
|
+
ruleId: row.ruleId,
|
|
27137
|
+
valueFingerprint: row.valueFingerprint,
|
|
27138
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
27139
|
+
};
|
|
27140
|
+
}
|
|
27141
|
+
/**
|
|
27142
|
+
* Mint the next vault key epoch and re-encrypt every entry under it. Pointers
|
|
27143
|
+
* already emitted keep verifying: their tag is checked against the historical
|
|
27144
|
+
* epoch they name, which the key provider retains.
|
|
27145
|
+
*
|
|
27146
|
+
* Safe to interrupt — each row carries the epoch its ciphertext is sealed
|
|
27147
|
+
* under, so a half-finished pass leaves every row openable.
|
|
27148
|
+
*
|
|
27149
|
+
* The rotation lock covers only the keyring mint inside `rotate()`; the
|
|
27150
|
+
* re-seal pass below runs unlocked. Two concurrent rotations therefore
|
|
27151
|
+
* serialize on the keyring but interleave over the rows, so a slower pass can
|
|
27152
|
+
* re-seal a row back to an epoch a faster one already moved past, and
|
|
27153
|
+
* `reEncrypted` can double-count. No value is lost either way — every epoch is
|
|
27154
|
+
* retained and every row stays openable — but "after rotation every row sits
|
|
27155
|
+
* at the newest epoch" does not hold under concurrency. Holding the lock
|
|
27156
|
+
* across the whole pass requires an async-aware lock, since a callback that
|
|
27157
|
+
* awaits would release the lock at its first suspension.
|
|
27158
|
+
*/
|
|
27159
|
+
async rotateVaultKey() {
|
|
27160
|
+
const next = await this.#keys.rotate();
|
|
27161
|
+
const nextEnc = deriveSubkeys(next.material).enc;
|
|
27162
|
+
let reEncrypted = 0;
|
|
27163
|
+
for (const row of this.#repo.listAll()) {
|
|
27164
|
+
if (row.keyVersion === next.version) continue;
|
|
27165
|
+
const pointerId = base32Decode(row.pointerId);
|
|
27166
|
+
let raw;
|
|
27167
|
+
try {
|
|
27168
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
27169
|
+
raw = open(
|
|
27170
|
+
deriveSubkeys(epoch.material).enc,
|
|
27171
|
+
{
|
|
27172
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
27173
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
27174
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
27175
|
+
},
|
|
27176
|
+
bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
|
|
27177
|
+
);
|
|
27178
|
+
} catch {
|
|
27179
|
+
raw = null;
|
|
27180
|
+
}
|
|
27181
|
+
if (raw === null) continue;
|
|
27182
|
+
const sealed = seal(
|
|
27183
|
+
nextEnc,
|
|
27184
|
+
raw,
|
|
27185
|
+
bindingInput(next.version, pointerId, row.category, row.formatVersion),
|
|
27186
|
+
randomBytes3(NONCE_BYTES)
|
|
27187
|
+
);
|
|
27188
|
+
this.#repo.replaceCiphertext(row.pointerId, {
|
|
27189
|
+
keyVersion: next.version,
|
|
27190
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
27191
|
+
nonce: sealed.nonce.toString("base64"),
|
|
27192
|
+
authTag: sealed.authTag.toString("base64")
|
|
27193
|
+
});
|
|
27194
|
+
reEncrypted += 1;
|
|
27195
|
+
}
|
|
27196
|
+
return { version: next.version, reEncrypted };
|
|
27197
|
+
}
|
|
27198
|
+
/**
|
|
27199
|
+
* Re-key every entry's value fingerprint after the exception key rotates,
|
|
27200
|
+
* PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
|
|
27201
|
+
* because the raw values are gone — the vault still holds the values, so
|
|
27202
|
+
* determinism, dedup, and every outstanding pointer survive the rotation.
|
|
27203
|
+
*
|
|
27204
|
+
* Every fingerprint-key rotation must run this: a row left at the old epoch
|
|
27205
|
+
* still resolves, but the same value detected again fingerprints under the
|
|
27206
|
+
* NEW key, misses the dedup lookup, and mints a second row and a second
|
|
27207
|
+
* pointer — one value, two tokens in circulation.
|
|
27208
|
+
*
|
|
27209
|
+
* Per-row best-effort: a row that cannot open, or whose refreshed
|
|
27210
|
+
* fingerprint collides with a row already refreshed, is skipped rather than
|
|
27211
|
+
* aborting the pass — one damaged entry must not strand the re-key of every
|
|
27212
|
+
* other. A skipped row keeps resolving under its old fingerprint epoch.
|
|
27213
|
+
*/
|
|
27214
|
+
async refreshFingerprints(next) {
|
|
27215
|
+
let refreshed = 0;
|
|
27216
|
+
for (const row of this.#repo.listAll()) {
|
|
27217
|
+
try {
|
|
27218
|
+
const raw = await this.#openRow(row);
|
|
27219
|
+
if (raw === null) continue;
|
|
27220
|
+
this.#repo.refreshFingerprint(row.pointerId, {
|
|
27221
|
+
valueFingerprint: fingerprintValue(next, raw),
|
|
27222
|
+
fingerprintKeyVersion: next.version
|
|
27223
|
+
});
|
|
27224
|
+
refreshed += 1;
|
|
27225
|
+
} catch {
|
|
27226
|
+
continue;
|
|
27227
|
+
}
|
|
27228
|
+
}
|
|
27229
|
+
return refreshed;
|
|
27230
|
+
}
|
|
27231
|
+
/**
|
|
27232
|
+
* Destroy every entry, making all outstanding pointers permanently
|
|
27233
|
+
* unresolvable.
|
|
27234
|
+
*
|
|
27235
|
+
* The count comes from `purgeAll` rather than a separate `countEntries` —
|
|
27236
|
+
* `purgeAll` counts inside the same transaction that deletes, so the audit row
|
|
27237
|
+
* reports what was actually destroyed. Counting beforehand would let a
|
|
27238
|
+
* concurrent write land between the two statements and put a number in the
|
|
27239
|
+
* durable record that never matched reality.
|
|
27240
|
+
*/
|
|
27241
|
+
purgeVault() {
|
|
27242
|
+
const destroyed = this.#repo.purgeAll();
|
|
27243
|
+
this.#repo.recordDeref({
|
|
27244
|
+
id: randomUUID10(),
|
|
27245
|
+
pointerId: VAULT_PURGE_POINTER_ID,
|
|
27246
|
+
at: this.#now(),
|
|
27247
|
+
target: "human",
|
|
27248
|
+
reason: "purge",
|
|
27249
|
+
outcome: "unavailable",
|
|
27250
|
+
pointerCount: Math.max(destroyed, 1)
|
|
27251
|
+
});
|
|
27252
|
+
return destroyed;
|
|
27253
|
+
}
|
|
27254
|
+
// Sign under the epoch the token names — which for a re-detected value is the
|
|
27255
|
+
// epoch its row currently sits at rather than whatever is current.
|
|
27256
|
+
//
|
|
27257
|
+
// The row's format version is NOT a tag input. It binds the row's ciphertext
|
|
27258
|
+
// (it is part of the AEAD AAD, so an old row stays openable) but never the
|
|
27259
|
+
// wire tag, which verification checks against POINTER_FORMAT_VERSION without
|
|
27260
|
+
// knowing any row. Signing a token here under a row's own generation is what
|
|
27261
|
+
// would make the vault emit tokens it then refuses.
|
|
27262
|
+
async #emitToken(keyVersion, pointerIdB32, category) {
|
|
27263
|
+
const pointerId = base32Decode(pointerIdB32);
|
|
27264
|
+
const epoch = await this.#keys.materialFor(keyVersion);
|
|
27265
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
27266
|
+
return formatPointer(
|
|
27267
|
+
category,
|
|
27268
|
+
keyVersion,
|
|
27269
|
+
pointerId,
|
|
27270
|
+
signPointer(signKey, keyVersion, pointerId, category)
|
|
27271
|
+
);
|
|
27272
|
+
}
|
|
27273
|
+
async #openRow(row) {
|
|
27274
|
+
try {
|
|
27275
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
27276
|
+
return open(
|
|
27277
|
+
deriveSubkeys(epoch.material).enc,
|
|
27278
|
+
{
|
|
27279
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
27280
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
27281
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
27282
|
+
},
|
|
27283
|
+
bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
|
|
27284
|
+
);
|
|
27285
|
+
} catch {
|
|
27286
|
+
return null;
|
|
27287
|
+
}
|
|
27288
|
+
}
|
|
27289
|
+
// Shared lookup for the read-only surfaces. It verifies the tag exactly as
|
|
27290
|
+
// detokenize does: a descriptor is not raw, but a token nobody can vouch for
|
|
27291
|
+
// should not resolve to anything at all — otherwise a fabricated pointer, or a
|
|
27292
|
+
// lookalike planted in a file, would still yield a category and a masked
|
|
27293
|
+
// preview. Verifying needs the historical epoch's key, which is why these
|
|
27294
|
+
// surfaces are async.
|
|
27295
|
+
async #rowFor(token) {
|
|
27296
|
+
const parsed = parsePointer(token);
|
|
27297
|
+
if (!parsed) return null;
|
|
27298
|
+
try {
|
|
27299
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
27300
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
27301
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
27302
|
+
return null;
|
|
27303
|
+
}
|
|
27304
|
+
} catch {
|
|
27305
|
+
return null;
|
|
27306
|
+
}
|
|
27307
|
+
const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
|
|
27308
|
+
if (row?.category !== parsed.category) return null;
|
|
27309
|
+
return row;
|
|
27310
|
+
}
|
|
27311
|
+
#audit(pointerId, opts, outcome) {
|
|
27312
|
+
this.#repo.recordDeref({
|
|
27313
|
+
id: randomUUID10(),
|
|
27314
|
+
pointerId,
|
|
27315
|
+
at: this.#now(),
|
|
27316
|
+
target: opts.target,
|
|
27317
|
+
reason: opts.reason,
|
|
27318
|
+
outcome,
|
|
27319
|
+
...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
|
|
27320
|
+
// Only the batched reasons carry a count above one; a model crossing is
|
|
27321
|
+
// always its own row.
|
|
27322
|
+
pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
|
|
27323
|
+
});
|
|
27324
|
+
}
|
|
27325
|
+
};
|
|
27326
|
+
|
|
27327
|
+
// ../packages/persistence/src/warn-era-cap.ts
|
|
27328
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
27329
|
+
import { join as join7 } from "path";
|
|
25976
27330
|
|
|
25977
27331
|
// ../packages/plugin-sdk/src/config.ts
|
|
25978
27332
|
import { existsSync as existsSync5 } from "fs";
|
|
25979
|
-
import { join as
|
|
27333
|
+
import { join as join8 } from "path";
|
|
25980
27334
|
|
|
25981
27335
|
// ../packages/plugin-sdk/src/provider-env.ts
|
|
25982
27336
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -25997,9 +27351,9 @@ var providerEnvShape = {
|
|
|
25997
27351
|
var ProviderEnvSchema = external_exports.object(providerEnvShape);
|
|
25998
27352
|
|
|
25999
27353
|
// ../packages/plugin-sdk/src/config-inventory.ts
|
|
26000
|
-
import { readdirSync, readFileSync as
|
|
27354
|
+
import { readdirSync, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
|
|
26001
27355
|
import { homedir as homedir4 } from "os";
|
|
26002
|
-
import { basename as basename2, join as
|
|
27356
|
+
import { basename as basename2, join as join10 } from "path";
|
|
26003
27357
|
|
|
26004
27358
|
// ../rules/code-flaws/auth-jwt-no-verify.json
|
|
26005
27359
|
var auth_jwt_no_verify_default = {
|
|
@@ -28034,8 +29388,8 @@ function bundledDetections() {
|
|
|
28034
29388
|
}
|
|
28035
29389
|
|
|
28036
29390
|
// ../packages/plugin-sdk/src/repo.ts
|
|
28037
|
-
import { existsSync as existsSync6, readFileSync as
|
|
28038
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
29391
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
29392
|
+
import { basename, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
|
|
28039
29393
|
function resolveRepoIdentity(cwd) {
|
|
28040
29394
|
try {
|
|
28041
29395
|
const root = findGitRoot(cwd);
|
|
@@ -28073,36 +29427,36 @@ function resolveHeadRoot(cwd) {
|
|
|
28073
29427
|
function findGitRoot(start) {
|
|
28074
29428
|
let dir = start;
|
|
28075
29429
|
for (; ; ) {
|
|
28076
|
-
if (existsSync6(
|
|
29430
|
+
if (existsSync6(join9(dir, ".git"))) return dir;
|
|
28077
29431
|
const parent = dirname2(dir);
|
|
28078
29432
|
if (parent === dir) return void 0;
|
|
28079
29433
|
dir = parent;
|
|
28080
29434
|
}
|
|
28081
29435
|
}
|
|
28082
29436
|
function resolveGitContext(root) {
|
|
28083
|
-
const dotGit =
|
|
29437
|
+
const dotGit = join9(root, ".git");
|
|
28084
29438
|
try {
|
|
28085
|
-
if (
|
|
28086
|
-
return { configPath:
|
|
29439
|
+
if (statSync2(dotGit).isDirectory()) {
|
|
29440
|
+
return { configPath: join9(dotGit, "config"), headRoot: root };
|
|
28087
29441
|
}
|
|
28088
29442
|
} catch {
|
|
28089
29443
|
return void 0;
|
|
28090
29444
|
}
|
|
28091
29445
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
28092
29446
|
if (!target) return void 0;
|
|
28093
|
-
const gitdir = isAbsolute(target) ? target :
|
|
28094
|
-
if (existsSync6(
|
|
28095
|
-
return { configPath:
|
|
29447
|
+
const gitdir = isAbsolute(target) ? target : join9(root, target);
|
|
29448
|
+
if (existsSync6(join9(gitdir, "config"))) {
|
|
29449
|
+
return { configPath: join9(gitdir, "config"), headRoot: root };
|
|
28096
29450
|
}
|
|
28097
|
-
const commonRaw = safeRead(
|
|
29451
|
+
const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
|
|
28098
29452
|
if (!commonRaw) return void 0;
|
|
28099
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
29453
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
|
|
28100
29454
|
const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
|
|
28101
|
-
return { configPath:
|
|
29455
|
+
return { configPath: join9(commonGitDir, "config"), headRoot };
|
|
28102
29456
|
}
|
|
28103
29457
|
function safeRead(path) {
|
|
28104
29458
|
try {
|
|
28105
|
-
return
|
|
29459
|
+
return readFileSync5(path, "utf8");
|
|
28106
29460
|
} catch {
|
|
28107
29461
|
return void 0;
|
|
28108
29462
|
}
|
|
@@ -28140,14 +29494,14 @@ function slugFromUrl(url2) {
|
|
|
28140
29494
|
}
|
|
28141
29495
|
|
|
28142
29496
|
// ../packages/plugin-sdk/src/events.ts
|
|
28143
|
-
import { createHash as createHash4, randomUUID as
|
|
29497
|
+
import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
|
|
28144
29498
|
|
|
28145
29499
|
// ../packages/plugin-sdk/src/inventory-resolver.ts
|
|
28146
29500
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
28147
29501
|
|
|
28148
29502
|
// ../packages/plugin-sdk/src/nudge.ts
|
|
28149
|
-
import { mkdirSync as
|
|
28150
|
-
import { join as
|
|
29503
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
29504
|
+
import { join as join11 } from "path";
|
|
28151
29505
|
|
|
28152
29506
|
// ../packages/plugin-sdk/src/paths.ts
|
|
28153
29507
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -28185,8 +29539,8 @@ function resolveNonGitProject(startDir, recognizeMarker) {
|
|
|
28185
29539
|
|
|
28186
29540
|
// ../packages/plugin-sdk/src/project-files.ts
|
|
28187
29541
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
28188
|
-
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as
|
|
28189
|
-
import { basename as basename4, join as
|
|
29542
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
29543
|
+
import { basename as basename4, join as join12, relative, sep as sep4 } from "path";
|
|
28190
29544
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
28191
29545
|
".git",
|
|
28192
29546
|
"node_modules",
|
|
@@ -28206,7 +29560,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
28206
29560
|
var MAX_FILES = 2e4;
|
|
28207
29561
|
function readIgnoreLayer(dir) {
|
|
28208
29562
|
try {
|
|
28209
|
-
const content =
|
|
29563
|
+
const content = readFileSync8(join12(dir, ".gitignore"), "utf8");
|
|
28210
29564
|
return { base: dir, matcher: (0, import_ignore.default)().add(content) };
|
|
28211
29565
|
} catch {
|
|
28212
29566
|
return void 0;
|
|
@@ -28284,10 +29638,10 @@ function resolveProjectFiles(cwd) {
|
|
|
28284
29638
|
const layer = readIgnoreLayer(dir);
|
|
28285
29639
|
const dirLayers = layer ? [...layers, layer] : layers;
|
|
28286
29640
|
for (const entry of dirents) {
|
|
28287
|
-
const fullPath =
|
|
29641
|
+
const fullPath = join12(dir, entry.name);
|
|
28288
29642
|
if (entry.isDirectory()) {
|
|
28289
29643
|
if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
|
|
28290
|
-
if (existsSync7(
|
|
29644
|
+
if (existsSync7(join12(fullPath, ".git"))) continue;
|
|
28291
29645
|
if (visit3(fullPath, dirLayers)) return true;
|
|
28292
29646
|
continue;
|
|
28293
29647
|
}
|
|
@@ -28322,21 +29676,21 @@ function resolveProjectFiles(cwd) {
|
|
|
28322
29676
|
}
|
|
28323
29677
|
|
|
28324
29678
|
// ../packages/plugin-sdk/src/runtime.ts
|
|
28325
|
-
import { randomUUID as
|
|
29679
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
28326
29680
|
|
|
28327
29681
|
// ../packages/plugin-sdk/src/suppressions.ts
|
|
28328
29682
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
28329
29683
|
|
|
28330
29684
|
// ../packages/plugin-sdk/src/throttle.ts
|
|
28331
|
-
import { mkdirSync as
|
|
28332
|
-
import { join as
|
|
29685
|
+
import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
29686
|
+
import { join as join13 } from "path";
|
|
28333
29687
|
|
|
28334
29688
|
// ../packages/local-ops/src/egress-record.ts
|
|
28335
29689
|
function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
|
|
28336
29690
|
try {
|
|
28337
29691
|
if (!readWorkspaceSettings(base).dataSharesInPlace) return null;
|
|
28338
29692
|
const abs = resolve(target);
|
|
28339
|
-
const targetDir =
|
|
29693
|
+
const targetDir = statSync5(abs).isDirectory() ? abs : dirname4(abs);
|
|
28340
29694
|
const identity = resolveRepoIdentity(abs);
|
|
28341
29695
|
const worktreeRoot = resolveWorktreeRoot(abs);
|
|
28342
29696
|
let root;
|
|
@@ -28377,9 +29731,9 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
|
|
|
28377
29731
|
}
|
|
28378
29732
|
|
|
28379
29733
|
// ../packages/local-ops/src/fs-scan.ts
|
|
28380
|
-
import { createHash as createHash5, randomUUID as
|
|
28381
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
28382
|
-
import { basename as basename5, extname, join as
|
|
29734
|
+
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
29735
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
29736
|
+
import { basename as basename5, extname, join as join14, relative as relative3, resolve as resolve2 } from "path";
|
|
28383
29737
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
28384
29738
|
var AKAIGNORE_FILENAME = ".akaignore";
|
|
28385
29739
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
@@ -28395,7 +29749,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
28395
29749
|
var MAX_BYTES = 1e6;
|
|
28396
29750
|
function readIgnoreLayer2(dir, filename) {
|
|
28397
29751
|
try {
|
|
28398
|
-
const content =
|
|
29752
|
+
const content = readFileSync9(join14(dir, filename), "utf8");
|
|
28399
29753
|
return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
|
|
28400
29754
|
} catch {
|
|
28401
29755
|
return void 0;
|
|
@@ -28414,7 +29768,7 @@ function evaluate(layers, absPath, isDir) {
|
|
|
28414
29768
|
function* collectFiles(target) {
|
|
28415
29769
|
let st;
|
|
28416
29770
|
try {
|
|
28417
|
-
st =
|
|
29771
|
+
st = statSync6(target);
|
|
28418
29772
|
} catch {
|
|
28419
29773
|
return;
|
|
28420
29774
|
}
|
|
@@ -28431,7 +29785,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
|
|
|
28431
29785
|
const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
|
|
28432
29786
|
const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
|
|
28433
29787
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
28434
|
-
const path =
|
|
29788
|
+
const path = join14(dir, entry.name);
|
|
28435
29789
|
if (entry.isDirectory()) {
|
|
28436
29790
|
const skipState = evaluate(dirSkipLayers, path, true);
|
|
28437
29791
|
if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
|
|
@@ -28443,7 +29797,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
|
|
|
28443
29797
|
if (evaluate(dirSkipLayers, path, false) === "ignored") continue;
|
|
28444
29798
|
let size;
|
|
28445
29799
|
try {
|
|
28446
|
-
size =
|
|
29800
|
+
size = statSync6(path).size;
|
|
28447
29801
|
} catch {
|
|
28448
29802
|
continue;
|
|
28449
29803
|
}
|
|
@@ -28484,16 +29838,17 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
28484
29838
|
for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
|
|
28485
29839
|
let text;
|
|
28486
29840
|
try {
|
|
28487
|
-
text =
|
|
29841
|
+
text = readFileSync9(file2, "utf8");
|
|
28488
29842
|
} catch {
|
|
28489
29843
|
continue;
|
|
28490
29844
|
}
|
|
28491
29845
|
scanned++;
|
|
28492
29846
|
const fileEgress = extractFileEgress(file2, text);
|
|
28493
29847
|
if (fileEgress) egressFiles.push(fileEgress);
|
|
28494
|
-
const
|
|
29848
|
+
const shielded = shieldPointers(text);
|
|
29849
|
+
const matches = dropShieldedFindings(scan(shielded.text, opts.rules), shielded.spans);
|
|
28495
29850
|
if (matches.length === 0) continue;
|
|
28496
|
-
const eventId =
|
|
29851
|
+
const eventId = randomUUID13();
|
|
28497
29852
|
const metadata = { filePath: file2 };
|
|
28498
29853
|
if (gitignored) metadata.gitignored = true;
|
|
28499
29854
|
const event = {
|
|
@@ -28511,7 +29866,7 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
28511
29866
|
const key = resolveFingerprintKey();
|
|
28512
29867
|
const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
|
|
28513
29868
|
return {
|
|
28514
|
-
id:
|
|
29869
|
+
id: randomUUID13(),
|
|
28515
29870
|
eventId,
|
|
28516
29871
|
ruleId: m.ruleId,
|
|
28517
29872
|
category: m.category,
|
|
@@ -28536,12 +29891,12 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
28536
29891
|
}
|
|
28537
29892
|
|
|
28538
29893
|
// ../packages/local-ops/src/project-inventory.ts
|
|
28539
|
-
import { statSync as
|
|
29894
|
+
import { statSync as statSync7 } from "fs";
|
|
28540
29895
|
import { resolve as resolve3 } from "path";
|
|
28541
29896
|
function recordProjectInventory(db, target) {
|
|
28542
29897
|
try {
|
|
28543
29898
|
const abs = resolve3(target);
|
|
28544
|
-
|
|
29899
|
+
statSync7(abs);
|
|
28545
29900
|
const identity = resolveRepoIdentity(abs);
|
|
28546
29901
|
if (!identity) return null;
|
|
28547
29902
|
const sourceProjectId2 = db.sourceProject.upsert({
|
|
@@ -28590,17 +29945,17 @@ function reinvokeArgv(subcommand, extraArgs = []) {
|
|
|
28590
29945
|
|
|
28591
29946
|
// ../packages/local-ops/src/update-cache.ts
|
|
28592
29947
|
import { spawn } from "child_process";
|
|
28593
|
-
import { existsSync as existsSync8, readFileSync as
|
|
28594
|
-
import { join as
|
|
29948
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
29949
|
+
import { join as join15 } from "path";
|
|
28595
29950
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
28596
29951
|
function cachePath(home) {
|
|
28597
|
-
return
|
|
29952
|
+
return join15(dataDir(home), "update-check.json");
|
|
28598
29953
|
}
|
|
28599
29954
|
function readCache(home) {
|
|
28600
29955
|
const path = cachePath(home);
|
|
28601
29956
|
if (!existsSync8(path)) return null;
|
|
28602
29957
|
try {
|
|
28603
|
-
const raw = JSON.parse(
|
|
29958
|
+
const raw = JSON.parse(readFileSync10(path, "utf8"));
|
|
28604
29959
|
if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
|
|
28605
29960
|
return raw;
|
|
28606
29961
|
} catch {
|
|
@@ -28610,7 +29965,7 @@ function readCache(home) {
|
|
|
28610
29965
|
function writeCache(home, cache) {
|
|
28611
29966
|
if (!existsSync8(dataDir(home))) return;
|
|
28612
29967
|
try {
|
|
28613
|
-
|
|
29968
|
+
writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
|
|
28614
29969
|
`);
|
|
28615
29970
|
} catch {
|
|
28616
29971
|
}
|
|
@@ -28620,7 +29975,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
|
|
|
28620
29975
|
}
|
|
28621
29976
|
function clearCache(home) {
|
|
28622
29977
|
try {
|
|
28623
|
-
|
|
29978
|
+
rmSync4(cachePath(home), { force: true });
|
|
28624
29979
|
} catch {
|
|
28625
29980
|
}
|
|
28626
29981
|
}
|
|
@@ -28742,6 +30097,12 @@ var COMMAND_SPECS = [
|
|
|
28742
30097
|
summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
|
|
28743
30098
|
args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
|
|
28744
30099
|
},
|
|
30100
|
+
{
|
|
30101
|
+
name: "vault",
|
|
30102
|
+
argHint: "[show]",
|
|
30103
|
+
summary: "Reveal a vaulted secret by its pointer (every reveal is audited)",
|
|
30104
|
+
args: ["show"]
|
|
30105
|
+
},
|
|
28745
30106
|
{
|
|
28746
30107
|
name: "tui",
|
|
28747
30108
|
argHint: "[view]",
|
|
@@ -28961,7 +30322,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
28961
30322
|
import { existsSync as existsSync10 } from "fs";
|
|
28962
30323
|
import { createRequire as createRequire2 } from "module";
|
|
28963
30324
|
import { createServer } from "net";
|
|
28964
|
-
import { dirname as dirname5, join as
|
|
30325
|
+
import { dirname as dirname5, join as join16 } from "path";
|
|
28965
30326
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
28966
30327
|
import { parseArgs as parseArgs2 } from "util";
|
|
28967
30328
|
|
|
@@ -28982,7 +30343,7 @@ var require3 = createRequire2(import.meta.url);
|
|
|
28982
30343
|
var here = dirname5(fileURLToPath2(import.meta.url));
|
|
28983
30344
|
var BIND_HOST = "127.0.0.1";
|
|
28984
30345
|
function pkgBase() {
|
|
28985
|
-
return isSea() ? dirname5(process.execPath) :
|
|
30346
|
+
return isSea() ? dirname5(process.execPath) : join16(here, "..");
|
|
28986
30347
|
}
|
|
28987
30348
|
function isPortFree(port) {
|
|
28988
30349
|
return new Promise((resolve4) => {
|
|
@@ -29036,8 +30397,8 @@ async function runDashboard(argv) {
|
|
|
29036
30397
|
refreshUpdateMirror(dataDir());
|
|
29037
30398
|
const base = pkgBase();
|
|
29038
30399
|
const bundledServer = [
|
|
29039
|
-
|
|
29040
|
-
|
|
30400
|
+
join16(base, "web-ui", "server.js"),
|
|
30401
|
+
join16(base, "web-ui", "web-ui", "server.js")
|
|
29041
30402
|
].find((p) => existsSync10(p));
|
|
29042
30403
|
if (bundledServer) {
|
|
29043
30404
|
launchStandalone(bundledServer, port, shouldOpen, url2);
|
|
@@ -29057,7 +30418,7 @@ async function runDashboard(argv) {
|
|
|
29057
30418
|
onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
|
|
29058
30419
|
`);
|
|
29059
30420
|
};
|
|
29060
|
-
if (existsSync10(
|
|
30421
|
+
if (existsSync10(join16(webUiDir, ".next", "BUILD_ID"))) {
|
|
29061
30422
|
startNext();
|
|
29062
30423
|
return;
|
|
29063
30424
|
}
|
|
@@ -29395,7 +30756,7 @@ var SCOPE_REQUIRED = `a scope is required \u2014 pick exactly one (there is no d
|
|
|
29395
30756
|
--for <duration> time-limited: 30m, 1h, 24h (max 24h)
|
|
29396
30757
|
--permanent until revoked (typed confirmation required)`;
|
|
29397
30758
|
var VERB_HELP = {
|
|
29398
|
-
approve: `Usage: aka exception approve [reference|value] [flags]
|
|
30759
|
+
approve: `Usage: aka exception approve [reference|value|pointer] [flags]
|
|
29399
30760
|
|
|
29400
30761
|
Grant an exception from a detection blocked in the last 30 minutes. Select the
|
|
29401
30762
|
block by the reference from the block message, the masked value shown there,
|
|
@@ -29403,13 +30764,21 @@ or by pasting the blocked value itself. A pasted value is matched by its keyed
|
|
|
29403
30764
|
fingerprint and never stored or echoed \u2014 but it does land in your shell
|
|
29404
30765
|
history, so prefer the reference where that matters.
|
|
29405
30766
|
|
|
30767
|
+
A vault pointer ([[aka:...]] \u2014 quote it in the shell) selects the vaulted
|
|
30768
|
+
value behind it and requires --reveal: the grant lets the model receive that
|
|
30769
|
+
value's RAW form at tool boundaries while it is active, with every crossing
|
|
30770
|
+
audited. Revoke any time with 'aka exception revoke <id>'.
|
|
30771
|
+
|
|
29406
30772
|
Flags:
|
|
30773
|
+
--reveal-to-model grant reveal-to-model instead of suppression (required with a
|
|
30774
|
+
pointer selector; --reveal is accepted as shorthand)
|
|
29407
30775
|
--once | --for <30m|1h|24h> | --permanent scope (required, pick one)
|
|
29408
30776
|
--reason "<why>" required; prompted on a terminal if omitted
|
|
29409
30777
|
--yes skip confirmations (non-interactive use)
|
|
29410
30778
|
--home <dir> alternate AKA home (default: ~/.aka)
|
|
29411
30779
|
|
|
29412
30780
|
Example: aka exception approve 3f2a --for 1h --reason "temp deploy creds"
|
|
30781
|
+
Example: aka exception approve '[[aka:secret:...]]' --reveal-to-model --for 1h --reason "agent needs the live key"
|
|
29413
30782
|
`,
|
|
29414
30783
|
add: `Usage: aka exception add --rule <ruleId> [--stdin] [flags]
|
|
29415
30784
|
|
|
@@ -29596,6 +30965,20 @@ ${keyAccessHint(err, dir)}`, {
|
|
|
29596
30965
|
});
|
|
29597
30966
|
}
|
|
29598
30967
|
}
|
|
30968
|
+
function grantFieldsFrom(entry, key) {
|
|
30969
|
+
if (!isCurrentKeyVersion(key, entry.keyVersion)) {
|
|
30970
|
+
throw new Error(
|
|
30971
|
+
key === null ? `cannot approve ${entry.reference}: the fingerprint key file is missing, so the fingerprint recorded for it can no longer be matched \u2014 trigger the detection again` : `cannot approve ${entry.reference}: it was blocked under fingerprint key v${String(entry.keyVersion)} and the key is now v${String(key.version)}, so a grant from it could never match \u2014 trigger the detection again`
|
|
30972
|
+
);
|
|
30973
|
+
}
|
|
30974
|
+
return {
|
|
30975
|
+
ruleId: entry.ruleId,
|
|
30976
|
+
category: entry.category,
|
|
30977
|
+
valueFingerprint: entry.valueFingerprint,
|
|
30978
|
+
keyVersion: entry.keyVersion,
|
|
30979
|
+
maskedValue: entry.maskedValue
|
|
30980
|
+
};
|
|
30981
|
+
}
|
|
29599
30982
|
async function pickBlocked(entries, selector, io, key) {
|
|
29600
30983
|
if (selector !== void 0) {
|
|
29601
30984
|
const matches = entries.filter(
|
|
@@ -29654,20 +31037,90 @@ async function pickBlocked(entries, selector, io, key) {
|
|
|
29654
31037
|
const answer = await io.ask(`Which one? [1-${String(entries.length)}]: `);
|
|
29655
31038
|
const index = Number.parseInt(answer.trim(), 10);
|
|
29656
31039
|
const picked = Number.isInteger(index) ? entries[index - 1] : void 0;
|
|
29657
|
-
if (!picked)
|
|
31040
|
+
if (!picked)
|
|
31041
|
+
throw new Error(`invalid choice \u2014 enter a number between 1 and ${String(entries.length)}`);
|
|
29658
31042
|
return picked;
|
|
29659
31043
|
}
|
|
31044
|
+
function pointerCategory(pointer) {
|
|
31045
|
+
const body = pointer.slice("[[aka:".length);
|
|
31046
|
+
return DetectionCategory.parse(body.slice(0, body.indexOf(":")));
|
|
31047
|
+
}
|
|
31048
|
+
async function runApproveReveal(pointer, values, io, base) {
|
|
31049
|
+
const dir = dataDir(base);
|
|
31050
|
+
const db = openLocalDatabase(dir);
|
|
31051
|
+
try {
|
|
31052
|
+
const vault = new SecretVault({
|
|
31053
|
+
repo: db.secretVault,
|
|
31054
|
+
keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
|
|
31055
|
+
fingerprintKey: loadOrCreateFingerprintKey(dir),
|
|
31056
|
+
// Read live so a consent revocation applies to the very next call.
|
|
31057
|
+
isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
|
|
31058
|
+
});
|
|
31059
|
+
const identity = await vault.resolvePointerIdentity(pointer);
|
|
31060
|
+
if (identity === null) {
|
|
31061
|
+
throw new Error(
|
|
31062
|
+
"the pointer is unknown here \u2014 not one this machine issued (forged or tampered), its vault entry was purged, or the key material is unavailable; nothing was granted"
|
|
31063
|
+
);
|
|
31064
|
+
}
|
|
31065
|
+
const descriptor = await vault.describePointer(pointer);
|
|
31066
|
+
const maskedValue = descriptor?.maskedMatch ?? "\xB7\xB7\xB7";
|
|
31067
|
+
const { scope, reason } = await resolveScopeAndReason(values, io);
|
|
31068
|
+
if (scope.scope === "permanent") {
|
|
31069
|
+
await confirmPermanent(io, maskedValue, values.yes === true);
|
|
31070
|
+
}
|
|
31071
|
+
const granted = await createGrant(db, {
|
|
31072
|
+
ruleId: identity.ruleId,
|
|
31073
|
+
category: descriptor?.category ?? pointerCategory(pointer),
|
|
31074
|
+
valueFingerprint: identity.valueFingerprint,
|
|
31075
|
+
keyVersion: identity.fingerprintKeyVersion,
|
|
31076
|
+
maskedValue,
|
|
31077
|
+
capability: "reveal_to_model",
|
|
31078
|
+
...scope,
|
|
31079
|
+
justification: reason,
|
|
31080
|
+
conditions: null,
|
|
31081
|
+
createdBy: resolveCreatedBy(),
|
|
31082
|
+
createdVia: "cli-approve"
|
|
31083
|
+
});
|
|
31084
|
+
io.out(
|
|
31085
|
+
`Reveal grant ${shortId(granted.id)}: ${granted.ruleId} ${granted.maskedValue} ${scopeSummary(granted)}
|
|
31086
|
+
While this grant is active the model can receive this value's RAW form at
|
|
31087
|
+
tool boundaries. Every crossing is audited. Revoke with: aka exception revoke ${shortId(granted.id)}
|
|
31088
|
+
`
|
|
31089
|
+
);
|
|
31090
|
+
} finally {
|
|
31091
|
+
db.close();
|
|
31092
|
+
}
|
|
31093
|
+
}
|
|
29660
31094
|
async function runApprove(argv, io) {
|
|
29661
31095
|
const { values, positionals } = parseArgs4({
|
|
29662
31096
|
args: argv,
|
|
29663
|
-
options: {
|
|
31097
|
+
options: {
|
|
31098
|
+
...HOME_OPTION,
|
|
31099
|
+
...SCOPE_OPTIONS,
|
|
31100
|
+
"reveal-to-model": { type: "boolean" },
|
|
31101
|
+
// Shorthand for --reveal-to-model.
|
|
31102
|
+
reveal: { type: "boolean" }
|
|
31103
|
+
},
|
|
29664
31104
|
allowPositionals: true
|
|
29665
31105
|
});
|
|
29666
31106
|
if (values.help) {
|
|
29667
31107
|
io.out(VERB_HELP.approve ?? "");
|
|
29668
31108
|
return;
|
|
29669
31109
|
}
|
|
29670
|
-
const
|
|
31110
|
+
const base = homeBase(values.home);
|
|
31111
|
+
const wantsReveal = values["reveal-to-model"] === true || values.reveal === true;
|
|
31112
|
+
const candidate = positionals[0]?.trim();
|
|
31113
|
+
const pointer = candidate === void 0 ? void 0 : PointerToken.safeParse(candidate);
|
|
31114
|
+
if (pointer?.success === true) {
|
|
31115
|
+
if (!wantsReveal) {
|
|
31116
|
+
throw new Error(
|
|
31117
|
+
"that selector is a vault pointer \u2014 approving one mints a reveal-to-model grant, which must be asked for explicitly: add --reveal-to-model"
|
|
31118
|
+
);
|
|
31119
|
+
}
|
|
31120
|
+
await runApproveReveal(pointer.data, values, io, base);
|
|
31121
|
+
return;
|
|
31122
|
+
}
|
|
31123
|
+
const dir = dataDir(base);
|
|
29671
31124
|
const db = openLocalDatabase(dir);
|
|
29672
31125
|
try {
|
|
29673
31126
|
const entries = await db.exceptions.recentBlocked();
|
|
@@ -29679,24 +31132,16 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
|
|
|
29679
31132
|
);
|
|
29680
31133
|
return;
|
|
29681
31134
|
}
|
|
29682
|
-
const selector = positionals[0]?.trim();
|
|
29683
31135
|
const key = readKeyForApprove(dir);
|
|
29684
|
-
const entry = await pickBlocked(entries,
|
|
29685
|
-
|
|
29686
|
-
throw new Error(
|
|
29687
|
-
key === null ? `cannot approve ${entry.reference}: the fingerprint key file is missing, so the fingerprint recorded for it can no longer be matched \u2014 trigger the detection again` : `cannot approve ${entry.reference}: it was blocked under fingerprint key v${String(entry.keyVersion)} and the key is now v${String(key.version)}, so a grant from it could never match \u2014 trigger the detection again`
|
|
29688
|
-
);
|
|
29689
|
-
}
|
|
31136
|
+
const entry = await pickBlocked(entries, candidate === "" ? void 0 : candidate, io, key);
|
|
31137
|
+
const grant = grantFieldsFrom(entry, key);
|
|
29690
31138
|
const { scope, reason } = await resolveScopeAndReason(values, io);
|
|
29691
31139
|
if (scope.scope === "permanent") {
|
|
29692
31140
|
await confirmPermanent(io, entry.maskedValue, values.yes === true);
|
|
29693
31141
|
}
|
|
29694
31142
|
const granted = await createGrant(db, {
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
valueFingerprint: entry.valueFingerprint,
|
|
29698
|
-
keyVersion: entry.keyVersion,
|
|
29699
|
-
maskedValue: entry.maskedValue,
|
|
31143
|
+
...grant,
|
|
31144
|
+
...wantsReveal ? { capability: "reveal_to_model" } : {},
|
|
29700
31145
|
...scope,
|
|
29701
31146
|
justification: reason,
|
|
29702
31147
|
conditions: null,
|
|
@@ -29704,6 +31149,11 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
|
|
|
29704
31149
|
createdVia: "cli-approve"
|
|
29705
31150
|
});
|
|
29706
31151
|
printGranted(io, granted);
|
|
31152
|
+
if (wantsReveal) {
|
|
31153
|
+
io.out(
|
|
31154
|
+
"While this grant is active the model can receive this value in RAW form at tool boundaries; every crossing is audited.\n"
|
|
31155
|
+
);
|
|
31156
|
+
}
|
|
29707
31157
|
io.out("Resubmit the blocked prompt \u2014 it will now pass.\n");
|
|
29708
31158
|
} finally {
|
|
29709
31159
|
db.close();
|
|
@@ -29840,7 +31290,11 @@ Just got blocked? Run 'aka exception approve'.
|
|
|
29840
31290
|
const row = [
|
|
29841
31291
|
shortId(ex.id),
|
|
29842
31292
|
ex.ruleId,
|
|
29843
|
-
|
|
31293
|
+
// A reveal grant is strictly stronger than a plain suppression: while
|
|
31294
|
+
// it is active the model can receive this value's RAW form at tool
|
|
31295
|
+
// boundaries. Tag the row so it reads differently from a suppress-only
|
|
31296
|
+
// grant. The value stays masked — the list never shows raw values.
|
|
31297
|
+
ex.capability === "reveal_to_model" ? `${ex.maskedValue} \xB7 REVEALS-TO-MODEL` : ex.maskedValue,
|
|
29844
31298
|
ex.scope,
|
|
29845
31299
|
// An expiry countdown is meaningless once the budget/revocation ended
|
|
29846
31300
|
// the grant ("consumed · expires in 24m" reads as a contradiction);
|
|
@@ -29994,19 +31448,39 @@ written under the old key simply stop matching. They remain listed for audit.
|
|
|
29994
31448
|
return;
|
|
29995
31449
|
}
|
|
29996
31450
|
}
|
|
29997
|
-
let
|
|
31451
|
+
let next;
|
|
29998
31452
|
try {
|
|
29999
|
-
|
|
31453
|
+
next = rotateFingerprintKey(dir);
|
|
30000
31454
|
} catch (err) {
|
|
30001
31455
|
const message = err instanceof Error ? err.message : String(err);
|
|
30002
31456
|
throw new Error(`cannot rotate: ${message}
|
|
30003
31457
|
${keyAccessHint(err, dir)}`, { cause: err });
|
|
30004
31458
|
}
|
|
30005
31459
|
io.out(
|
|
30006
|
-
`Fingerprint key rotated \u2014 new key version v${String(
|
|
31460
|
+
`Fingerprint key rotated \u2014 new key version v${String(next.version)}.
|
|
30007
31461
|
Existing grants no longer match; re-approve deliberately where still needed.
|
|
30008
31462
|
`
|
|
30009
31463
|
);
|
|
31464
|
+
try {
|
|
31465
|
+
const base = homeBase(values.home);
|
|
31466
|
+
const vault = new SecretVault({
|
|
31467
|
+
repo: db.secretVault,
|
|
31468
|
+
keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
|
|
31469
|
+
fingerprintKey: next,
|
|
31470
|
+
isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
|
|
31471
|
+
});
|
|
31472
|
+
const refreshed = await vault.refreshFingerprints(next);
|
|
31473
|
+
if (refreshed > 0) {
|
|
31474
|
+
io.out(
|
|
31475
|
+
`Vault re-keyed \u2014 ${String(refreshed)} stored ${refreshed === 1 ? "value" : "values"} follow the new key; pointers are unchanged.
|
|
31476
|
+
`
|
|
31477
|
+
);
|
|
31478
|
+
}
|
|
31479
|
+
} catch {
|
|
31480
|
+
io.out(
|
|
31481
|
+
"Warning: the vault could not be re-keyed under the new fingerprint key.\nStored values still resolve, but re-detections may mint new pointers until it succeeds.\n"
|
|
31482
|
+
);
|
|
31483
|
+
}
|
|
30010
31484
|
} finally {
|
|
30011
31485
|
db.close();
|
|
30012
31486
|
}
|
|
@@ -30034,8 +31508,8 @@ async function runException(argv, io = terminalPrompter()) {
|
|
|
30034
31508
|
}
|
|
30035
31509
|
|
|
30036
31510
|
// src/commands/init.ts
|
|
30037
|
-
import { existsSync as existsSync12, statSync as
|
|
30038
|
-
import { join as
|
|
31511
|
+
import { existsSync as existsSync12, statSync as statSync8 } from "fs";
|
|
31512
|
+
import { join as join17 } from "path";
|
|
30039
31513
|
import * as readline from "readline/promises";
|
|
30040
31514
|
import { parseArgs as parseArgs6 } from "util";
|
|
30041
31515
|
|
|
@@ -30147,12 +31621,12 @@ function looseStorePaths(home) {
|
|
|
30147
31621
|
home,
|
|
30148
31622
|
settingsDir(home),
|
|
30149
31623
|
dataDir(home),
|
|
30150
|
-
|
|
31624
|
+
join17(settingsDir(home), "settings.json"),
|
|
30151
31625
|
dbPath(home)
|
|
30152
31626
|
];
|
|
30153
31627
|
return targets.filter((p) => {
|
|
30154
31628
|
try {
|
|
30155
|
-
return (
|
|
31629
|
+
return (statSync8(p).mode & 63) !== 0;
|
|
30156
31630
|
} catch {
|
|
30157
31631
|
return false;
|
|
30158
31632
|
}
|
|
@@ -30167,7 +31641,7 @@ async function runInit(argv) {
|
|
|
30167
31641
|
ensureLayoutDirSync(home);
|
|
30168
31642
|
const settings = settingsDir(home);
|
|
30169
31643
|
ensureLayoutDirSync(settings);
|
|
30170
|
-
const settingsFile =
|
|
31644
|
+
const settingsFile = join17(settings, "settings.json");
|
|
30171
31645
|
const settingsCreated = !existsSync12(settingsFile);
|
|
30172
31646
|
if (settingsCreated) {
|
|
30173
31647
|
writeOwnerOnlyFileSync(
|
|
@@ -30242,7 +31716,7 @@ async function confirm(question) {
|
|
|
30242
31716
|
}
|
|
30243
31717
|
|
|
30244
31718
|
// src/commands/scan.ts
|
|
30245
|
-
import { statSync as
|
|
31719
|
+
import { statSync as statSync9 } from "fs";
|
|
30246
31720
|
import { parseArgs as parseArgs7 } from "util";
|
|
30247
31721
|
function severityRank(s) {
|
|
30248
31722
|
return Severity.options.indexOf(s);
|
|
@@ -30289,7 +31763,7 @@ function runScan(argv) {
|
|
|
30289
31763
|
failOn = parsed.data;
|
|
30290
31764
|
}
|
|
30291
31765
|
try {
|
|
30292
|
-
|
|
31766
|
+
statSync9(target);
|
|
30293
31767
|
} catch {
|
|
30294
31768
|
process.stderr.write(`aka scan: no such file or directory: ${target}
|
|
30295
31769
|
`);
|
|
@@ -31196,6 +32670,92 @@ function applyPluginUpdate2(status) {
|
|
|
31196
32670
|
return ok;
|
|
31197
32671
|
}
|
|
31198
32672
|
|
|
32673
|
+
// src/commands/vault.ts
|
|
32674
|
+
import { parseArgs as parseArgs11 } from "util";
|
|
32675
|
+
var VAULT_HELP = `Reveal a vaulted secret by its pointer (every reveal is audited).
|
|
32676
|
+
|
|
32677
|
+
Resolve a pointer: aka vault show '[[aka:...]]' prints the raw value
|
|
32678
|
+
|
|
32679
|
+
The pointer is the [[aka:...]] token that replaced a detected value. Quote it \u2014
|
|
32680
|
+
the double brackets are shell syntax otherwise. Each successful reveal writes an
|
|
32681
|
+
audit row (reason: explicit-reveal) to the local store.
|
|
32682
|
+
|
|
32683
|
+
Flags:
|
|
32684
|
+
--home <dir> alternate AKA home (default: ~/.aka)
|
|
32685
|
+
`;
|
|
32686
|
+
var UNAVAILABLE_MESSAGE = `the pointer could not be resolved.
|
|
32687
|
+
This means one of: the pointer is not one this machine issued (forged or
|
|
32688
|
+
tampered), its entry was purged from the vault, or the vault key material is
|
|
32689
|
+
unavailable. The vault does not distinguish these cases \u2014 a token nobody can
|
|
32690
|
+
vouch for resolves to nothing.`;
|
|
32691
|
+
function descriptorLine(d) {
|
|
32692
|
+
const kind = d.provider === void 0 ? d.category : `${d.category}/${d.provider}`;
|
|
32693
|
+
return `${kind} \xB7 seen ${String(d.occurrences)}\xD7 \xB7 first ${d.firstSeen.slice(0, 10)}`;
|
|
32694
|
+
}
|
|
32695
|
+
async function runShow2(argv, io) {
|
|
32696
|
+
const { values, positionals } = parseArgs11({
|
|
32697
|
+
args: argv,
|
|
32698
|
+
options: { ...HOME_OPTION, help: { type: "boolean", short: "h" } },
|
|
32699
|
+
allowPositionals: true
|
|
32700
|
+
});
|
|
32701
|
+
if (values.help) {
|
|
32702
|
+
io.out(VAULT_HELP);
|
|
32703
|
+
return;
|
|
32704
|
+
}
|
|
32705
|
+
const candidate = positionals[0]?.trim();
|
|
32706
|
+
if (candidate === void 0 || candidate === "") {
|
|
32707
|
+
throw new Error(`usage: aka vault show '[[aka:...]]'`);
|
|
32708
|
+
}
|
|
32709
|
+
const parsed = PointerToken.safeParse(candidate);
|
|
32710
|
+
if (!parsed.success) {
|
|
32711
|
+
throw new Error(
|
|
32712
|
+
"that is not a vault pointer \u2014 expected the full [[aka:...]] token (quote it in the shell)"
|
|
32713
|
+
);
|
|
32714
|
+
}
|
|
32715
|
+
const base = homeBase(values.home);
|
|
32716
|
+
const dir = dataDir(base);
|
|
32717
|
+
const db = openLocalDatabase(dir);
|
|
32718
|
+
try {
|
|
32719
|
+
const vault = new SecretVault({
|
|
32720
|
+
repo: db.secretVault,
|
|
32721
|
+
keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
|
|
32722
|
+
fingerprintKey: loadOrCreateFingerprintKey(dir),
|
|
32723
|
+
// Read live so a consent revocation applies to the very next call.
|
|
32724
|
+
isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
|
|
32725
|
+
});
|
|
32726
|
+
const value = await vault.detokenize(parsed.data, {
|
|
32727
|
+
target: "human",
|
|
32728
|
+
reason: "explicit-reveal"
|
|
32729
|
+
});
|
|
32730
|
+
if (typeof value !== "string") {
|
|
32731
|
+
throw new Error(UNAVAILABLE_MESSAGE);
|
|
32732
|
+
}
|
|
32733
|
+
io.out(`${value}
|
|
32734
|
+
`);
|
|
32735
|
+
const descriptor = await vault.describePointer(parsed.data);
|
|
32736
|
+
if (descriptor) io.out(`${descriptorLine(descriptor)}
|
|
32737
|
+
`);
|
|
32738
|
+
} finally {
|
|
32739
|
+
db.close();
|
|
32740
|
+
}
|
|
32741
|
+
}
|
|
32742
|
+
var VERBS2 = {
|
|
32743
|
+
show: runShow2
|
|
32744
|
+
};
|
|
32745
|
+
async function runVault(argv, io = terminalPrompter()) {
|
|
32746
|
+
const [verb, ...rest] = argv;
|
|
32747
|
+
if (verb === void 0 || verb === "-h" || verb === "--help") {
|
|
32748
|
+
io.out(VAULT_HELP);
|
|
32749
|
+
return;
|
|
32750
|
+
}
|
|
32751
|
+
const handler = VERBS2[verb];
|
|
32752
|
+
if (!handler) {
|
|
32753
|
+
io.out(VAULT_HELP);
|
|
32754
|
+
return;
|
|
32755
|
+
}
|
|
32756
|
+
await handler(rest, io);
|
|
32757
|
+
}
|
|
32758
|
+
|
|
31199
32759
|
// src/lib/external-dispatch.ts
|
|
31200
32760
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
31201
32761
|
import { constants } from "os";
|
|
@@ -31248,6 +32808,7 @@ var COMMANDS = {
|
|
|
31248
32808
|
plugins: runPlugins,
|
|
31249
32809
|
dashboard: runDashboard,
|
|
31250
32810
|
exception: (argv) => runException(argv),
|
|
32811
|
+
vault: (argv) => runVault(argv),
|
|
31251
32812
|
tui: runTui,
|
|
31252
32813
|
update: runUpdate,
|
|
31253
32814
|
"check-updates": runCheckUpdates,
|