@akasecurity/cli 0.9.1 → 0.9.3
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/README.md +4 -0
- package/dist/cli.js +1233 -533
- package/package.json +6 -6
- package/web-ui/package.json +2 -2
- 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 +7 -7
- package/web-ui/web-ui/.next/BUILD_ID +1 -1
- package/web-ui/web-ui/.next/app-build-manifest.json +129 -129
- package/web-ui/web-ui/.next/app-path-routes-manifest.json +6 -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 -1
- 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/_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 +6 -6
- package/web-ui/web-ui/.next/server/chunks/100.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/142.js +700 -0
- package/web-ui/web-ui/.next/server/chunks/472.js +84 -0
- package/web-ui/web-ui/.next/server/chunks/723.js +22 -0
- package/web-ui/web-ui/.next/server/chunks/75.js +6 -0
- package/web-ui/web-ui/.next/server/chunks/830.js +123 -0
- package/web-ui/web-ui/.next/server/chunks/850.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/853.js +698 -0
- package/web-ui/web-ui/.next/server/chunks/871.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/924.js +2 -0
- package/web-ui/web-ui/.next/server/chunks/959.js +9 -0
- package/web-ui/web-ui/.next/server/functions-config-manifest.json +6 -6
- 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/{ez8SLWYcUCScGuFekZ-G_ → X_Z1LZQpnGXTuZXKkv4Vb}/_buildManifest.js +1 -1
- package/web-ui/web-ui/.next/static/chunks/137-8f757e04d1877a6c.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/517-a4643cfd51efbca4.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/613-734dcad67e1fa08c.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/784-25fa9bc074877c93.js +62 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-72558bcb945a0abc.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-540aa3dbe1c0b3ad.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-8fac90d5b77b7f89.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-06ee1868466199df.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-8b2ea0bbbc4fc69c.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-2aa604a6c36786fc.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-28f48db02051d88c.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-15bc43471843cf2e.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-f034effcd10a332b.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-de465eacbb359987.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-fa10c50b1436102d.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-a61b90eb3a575f85.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-be0284caa1e78227.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-eec4e54274dcbb4d.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-26662e9d7ee32abc.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/layout-e531d303d8c8a313.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/page-3ffe075f223c0af9.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/{813c9d6d-dc0a9294021710c1.js → dddf6c0a-f09ab3fc84be1c52.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/framework-f8855380599951ba.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/main-app-7291de73392cac56.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/main-ff0082770df07ddc.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/pages/_app-56d1709db70ee49f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/pages/_error-63dd320a91a40541.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/{webpack-14166b28d6557de1.js → webpack-ea80c9c368a8364e.js} +1 -1
- package/web-ui/web-ui/.next/static/css/a30725ee12aca680.css +2 -0
- package/web-ui/web-ui/package.json +12 -9
- package/web-ui/web-ui/.next/server/chunks/244.js +0 -649
- package/web-ui/web-ui/.next/server/chunks/264.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/291.js +0 -647
- package/web-ui/web-ui/.next/server/chunks/345.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/367.js +0 -6
- package/web-ui/web-ui/.next/server/chunks/397.js +0 -22
- package/web-ui/web-ui/.next/server/chunks/45.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/461.js +0 -123
- package/web-ui/web-ui/.next/server/chunks/773.js +0 -84
- package/web-ui/web-ui/.next/server/chunks/849.js +0 -9
- package/web-ui/web-ui/.next/server/chunks/983.js +0 -2
- package/web-ui/web-ui/.next/static/chunks/24-d36fb6bd72e29537.js +0 -62
- package/web-ui/web-ui/.next/static/chunks/338-78876603768c14bb.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/496-a8b6c34560896a23.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/561-52ad6ae6bf64b471.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-797e2ce43b8db7e1.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-d9e9023871035a2d.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-5bd8e04955058e46.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-a254c9fc7cc27b97.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-622151ff8e6960c8.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-c1221106bd6ba356.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-fc68b0fb9b669a03.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-68418378cfb10a0f.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-d15081016f40ac92.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-a73f4fb185479b19.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-2b9ea3e95dbe82fb.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-2675f7fb69829b71.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-315b8b57329d5eb6.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-77adb595bb447040.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-5728154141ad0edb.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/layout-8b19b03146218dc7.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/page-79d71c1635fbf565.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/framework-09f91e50832141e4.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/main-80ce91d706b796ad.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/main-app-23de550d13c8bec1.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/pages/_app-ab1bd755bf3a4f53.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/pages/_error-541b64fd411d8da6.js +0 -1
- package/web-ui/web-ui/.next/static/css/500a1c2bf0fc64fb.css +0 -2
- /package/web-ui/web-ui/.next/static/{ez8SLWYcUCScGuFekZ-G_ → X_Z1LZQpnGXTuZXKkv4Vb}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -2493,7 +2493,7 @@ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].m
|
|
|
2493
2493
|
|
|
2494
2494
|
// ../packages/persistence/src/database.ts
|
|
2495
2495
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
2496
|
-
import { existsSync as existsSync2, renameSync, rmSync } from "fs";
|
|
2496
|
+
import { existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
2497
2497
|
import { join as join2, sep } from "path";
|
|
2498
2498
|
import { DatabaseSync } from "node:sqlite";
|
|
2499
2499
|
|
|
@@ -2546,6 +2546,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
2546
2546
|
{
|
|
2547
2547
|
tag: "0011_egress_writer",
|
|
2548
2548
|
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
tag: "0012_handy_the_captain",
|
|
2552
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
2553
|
+
},
|
|
2554
|
+
{
|
|
2555
|
+
tag: "0013_legacy_history_backfill_support",
|
|
2556
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
2557
|
+
},
|
|
2558
|
+
{
|
|
2559
|
+
tag: "0014_drop_legacy_events_findings",
|
|
2560
|
+
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"
|
|
2549
2561
|
}
|
|
2550
2562
|
];
|
|
2551
2563
|
|
|
@@ -17499,7 +17511,12 @@ var FindingFacets = external_exports.object({
|
|
|
17499
17511
|
severity: external_exports.array(FindingFacetItem),
|
|
17500
17512
|
subtype: external_exports.array(FindingFacetItem),
|
|
17501
17513
|
provider: external_exports.array(FindingFacetItem),
|
|
17502
|
-
action: external_exports.array(FindingFacetItem)
|
|
17514
|
+
action: external_exports.array(FindingFacetItem),
|
|
17515
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
17516
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
17517
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
17518
|
+
// counted under no value.
|
|
17519
|
+
status: external_exports.array(FindingFacetItem)
|
|
17503
17520
|
}).meta({ id: "FindingFacets" });
|
|
17504
17521
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
17505
17522
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -17509,6 +17526,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
17509
17526
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
17510
17527
|
provider: external_exports.array(FindingProvider).optional(),
|
|
17511
17528
|
action: external_exports.array(FindingAction).optional(),
|
|
17529
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
17530
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
17531
|
+
// one of the requested values.
|
|
17532
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
17512
17533
|
q: external_exports.string().optional(),
|
|
17513
17534
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
17514
17535
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -17696,6 +17717,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
17696
17717
|
parent_uuid: external_exports.string().optional(),
|
|
17697
17718
|
run_key: external_exports.string().optional()
|
|
17698
17719
|
}).catchall(external_exports.unknown());
|
|
17720
|
+
var CaptureAttributes = external_exports.object({
|
|
17721
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
17722
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
17723
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
17724
|
+
source_tool: external_exports.string().optional(),
|
|
17725
|
+
file_path: external_exports.string().optional(),
|
|
17726
|
+
repo: external_exports.string().optional(),
|
|
17727
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
17728
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
17729
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
17730
|
+
tool_name: external_exports.string().optional(),
|
|
17731
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
17732
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
17733
|
+
gitignored: external_exports.boolean().optional(),
|
|
17734
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
17735
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
17736
|
+
whole_file: external_exports.boolean().optional(),
|
|
17737
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
17738
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
17739
|
+
// trace id when telemetry is enabled.
|
|
17740
|
+
correlation_id: external_exports.uuid().optional(),
|
|
17741
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
17742
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
17743
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
17744
|
+
// authorized the bypass.
|
|
17745
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
17746
|
+
}).catchall(external_exports.unknown());
|
|
17699
17747
|
var ToolCallInspection = external_exports.object({
|
|
17700
17748
|
ruleId: external_exports.string().min(1),
|
|
17701
17749
|
ruleName: external_exports.string(),
|
|
@@ -17782,7 +17830,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
17782
17830
|
span: Span,
|
|
17783
17831
|
maskedMatch: external_exports.string(),
|
|
17784
17832
|
actionTaken: ActionTaken,
|
|
17785
|
-
confidence: external_exports.number().min(0).max(1)
|
|
17833
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17834
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
17835
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
17836
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
17837
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
17838
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
17839
|
+
findingKey: external_exports.string().optional(),
|
|
17840
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
17841
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
17842
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
17843
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
17844
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
17786
17845
|
});
|
|
17787
17846
|
var InventoryContext = external_exports.object({
|
|
17788
17847
|
host: InventoryInput.optional(),
|
|
@@ -17984,6 +18043,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
17984
18043
|
|
|
17985
18044
|
// ../packages/schema/src/zod/event.ts
|
|
17986
18045
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
18046
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
17987
18047
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
17988
18048
|
var EventMetadata = external_exports.object({
|
|
17989
18049
|
sessionId: external_exports.string().optional(),
|
|
@@ -18321,6 +18381,9 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
18321
18381
|
useCount: true,
|
|
18322
18382
|
conditions: true
|
|
18323
18383
|
});
|
|
18384
|
+
function isMatchableUnder(keyVersion, key) {
|
|
18385
|
+
return key.status === "present" && key.version === keyVersion;
|
|
18386
|
+
}
|
|
18324
18387
|
|
|
18325
18388
|
// ../packages/schema/src/zod/rule.ts
|
|
18326
18389
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
@@ -18473,6 +18536,12 @@ var PolicyBundle = external_exports.object({
|
|
|
18473
18536
|
// on-disk caches — that omit the field still parse; consumers read
|
|
18474
18537
|
// `bundle.exceptions ?? []`.
|
|
18475
18538
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
18539
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
18540
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
18541
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
18542
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
18543
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
18544
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
18476
18545
|
customKeywords: external_exports.array(external_exports.string()),
|
|
18477
18546
|
fetchedAt: external_exports.iso.datetime()
|
|
18478
18547
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -19272,6 +19341,15 @@ function groupActions(g) {
|
|
|
19272
19341
|
actionsCache.set(g, actions);
|
|
19273
19342
|
return actions;
|
|
19274
19343
|
}
|
|
19344
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
19345
|
+
const statusSet = new Set(statuses);
|
|
19346
|
+
let sum = 0;
|
|
19347
|
+
for (const input of statusInputs) {
|
|
19348
|
+
if (input.count === void 0) return null;
|
|
19349
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
19350
|
+
}
|
|
19351
|
+
return sum;
|
|
19352
|
+
}
|
|
19275
19353
|
function applyFindingFilters(groups, opts) {
|
|
19276
19354
|
let filtered = groups;
|
|
19277
19355
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -19290,6 +19368,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
19290
19368
|
const subtypeSet = new Set(opts.subtype);
|
|
19291
19369
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
19292
19370
|
}
|
|
19371
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
19372
|
+
const statusSet = new Set(opts.statuses);
|
|
19373
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
19374
|
+
}
|
|
19293
19375
|
if (opts.q) {
|
|
19294
19376
|
const q = opts.q.toLowerCase();
|
|
19295
19377
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -19311,6 +19393,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
19311
19393
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
19312
19394
|
providers: opts.providers,
|
|
19313
19395
|
actions: opts.actions,
|
|
19396
|
+
statuses: opts.statuses,
|
|
19314
19397
|
q: opts.q,
|
|
19315
19398
|
subtype: opts.subtype
|
|
19316
19399
|
});
|
|
@@ -19320,6 +19403,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
19320
19403
|
}
|
|
19321
19404
|
const forProvider = applyFindingFilters(allGroups, {
|
|
19322
19405
|
actions: opts.actions,
|
|
19406
|
+
statuses: opts.statuses,
|
|
19323
19407
|
q: opts.q,
|
|
19324
19408
|
subtype: opts.subtype,
|
|
19325
19409
|
severity: opts.severity
|
|
@@ -19330,6 +19414,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
19330
19414
|
}
|
|
19331
19415
|
const forAction = applyFindingFilters(allGroups, {
|
|
19332
19416
|
providers: opts.providers,
|
|
19417
|
+
statuses: opts.statuses,
|
|
19333
19418
|
q: opts.q,
|
|
19334
19419
|
subtype: opts.subtype,
|
|
19335
19420
|
severity: opts.severity
|
|
@@ -19341,17 +19426,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
19341
19426
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
19342
19427
|
providers: opts.providers,
|
|
19343
19428
|
actions: opts.actions,
|
|
19429
|
+
statuses: opts.statuses,
|
|
19344
19430
|
q: opts.q,
|
|
19345
19431
|
severity: opts.severity
|
|
19346
19432
|
});
|
|
19347
19433
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
19348
19434
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
19435
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
19436
|
+
providers: opts.providers,
|
|
19437
|
+
actions: opts.actions,
|
|
19438
|
+
q: opts.q,
|
|
19439
|
+
subtype: opts.subtype,
|
|
19440
|
+
severity: opts.severity
|
|
19441
|
+
});
|
|
19442
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
19443
|
+
for (const g of forStatus) {
|
|
19444
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
19445
|
+
}
|
|
19349
19446
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
19350
19447
|
return {
|
|
19351
19448
|
severity: toItems(severityMap),
|
|
19352
19449
|
provider: toItems(providerMap),
|
|
19353
19450
|
action: toItems(actionMap),
|
|
19354
|
-
subtype: toItems(subtypeMap)
|
|
19451
|
+
subtype: toItems(subtypeMap),
|
|
19452
|
+
status: toItems(statusMap)
|
|
19355
19453
|
};
|
|
19356
19454
|
}
|
|
19357
19455
|
|
|
@@ -19386,10 +19484,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
19386
19484
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
19387
19485
|
|
|
19388
19486
|
// ../packages/schema/src/zod/local.ts
|
|
19389
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
19487
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
19390
19488
|
var RunMode = external_exports.enum(["standalone"]);
|
|
19391
19489
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
19392
19490
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
19491
|
+
var ModelJudgeConsent = external_exports.object({
|
|
19492
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
19493
|
+
payloadVersion: external_exports.number().int().positive()
|
|
19494
|
+
});
|
|
19393
19495
|
var WorkspaceSettings = external_exports.object({
|
|
19394
19496
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
19395
19497
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -19405,37 +19507,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
19405
19507
|
// Shares writes.
|
|
19406
19508
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
19407
19509
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
19408
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
19510
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
19511
|
+
// Records that the user consented to sending findings to the model API for
|
|
19512
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
19513
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
19514
|
+
// covers the current payload and must be re-granted.
|
|
19515
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
19409
19516
|
});
|
|
19410
19517
|
function defaultWorkspaceSettings() {
|
|
19411
19518
|
return WorkspaceSettings.parse({});
|
|
19412
19519
|
}
|
|
19413
|
-
function toEventRow(event) {
|
|
19414
|
-
return {
|
|
19415
|
-
id: event.id,
|
|
19416
|
-
sourceTool: event.sourceTool,
|
|
19417
|
-
kind: event.kind,
|
|
19418
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
19419
|
-
contentHash: event.contentHash,
|
|
19420
|
-
content: event.content,
|
|
19421
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
19422
|
-
};
|
|
19423
|
-
}
|
|
19424
|
-
function toFindingRow(finding) {
|
|
19425
|
-
return {
|
|
19426
|
-
id: finding.id,
|
|
19427
|
-
eventId: finding.eventId,
|
|
19428
|
-
ruleId: finding.ruleId,
|
|
19429
|
-
category: finding.category,
|
|
19430
|
-
severity: finding.severity,
|
|
19431
|
-
spanStart: finding.span.start,
|
|
19432
|
-
spanEnd: finding.span.end,
|
|
19433
|
-
maskedMatch: finding.maskedMatch,
|
|
19434
|
-
actionTaken: finding.actionTaken,
|
|
19435
|
-
confidence: finding.confidence,
|
|
19436
|
-
findingKey: finding.findingKey ?? null
|
|
19437
|
-
};
|
|
19438
|
-
}
|
|
19439
19520
|
function toInventoryRow(input, id, now) {
|
|
19440
19521
|
return {
|
|
19441
19522
|
id,
|
|
@@ -19505,7 +19586,42 @@ function toInspectionFindingRow(input) {
|
|
|
19505
19586
|
spanEnd: input.span.end,
|
|
19506
19587
|
maskedMatch: input.maskedMatch,
|
|
19507
19588
|
actionTaken: input.actionTaken,
|
|
19508
|
-
confidence: input.confidence
|
|
19589
|
+
confidence: input.confidence,
|
|
19590
|
+
findingKey: input.findingKey ?? null,
|
|
19591
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
19592
|
+
};
|
|
19593
|
+
}
|
|
19594
|
+
function toCaptureAttributes(event) {
|
|
19595
|
+
const metadata = event.metadata;
|
|
19596
|
+
return {
|
|
19597
|
+
source_tool: event.sourceTool,
|
|
19598
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
19599
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
19600
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
19601
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
19602
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
19603
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
19604
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
19605
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
19606
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
19607
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
19608
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
19609
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
19610
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
19611
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
19612
|
+
};
|
|
19613
|
+
}
|
|
19614
|
+
function captureDefinitionVersion(finding) {
|
|
19615
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
19616
|
+
}
|
|
19617
|
+
function toCaptureDefinitionInput(finding) {
|
|
19618
|
+
return {
|
|
19619
|
+
ruleId: finding.ruleId,
|
|
19620
|
+
version: captureDefinitionVersion(finding),
|
|
19621
|
+
name: finding.ruleId,
|
|
19622
|
+
category: finding.category,
|
|
19623
|
+
severity: finding.severity,
|
|
19624
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
19509
19625
|
};
|
|
19510
19626
|
}
|
|
19511
19627
|
|
|
@@ -19936,6 +20052,37 @@ function reviewSeverityRank(reasons) {
|
|
|
19936
20052
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
19937
20053
|
}
|
|
19938
20054
|
|
|
20055
|
+
// ../packages/persistence/src/ids.ts
|
|
20056
|
+
import { createHash } from "crypto";
|
|
20057
|
+
function sha256Hex(input) {
|
|
20058
|
+
return createHash("sha256").update(input).digest("hex");
|
|
20059
|
+
}
|
|
20060
|
+
function inventoryId(objectType, identityKey) {
|
|
20061
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
20062
|
+
}
|
|
20063
|
+
function sourceProjectId(url2) {
|
|
20064
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
20065
|
+
}
|
|
20066
|
+
function classifiedDataId(cls) {
|
|
20067
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
20068
|
+
}
|
|
20069
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
20070
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
20071
|
+
}
|
|
20072
|
+
function llmCallId(sessionId, messageId) {
|
|
20073
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
20074
|
+
}
|
|
20075
|
+
function toolCallId(sessionId, toolUseId) {
|
|
20076
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
20077
|
+
}
|
|
20078
|
+
var NO_SESSION = "no_session";
|
|
20079
|
+
var NO_PATH = "no_path";
|
|
20080
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
20081
|
+
return sha256Hex(
|
|
20082
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
20083
|
+
);
|
|
20084
|
+
}
|
|
20085
|
+
|
|
19939
20086
|
// ../packages/persistence/src/internal/sql-text.ts
|
|
19940
20087
|
function escapeLikePattern(s) {
|
|
19941
20088
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -20032,28 +20179,98 @@ function evidenceExists(db, object2) {
|
|
|
20032
20179
|
return schemaObjectExists(db, "table", object2.name);
|
|
20033
20180
|
}
|
|
20034
20181
|
|
|
20035
|
-
// ../packages/persistence/src/
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20182
|
+
// ../packages/persistence/src/internal/rows.ts
|
|
20183
|
+
function allRows(stmt, params) {
|
|
20184
|
+
if (params === void 0) return stmt.all();
|
|
20185
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
20186
|
+
return stmt.all(params);
|
|
20039
20187
|
}
|
|
20040
|
-
function
|
|
20041
|
-
|
|
20188
|
+
function getRow(stmt, params) {
|
|
20189
|
+
if (params === void 0) return stmt.get();
|
|
20190
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
20191
|
+
return stmt.get(params);
|
|
20042
20192
|
}
|
|
20043
|
-
function
|
|
20044
|
-
return
|
|
20193
|
+
function intToBool(raw) {
|
|
20194
|
+
return raw === 1 || raw === true;
|
|
20045
20195
|
}
|
|
20046
|
-
function
|
|
20047
|
-
return
|
|
20196
|
+
function boolToInt(b) {
|
|
20197
|
+
return b ? 1 : 0;
|
|
20048
20198
|
}
|
|
20049
|
-
function
|
|
20050
|
-
|
|
20199
|
+
function bindParams(row) {
|
|
20200
|
+
const out = {};
|
|
20201
|
+
for (const [key, value] of Object.entries(row)) {
|
|
20202
|
+
out[key] = value === void 0 ? null : value;
|
|
20203
|
+
}
|
|
20204
|
+
return out;
|
|
20051
20205
|
}
|
|
20052
|
-
function
|
|
20053
|
-
return
|
|
20206
|
+
function countScalar(db, sql, params) {
|
|
20207
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
20054
20208
|
}
|
|
20055
|
-
function
|
|
20056
|
-
|
|
20209
|
+
function countBy(db, sql, params) {
|
|
20210
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
20211
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
20212
|
+
map2.set(row.k, row.n);
|
|
20213
|
+
}
|
|
20214
|
+
return map2;
|
|
20215
|
+
}
|
|
20216
|
+
function mapRowsTolerant(rows, map2) {
|
|
20217
|
+
const out = [];
|
|
20218
|
+
for (const row of rows) {
|
|
20219
|
+
try {
|
|
20220
|
+
out.push(map2(row));
|
|
20221
|
+
} catch {
|
|
20222
|
+
}
|
|
20223
|
+
}
|
|
20224
|
+
return out;
|
|
20225
|
+
}
|
|
20226
|
+
|
|
20227
|
+
// ../packages/persistence/src/paths.ts
|
|
20228
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
20229
|
+
var DATA_DIR_MODE = 448;
|
|
20230
|
+
var DATA_FILE_MODE = 384;
|
|
20231
|
+
var DB_FILENAME = "aka.db";
|
|
20232
|
+
function chmodBestEffort(path, mode) {
|
|
20233
|
+
try {
|
|
20234
|
+
chmodSync(path, mode);
|
|
20235
|
+
} catch {
|
|
20236
|
+
}
|
|
20237
|
+
}
|
|
20238
|
+
function tightenDir(dir) {
|
|
20239
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
20240
|
+
}
|
|
20241
|
+
function ensureDataDirSync(dir) {
|
|
20242
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
20243
|
+
tightenDir(dir);
|
|
20244
|
+
}
|
|
20245
|
+
function dbSidecars(file2) {
|
|
20246
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
20247
|
+
}
|
|
20248
|
+
function tightenFile(file2) {
|
|
20249
|
+
try {
|
|
20250
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
20251
|
+
} catch {
|
|
20252
|
+
}
|
|
20253
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
20254
|
+
}
|
|
20255
|
+
function tightenPerms(file2) {
|
|
20256
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
20257
|
+
}
|
|
20258
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
20259
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
20260
|
+
try {
|
|
20261
|
+
rmSync(tmp, { force: true });
|
|
20262
|
+
} catch {
|
|
20263
|
+
}
|
|
20264
|
+
try {
|
|
20265
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
20266
|
+
renameSync(tmp, file2);
|
|
20267
|
+
} finally {
|
|
20268
|
+
try {
|
|
20269
|
+
rmSync(tmp, { force: true });
|
|
20270
|
+
} catch {
|
|
20271
|
+
}
|
|
20272
|
+
}
|
|
20273
|
+
tightenFile(file2);
|
|
20057
20274
|
}
|
|
20058
20275
|
|
|
20059
20276
|
// ../packages/persistence/src/migrations.ts
|
|
@@ -20067,7 +20284,8 @@ function createdIndexName(statement) {
|
|
|
20067
20284
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
20068
20285
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
20069
20286
|
}
|
|
20070
|
-
|
|
20287
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
20288
|
+
function applyMigrations(db, file2) {
|
|
20071
20289
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
20072
20290
|
db.exec(
|
|
20073
20291
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -20081,6 +20299,7 @@ function applyMigrations(db) {
|
|
|
20081
20299
|
);
|
|
20082
20300
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
20083
20301
|
if (applied.has(migration.tag)) continue;
|
|
20302
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
20084
20303
|
const evidence = evidenceObjects(migration.sql);
|
|
20085
20304
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
20086
20305
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -20125,7 +20344,6 @@ function applyMigrations(db) {
|
|
|
20125
20344
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
20126
20345
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
20127
20346
|
}
|
|
20128
|
-
ensureSyncedAtColumn(db, "events");
|
|
20129
20347
|
ensureSyncedAtColumn(db, "audit_events");
|
|
20130
20348
|
ensureScanLedgerTable(db);
|
|
20131
20349
|
ensureBlockedDetectionsTable(db);
|
|
@@ -20133,6 +20351,47 @@ function applyMigrations(db) {
|
|
|
20133
20351
|
ensureWriteGateTrigger(db);
|
|
20134
20352
|
ensureTokenUsageColumns(db);
|
|
20135
20353
|
reconcileSourceProjectIds(db);
|
|
20354
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
20355
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
20356
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
20357
|
+
}
|
|
20358
|
+
}
|
|
20359
|
+
function applyLegacyDropMigration(db, file2) {
|
|
20360
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
20361
|
+
if (!migration) return;
|
|
20362
|
+
if (file2) {
|
|
20363
|
+
try {
|
|
20364
|
+
backupBeforeLegacyDrop(db, file2);
|
|
20365
|
+
} catch (error51) {
|
|
20366
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
20367
|
+
return;
|
|
20368
|
+
}
|
|
20369
|
+
}
|
|
20370
|
+
try {
|
|
20371
|
+
withTransaction(
|
|
20372
|
+
db,
|
|
20373
|
+
() => {
|
|
20374
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
20375
|
+
if (alreadyDropped) return;
|
|
20376
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
20377
|
+
db.exec(statement);
|
|
20378
|
+
}
|
|
20379
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
20380
|
+
migration.tag,
|
|
20381
|
+
Date.now()
|
|
20382
|
+
);
|
|
20383
|
+
},
|
|
20384
|
+
"IMMEDIATE"
|
|
20385
|
+
);
|
|
20386
|
+
} catch (error51) {
|
|
20387
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
20388
|
+
}
|
|
20389
|
+
}
|
|
20390
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
20391
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
20392
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
20393
|
+
tightenFile(backup);
|
|
20394
|
+
return backup;
|
|
20136
20395
|
}
|
|
20137
20396
|
var TOKEN_USAGE_COLUMNS = [
|
|
20138
20397
|
{
|
|
@@ -20161,6 +20420,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
20161
20420
|
}
|
|
20162
20421
|
];
|
|
20163
20422
|
function ensureTokenUsageColumns(db) {
|
|
20423
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
20164
20424
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
20165
20425
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
20166
20426
|
if (!existing.has(column.name)) {
|
|
@@ -20226,11 +20486,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
20226
20486
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
20227
20487
|
}
|
|
20228
20488
|
}
|
|
20489
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
20490
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
20491
|
+
function getLegacyCopyWatermark(db, source) {
|
|
20492
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
20493
|
+
return row?.lastRowid ?? 0;
|
|
20494
|
+
}
|
|
20495
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
20496
|
+
db.prepare(
|
|
20497
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
20498
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
20499
|
+
).run(source, lastRowid);
|
|
20500
|
+
}
|
|
20501
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
20502
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
20503
|
+
let processed = 0;
|
|
20504
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
20505
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
20506
|
+
if (rows.length === 0) return true;
|
|
20507
|
+
withTransaction(
|
|
20508
|
+
db,
|
|
20509
|
+
() => {
|
|
20510
|
+
handleRows(rows);
|
|
20511
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
20512
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
20513
|
+
},
|
|
20514
|
+
"IMMEDIATE"
|
|
20515
|
+
);
|
|
20516
|
+
processed += rows.length;
|
|
20517
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
20518
|
+
}
|
|
20519
|
+
return false;
|
|
20520
|
+
}
|
|
20521
|
+
function parseLegacyEventMetadata(raw) {
|
|
20522
|
+
if (raw === null) return void 0;
|
|
20523
|
+
try {
|
|
20524
|
+
return JSON.parse(raw);
|
|
20525
|
+
} catch {
|
|
20526
|
+
return void 0;
|
|
20527
|
+
}
|
|
20528
|
+
}
|
|
20529
|
+
function toLegacyAuditAttributesJson(row) {
|
|
20530
|
+
return JSON.stringify(
|
|
20531
|
+
toCaptureAttributes({
|
|
20532
|
+
id: row.id,
|
|
20533
|
+
sourceTool: row.sourceTool,
|
|
20534
|
+
kind: row.kind,
|
|
20535
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
20536
|
+
contentHash: row.contentHash,
|
|
20537
|
+
content: row.content,
|
|
20538
|
+
metadata: row.metadata
|
|
20539
|
+
})
|
|
20540
|
+
);
|
|
20541
|
+
}
|
|
20542
|
+
function copyLegacyEvents(db) {
|
|
20543
|
+
const selectStmt = db.prepare(
|
|
20544
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
20545
|
+
content_hash AS contentHash, content, metadata
|
|
20546
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
20547
|
+
);
|
|
20548
|
+
const insertStmt = db.prepare(
|
|
20549
|
+
`INSERT OR IGNORE INTO audit_events
|
|
20550
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
20551
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
20552
|
+
);
|
|
20553
|
+
const stubRootStmt = db.prepare(
|
|
20554
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
20555
|
+
);
|
|
20556
|
+
return drainLegacyTable(
|
|
20557
|
+
db,
|
|
20558
|
+
"events",
|
|
20559
|
+
selectStmt,
|
|
20560
|
+
(rows) => {
|
|
20561
|
+
for (const row of rows) {
|
|
20562
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
20563
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
20564
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
20565
|
+
insertStmt.run(
|
|
20566
|
+
bindParams({
|
|
20567
|
+
id: row.id,
|
|
20568
|
+
parentId: sessionId,
|
|
20569
|
+
rootSessionId: sessionId,
|
|
20570
|
+
eventType: row.kind,
|
|
20571
|
+
startedAt: row.occurredAt,
|
|
20572
|
+
content: row.content,
|
|
20573
|
+
contentHash: row.contentHash,
|
|
20574
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
20575
|
+
})
|
|
20576
|
+
);
|
|
20577
|
+
}
|
|
20578
|
+
}
|
|
20579
|
+
);
|
|
20580
|
+
}
|
|
20581
|
+
function copyLegacyFindings(db) {
|
|
20582
|
+
const selectStmt = db.prepare(
|
|
20583
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
20584
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
20585
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
20586
|
+
first_detected_at AS firstDetectedAt
|
|
20587
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
20588
|
+
);
|
|
20589
|
+
const definitionStmt = db.prepare(
|
|
20590
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
20591
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
20592
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
20593
|
+
);
|
|
20594
|
+
const findingStmt = db.prepare(
|
|
20595
|
+
`INSERT INTO inspection_findings
|
|
20596
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20597
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20598
|
+
finding_key, first_detected_at)
|
|
20599
|
+
VALUES
|
|
20600
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
20601
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20602
|
+
:findingKey, :firstDetectedAt)
|
|
20603
|
+
ON CONFLICT(id) DO NOTHING
|
|
20604
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20605
|
+
first_detected_at = CASE
|
|
20606
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
20607
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
20608
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
20609
|
+
END`
|
|
20610
|
+
);
|
|
20611
|
+
return drainLegacyTable(
|
|
20612
|
+
db,
|
|
20613
|
+
"findings",
|
|
20614
|
+
selectStmt,
|
|
20615
|
+
(rows) => {
|
|
20616
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
20617
|
+
for (const row of rows) {
|
|
20618
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
20619
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
20620
|
+
if (definitionId === void 0) {
|
|
20621
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
20622
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
20623
|
+
definitionStmt.run(
|
|
20624
|
+
bindParams({
|
|
20625
|
+
id: definitionId,
|
|
20626
|
+
ruleId: row.ruleId,
|
|
20627
|
+
name: row.ruleId,
|
|
20628
|
+
category: row.category,
|
|
20629
|
+
severity: row.severity,
|
|
20630
|
+
definition: "",
|
|
20631
|
+
version: version2
|
|
20632
|
+
})
|
|
20633
|
+
);
|
|
20634
|
+
definitionIds.set(tupleKey, definitionId);
|
|
20635
|
+
}
|
|
20636
|
+
findingStmt.run(
|
|
20637
|
+
bindParams({
|
|
20638
|
+
id: row.id,
|
|
20639
|
+
auditEventId: row.eventId,
|
|
20640
|
+
inspectionDefinitionId: definitionId,
|
|
20641
|
+
spanStart: row.spanStart,
|
|
20642
|
+
spanEnd: row.spanEnd,
|
|
20643
|
+
maskedMatch: row.maskedMatch,
|
|
20644
|
+
actionTaken: row.actionTaken,
|
|
20645
|
+
confidence: row.confidence,
|
|
20646
|
+
findingKey: row.findingKey,
|
|
20647
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20648
|
+
})
|
|
20649
|
+
);
|
|
20650
|
+
}
|
|
20651
|
+
}
|
|
20652
|
+
);
|
|
20653
|
+
}
|
|
20654
|
+
function runLegacyHistoryBackfill(db) {
|
|
20655
|
+
try {
|
|
20656
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
20657
|
+
if (!eventsCaughtUp) return false;
|
|
20658
|
+
return copyLegacyFindings(db);
|
|
20659
|
+
} catch (error51) {
|
|
20660
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
20661
|
+
return false;
|
|
20662
|
+
}
|
|
20663
|
+
}
|
|
20229
20664
|
function isForeignSqliteLineage(db) {
|
|
20230
20665
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
20231
20666
|
return columnNames(db, "events").includes("tenant_id");
|
|
20232
20667
|
}
|
|
20233
20668
|
function ensureSyncedAtColumn(db, table) {
|
|
20669
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
20234
20670
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
20235
20671
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
20236
20672
|
}
|
|
@@ -20251,6 +20687,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
20251
20687
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
20252
20688
|
)`);
|
|
20253
20689
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
20690
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
20254
20691
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
20255
20692
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
20256
20693
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -20278,30 +20715,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
20278
20715
|
)`);
|
|
20279
20716
|
}
|
|
20280
20717
|
|
|
20281
|
-
// ../packages/persistence/src/paths.ts
|
|
20282
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
20283
|
-
var DATA_DIR_MODE = 448;
|
|
20284
|
-
var DATA_FILE_MODE = 384;
|
|
20285
|
-
var DB_FILENAME = "aka.db";
|
|
20286
|
-
function ensureDataDirSync(dir) {
|
|
20287
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
20288
|
-
try {
|
|
20289
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
20290
|
-
} catch {
|
|
20291
|
-
}
|
|
20292
|
-
}
|
|
20293
|
-
function walSidecars(file2) {
|
|
20294
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
20295
|
-
}
|
|
20296
|
-
function tightenPerms(file2) {
|
|
20297
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
20298
|
-
try {
|
|
20299
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
20300
|
-
} catch {
|
|
20301
|
-
}
|
|
20302
|
-
}
|
|
20303
|
-
}
|
|
20304
|
-
|
|
20305
20718
|
// ../packages/persistence/src/internal/json.ts
|
|
20306
20719
|
function safeJson(s, fallback) {
|
|
20307
20720
|
if (s == null) return fallback;
|
|
@@ -20321,51 +20734,6 @@ function parseJsonObject(s) {
|
|
|
20321
20734
|
return void 0;
|
|
20322
20735
|
}
|
|
20323
20736
|
|
|
20324
|
-
// ../packages/persistence/src/internal/rows.ts
|
|
20325
|
-
function allRows(stmt, params) {
|
|
20326
|
-
if (params === void 0) return stmt.all();
|
|
20327
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
20328
|
-
return stmt.all(params);
|
|
20329
|
-
}
|
|
20330
|
-
function getRow(stmt, params) {
|
|
20331
|
-
if (params === void 0) return stmt.get();
|
|
20332
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
20333
|
-
return stmt.get(params);
|
|
20334
|
-
}
|
|
20335
|
-
function intToBool(raw) {
|
|
20336
|
-
return raw === 1 || raw === true;
|
|
20337
|
-
}
|
|
20338
|
-
function boolToInt(b) {
|
|
20339
|
-
return b ? 1 : 0;
|
|
20340
|
-
}
|
|
20341
|
-
function bindParams(row) {
|
|
20342
|
-
const out = {};
|
|
20343
|
-
for (const [key, value] of Object.entries(row)) {
|
|
20344
|
-
out[key] = value === void 0 ? null : value;
|
|
20345
|
-
}
|
|
20346
|
-
return out;
|
|
20347
|
-
}
|
|
20348
|
-
function countScalar(db, sql, params) {
|
|
20349
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
20350
|
-
}
|
|
20351
|
-
function countBy(db, sql, params) {
|
|
20352
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
20353
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
20354
|
-
map2.set(row.k, row.n);
|
|
20355
|
-
}
|
|
20356
|
-
return map2;
|
|
20357
|
-
}
|
|
20358
|
-
function mapRowsTolerant(rows, map2) {
|
|
20359
|
-
const out = [];
|
|
20360
|
-
for (const row of rows) {
|
|
20361
|
-
try {
|
|
20362
|
-
out.push(map2(row));
|
|
20363
|
-
} catch {
|
|
20364
|
-
}
|
|
20365
|
-
}
|
|
20366
|
-
return out;
|
|
20367
|
-
}
|
|
20368
|
-
|
|
20369
20737
|
// ../packages/persistence/src/repositories/activity.ts
|
|
20370
20738
|
var DAY_MS = 864e5;
|
|
20371
20739
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -20982,6 +21350,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
20982
21350
|
})
|
|
20983
21351
|
);
|
|
20984
21352
|
}
|
|
21353
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
21354
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
21355
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
21356
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
21357
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
21358
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
21359
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
21360
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
21361
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
21362
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
21363
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
21364
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
21365
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
21366
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
21367
|
+
}
|
|
20985
21368
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
20986
21369
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
20987
21370
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -21443,8 +21826,14 @@ var SqliteDetectionsRepository = class {
|
|
|
21443
21826
|
)
|
|
21444
21827
|
);
|
|
21445
21828
|
}
|
|
21446
|
-
// Findings whose parent event occurred in the last 30 days
|
|
21447
|
-
//
|
|
21829
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
21830
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
21831
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
21832
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
21833
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
21834
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
21835
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
21836
|
+
// to the old findings⋈events one.
|
|
21448
21837
|
countFindingsLast30d(ruleIds) {
|
|
21449
21838
|
if (ruleIds.length === 0) return 0;
|
|
21450
21839
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -21452,8 +21841,12 @@ var SqliteDetectionsRepository = class {
|
|
|
21452
21841
|
return countScalar(
|
|
21453
21842
|
this.db,
|
|
21454
21843
|
`SELECT count(*) AS n
|
|
21455
|
-
FROM
|
|
21456
|
-
|
|
21844
|
+
FROM inspection_findings f
|
|
21845
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
21846
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21847
|
+
WHERE e.started_at >= ?
|
|
21848
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21849
|
+
AND d.rule_id IN (${inClause})`,
|
|
21457
21850
|
[since, ...ruleIds]
|
|
21458
21851
|
);
|
|
21459
21852
|
}
|
|
@@ -21463,35 +21856,24 @@ var SqliteDetectionsRepository = class {
|
|
|
21463
21856
|
var SqliteEventsRepository = class {
|
|
21464
21857
|
constructor(db) {
|
|
21465
21858
|
this.db = db;
|
|
21466
|
-
this.insertStmt = db.prepare(
|
|
21467
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
21468
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
21469
|
-
);
|
|
21470
21859
|
}
|
|
21471
21860
|
db;
|
|
21472
|
-
|
|
21473
|
-
|
|
21474
|
-
|
|
21475
|
-
this.insertStmt.run(
|
|
21476
|
-
bindParams({
|
|
21477
|
-
id: row.id,
|
|
21478
|
-
sourceTool: row.sourceTool,
|
|
21479
|
-
kind: row.kind,
|
|
21480
|
-
occurredAt: row.occurredAt,
|
|
21481
|
-
contentHash: row.contentHash,
|
|
21482
|
-
content: row.content,
|
|
21483
|
-
metadata: row.metadata
|
|
21484
|
-
})
|
|
21485
|
-
);
|
|
21486
|
-
}
|
|
21487
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
21488
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
21489
|
-
// never duplicates findings.
|
|
21861
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
21862
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
21863
|
+
// scan never duplicates findings.
|
|
21490
21864
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
21491
21865
|
// async EventsReadPort contract.
|
|
21866
|
+
//
|
|
21867
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
21868
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
21869
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
21870
|
+
// index-friendly rather than walking rows that can never match.
|
|
21492
21871
|
contentHashes() {
|
|
21493
21872
|
const rows = allRows(
|
|
21494
|
-
this.db.prepare(
|
|
21873
|
+
this.db.prepare(
|
|
21874
|
+
`SELECT content_hash FROM audit_events
|
|
21875
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
21876
|
+
)
|
|
21495
21877
|
);
|
|
21496
21878
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
21497
21879
|
}
|
|
@@ -21827,17 +22209,20 @@ function parseExceptionRow(row) {
|
|
|
21827
22209
|
}
|
|
21828
22210
|
|
|
21829
22211
|
// ../packages/persistence/src/repositories/resolution-sql.ts
|
|
21830
|
-
function
|
|
22212
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
21831
22213
|
return `(
|
|
21832
|
-
SELECT fr
|
|
22214
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
21833
22215
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
21834
22216
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21835
22217
|
LIMIT 1
|
|
21836
22218
|
)`;
|
|
21837
22219
|
}
|
|
22220
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
22221
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
22222
|
+
}
|
|
21838
22223
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
21839
|
-
SELECT finding_key, status FROM (
|
|
21840
|
-
SELECT fr.finding_key, fr.status,
|
|
22224
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
22225
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
21841
22226
|
ROW_NUMBER() OVER (
|
|
21842
22227
|
PARTITION BY fr.finding_key
|
|
21843
22228
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -21864,68 +22249,21 @@ var DAY_MS3 = 864e5;
|
|
|
21864
22249
|
var SqliteFindingsRepository = class {
|
|
21865
22250
|
constructor(db) {
|
|
21866
22251
|
this.db = db;
|
|
21867
|
-
this.insertStmt = db.prepare(
|
|
21868
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
21869
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
21870
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
21871
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
21872
|
-
event_id = excluded.event_id,
|
|
21873
|
-
category = excluded.category,
|
|
21874
|
-
severity = excluded.severity,
|
|
21875
|
-
span_start = excluded.span_start,
|
|
21876
|
-
span_end = excluded.span_end,
|
|
21877
|
-
masked_match = excluded.masked_match,
|
|
21878
|
-
action_taken = excluded.action_taken,
|
|
21879
|
-
confidence = excluded.confidence`
|
|
21880
|
-
);
|
|
21881
|
-
this.sessionDupStmt = db.prepare(
|
|
21882
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
21883
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
21884
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
21885
|
-
LIMIT 1`
|
|
21886
|
-
);
|
|
21887
22252
|
}
|
|
21888
22253
|
db;
|
|
21889
|
-
insertStmt;
|
|
21890
|
-
sessionDupStmt;
|
|
21891
|
-
insertFindings(findings, scope = {}) {
|
|
21892
|
-
for (const finding of findings) {
|
|
21893
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
21894
|
-
const row = toFindingRow(finding);
|
|
21895
|
-
this.insertStmt.run({
|
|
21896
|
-
id: row.id,
|
|
21897
|
-
eventId: row.eventId,
|
|
21898
|
-
ruleId: row.ruleId,
|
|
21899
|
-
category: row.category,
|
|
21900
|
-
severity: row.severity,
|
|
21901
|
-
spanStart: row.spanStart,
|
|
21902
|
-
spanEnd: row.spanEnd,
|
|
21903
|
-
maskedMatch: row.maskedMatch,
|
|
21904
|
-
actionTaken: row.actionTaken,
|
|
21905
|
-
confidence: row.confidence,
|
|
21906
|
-
findingKey: row.findingKey ?? null
|
|
21907
|
-
});
|
|
21908
|
-
}
|
|
21909
|
-
}
|
|
21910
|
-
// True when an earlier event in the same session already recorded a finding
|
|
21911
|
-
// with the same rule and masked value. The current event is inserted before
|
|
21912
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
21913
|
-
isSessionDuplicate(finding, sessionId) {
|
|
21914
|
-
const hit = this.sessionDupStmt.get({
|
|
21915
|
-
ruleId: finding.ruleId,
|
|
21916
|
-
maskedMatch: finding.maskedMatch,
|
|
21917
|
-
sessionId
|
|
21918
|
-
});
|
|
21919
|
-
return hit !== void 0;
|
|
21920
|
-
}
|
|
21921
22254
|
recentFindings(opts) {
|
|
21922
22255
|
const limit = opts?.limit ?? 50;
|
|
21923
22256
|
const rows = allRows(
|
|
21924
22257
|
this.db.prepare(
|
|
21925
|
-
`SELECT f.id, f.event_id,
|
|
21926
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
21927
|
-
|
|
21928
|
-
|
|
22258
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
22259
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
22260
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
22261
|
+
e.event_type AS kind
|
|
22262
|
+
FROM inspection_findings f
|
|
22263
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22264
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22265
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22266
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
21929
22267
|
LIMIT :limit`
|
|
21930
22268
|
),
|
|
21931
22269
|
{ limit }
|
|
@@ -21947,25 +22285,34 @@ var SqliteFindingsRepository = class {
|
|
|
21947
22285
|
);
|
|
21948
22286
|
}
|
|
21949
22287
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
21950
|
-
* session-stamped
|
|
22288
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
21951
22289
|
* page can label its findings link without the grouped pipeline. */
|
|
21952
22290
|
sessionFindingsCount(sessionId) {
|
|
21953
22291
|
if (!sessionId) return Promise.resolve(0);
|
|
21954
22292
|
return Promise.resolve(
|
|
21955
22293
|
countScalar(
|
|
21956
22294
|
this.db,
|
|
21957
|
-
`SELECT count(*) AS n FROM
|
|
21958
|
-
JOIN
|
|
21959
|
-
WHERE
|
|
22295
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
22296
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22297
|
+
WHERE e.root_session_id = :sessionId
|
|
22298
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
21960
22299
|
{ sessionId }
|
|
21961
22300
|
)
|
|
21962
22301
|
);
|
|
21963
22302
|
}
|
|
21964
|
-
/** Per-rule transcript firing tally for one session —
|
|
21965
|
-
*
|
|
21966
|
-
*
|
|
21967
|
-
*
|
|
21968
|
-
*
|
|
22303
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
22304
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
22305
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
22306
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
22307
|
+
* tally with the deduped groups it lists.
|
|
22308
|
+
*
|
|
22309
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
22310
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
22311
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
22312
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
22313
|
+
* session would be tallied here too, double-counting against the grouped
|
|
22314
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
22315
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
21969
22316
|
sessionFirings(sessionId) {
|
|
21970
22317
|
return Object.fromEntries(
|
|
21971
22318
|
countBy(
|
|
@@ -21975,18 +22322,25 @@ var SqliteFindingsRepository = class {
|
|
|
21975
22322
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
21976
22323
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21977
22324
|
WHERE e.root_session_id = :sessionId
|
|
22325
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21978
22326
|
GROUP BY d.rule_id`,
|
|
21979
22327
|
{ sessionId }
|
|
21980
22328
|
)
|
|
21981
22329
|
);
|
|
21982
22330
|
}
|
|
21983
22331
|
/**
|
|
21984
|
-
* Grouped findings for the dashboard — joins
|
|
21985
|
-
* toolName from
|
|
21986
|
-
*
|
|
22332
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
22333
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
22334
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
22335
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
22336
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
22337
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
22338
|
+
* severity then recency. Filtering
|
|
21987
22339
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
21988
22340
|
* reflect the full filtered set; `items` is the requested
|
|
21989
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
22341
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
22342
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
22343
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
21990
22344
|
*
|
|
21991
22345
|
* Two reads, neither of which materializes a row per finding:
|
|
21992
22346
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -21999,10 +22353,11 @@ var SqliteFindingsRepository = class {
|
|
|
21999
22353
|
* rule is ever restated in SQL.
|
|
22000
22354
|
*/
|
|
22001
22355
|
listGroupedFindings(query) {
|
|
22002
|
-
const sessionPredicate = query.sessionId ? `
|
|
22356
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
22357
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
22003
22358
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
22004
22359
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
22005
|
-
predicate
|
|
22360
|
+
predicate,
|
|
22006
22361
|
params: sessionParams
|
|
22007
22362
|
});
|
|
22008
22363
|
const rows = allRows(
|
|
@@ -22010,24 +22365,26 @@ var SqliteFindingsRepository = class {
|
|
|
22010
22365
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
22011
22366
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
22012
22367
|
FROM (
|
|
22013
|
-
SELECT f.id AS id,
|
|
22014
|
-
|
|
22368
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
22369
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
22015
22370
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
22016
|
-
e.
|
|
22017
|
-
json_extract(e.
|
|
22018
|
-
json_extract(e.
|
|
22019
|
-
json_extract(e.
|
|
22020
|
-
e.
|
|
22371
|
+
e.started_at AS occurred_at,
|
|
22372
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
22373
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
22374
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
22375
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
22376
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
22021
22377
|
latest.status AS latest_status,
|
|
22022
22378
|
ROW_NUMBER() OVER (
|
|
22023
|
-
PARTITION BY
|
|
22024
|
-
ORDER BY e.
|
|
22379
|
+
PARTITION BY d.rule_id
|
|
22380
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
22025
22381
|
) AS rn
|
|
22026
|
-
FROM
|
|
22027
|
-
JOIN
|
|
22382
|
+
FROM inspection_findings f
|
|
22383
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22384
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22028
22385
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22029
22386
|
ON latest.finding_key = f.finding_key
|
|
22030
|
-
${
|
|
22387
|
+
${predicate}
|
|
22031
22388
|
)
|
|
22032
22389
|
WHERE rn <= :cap
|
|
22033
22390
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -22054,17 +22411,29 @@ var SqliteFindingsRepository = class {
|
|
|
22054
22411
|
severity: query.severity,
|
|
22055
22412
|
providers: query.provider,
|
|
22056
22413
|
actions: query.action,
|
|
22414
|
+
statuses: query.status,
|
|
22057
22415
|
subtype: query.subtype,
|
|
22058
22416
|
q: query.q
|
|
22059
22417
|
};
|
|
22060
22418
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
22061
22419
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
22420
|
+
const statusFilter = query.status ?? [];
|
|
22062
22421
|
const totals = {
|
|
22063
|
-
findings: sorted.reduce((acc, g) =>
|
|
22422
|
+
findings: sorted.reduce((acc, g) => {
|
|
22423
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
22424
|
+
const agg = aggregates.get(g.id);
|
|
22425
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
22426
|
+
}, 0),
|
|
22064
22427
|
groups: sorted.length
|
|
22065
22428
|
};
|
|
22066
22429
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
22067
|
-
const
|
|
22430
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
22431
|
+
const items = sorted.slice(0, limit).map(
|
|
22432
|
+
(g) => statusSet ? {
|
|
22433
|
+
...g,
|
|
22434
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
22435
|
+
} : g
|
|
22436
|
+
);
|
|
22068
22437
|
return Promise.resolve({
|
|
22069
22438
|
totals,
|
|
22070
22439
|
facets,
|
|
@@ -22078,45 +22447,62 @@ var SqliteFindingsRepository = class {
|
|
|
22078
22447
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
22079
22448
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
22080
22449
|
*
|
|
22081
|
-
*
|
|
22082
|
-
*
|
|
22083
|
-
*
|
|
22084
|
-
* status
|
|
22085
|
-
*
|
|
22086
|
-
*
|
|
22450
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
22451
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
22452
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
22453
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
22454
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
22455
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
22456
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
22457
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
22458
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
22459
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
22460
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
22087
22461
|
* a group's row stays small however many findings it holds.
|
|
22088
22462
|
*
|
|
22089
22463
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
22090
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
22091
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
22092
|
-
* that is a string proportional to the store (~8MB over
|
|
22093
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
22094
|
-
* match an instance outside the preview, which searching
|
|
22095
|
-
* would silently lose, so it is fetched only when the
|
|
22096
|
-
* carries a `q`.
|
|
22464
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
22465
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
22466
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
22467
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
22468
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
22469
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
22470
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
22471
|
+
* path repeating across tuples.)
|
|
22097
22472
|
*/
|
|
22098
22473
|
groupAggregates(withSearchText, scope) {
|
|
22099
|
-
const
|
|
22100
|
-
group_concat(DISTINCT json_extract(e.
|
|
22101
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
22474
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
22475
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
22476
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
22102
22477
|
const rows = this.db.prepare(
|
|
22103
|
-
`SELECT
|
|
22104
|
-
|
|
22105
|
-
max(
|
|
22106
|
-
group_concat(
|
|
22107
|
-
group_concat(
|
|
22108
|
-
group_concat(
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22114
|
-
|
|
22115
|
-
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22478
|
+
`SELECT rule_id,
|
|
22479
|
+
sum(tuple_count) AS instance_count,
|
|
22480
|
+
max(latest_at) AS latest_at,
|
|
22481
|
+
group_concat(source_tools) AS source_tools,
|
|
22482
|
+
group_concat(actions_taken) AS actions_taken,
|
|
22483
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
22484
|
+
group_concat(repos) AS repos,
|
|
22485
|
+
group_concat(files) AS files,
|
|
22486
|
+
group_concat(tool_names) AS tool_names
|
|
22487
|
+
FROM (
|
|
22488
|
+
SELECT d.rule_id AS rule_id,
|
|
22489
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
22490
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
22491
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
22492
|
+
count(*) AS tuple_count,
|
|
22493
|
+
max(e.started_at) AS latest_at,
|
|
22494
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
22495
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
22496
|
+
${innerSearchColumns}
|
|
22497
|
+
FROM inspection_findings f
|
|
22498
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22499
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22500
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22501
|
+
ON latest.finding_key = f.finding_key
|
|
22502
|
+
${scope.predicate}
|
|
22503
|
+
GROUP BY d.rule_id, status_tuple
|
|
22504
|
+
)
|
|
22505
|
+
GROUP BY rule_id`
|
|
22120
22506
|
).all(scope.params);
|
|
22121
22507
|
return new Map(
|
|
22122
22508
|
rows.map((r) => [
|
|
@@ -22126,13 +22512,14 @@ var SqliteFindingsRepository = class {
|
|
|
22126
22512
|
sourceTools: splitConcat(r.source_tools),
|
|
22127
22513
|
actionsTaken: splitConcat(r.actions_taken),
|
|
22128
22514
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
22129
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
22515
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
22130
22516
|
return {
|
|
22131
22517
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
22132
22518
|
// so the marker stands in for the key itself (never rendered).
|
|
22133
22519
|
kind,
|
|
22134
22520
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
22135
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
22521
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
22522
|
+
count: Number(count)
|
|
22136
22523
|
};
|
|
22137
22524
|
}),
|
|
22138
22525
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -22149,10 +22536,21 @@ var SqliteFindingsRepository = class {
|
|
|
22149
22536
|
);
|
|
22150
22537
|
}
|
|
22151
22538
|
healthSummary() {
|
|
22152
|
-
const total = countScalar(
|
|
22539
|
+
const total = countScalar(
|
|
22540
|
+
this.db,
|
|
22541
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
22542
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22543
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
22544
|
+
);
|
|
22153
22545
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
22154
22546
|
const grouped = allRows(
|
|
22155
|
-
this.db.prepare(
|
|
22547
|
+
this.db.prepare(
|
|
22548
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
22549
|
+
FROM inspection_findings f
|
|
22550
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22551
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22552
|
+
GROUP BY f.action_taken`
|
|
22553
|
+
)
|
|
22156
22554
|
);
|
|
22157
22555
|
for (const row of grouped) {
|
|
22158
22556
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -22160,12 +22558,15 @@ var SqliteFindingsRepository = class {
|
|
|
22160
22558
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
22161
22559
|
const sevRows = allRows(
|
|
22162
22560
|
this.db.prepare(
|
|
22163
|
-
`SELECT
|
|
22164
|
-
FROM
|
|
22561
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
22562
|
+
FROM inspection_findings f
|
|
22563
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22564
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22165
22565
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22166
22566
|
ON latest.finding_key = f.finding_key
|
|
22167
|
-
WHERE
|
|
22168
|
-
|
|
22567
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22568
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
22569
|
+
GROUP BY d.severity`
|
|
22169
22570
|
)
|
|
22170
22571
|
);
|
|
22171
22572
|
for (const row of sevRows) {
|
|
@@ -22186,9 +22587,11 @@ var SqliteFindingsRepository = class {
|
|
|
22186
22587
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
22187
22588
|
const rows = allRows(
|
|
22188
22589
|
this.db.prepare(
|
|
22189
|
-
`SELECT date(e.
|
|
22190
|
-
FROM
|
|
22191
|
-
|
|
22590
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
22591
|
+
FROM inspection_findings f
|
|
22592
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22593
|
+
WHERE e.started_at >= :since
|
|
22594
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22192
22595
|
GROUP BY day, f.action_taken`
|
|
22193
22596
|
),
|
|
22194
22597
|
{ since }
|
|
@@ -22253,15 +22656,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
22253
22656
|
this.insertStmt = db.prepare(
|
|
22254
22657
|
`INSERT INTO inspection_findings
|
|
22255
22658
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
22256
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
22659
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
22660
|
+
finding_key, first_detected_at)
|
|
22257
22661
|
VALUES
|
|
22258
22662
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
22259
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
22260
|
-
|
|
22663
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
22664
|
+
:findingKey,
|
|
22665
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
22666
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
22667
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
22668
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
22669
|
+
audit_event_id = excluded.audit_event_id,
|
|
22670
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
22671
|
+
classified_data_id = excluded.classified_data_id,
|
|
22672
|
+
span_start = excluded.span_start,
|
|
22673
|
+
span_end = excluded.span_end,
|
|
22674
|
+
masked_match = excluded.masked_match,
|
|
22675
|
+
action_taken = excluded.action_taken,
|
|
22676
|
+
confidence = excluded.confidence`
|
|
22677
|
+
);
|
|
22678
|
+
this.sessionDupStmt = db.prepare(
|
|
22679
|
+
`SELECT 1 FROM inspection_findings f
|
|
22680
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22681
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22682
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
22683
|
+
AND e.root_session_id = :sessionId
|
|
22684
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22685
|
+
LIMIT 1`
|
|
22686
|
+
);
|
|
22687
|
+
this.eventDupStmt = db.prepare(
|
|
22688
|
+
`SELECT 1 FROM inspection_findings f
|
|
22689
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22690
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
22691
|
+
AND f.masked_match = :maskedMatch
|
|
22692
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
22693
|
+
LIMIT 1`
|
|
22261
22694
|
);
|
|
22262
22695
|
}
|
|
22263
22696
|
db;
|
|
22264
22697
|
insertStmt;
|
|
22698
|
+
sessionDupStmt;
|
|
22699
|
+
eventDupStmt;
|
|
22700
|
+
// True when an earlier event in the same session already recorded a finding
|
|
22701
|
+
// with the same rule and masked value. The current event's own findings are
|
|
22702
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
22703
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
22704
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
22705
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
22706
|
+
}
|
|
22707
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
22708
|
+
// recorded against the given audit event.
|
|
22709
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
22710
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
22711
|
+
}
|
|
22265
22712
|
insertFinding(input) {
|
|
22266
22713
|
const row = toInspectionFindingRow(input);
|
|
22267
22714
|
this.insertStmt.run(
|
|
@@ -22274,7 +22721,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
22274
22721
|
spanEnd: row.spanEnd,
|
|
22275
22722
|
maskedMatch: row.maskedMatch,
|
|
22276
22723
|
actionTaken: row.actionTaken,
|
|
22277
|
-
confidence: row.confidence
|
|
22724
|
+
confidence: row.confidence,
|
|
22725
|
+
findingKey: row.findingKey,
|
|
22726
|
+
firstDetectedAt: row.firstDetectedAt
|
|
22278
22727
|
})
|
|
22279
22728
|
);
|
|
22280
22729
|
}
|
|
@@ -22546,7 +22995,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
22546
22995
|
installedRuleset() {
|
|
22547
22996
|
const rows = allRows(
|
|
22548
22997
|
this.db.prepare(
|
|
22549
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
22998
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
22550
22999
|
)
|
|
22551
23000
|
);
|
|
22552
23001
|
const out = {
|
|
@@ -22554,7 +23003,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
22554
23003
|
enabledPacks: 0,
|
|
22555
23004
|
rules: [],
|
|
22556
23005
|
invalidRules: 0,
|
|
22557
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
23006
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
23007
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
22558
23008
|
};
|
|
22559
23009
|
for (const row of rows) {
|
|
22560
23010
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -22576,6 +23026,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
22576
23026
|
if (parsed.success) {
|
|
22577
23027
|
out.rules.push(parsed.data);
|
|
22578
23028
|
out.ruleActions.set(parsed.data.id, action);
|
|
23029
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
22579
23030
|
} else out.invalidRules += 1;
|
|
22580
23031
|
}
|
|
22581
23032
|
}
|
|
@@ -23750,19 +24201,19 @@ var SqliteResolutionsRepository = class {
|
|
|
23750
24201
|
);
|
|
23751
24202
|
this.openAtRestStmt = db.prepare(
|
|
23752
24203
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
23753
|
-
FROM
|
|
23754
|
-
JOIN
|
|
23755
|
-
WHERE e.
|
|
23756
|
-
AND json_extract(e.
|
|
24204
|
+
FROM inspection_findings f
|
|
24205
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24206
|
+
WHERE e.event_type = 'code_change'
|
|
24207
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
23757
24208
|
AND f.finding_key IS NOT NULL
|
|
23758
24209
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
23759
24210
|
);
|
|
23760
24211
|
this.resolvedAtRestStmt = db.prepare(
|
|
23761
24212
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
23762
|
-
FROM
|
|
23763
|
-
JOIN
|
|
23764
|
-
WHERE e.
|
|
23765
|
-
AND json_extract(e.
|
|
24213
|
+
FROM inspection_findings f
|
|
24214
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24215
|
+
WHERE e.event_type = 'code_change'
|
|
24216
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
23766
24217
|
AND f.finding_key IS NOT NULL
|
|
23767
24218
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
23768
24219
|
);
|
|
@@ -23985,25 +24436,27 @@ var SqliteSecurityRepository = class {
|
|
|
23985
24436
|
severitySummary() {
|
|
23986
24437
|
const rows = allRows(
|
|
23987
24438
|
this.db.prepare(
|
|
23988
|
-
`SELECT
|
|
24439
|
+
`SELECT d.severity AS severity,
|
|
23989
24440
|
COUNT(*) AS count,
|
|
23990
24441
|
SUM(CASE
|
|
23991
|
-
WHEN e.
|
|
24442
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
23992
24443
|
WHEN f.finding_key IS NULL THEN 0
|
|
23993
24444
|
WHEN latest.status = 'resolved' THEN 1
|
|
23994
24445
|
ELSE 0
|
|
23995
24446
|
END) AS caught,
|
|
23996
24447
|
SUM(CASE
|
|
23997
|
-
WHEN e.
|
|
24448
|
+
WHEN e.event_type = 'code_change'
|
|
23998
24449
|
AND f.finding_key IS NOT NULL
|
|
23999
24450
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
24000
24451
|
ELSE 0
|
|
24001
24452
|
END) AS open_at_rest
|
|
24002
|
-
FROM
|
|
24003
|
-
JOIN
|
|
24453
|
+
FROM inspection_findings f
|
|
24454
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24455
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
24004
24456
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
24005
24457
|
ON latest.finding_key = f.finding_key
|
|
24006
|
-
|
|
24458
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
24459
|
+
GROUP BY d.severity`
|
|
24007
24460
|
)
|
|
24008
24461
|
);
|
|
24009
24462
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -24069,7 +24522,7 @@ var SqliteSecurityRepository = class {
|
|
|
24069
24522
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
24070
24523
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
24071
24524
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
24072
|
-
// (
|
|
24525
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
24073
24526
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
24074
24527
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
24075
24528
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -24094,30 +24547,20 @@ var SqliteSecurityRepository = class {
|
|
|
24094
24547
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
24095
24548
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
24096
24549
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
24097
|
-
//
|
|
24098
|
-
// the parent event's
|
|
24099
|
-
// backfill left null.
|
|
24100
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
24101
|
-
|
|
24102
|
-
|
|
24103
|
-
|
|
24104
|
-
|
|
24105
|
-
|
|
24106
|
-
|
|
24107
|
-
|
|
24108
|
-
|
|
24109
|
-
WHERE fr.finding_key = f.finding_key
|
|
24110
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
24111
|
-
LIMIT 1
|
|
24112
|
-
) AS latest_method,
|
|
24113
|
-
(
|
|
24114
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
24115
|
-
WHERE fr.finding_key = f.finding_key
|
|
24116
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
24117
|
-
LIMIT 1
|
|
24118
|
-
) AS latest_resolved_at
|
|
24119
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
24550
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
24551
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
24552
|
+
// legacy/edge row the backfill left null.
|
|
24553
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
24554
|
+
latest.status AS latest_status,
|
|
24555
|
+
latest.method AS latest_method,
|
|
24556
|
+
latest.resolved_at AS latest_resolved_at
|
|
24557
|
+
FROM inspection_findings f
|
|
24558
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24559
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
24560
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
24561
|
+
ON latest.finding_key = f.finding_key
|
|
24120
24562
|
WHERE f.finding_key IS NOT NULL
|
|
24563
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
24121
24564
|
AND EXISTS (
|
|
24122
24565
|
SELECT 1 FROM finding_resolution fr
|
|
24123
24566
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -24164,11 +24607,13 @@ var SqliteSecurityRepository = class {
|
|
|
24164
24607
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
24165
24608
|
const rows = allRows(
|
|
24166
24609
|
this.db.prepare(
|
|
24167
|
-
`SELECT json_extract(e.
|
|
24168
|
-
FROM
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
AND
|
|
24610
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
24611
|
+
FROM inspection_findings f
|
|
24612
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24613
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
24614
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
24615
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
24616
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
24172
24617
|
GROUP BY repo
|
|
24173
24618
|
ORDER BY c DESC, repo
|
|
24174
24619
|
LIMIT :limit`
|
|
@@ -24192,44 +24637,28 @@ var SqliteSecurityRepository = class {
|
|
|
24192
24637
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
24193
24638
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
24194
24639
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
24195
|
-
// finding's parent event (
|
|
24196
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
24197
|
-
// capped at `limit`.
|
|
24640
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
24641
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
24642
|
+
// DESC, capped at `limit`.
|
|
24198
24643
|
recentlyResolved(limit = 20) {
|
|
24199
24644
|
const rows = allRows(
|
|
24200
24645
|
this.db.prepare(
|
|
24201
24646
|
`SELECT f.finding_key AS finding_key,
|
|
24202
|
-
|
|
24203
|
-
|
|
24204
|
-
json_extract(e.
|
|
24205
|
-
COALESCE(f.first_detected_at, e.
|
|
24206
|
-
|
|
24207
|
-
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
WHERE e.kind = 'code_change'
|
|
24647
|
+
d.rule_id AS rule_id,
|
|
24648
|
+
d.severity AS severity,
|
|
24649
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
24650
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
24651
|
+
latest.resolved_at AS latest_resolved_at
|
|
24652
|
+
FROM inspection_findings f
|
|
24653
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24654
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
24655
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
24656
|
+
ON latest.finding_key = f.finding_key
|
|
24657
|
+
WHERE e.event_type = 'code_change'
|
|
24214
24658
|
AND f.finding_key IS NOT NULL
|
|
24215
|
-
AND
|
|
24216
|
-
|
|
24217
|
-
|
|
24218
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
24219
|
-
LIMIT 1
|
|
24220
|
-
) = 'resolved'
|
|
24221
|
-
AND (
|
|
24222
|
-
SELECT fr.method FROM finding_resolution fr
|
|
24223
|
-
WHERE fr.finding_key = f.finding_key
|
|
24224
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
24225
|
-
LIMIT 1
|
|
24226
|
-
) = 'fixed-at-source'
|
|
24227
|
-
AND (
|
|
24228
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
24229
|
-
WHERE fr.finding_key = f.finding_key
|
|
24230
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
24231
|
-
LIMIT 1
|
|
24232
|
-
) IS NOT NULL
|
|
24659
|
+
AND latest.status = 'resolved'
|
|
24660
|
+
AND latest.method = 'fixed-at-source'
|
|
24661
|
+
AND latest.resolved_at IS NOT NULL
|
|
24233
24662
|
ORDER BY latest_resolved_at DESC
|
|
24234
24663
|
LIMIT :limit`
|
|
24235
24664
|
),
|
|
@@ -24248,15 +24677,18 @@ var SqliteSecurityRepository = class {
|
|
|
24248
24677
|
return Promise.resolve({ items });
|
|
24249
24678
|
}
|
|
24250
24679
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
24251
|
-
// epoch-millis timestamp.
|
|
24680
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
24252
24681
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
24253
24682
|
findingsInRange(fromMs, toMs) {
|
|
24254
24683
|
const rows = allRows(
|
|
24255
24684
|
this.db.prepare(
|
|
24256
|
-
`SELECT e.
|
|
24257
|
-
FROM
|
|
24258
|
-
|
|
24259
|
-
|
|
24685
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
24686
|
+
FROM inspection_findings f
|
|
24687
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
24688
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
24689
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
24690
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
24691
|
+
ORDER BY e.started_at`
|
|
24260
24692
|
),
|
|
24261
24693
|
{ from: fromMs, to: toMs }
|
|
24262
24694
|
);
|
|
@@ -25071,63 +25503,159 @@ function purgeSampleData(db) {
|
|
|
25071
25503
|
function linkHost(input, hostId) {
|
|
25072
25504
|
return hostId ? { ...input, hostId } : input;
|
|
25073
25505
|
}
|
|
25506
|
+
function closeQuietly(db) {
|
|
25507
|
+
try {
|
|
25508
|
+
db.close();
|
|
25509
|
+
} catch {
|
|
25510
|
+
}
|
|
25511
|
+
}
|
|
25074
25512
|
function openWithPragmas(file2) {
|
|
25075
25513
|
const db = new DatabaseSync(file2);
|
|
25076
|
-
|
|
25077
|
-
|
|
25078
|
-
|
|
25514
|
+
try {
|
|
25515
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
25516
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
25517
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
25518
|
+
} catch (err) {
|
|
25519
|
+
closeQuietly(db);
|
|
25520
|
+
throw err;
|
|
25521
|
+
}
|
|
25079
25522
|
return db;
|
|
25080
25523
|
}
|
|
25081
25524
|
function backupLegacyStore(file2) {
|
|
25082
25525
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
25083
|
-
|
|
25084
|
-
|
|
25085
|
-
|
|
25526
|
+
renameSync2(file2, backup);
|
|
25527
|
+
tightenFile(backup);
|
|
25528
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
25529
|
+
if (existsSync2(sidecar)) rmSync2(sidecar);
|
|
25086
25530
|
}
|
|
25087
25531
|
return backup;
|
|
25088
25532
|
}
|
|
25533
|
+
function openAndInitialize(file2) {
|
|
25534
|
+
let db = openWithPragmas(file2);
|
|
25535
|
+
try {
|
|
25536
|
+
if (isForeignSqliteLineage(db)) {
|
|
25537
|
+
db.close();
|
|
25538
|
+
const backup = backupLegacyStore(file2);
|
|
25539
|
+
db = openWithPragmas(file2);
|
|
25540
|
+
akaWarn(
|
|
25541
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
25542
|
+
);
|
|
25543
|
+
}
|
|
25544
|
+
applyMigrations(db, file2);
|
|
25545
|
+
tightenPerms(file2);
|
|
25546
|
+
const policies = new SqlitePoliciesRepository(db);
|
|
25547
|
+
const installedPacks = new SqliteInstalledPacksRepository(db);
|
|
25548
|
+
const repositories = {
|
|
25549
|
+
events: new SqliteEventsRepository(db),
|
|
25550
|
+
findings: new SqliteFindingsRepository(db),
|
|
25551
|
+
policies,
|
|
25552
|
+
installedPacks,
|
|
25553
|
+
scanLedger: new SqliteScanLedgerRepository(db),
|
|
25554
|
+
exceptions: new SqliteExceptionsRepository(db),
|
|
25555
|
+
resolutions: new SqliteResolutionsRepository(db),
|
|
25556
|
+
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
25557
|
+
security: new SqliteSecurityRepository(db),
|
|
25558
|
+
detections: new SqliteDetectionsRepository(db),
|
|
25559
|
+
shares: new SqliteSharesRepository(db),
|
|
25560
|
+
policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
|
|
25561
|
+
inventory: new SqliteInventoryRepository(db),
|
|
25562
|
+
inventoryAssets: new SqliteInventoryAssetsRepository(db),
|
|
25563
|
+
projectFiles: new SqliteProjectFilesRepository(db),
|
|
25564
|
+
activity: new SqliteActivityRepository(db),
|
|
25565
|
+
sourceProject: new SqliteSourceProjectRepository(db),
|
|
25566
|
+
auditEvents: new SqliteAuditEventsRepository(db),
|
|
25567
|
+
classifiedData: new SqliteClassifiedDataRepository(db),
|
|
25568
|
+
inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
|
|
25569
|
+
inspectionFindings: new SqliteInspectionFindingsRepository(db),
|
|
25570
|
+
configInventory: new SqliteConfigInventoryRepository(db)
|
|
25571
|
+
};
|
|
25572
|
+
policies.seedDefaults();
|
|
25573
|
+
return { db, ...repositories };
|
|
25574
|
+
} catch (err) {
|
|
25575
|
+
closeQuietly(db);
|
|
25576
|
+
throw err;
|
|
25577
|
+
}
|
|
25578
|
+
}
|
|
25089
25579
|
function openLocalDatabase(dir) {
|
|
25090
25580
|
ensureDataDirSync(dir);
|
|
25091
25581
|
const file2 = join2(dir, DB_FILENAME);
|
|
25092
|
-
|
|
25093
|
-
|
|
25094
|
-
|
|
25095
|
-
|
|
25096
|
-
|
|
25097
|
-
|
|
25098
|
-
|
|
25099
|
-
|
|
25100
|
-
|
|
25101
|
-
|
|
25102
|
-
|
|
25103
|
-
|
|
25104
|
-
|
|
25105
|
-
|
|
25106
|
-
|
|
25107
|
-
|
|
25108
|
-
|
|
25109
|
-
|
|
25110
|
-
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
const projectFiles = new SqliteProjectFilesRepository(db);
|
|
25118
|
-
const activity = new SqliteActivityRepository(db);
|
|
25119
|
-
const sourceProject = new SqliteSourceProjectRepository(db);
|
|
25120
|
-
const auditEvents = new SqliteAuditEventsRepository(db);
|
|
25121
|
-
const classifiedData = new SqliteClassifiedDataRepository(db);
|
|
25122
|
-
const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
|
|
25123
|
-
const inspectionFindings = new SqliteInspectionFindingsRepository(db);
|
|
25124
|
-
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
25125
|
-
policies.seedDefaults();
|
|
25582
|
+
const {
|
|
25583
|
+
db,
|
|
25584
|
+
events,
|
|
25585
|
+
findings,
|
|
25586
|
+
policies,
|
|
25587
|
+
installedPacks,
|
|
25588
|
+
scanLedger,
|
|
25589
|
+
exceptions,
|
|
25590
|
+
resolutions,
|
|
25591
|
+
ruleProbeCache,
|
|
25592
|
+
security,
|
|
25593
|
+
detections,
|
|
25594
|
+
shares,
|
|
25595
|
+
policyCatalog,
|
|
25596
|
+
inventory,
|
|
25597
|
+
inventoryAssets,
|
|
25598
|
+
projectFiles,
|
|
25599
|
+
activity,
|
|
25600
|
+
sourceProject,
|
|
25601
|
+
auditEvents,
|
|
25602
|
+
classifiedData,
|
|
25603
|
+
inspectionDefinitions,
|
|
25604
|
+
inspectionFindings,
|
|
25605
|
+
configInventory
|
|
25606
|
+
} = openAndInitialize(file2);
|
|
25126
25607
|
function recordCapture(event, detected) {
|
|
25127
25608
|
failOpenTransaction(db, () => {
|
|
25128
|
-
events.insertEvent(event);
|
|
25129
25609
|
const sessionId = event.metadata?.sessionId;
|
|
25130
|
-
|
|
25610
|
+
if (sessionId) {
|
|
25611
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
25612
|
+
}
|
|
25613
|
+
const auditEventId = captureId(
|
|
25614
|
+
sessionId ?? null,
|
|
25615
|
+
event.contentHash,
|
|
25616
|
+
event.metadata?.filePath ?? null
|
|
25617
|
+
);
|
|
25618
|
+
auditEvents.insertAuditEvent({
|
|
25619
|
+
id: auditEventId,
|
|
25620
|
+
eventType: event.kind,
|
|
25621
|
+
startedAt: event.occurredAt,
|
|
25622
|
+
parentId: sessionId,
|
|
25623
|
+
rootSessionId: sessionId,
|
|
25624
|
+
content: event.content,
|
|
25625
|
+
contentHash: event.contentHash,
|
|
25626
|
+
attributes: toCaptureAttributes(event)
|
|
25627
|
+
});
|
|
25628
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
25629
|
+
for (const finding of detected) {
|
|
25630
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
25631
|
+
continue;
|
|
25632
|
+
}
|
|
25633
|
+
if (inspectionFindings.isEventDuplicate(
|
|
25634
|
+
auditEventId,
|
|
25635
|
+
finding.ruleId,
|
|
25636
|
+
finding.maskedMatch,
|
|
25637
|
+
finding.span.start,
|
|
25638
|
+
finding.span.end
|
|
25639
|
+
)) {
|
|
25640
|
+
continue;
|
|
25641
|
+
}
|
|
25642
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
25643
|
+
let definitionId = definitionIds.get(key);
|
|
25644
|
+
if (!definitionId) {
|
|
25645
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
25646
|
+
definitionIds.set(key, definitionId);
|
|
25647
|
+
}
|
|
25648
|
+
inspectionFindings.insertFinding({
|
|
25649
|
+
id: finding.id,
|
|
25650
|
+
auditEventId,
|
|
25651
|
+
inspectionDefinitionId: definitionId,
|
|
25652
|
+
span: finding.span,
|
|
25653
|
+
maskedMatch: finding.maskedMatch,
|
|
25654
|
+
actionTaken: finding.actionTaken,
|
|
25655
|
+
confidence: finding.confidence,
|
|
25656
|
+
findingKey: finding.findingKey ?? void 0
|
|
25657
|
+
});
|
|
25658
|
+
}
|
|
25131
25659
|
});
|
|
25132
25660
|
}
|
|
25133
25661
|
function ensureInventory(ctx) {
|
|
@@ -25275,10 +25803,21 @@ function openLocalDatabase(dir) {
|
|
|
25275
25803
|
};
|
|
25276
25804
|
}
|
|
25277
25805
|
|
|
25806
|
+
// ../packages/persistence/src/finding-key.ts
|
|
25807
|
+
import { createHash as createHash3 } from "crypto";
|
|
25808
|
+
function normalizeFilePath(filePath) {
|
|
25809
|
+
return filePath.replaceAll("\\", "/");
|
|
25810
|
+
}
|
|
25811
|
+
function computeFindingKey(input) {
|
|
25812
|
+
const normalizedPath = normalizeFilePath(input.filePath);
|
|
25813
|
+
return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
25814
|
+
}
|
|
25815
|
+
|
|
25278
25816
|
// ../packages/persistence/src/fingerprint.ts
|
|
25279
25817
|
import { createHmac, randomBytes } from "crypto";
|
|
25280
|
-
import {
|
|
25818
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
25281
25819
|
import { join as join3 } from "path";
|
|
25820
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
25282
25821
|
var KEY_FILENAME = "exception.key";
|
|
25283
25822
|
var KEY_MATERIAL_BYTES = 32;
|
|
25284
25823
|
function keyFilePath(dataDir2) {
|
|
@@ -25302,18 +25841,52 @@ function parseKeyFile(raw) {
|
|
|
25302
25841
|
}
|
|
25303
25842
|
return { version: version2, material: bytes };
|
|
25304
25843
|
}
|
|
25844
|
+
var KEY_VERSION_TABLES = ["exceptions", "blocked_detections"];
|
|
25845
|
+
var SQLITE_ERROR = 1;
|
|
25846
|
+
var FLOOR_BUSY_TIMEOUT_MS = 250;
|
|
25847
|
+
var FloorUnreadableError = class extends Error {
|
|
25848
|
+
code = "floor-unreadable";
|
|
25849
|
+
constructor(cause) {
|
|
25850
|
+
super(
|
|
25851
|
+
`cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
25852
|
+
{ cause }
|
|
25853
|
+
);
|
|
25854
|
+
this.name = "FloorUnreadableError";
|
|
25855
|
+
}
|
|
25856
|
+
};
|
|
25857
|
+
function storedKeyVersionFloor(dataDir2) {
|
|
25858
|
+
const file2 = join3(dataDir2, DB_FILENAME);
|
|
25859
|
+
if (!existsSync3(file2)) return 0;
|
|
25860
|
+
let db;
|
|
25861
|
+
try {
|
|
25862
|
+
db = new DatabaseSync2(file2, { readOnly: true });
|
|
25863
|
+
db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
|
|
25864
|
+
let floor = 0;
|
|
25865
|
+
for (const table of KEY_VERSION_TABLES) {
|
|
25866
|
+
try {
|
|
25867
|
+
const row = getRow(
|
|
25868
|
+
db.prepare(`SELECT MAX(key_version) AS v FROM ${table}`)
|
|
25869
|
+
);
|
|
25870
|
+
floor = Math.max(floor, row?.v ?? 0);
|
|
25871
|
+
} catch (err) {
|
|
25872
|
+
if (err.errcode !== SQLITE_ERROR) {
|
|
25873
|
+
throw new FloorUnreadableError(err);
|
|
25874
|
+
}
|
|
25875
|
+
}
|
|
25876
|
+
}
|
|
25877
|
+
return floor;
|
|
25878
|
+
} catch (err) {
|
|
25879
|
+
throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
|
|
25880
|
+
} finally {
|
|
25881
|
+
db?.close();
|
|
25882
|
+
}
|
|
25883
|
+
}
|
|
25305
25884
|
function writeKeyFile(dataDir2, key) {
|
|
25306
25885
|
ensureDataDirSync(dataDir2);
|
|
25307
25886
|
const file2 = keyFilePath(dataDir2);
|
|
25308
|
-
const tmp = `${file2}.tmp`;
|
|
25309
25887
|
const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
|
|
25310
|
-
|
|
25311
|
-
|
|
25312
|
-
renameSync2(tmp, file2);
|
|
25313
|
-
try {
|
|
25314
|
-
chmodSync2(file2, DATA_FILE_MODE);
|
|
25315
|
-
} catch {
|
|
25316
|
-
}
|
|
25888
|
+
writeOwnerOnlyFileSync(file2, `${body}
|
|
25889
|
+
`);
|
|
25317
25890
|
return key;
|
|
25318
25891
|
}
|
|
25319
25892
|
function readFingerprintKey(dataDir2) {
|
|
@@ -25329,28 +25902,34 @@ function readFingerprintKey(dataDir2) {
|
|
|
25329
25902
|
function loadOrCreateFingerprintKey(dataDir2) {
|
|
25330
25903
|
const existing = readFingerprintKey(dataDir2);
|
|
25331
25904
|
if (existing) {
|
|
25332
|
-
|
|
25333
|
-
chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
|
|
25334
|
-
} catch {
|
|
25335
|
-
}
|
|
25905
|
+
tightenFile(keyFilePath(dataDir2));
|
|
25336
25906
|
return existing;
|
|
25337
25907
|
}
|
|
25338
|
-
return writeKeyFile(dataDir2, {
|
|
25908
|
+
return writeKeyFile(dataDir2, {
|
|
25909
|
+
version: storedKeyVersionFloor(dataDir2) + 1,
|
|
25910
|
+
material: randomBytes(KEY_MATERIAL_BYTES)
|
|
25911
|
+
});
|
|
25339
25912
|
}
|
|
25340
25913
|
function rotateFingerprintKey(dataDir2) {
|
|
25341
25914
|
const existing = readFingerprintKey(dataDir2);
|
|
25342
25915
|
return writeKeyFile(dataDir2, {
|
|
25343
|
-
version: (existing?.version ?? 0) + 1,
|
|
25916
|
+
version: Math.max(existing?.version ?? 0, storedKeyVersionFloor(dataDir2)) + 1,
|
|
25344
25917
|
material: randomBytes(KEY_MATERIAL_BYTES)
|
|
25345
25918
|
});
|
|
25346
25919
|
}
|
|
25347
25920
|
function fingerprintValue(key, raw) {
|
|
25348
25921
|
return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
|
|
25349
25922
|
}
|
|
25923
|
+
function isCurrentKeyVersion(key, keyVersion) {
|
|
25924
|
+
return isMatchableUnder(keyVersion, keyStateOf(key));
|
|
25925
|
+
}
|
|
25926
|
+
function keyStateOf(key) {
|
|
25927
|
+
return key === null ? { status: "absent" } : { status: "present", version: key.version };
|
|
25928
|
+
}
|
|
25350
25929
|
|
|
25351
25930
|
// ../packages/persistence/src/local-layout.ts
|
|
25352
|
-
import {
|
|
25353
|
-
import {
|
|
25931
|
+
import { renameSync as renameSync3 } from "fs";
|
|
25932
|
+
import { mkdir } from "fs/promises";
|
|
25354
25933
|
import { homedir as homedir3 } from "os";
|
|
25355
25934
|
import { join as join4 } from "path";
|
|
25356
25935
|
function defaultDataDir() {
|
|
@@ -25370,7 +25949,7 @@ function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
|
25370
25949
|
}
|
|
25371
25950
|
|
|
25372
25951
|
// ../packages/persistence/src/settings.ts
|
|
25373
|
-
import { readFileSync as readFileSync3
|
|
25952
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
25374
25953
|
import { join as join5 } from "path";
|
|
25375
25954
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
25376
25955
|
const record2 = readJson(join5(settingsDir(base), "settings.json"));
|
|
@@ -25392,9 +25971,13 @@ function readJson(file2) {
|
|
|
25392
25971
|
}
|
|
25393
25972
|
|
|
25394
25973
|
// ../packages/persistence/src/warn-era-cap.ts
|
|
25395
|
-
import { existsSync as
|
|
25974
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
25396
25975
|
import { join as join6 } from "path";
|
|
25397
25976
|
|
|
25977
|
+
// ../packages/plugin-sdk/src/config.ts
|
|
25978
|
+
import { existsSync as existsSync5 } from "fs";
|
|
25979
|
+
import { join as join7 } from "path";
|
|
25980
|
+
|
|
25398
25981
|
// ../packages/plugin-sdk/src/provider-env.ts
|
|
25399
25982
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
25400
25983
|
if (v === void 0) return void 0;
|
|
@@ -25416,7 +25999,7 @@ var ProviderEnvSchema = external_exports.object(providerEnvShape);
|
|
|
25416
25999
|
// ../packages/plugin-sdk/src/config-inventory.ts
|
|
25417
26000
|
import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync2 } from "fs";
|
|
25418
26001
|
import { homedir as homedir4 } from "os";
|
|
25419
|
-
import { basename as basename2, join as
|
|
26002
|
+
import { basename as basename2, join as join9 } from "path";
|
|
25420
26003
|
|
|
25421
26004
|
// ../rules/code-flaws/auth-jwt-no-verify.json
|
|
25422
26005
|
var auth_jwt_no_verify_default = {
|
|
@@ -27451,8 +28034,8 @@ function bundledDetections() {
|
|
|
27451
28034
|
}
|
|
27452
28035
|
|
|
27453
28036
|
// ../packages/plugin-sdk/src/repo.ts
|
|
27454
|
-
import { existsSync as
|
|
27455
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
28037
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, statSync } from "fs";
|
|
28038
|
+
import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep2 } from "path";
|
|
27456
28039
|
function resolveRepoIdentity(cwd) {
|
|
27457
28040
|
try {
|
|
27458
28041
|
const root = findGitRoot(cwd);
|
|
@@ -27490,32 +28073,32 @@ function resolveHeadRoot(cwd) {
|
|
|
27490
28073
|
function findGitRoot(start) {
|
|
27491
28074
|
let dir = start;
|
|
27492
28075
|
for (; ; ) {
|
|
27493
|
-
if (
|
|
28076
|
+
if (existsSync6(join8(dir, ".git"))) return dir;
|
|
27494
28077
|
const parent = dirname2(dir);
|
|
27495
28078
|
if (parent === dir) return void 0;
|
|
27496
28079
|
dir = parent;
|
|
27497
28080
|
}
|
|
27498
28081
|
}
|
|
27499
28082
|
function resolveGitContext(root) {
|
|
27500
|
-
const dotGit =
|
|
28083
|
+
const dotGit = join8(root, ".git");
|
|
27501
28084
|
try {
|
|
27502
28085
|
if (statSync(dotGit).isDirectory()) {
|
|
27503
|
-
return { configPath:
|
|
28086
|
+
return { configPath: join8(dotGit, "config"), headRoot: root };
|
|
27504
28087
|
}
|
|
27505
28088
|
} catch {
|
|
27506
28089
|
return void 0;
|
|
27507
28090
|
}
|
|
27508
28091
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
27509
28092
|
if (!target) return void 0;
|
|
27510
|
-
const gitdir = isAbsolute(target) ? target :
|
|
27511
|
-
if (
|
|
27512
|
-
return { configPath:
|
|
28093
|
+
const gitdir = isAbsolute(target) ? target : join8(root, target);
|
|
28094
|
+
if (existsSync6(join8(gitdir, "config"))) {
|
|
28095
|
+
return { configPath: join8(gitdir, "config"), headRoot: root };
|
|
27513
28096
|
}
|
|
27514
|
-
const commonRaw = safeRead(
|
|
28097
|
+
const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
|
|
27515
28098
|
if (!commonRaw) return void 0;
|
|
27516
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
28099
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
|
|
27517
28100
|
const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
|
|
27518
|
-
return { configPath:
|
|
28101
|
+
return { configPath: join8(commonGitDir, "config"), headRoot };
|
|
27519
28102
|
}
|
|
27520
28103
|
function safeRead(path) {
|
|
27521
28104
|
try {
|
|
@@ -27557,17 +28140,14 @@ function slugFromUrl(url2) {
|
|
|
27557
28140
|
}
|
|
27558
28141
|
|
|
27559
28142
|
// ../packages/plugin-sdk/src/events.ts
|
|
27560
|
-
import { createHash as
|
|
27561
|
-
|
|
27562
|
-
// ../packages/plugin-sdk/src/finding-key.ts
|
|
27563
|
-
import { createHash as createHash4 } from "crypto";
|
|
28143
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
27564
28144
|
|
|
27565
28145
|
// ../packages/plugin-sdk/src/inventory-resolver.ts
|
|
27566
28146
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
27567
28147
|
|
|
27568
28148
|
// ../packages/plugin-sdk/src/nudge.ts
|
|
27569
|
-
import { mkdirSync as
|
|
27570
|
-
import { join as
|
|
28149
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
28150
|
+
import { join as join10 } from "path";
|
|
27571
28151
|
|
|
27572
28152
|
// ../packages/plugin-sdk/src/paths.ts
|
|
27573
28153
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -27605,8 +28185,8 @@ function resolveNonGitProject(startDir, recognizeMarker) {
|
|
|
27605
28185
|
|
|
27606
28186
|
// ../packages/plugin-sdk/src/project-files.ts
|
|
27607
28187
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
27608
|
-
import { existsSync as
|
|
27609
|
-
import { basename as basename4, join as
|
|
28188
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
28189
|
+
import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
|
|
27610
28190
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
27611
28191
|
".git",
|
|
27612
28192
|
"node_modules",
|
|
@@ -27626,7 +28206,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
27626
28206
|
var MAX_FILES = 2e4;
|
|
27627
28207
|
function readIgnoreLayer(dir) {
|
|
27628
28208
|
try {
|
|
27629
|
-
const content = readFileSync7(
|
|
28209
|
+
const content = readFileSync7(join11(dir, ".gitignore"), "utf8");
|
|
27630
28210
|
return { base: dir, matcher: (0, import_ignore.default)().add(content) };
|
|
27631
28211
|
} catch {
|
|
27632
28212
|
return void 0;
|
|
@@ -27704,10 +28284,10 @@ function resolveProjectFiles(cwd) {
|
|
|
27704
28284
|
const layer = readIgnoreLayer(dir);
|
|
27705
28285
|
const dirLayers = layer ? [...layers, layer] : layers;
|
|
27706
28286
|
for (const entry of dirents) {
|
|
27707
|
-
const fullPath =
|
|
28287
|
+
const fullPath = join11(dir, entry.name);
|
|
27708
28288
|
if (entry.isDirectory()) {
|
|
27709
28289
|
if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
|
|
27710
|
-
if (
|
|
28290
|
+
if (existsSync7(join11(fullPath, ".git"))) continue;
|
|
27711
28291
|
if (visit3(fullPath, dirLayers)) return true;
|
|
27712
28292
|
continue;
|
|
27713
28293
|
}
|
|
@@ -27748,8 +28328,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
27748
28328
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27749
28329
|
|
|
27750
28330
|
// ../packages/plugin-sdk/src/throttle.ts
|
|
27751
|
-
import { mkdirSync as
|
|
27752
|
-
import { join as
|
|
28331
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
28332
|
+
import { join as join12 } from "path";
|
|
27753
28333
|
|
|
27754
28334
|
// ../packages/local-ops/src/egress-record.ts
|
|
27755
28335
|
function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
|
|
@@ -27799,7 +28379,7 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
|
|
|
27799
28379
|
// ../packages/local-ops/src/fs-scan.ts
|
|
27800
28380
|
import { createHash as createHash5, randomUUID as randomUUID11 } from "crypto";
|
|
27801
28381
|
import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
|
|
27802
|
-
import { basename as basename5, extname, join as
|
|
28382
|
+
import { basename as basename5, extname, join as join13, relative as relative3, resolve as resolve2 } from "path";
|
|
27803
28383
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
27804
28384
|
var AKAIGNORE_FILENAME = ".akaignore";
|
|
27805
28385
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
@@ -27815,7 +28395,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
27815
28395
|
var MAX_BYTES = 1e6;
|
|
27816
28396
|
function readIgnoreLayer2(dir, filename) {
|
|
27817
28397
|
try {
|
|
27818
|
-
const content = readFileSync8(
|
|
28398
|
+
const content = readFileSync8(join13(dir, filename), "utf8");
|
|
27819
28399
|
return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
|
|
27820
28400
|
} catch {
|
|
27821
28401
|
return void 0;
|
|
@@ -27851,7 +28431,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
|
|
|
27851
28431
|
const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
|
|
27852
28432
|
const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
|
|
27853
28433
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
27854
|
-
const path =
|
|
28434
|
+
const path = join13(dir, entry.name);
|
|
27855
28435
|
if (entry.isDirectory()) {
|
|
27856
28436
|
const skipState = evaluate(dirSkipLayers, path, true);
|
|
27857
28437
|
if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
|
|
@@ -27890,7 +28470,18 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
27890
28470
|
let findingCount = 0;
|
|
27891
28471
|
const files = [];
|
|
27892
28472
|
const egressFiles = [];
|
|
27893
|
-
|
|
28473
|
+
let fingerprintKey;
|
|
28474
|
+
function resolveFingerprintKey() {
|
|
28475
|
+
if (fingerprintKey === void 0) {
|
|
28476
|
+
try {
|
|
28477
|
+
fingerprintKey = opts.dataDir ? loadOrCreateFingerprintKey(opts.dataDir) : null;
|
|
28478
|
+
} catch {
|
|
28479
|
+
fingerprintKey = null;
|
|
28480
|
+
}
|
|
28481
|
+
}
|
|
28482
|
+
return fingerprintKey;
|
|
28483
|
+
}
|
|
28484
|
+
for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
|
|
27894
28485
|
let text;
|
|
27895
28486
|
try {
|
|
27896
28487
|
text = readFileSync8(file2, "utf8");
|
|
@@ -27915,19 +28506,28 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
27915
28506
|
// store the REDACTED file, never the raw secret
|
|
27916
28507
|
metadata
|
|
27917
28508
|
};
|
|
27918
|
-
const findings = matches.map((m) =>
|
|
27919
|
-
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
27923
|
-
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
|
|
27930
|
-
|
|
28509
|
+
const findings = matches.map((m) => {
|
|
28510
|
+
const maskedMatch = maskMatch(m.rawMatch);
|
|
28511
|
+
const key = resolveFingerprintKey();
|
|
28512
|
+
const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
|
|
28513
|
+
return {
|
|
28514
|
+
id: randomUUID11(),
|
|
28515
|
+
eventId,
|
|
28516
|
+
ruleId: m.ruleId,
|
|
28517
|
+
category: m.category,
|
|
28518
|
+
severity: m.severity,
|
|
28519
|
+
span: m.span,
|
|
28520
|
+
maskedMatch,
|
|
28521
|
+
// Per-pack action (monitor-by-default) when the installed snapshot supplies
|
|
28522
|
+
// one, else the per-category fallback — mirrors the live path's resolveAction.
|
|
28523
|
+
actionTaken: opts.ruleActions?.get(m.ruleId) ?? DEFAULT_ACTIONS[m.category],
|
|
28524
|
+
confidence: m.confidence,
|
|
28525
|
+
// Every fs-scan finding is at-rest (kind: 'code_change' with a
|
|
28526
|
+
// filePath), unlike the plugin's in-flight captures, so — unlike
|
|
28527
|
+
// runtime.ts's isAtRest branch — a finding_key is unconditional here.
|
|
28528
|
+
findingKey: computeFindingKey({ ruleId: m.ruleId, filePath: file2, valueFingerprint })
|
|
28529
|
+
};
|
|
28530
|
+
});
|
|
27931
28531
|
db.recordCapture(event, findings);
|
|
27932
28532
|
files.push({ path: file2, gitignored, findings });
|
|
27933
28533
|
findingCount += findings.length;
|
|
@@ -27937,10 +28537,10 @@ function scanPathIntoStore(db, target, opts = {}) {
|
|
|
27937
28537
|
|
|
27938
28538
|
// ../packages/local-ops/src/project-inventory.ts
|
|
27939
28539
|
import { statSync as statSync6 } from "fs";
|
|
27940
|
-
import { resolve as
|
|
28540
|
+
import { resolve as resolve3 } from "path";
|
|
27941
28541
|
function recordProjectInventory(db, target) {
|
|
27942
28542
|
try {
|
|
27943
|
-
const abs =
|
|
28543
|
+
const abs = resolve3(target);
|
|
27944
28544
|
statSync6(abs);
|
|
27945
28545
|
const identity = resolveRepoIdentity(abs);
|
|
27946
28546
|
if (!identity) return null;
|
|
@@ -27990,15 +28590,15 @@ function reinvokeArgv(subcommand, extraArgs = []) {
|
|
|
27990
28590
|
|
|
27991
28591
|
// ../packages/local-ops/src/update-cache.ts
|
|
27992
28592
|
import { spawn } from "child_process";
|
|
27993
|
-
import { existsSync as
|
|
27994
|
-
import { join as
|
|
28593
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
28594
|
+
import { join as join14 } from "path";
|
|
27995
28595
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
27996
28596
|
function cachePath(home) {
|
|
27997
|
-
return
|
|
28597
|
+
return join14(dataDir(home), "update-check.json");
|
|
27998
28598
|
}
|
|
27999
28599
|
function readCache(home) {
|
|
28000
28600
|
const path = cachePath(home);
|
|
28001
|
-
if (!
|
|
28601
|
+
if (!existsSync8(path)) return null;
|
|
28002
28602
|
try {
|
|
28003
28603
|
const raw = JSON.parse(readFileSync9(path, "utf8"));
|
|
28004
28604
|
if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
|
|
@@ -28008,9 +28608,9 @@ function readCache(home) {
|
|
|
28008
28608
|
}
|
|
28009
28609
|
}
|
|
28010
28610
|
function writeCache(home, cache) {
|
|
28011
|
-
if (!
|
|
28611
|
+
if (!existsSync8(dataDir(home))) return;
|
|
28012
28612
|
try {
|
|
28013
|
-
|
|
28613
|
+
writeFileSync5(cachePath(home), `${JSON.stringify(cache, null, 2)}
|
|
28014
28614
|
`);
|
|
28015
28615
|
} catch {
|
|
28016
28616
|
}
|
|
@@ -28020,7 +28620,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
|
|
|
28020
28620
|
}
|
|
28021
28621
|
function clearCache(home) {
|
|
28022
28622
|
try {
|
|
28023
|
-
|
|
28623
|
+
rmSync3(cachePath(home), { force: true });
|
|
28024
28624
|
} catch {
|
|
28025
28625
|
}
|
|
28026
28626
|
}
|
|
@@ -28173,7 +28773,7 @@ function commandsHelp() {
|
|
|
28173
28773
|
}
|
|
28174
28774
|
|
|
28175
28775
|
// src/commands/check-updates.ts
|
|
28176
|
-
import { existsSync as
|
|
28776
|
+
import { existsSync as existsSync9 } from "fs";
|
|
28177
28777
|
import { parseArgs } from "util";
|
|
28178
28778
|
|
|
28179
28779
|
// src/lib/args.ts
|
|
@@ -28204,7 +28804,7 @@ function runCheckUpdates(argv) {
|
|
|
28204
28804
|
out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
|
|
28205
28805
|
`);
|
|
28206
28806
|
}
|
|
28207
|
-
if (
|
|
28807
|
+
if (existsSync9(dataDir(home))) {
|
|
28208
28808
|
const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
|
|
28209
28809
|
writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
|
|
28210
28810
|
}
|
|
@@ -28358,10 +28958,10 @@ function runCompletion(argv) {
|
|
|
28358
28958
|
|
|
28359
28959
|
// src/commands/dashboard.ts
|
|
28360
28960
|
import { spawn as spawn3 } from "child_process";
|
|
28361
|
-
import { existsSync as
|
|
28961
|
+
import { existsSync as existsSync10 } from "fs";
|
|
28362
28962
|
import { createRequire as createRequire2 } from "module";
|
|
28363
28963
|
import { createServer } from "net";
|
|
28364
|
-
import { dirname as dirname5, join as
|
|
28964
|
+
import { dirname as dirname5, join as join15 } from "path";
|
|
28365
28965
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
28366
28966
|
import { parseArgs as parseArgs2 } from "util";
|
|
28367
28967
|
|
|
@@ -28380,21 +28980,22 @@ function openUrl(url2) {
|
|
|
28380
28980
|
// src/commands/dashboard.ts
|
|
28381
28981
|
var require3 = createRequire2(import.meta.url);
|
|
28382
28982
|
var here = dirname5(fileURLToPath2(import.meta.url));
|
|
28983
|
+
var BIND_HOST = "127.0.0.1";
|
|
28383
28984
|
function pkgBase() {
|
|
28384
|
-
return isSea() ? dirname5(process.execPath) :
|
|
28985
|
+
return isSea() ? dirname5(process.execPath) : join15(here, "..");
|
|
28385
28986
|
}
|
|
28386
28987
|
function isPortFree(port) {
|
|
28387
|
-
return new Promise((
|
|
28988
|
+
return new Promise((resolve4) => {
|
|
28388
28989
|
const probe = createServer();
|
|
28389
28990
|
probe.once("error", () => {
|
|
28390
|
-
|
|
28991
|
+
resolve4(false);
|
|
28391
28992
|
});
|
|
28392
28993
|
probe.once("listening", () => {
|
|
28393
28994
|
probe.close(() => {
|
|
28394
|
-
|
|
28995
|
+
resolve4(true);
|
|
28395
28996
|
});
|
|
28396
28997
|
});
|
|
28397
|
-
probe.listen(port,
|
|
28998
|
+
probe.listen(port, BIND_HOST);
|
|
28398
28999
|
});
|
|
28399
29000
|
}
|
|
28400
29001
|
function refreshUpdateMirror(dir) {
|
|
@@ -28435,9 +29036,9 @@ async function runDashboard(argv) {
|
|
|
28435
29036
|
refreshUpdateMirror(dataDir());
|
|
28436
29037
|
const base = pkgBase();
|
|
28437
29038
|
const bundledServer = [
|
|
28438
|
-
|
|
28439
|
-
|
|
28440
|
-
].find((p) =>
|
|
29039
|
+
join15(base, "web-ui", "server.js"),
|
|
29040
|
+
join15(base, "web-ui", "web-ui", "server.js")
|
|
29041
|
+
].find((p) => existsSync10(p));
|
|
28441
29042
|
if (bundledServer) {
|
|
28442
29043
|
launchStandalone(bundledServer, port, shouldOpen, url2);
|
|
28443
29044
|
return;
|
|
@@ -28447,7 +29048,7 @@ async function runDashboard(argv) {
|
|
|
28447
29048
|
const startNext = () => {
|
|
28448
29049
|
const child = spawn3(
|
|
28449
29050
|
process.execPath,
|
|
28450
|
-
[nextBin, "start", "--port", port, "--hostname",
|
|
29051
|
+
[nextBin, "start", "--port", port, "--hostname", BIND_HOST],
|
|
28451
29052
|
{
|
|
28452
29053
|
cwd: webUiDir,
|
|
28453
29054
|
stdio: ["inherit", "pipe", "inherit"]
|
|
@@ -28456,7 +29057,7 @@ async function runDashboard(argv) {
|
|
|
28456
29057
|
onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
|
|
28457
29058
|
`);
|
|
28458
29059
|
};
|
|
28459
|
-
if (
|
|
29060
|
+
if (existsSync10(join15(webUiDir, ".next", "BUILD_ID"))) {
|
|
28460
29061
|
startNext();
|
|
28461
29062
|
return;
|
|
28462
29063
|
}
|
|
@@ -28484,7 +29085,7 @@ function launchStandalone(serverJs, port, shouldOpen, url2) {
|
|
|
28484
29085
|
// Inherit the parent env so the child server sees PATH etc.; PORT/HOSTNAME tell
|
|
28485
29086
|
// Next's standalone server where to bind (its only port input).
|
|
28486
29087
|
// eslint-disable-next-line n/no-process-env
|
|
28487
|
-
env: { ...process.env, PORT: port, HOSTNAME:
|
|
29088
|
+
env: { ...process.env, PORT: port, HOSTNAME: BIND_HOST }
|
|
28488
29089
|
});
|
|
28489
29090
|
onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
|
|
28490
29091
|
`);
|
|
@@ -28698,11 +29299,11 @@ function terminalPrompter() {
|
|
|
28698
29299
|
},
|
|
28699
29300
|
isInteractive: process.stdin.isTTY && process.stdout.isTTY,
|
|
28700
29301
|
ask(question) {
|
|
28701
|
-
return new Promise((
|
|
29302
|
+
return new Promise((resolve4) => {
|
|
28702
29303
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
28703
29304
|
rl.question(question, (answer) => {
|
|
28704
29305
|
rl.close();
|
|
28705
|
-
|
|
29306
|
+
resolve4(answer);
|
|
28706
29307
|
});
|
|
28707
29308
|
});
|
|
28708
29309
|
},
|
|
@@ -28713,7 +29314,7 @@ function terminalPrompter() {
|
|
|
28713
29314
|
// fatal signals: a security tool must never brick the terminal it was
|
|
28714
29315
|
// invoked from.
|
|
28715
29316
|
askHidden(question) {
|
|
28716
|
-
return new Promise((
|
|
29317
|
+
return new Promise((resolve4, reject) => {
|
|
28717
29318
|
const stdin = process.stdin;
|
|
28718
29319
|
process.stdout.write(question);
|
|
28719
29320
|
stdin.setRawMode(true);
|
|
@@ -28736,7 +29337,7 @@ function terminalPrompter() {
|
|
|
28736
29337
|
restore();
|
|
28737
29338
|
process.stdout.write("\n");
|
|
28738
29339
|
if (err) reject(err);
|
|
28739
|
-
else
|
|
29340
|
+
else resolve4(state.value);
|
|
28740
29341
|
};
|
|
28741
29342
|
const onError = (err) => {
|
|
28742
29343
|
finish(err);
|
|
@@ -28973,10 +29574,29 @@ function printGranted(io, ex) {
|
|
|
28973
29574
|
`
|
|
28974
29575
|
);
|
|
28975
29576
|
}
|
|
28976
|
-
function blockedLine(entry) {
|
|
28977
|
-
|
|
29577
|
+
function blockedLine(entry, key) {
|
|
29578
|
+
const line = `${entry.reference} ${entry.maskedValue} ${entry.ruleId} ${formatRelative(entry.blockedAt)}`;
|
|
29579
|
+
return isCurrentKeyVersion(key, entry.keyVersion) ? line : `${line} [not approvable: recorded under key v${String(entry.keyVersion)}]`;
|
|
29580
|
+
}
|
|
29581
|
+
function keyAccessHint(err, dir) {
|
|
29582
|
+
const code = err?.code;
|
|
29583
|
+
if (code === "floor-unreadable") {
|
|
29584
|
+
return `The local store could not be read, and a key minted without it could reuse a version already in use \u2014 fix the store first (${dir}/aka.db).`;
|
|
29585
|
+
}
|
|
29586
|
+
return typeof code === "string" ? `Check the permissions on ${dir}/exception.key \u2014 do not delete it, that invalidates every existing grant.` : `Delete ${dir}/exception.key to start fresh \u2014 grants written under it already cannot match.`;
|
|
29587
|
+
}
|
|
29588
|
+
function readKeyForApprove(dir) {
|
|
29589
|
+
try {
|
|
29590
|
+
return readFingerprintKey(dir);
|
|
29591
|
+
} catch (err) {
|
|
29592
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29593
|
+
throw new Error(`cannot read the fingerprint key: ${message}
|
|
29594
|
+
${keyAccessHint(err, dir)}`, {
|
|
29595
|
+
cause: err
|
|
29596
|
+
});
|
|
29597
|
+
}
|
|
28978
29598
|
}
|
|
28979
|
-
async function pickBlocked(entries, selector, io,
|
|
29599
|
+
async function pickBlocked(entries, selector, io, key) {
|
|
28980
29600
|
if (selector !== void 0) {
|
|
28981
29601
|
const matches = entries.filter(
|
|
28982
29602
|
(e) => e.reference.startsWith(selector) || e.maskedValue === selector
|
|
@@ -28989,10 +29609,14 @@ async function pickBlocked(entries, selector, io, dir) {
|
|
|
28989
29609
|
`the selector matches ${String(matches.length)} recent blocks \u2014 run 'aka exception approve' bare to pick from the list`
|
|
28990
29610
|
);
|
|
28991
29611
|
}
|
|
28992
|
-
|
|
29612
|
+
if (key === null) {
|
|
29613
|
+
throw new Error(
|
|
29614
|
+
`no blocked detection matches that reference, and matching by value needs the fingerprint key \u2014 none exists on this machine`
|
|
29615
|
+
);
|
|
29616
|
+
}
|
|
28993
29617
|
const fingerprint = fingerprintValue(key, selector);
|
|
28994
29618
|
const byValue = entries.filter(
|
|
28995
|
-
(e) => e.keyVersion
|
|
29619
|
+
(e) => isCurrentKeyVersion(key, e.keyVersion) && e.valueFingerprint === fingerprint
|
|
28996
29620
|
);
|
|
28997
29621
|
const newest = byValue[0];
|
|
28998
29622
|
if (newest) {
|
|
@@ -29011,20 +29635,20 @@ async function pickBlocked(entries, selector, io, dir) {
|
|
|
29011
29635
|
const only = entries[0];
|
|
29012
29636
|
if (entries.length === 1 && only) {
|
|
29013
29637
|
io.out(`Approving the only recent block:
|
|
29014
|
-
${blockedLine(only)}
|
|
29638
|
+
${blockedLine(only, key)}
|
|
29015
29639
|
`);
|
|
29016
29640
|
return only;
|
|
29017
29641
|
}
|
|
29018
29642
|
if (!io.isInteractive) {
|
|
29019
29643
|
io.err(`Blocked in the last 30 minutes:
|
|
29020
29644
|
`);
|
|
29021
|
-
for (const entry of entries) io.err(` ${blockedLine(entry)}
|
|
29645
|
+
for (const entry of entries) io.err(` ${blockedLine(entry, key)}
|
|
29022
29646
|
`);
|
|
29023
29647
|
throw new Error("pass the reference to approve: aka exception approve <reference>");
|
|
29024
29648
|
}
|
|
29025
29649
|
io.out("Blocked in the last 30 minutes:\n");
|
|
29026
29650
|
entries.forEach((entry, i) => {
|
|
29027
|
-
io.out(` ${String(i + 1)}) ${blockedLine(entry)}
|
|
29651
|
+
io.out(` ${String(i + 1)}) ${blockedLine(entry, key)}
|
|
29028
29652
|
`);
|
|
29029
29653
|
});
|
|
29030
29654
|
const answer = await io.ask(`Which one? [1-${String(entries.length)}]: `);
|
|
@@ -29056,7 +29680,13 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
|
|
|
29056
29680
|
return;
|
|
29057
29681
|
}
|
|
29058
29682
|
const selector = positionals[0]?.trim();
|
|
29059
|
-
const
|
|
29683
|
+
const key = readKeyForApprove(dir);
|
|
29684
|
+
const entry = await pickBlocked(entries, selector === "" ? void 0 : selector, io, key);
|
|
29685
|
+
if (!isCurrentKeyVersion(key, entry.keyVersion)) {
|
|
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
|
+
}
|
|
29060
29690
|
const { scope, reason } = await resolveScopeAndReason(values, io);
|
|
29061
29691
|
if (scope.scope === "permanent") {
|
|
29062
29692
|
await confirmPermanent(io, entry.maskedValue, values.yes === true);
|
|
@@ -29369,11 +29999,8 @@ written under the old key simply stop matching. They remain listed for audit.
|
|
|
29369
29999
|
version2 = rotateFingerprintKey(dir).version;
|
|
29370
30000
|
} catch (err) {
|
|
29371
30001
|
const message = err instanceof Error ? err.message : String(err);
|
|
29372
|
-
throw new Error(
|
|
29373
|
-
|
|
29374
|
-
Delete ${dir}/exception.key to start fresh \u2014 grants written under it already cannot match.`,
|
|
29375
|
-
{ cause: err }
|
|
29376
|
-
);
|
|
30002
|
+
throw new Error(`cannot rotate: ${message}
|
|
30003
|
+
${keyAccessHint(err, dir)}`, { cause: err });
|
|
29377
30004
|
}
|
|
29378
30005
|
io.out(
|
|
29379
30006
|
`Fingerprint key rotated \u2014 new key version v${String(version2)}.
|
|
@@ -29407,13 +30034,13 @@ async function runException(argv, io = terminalPrompter()) {
|
|
|
29407
30034
|
}
|
|
29408
30035
|
|
|
29409
30036
|
// src/commands/init.ts
|
|
29410
|
-
import { existsSync as
|
|
29411
|
-
import { join as
|
|
30037
|
+
import { existsSync as existsSync12, statSync as statSync7 } from "fs";
|
|
30038
|
+
import { join as join16 } from "path";
|
|
29412
30039
|
import * as readline from "readline/promises";
|
|
29413
30040
|
import { parseArgs as parseArgs6 } from "util";
|
|
29414
30041
|
|
|
29415
30042
|
// src/commands/plugins.ts
|
|
29416
|
-
import { existsSync as
|
|
30043
|
+
import { existsSync as existsSync11 } from "fs";
|
|
29417
30044
|
import { parseArgs as parseArgs5 } from "util";
|
|
29418
30045
|
function runPlugins(argv) {
|
|
29419
30046
|
const [sub, ...rest] = argv;
|
|
@@ -29430,7 +30057,7 @@ async function listPlugins(argv) {
|
|
|
29430
30057
|
const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
|
|
29431
30058
|
const home = homeBase(values.home);
|
|
29432
30059
|
const active = /* @__PURE__ */ new Set();
|
|
29433
|
-
if (
|
|
30060
|
+
if (existsSync11(dbPath(home))) {
|
|
29434
30061
|
const db = openLocalDatabase(dataDir(home));
|
|
29435
30062
|
try {
|
|
29436
30063
|
const recent = await db.findings.recentFindings({ limit: 1e3 });
|
|
@@ -29514,6 +30141,23 @@ Then run \`aka init\` to set up the local store.
|
|
|
29514
30141
|
|
|
29515
30142
|
// src/commands/init.ts
|
|
29516
30143
|
var PLUGIN_OFFER_IDENTITY = `${PRODUCT_NAME} \u2014 ${PRODUCT_TAGLINE}`;
|
|
30144
|
+
function looseStorePaths(home) {
|
|
30145
|
+
if (process.platform === "win32") return [];
|
|
30146
|
+
const targets = [
|
|
30147
|
+
home,
|
|
30148
|
+
settingsDir(home),
|
|
30149
|
+
dataDir(home),
|
|
30150
|
+
join16(settingsDir(home), "settings.json"),
|
|
30151
|
+
dbPath(home)
|
|
30152
|
+
];
|
|
30153
|
+
return targets.filter((p) => {
|
|
30154
|
+
try {
|
|
30155
|
+
return (statSync7(p).mode & 63) !== 0;
|
|
30156
|
+
} catch {
|
|
30157
|
+
return false;
|
|
30158
|
+
}
|
|
30159
|
+
});
|
|
30160
|
+
}
|
|
29517
30161
|
async function runInit(argv) {
|
|
29518
30162
|
const { values } = parseArgs6({
|
|
29519
30163
|
args: argv,
|
|
@@ -29523,16 +30167,16 @@ async function runInit(argv) {
|
|
|
29523
30167
|
ensureLayoutDirSync(home);
|
|
29524
30168
|
const settings = settingsDir(home);
|
|
29525
30169
|
ensureLayoutDirSync(settings);
|
|
29526
|
-
const settingsFile =
|
|
29527
|
-
const settingsCreated = !
|
|
30170
|
+
const settingsFile = join16(settings, "settings.json");
|
|
30171
|
+
const settingsCreated = !existsSync12(settingsFile);
|
|
29528
30172
|
if (settingsCreated) {
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
|
|
29532
|
-
|
|
29533
|
-
|
|
29534
|
-
renameSync5(tmp, settingsFile);
|
|
30173
|
+
writeOwnerOnlyFileSync(
|
|
30174
|
+
settingsFile,
|
|
30175
|
+
`${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
|
|
30176
|
+
`
|
|
30177
|
+
);
|
|
29535
30178
|
}
|
|
30179
|
+
tightenFile(settingsFile);
|
|
29536
30180
|
const db = openLocalDatabase(dataDir(home));
|
|
29537
30181
|
let policyCount;
|
|
29538
30182
|
let packCount;
|
|
@@ -29545,12 +30189,14 @@ async function runInit(argv) {
|
|
|
29545
30189
|
} finally {
|
|
29546
30190
|
db.close();
|
|
29547
30191
|
}
|
|
30192
|
+
const loose = looseStorePaths(home);
|
|
29548
30193
|
process.stdout.write(
|
|
29549
30194
|
`\u2713 Initialized AKA at ${home}
|
|
29550
30195
|
settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
|
|
29551
30196
|
database: ${dbPath(home)}
|
|
29552
30197
|
seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
|
|
29553
30198
|
` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
|
|
30199
|
+
` : "") + (loose.length > 0 ? ` \u26A0 could not enforce owner-only permissions on ${loose.join(", ")} \u2014 this filesystem rejects chmod, so the store has no at-rest protection here (see the "Data at rest" note in SECURITY.md)
|
|
29554
30200
|
` : "")
|
|
29555
30201
|
);
|
|
29556
30202
|
await offerPluginInstall(values.yes === true);
|
|
@@ -29596,7 +30242,7 @@ async function confirm(question) {
|
|
|
29596
30242
|
}
|
|
29597
30243
|
|
|
29598
30244
|
// src/commands/scan.ts
|
|
29599
|
-
import { statSync as
|
|
30245
|
+
import { statSync as statSync8 } from "fs";
|
|
29600
30246
|
import { parseArgs as parseArgs7 } from "util";
|
|
29601
30247
|
function severityRank(s) {
|
|
29602
30248
|
return Severity.options.indexOf(s);
|
|
@@ -29643,7 +30289,7 @@ function runScan(argv) {
|
|
|
29643
30289
|
failOn = parsed.data;
|
|
29644
30290
|
}
|
|
29645
30291
|
try {
|
|
29646
|
-
|
|
30292
|
+
statSync8(target);
|
|
29647
30293
|
} catch {
|
|
29648
30294
|
process.stderr.write(`aka scan: no such file or directory: ${target}
|
|
29649
30295
|
`);
|
|
@@ -29651,13 +30297,14 @@ function runScan(argv) {
|
|
|
29651
30297
|
return;
|
|
29652
30298
|
}
|
|
29653
30299
|
registerBundledPacks();
|
|
29654
|
-
const
|
|
30300
|
+
const storeDir = dataDir(home);
|
|
30301
|
+
const db = openLocalDatabase(storeDir);
|
|
29655
30302
|
let result;
|
|
29656
30303
|
let inventory;
|
|
29657
30304
|
let egress;
|
|
29658
30305
|
try {
|
|
29659
30306
|
const { ruleActions } = db.installedPacks.installedRuleset();
|
|
29660
|
-
result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli" });
|
|
30307
|
+
result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli", dataDir: storeDir });
|
|
29661
30308
|
inventory = recordProjectInventory(db, target);
|
|
29662
30309
|
egress = recordProjectEgress(db, target, result.egress, home);
|
|
29663
30310
|
} finally {
|
|
@@ -30549,7 +31196,50 @@ function applyPluginUpdate2(status) {
|
|
|
30549
31196
|
return ok;
|
|
30550
31197
|
}
|
|
30551
31198
|
|
|
30552
|
-
// src/
|
|
31199
|
+
// src/lib/external-dispatch.ts
|
|
31200
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
31201
|
+
import { constants } from "os";
|
|
31202
|
+
function commandOnPath(command) {
|
|
31203
|
+
const probe = spawnSync2("/bin/sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", command], {
|
|
31204
|
+
stdio: "ignore"
|
|
31205
|
+
});
|
|
31206
|
+
return probe.status === 0;
|
|
31207
|
+
}
|
|
31208
|
+
function isExternalCommandName(name) {
|
|
31209
|
+
return /^[a-z][a-z0-9-]*$/.test(name);
|
|
31210
|
+
}
|
|
31211
|
+
function shouldDispatchExternal(command, isBuiltin) {
|
|
31212
|
+
return !isBuiltin && isExternalCommandName(command);
|
|
31213
|
+
}
|
|
31214
|
+
function externalDispatchSupported(platform2 = process.platform) {
|
|
31215
|
+
return platform2 !== "win32";
|
|
31216
|
+
}
|
|
31217
|
+
function signalExitCode(signal) {
|
|
31218
|
+
const number4 = signal ? constants.signals[signal] : void 0;
|
|
31219
|
+
return number4 === void 0 ? 1 : 128 + number4;
|
|
31220
|
+
}
|
|
31221
|
+
function dispatchExternal(command, argv, deps = {}) {
|
|
31222
|
+
const spawn4 = deps.spawn ?? spawnSync2;
|
|
31223
|
+
const platform2 = deps.platform ?? process.platform;
|
|
31224
|
+
const exists = deps.exists ?? commandOnPath;
|
|
31225
|
+
if (!externalDispatchSupported(platform2)) return { found: false, status: 1 };
|
|
31226
|
+
const target = `aka-${command}`;
|
|
31227
|
+
const result = spawn4(target, argv, { stdio: "inherit" });
|
|
31228
|
+
if (result.error) {
|
|
31229
|
+
if (result.error.code === "ENOENT" && !exists(target)) return { found: false, status: 1 };
|
|
31230
|
+
process.stderr.write(`aka: ${target}: ${result.error.message}
|
|
31231
|
+
`);
|
|
31232
|
+
return { found: true, status: 1 };
|
|
31233
|
+
}
|
|
31234
|
+
if (result.status === null) {
|
|
31235
|
+
if (result.signal) process.stderr.write(`aka: ${target}: terminated by ${result.signal}
|
|
31236
|
+
`);
|
|
31237
|
+
return { found: true, status: signalExitCode(result.signal) };
|
|
31238
|
+
}
|
|
31239
|
+
return { found: true, status: result.status };
|
|
31240
|
+
}
|
|
31241
|
+
|
|
31242
|
+
// src/main.ts
|
|
30553
31243
|
var COMMANDS = {
|
|
30554
31244
|
init: runInit,
|
|
30555
31245
|
scan: runScan,
|
|
@@ -30586,7 +31276,9 @@ Usage: aka <command> [options]
|
|
|
30586
31276
|
|
|
30587
31277
|
Commands:
|
|
30588
31278
|
${commandsHelp()}
|
|
30589
|
-
|
|
31279
|
+
${externalDispatchSupported() ? `
|
|
31280
|
+
Anything else runs an external 'aka-<command>' from your PATH (so 'aka claude' runs aka-claude).
|
|
31281
|
+
` : ""}
|
|
30590
31282
|
Options:
|
|
30591
31283
|
--home <dir> Use an alternate AKA home (default: ~/.aka)
|
|
30592
31284
|
--no-update-check Skip the post-command "updates available" notice
|
|
@@ -30602,8 +31294,7 @@ function homeFromArgv(argv) {
|
|
|
30602
31294
|
}
|
|
30603
31295
|
return homeBase(void 0);
|
|
30604
31296
|
}
|
|
30605
|
-
async function main() {
|
|
30606
|
-
const argv = process.argv.slice(2);
|
|
31297
|
+
async function main(argv = process.argv.slice(2), dispatchDeps = {}) {
|
|
30607
31298
|
if (argv[0] === "-v" || argv[0] === "--version" || argv[0] === "version") {
|
|
30608
31299
|
process.stdout.write(`${cliVersion() ?? "unknown"}
|
|
30609
31300
|
`);
|
|
@@ -30616,7 +31307,14 @@ async function main() {
|
|
|
30616
31307
|
}
|
|
30617
31308
|
const noUpdateCheck = rawRest.includes("--no-update-check");
|
|
30618
31309
|
const rest = rawRest.filter((a) => a !== "--no-update-check");
|
|
30619
|
-
const handler = COMMANDS[command];
|
|
31310
|
+
const handler = Object.hasOwn(COMMANDS, command) ? COMMANDS[command] : void 0;
|
|
31311
|
+
if (shouldDispatchExternal(command, handler !== void 0)) {
|
|
31312
|
+
const external = dispatchExternal(command, rawRest, dispatchDeps);
|
|
31313
|
+
if (external.found) {
|
|
31314
|
+
process.exitCode = external.status;
|
|
31315
|
+
return;
|
|
31316
|
+
}
|
|
31317
|
+
}
|
|
30620
31318
|
if (!handler) {
|
|
30621
31319
|
process.stderr.write(`aka: unknown command '${command}'
|
|
30622
31320
|
|
|
@@ -30632,6 +31330,8 @@ ${USAGE}`);
|
|
|
30632
31330
|
}
|
|
30633
31331
|
}
|
|
30634
31332
|
}
|
|
31333
|
+
|
|
31334
|
+
// src/cli.ts
|
|
30635
31335
|
main().catch((err) => {
|
|
30636
31336
|
process.stderr.write(`aka: ${err instanceof Error ? err.message : String(err)}
|
|
30637
31337
|
`);
|