@akasecurity/cli 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/cli.js +996 -476
  2. package/package.json +8 -8
  3. package/web-ui/package.json +2 -2
  4. package/web-ui/packages/plugin-sdk/package.json +1 -1
  5. package/web-ui/packages/schema/package.json +1 -1
  6. package/web-ui/packages/ui-kit/package.json +7 -7
  7. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  8. package/web-ui/web-ui/.next/app-build-manifest.json +127 -127
  9. package/web-ui/web-ui/.next/app-path-routes-manifest.json +5 -5
  10. package/web-ui/web-ui/.next/build-manifest.json +14 -14
  11. package/web-ui/web-ui/.next/prerender-manifest.json +15 -15
  12. package/web-ui/web-ui/.next/required-server-files.json +1 -1
  13. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  14. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  15. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  16. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  17. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  18. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  19. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  20. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  21. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  22. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  23. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  24. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  25. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
  26. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  27. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  28. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
  29. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  31. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  32. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  33. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  34. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  35. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  36. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  37. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  38. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  39. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  40. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  41. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  42. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  43. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
  44. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  45. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  46. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  47. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  48. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  49. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  50. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  51. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  52. package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
  53. package/web-ui/web-ui/.next/server/app/_not-found/page.js.nft.json +1 -1
  54. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  55. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  56. package/web-ui/web-ui/.next/server/app/_not-found.rsc +10 -10
  57. package/web-ui/web-ui/.next/server/app/icon.svg/route.js +1 -1
  58. package/web-ui/web-ui/.next/server/app/icon.svg/route.js.nft.json +1 -1
  59. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  60. package/web-ui/web-ui/.next/server/app/index.rsc +10 -10
  61. package/web-ui/web-ui/.next/server/app/page.js +2 -2
  62. package/web-ui/web-ui/.next/server/app/page.js.nft.json +1 -1
  63. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  64. package/web-ui/web-ui/.next/server/app-paths-manifest.json +5 -5
  65. package/web-ui/web-ui/.next/server/chunks/100.js +1 -0
  66. package/web-ui/web-ui/.next/server/chunks/142.js +700 -0
  67. package/web-ui/web-ui/.next/server/chunks/472.js +84 -0
  68. package/web-ui/web-ui/.next/server/chunks/723.js +22 -0
  69. package/web-ui/web-ui/.next/server/chunks/75.js +6 -0
  70. package/web-ui/web-ui/.next/server/chunks/830.js +123 -0
  71. package/web-ui/web-ui/.next/server/chunks/850.js +1 -0
  72. package/web-ui/web-ui/.next/server/chunks/853.js +698 -0
  73. package/web-ui/web-ui/.next/server/chunks/871.js +1 -0
  74. package/web-ui/web-ui/.next/server/chunks/924.js +2 -0
  75. package/web-ui/web-ui/.next/server/chunks/959.js +9 -0
  76. package/web-ui/web-ui/.next/server/functions-config-manifest.json +4 -4
  77. package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
  78. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  79. package/web-ui/web-ui/.next/server/middleware.js +2 -2
  80. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  81. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  82. package/web-ui/web-ui/.next/server/pages/_app.js +1 -1
  83. package/web-ui/web-ui/.next/server/pages/_app.js.nft.json +1 -1
  84. package/web-ui/web-ui/.next/server/pages/_document.js +1 -1
  85. package/web-ui/web-ui/.next/server/pages/_document.js.nft.json +1 -1
  86. package/web-ui/web-ui/.next/server/pages/_error.js +3 -3
  87. package/web-ui/web-ui/.next/server/pages/_error.js.nft.json +1 -1
  88. package/web-ui/web-ui/.next/server/pages-manifest.json +1 -1
  89. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  90. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  91. package/web-ui/web-ui/.next/static/chunks/137-8f757e04d1877a6c.js +1 -0
  92. package/web-ui/web-ui/.next/static/chunks/517-977bc0a7a7be88cc.js +1 -0
  93. package/web-ui/web-ui/.next/static/chunks/613-734dcad67e1fa08c.js +1 -0
  94. package/web-ui/web-ui/.next/static/chunks/784-25fa9bc074877c93.js +62 -0
  95. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-96995843d6cbfb84.js +1 -0
  96. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-28eeea5c4a816f53.js +1 -0
  97. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-0c3888b9381a6e80.js +1 -0
  98. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-3a5138f424e43b6e.js +1 -0
  99. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-32d67b6c8165acda.js +1 -0
  100. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-1c376f3818bfba45.js +1 -0
  101. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-9c9b1d29895a0592.js +1 -0
  102. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-4334a50ed9dcace4.js +1 -0
  103. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-de6da4ea92784950.js +1 -0
  104. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-4817c44647618832.js +1 -0
  105. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-5d2aa8c946dd2cae.js +1 -0
  106. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-c0597d1c1acb1cc4.js +1 -0
  107. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-2a7b0d9dab536ae1.js +1 -0
  108. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-576a976afbe33369.js +1 -0
  109. package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-26662e9d7ee32abc.js +1 -0
  110. package/web-ui/web-ui/.next/static/chunks/app/layout-e531d303d8c8a313.js +1 -0
  111. package/web-ui/web-ui/.next/static/chunks/app/page-3ffe075f223c0af9.js +1 -0
  112. package/web-ui/web-ui/.next/static/chunks/{813c9d6d-dc0a9294021710c1.js → dddf6c0a-f09ab3fc84be1c52.js} +1 -1
  113. package/web-ui/web-ui/.next/static/chunks/framework-f8855380599951ba.js +1 -0
  114. package/web-ui/web-ui/.next/static/chunks/main-app-7291de73392cac56.js +1 -0
  115. package/web-ui/web-ui/.next/static/chunks/main-ff0082770df07ddc.js +1 -0
  116. package/web-ui/web-ui/.next/static/chunks/pages/_app-56d1709db70ee49f.js +1 -0
  117. package/web-ui/web-ui/.next/static/chunks/pages/_error-63dd320a91a40541.js +1 -0
  118. package/web-ui/web-ui/.next/static/chunks/{webpack-14166b28d6557de1.js → webpack-ea80c9c368a8364e.js} +1 -1
  119. package/web-ui/web-ui/.next/static/css/1ea302d29fc67ba4.css +2 -0
  120. package/web-ui/web-ui/.next/static/{ez8SLWYcUCScGuFekZ-G_ → mAOCejrDqkMoDfzjWr6qu}/_buildManifest.js +1 -1
  121. package/web-ui/web-ui/package.json +12 -9
  122. package/web-ui/web-ui/.next/server/chunks/244.js +0 -649
  123. package/web-ui/web-ui/.next/server/chunks/264.js +0 -1
  124. package/web-ui/web-ui/.next/server/chunks/291.js +0 -647
  125. package/web-ui/web-ui/.next/server/chunks/345.js +0 -1
  126. package/web-ui/web-ui/.next/server/chunks/367.js +0 -6
  127. package/web-ui/web-ui/.next/server/chunks/397.js +0 -22
  128. package/web-ui/web-ui/.next/server/chunks/45.js +0 -1
  129. package/web-ui/web-ui/.next/server/chunks/461.js +0 -123
  130. package/web-ui/web-ui/.next/server/chunks/773.js +0 -84
  131. package/web-ui/web-ui/.next/server/chunks/849.js +0 -9
  132. package/web-ui/web-ui/.next/server/chunks/983.js +0 -2
  133. package/web-ui/web-ui/.next/static/chunks/24-d36fb6bd72e29537.js +0 -62
  134. package/web-ui/web-ui/.next/static/chunks/338-78876603768c14bb.js +0 -1
  135. package/web-ui/web-ui/.next/static/chunks/496-a8b6c34560896a23.js +0 -1
  136. package/web-ui/web-ui/.next/static/chunks/561-52ad6ae6bf64b471.js +0 -1
  137. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-797e2ce43b8db7e1.js +0 -1
  138. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-d9e9023871035a2d.js +0 -1
  139. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-5bd8e04955058e46.js +0 -1
  140. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-a254c9fc7cc27b97.js +0 -1
  141. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-622151ff8e6960c8.js +0 -1
  142. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-c1221106bd6ba356.js +0 -1
  143. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-fc68b0fb9b669a03.js +0 -1
  144. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-68418378cfb10a0f.js +0 -1
  145. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-d15081016f40ac92.js +0 -1
  146. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-a73f4fb185479b19.js +0 -1
  147. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-2b9ea3e95dbe82fb.js +0 -1
  148. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-2675f7fb69829b71.js +0 -1
  149. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-315b8b57329d5eb6.js +0 -1
  150. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-77adb595bb447040.js +0 -1
  151. package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-5728154141ad0edb.js +0 -1
  152. package/web-ui/web-ui/.next/static/chunks/app/layout-8b19b03146218dc7.js +0 -1
  153. package/web-ui/web-ui/.next/static/chunks/app/page-79d71c1635fbf565.js +0 -1
  154. package/web-ui/web-ui/.next/static/chunks/framework-09f91e50832141e4.js +0 -1
  155. package/web-ui/web-ui/.next/static/chunks/main-80ce91d706b796ad.js +0 -1
  156. package/web-ui/web-ui/.next/static/chunks/main-app-23de550d13c8bec1.js +0 -1
  157. package/web-ui/web-ui/.next/static/chunks/pages/_app-ab1bd755bf3a4f53.js +0 -1
  158. package/web-ui/web-ui/.next/static/chunks/pages/_error-541b64fd411d8da6.js +0 -1
  159. package/web-ui/web-ui/.next/static/css/500a1c2bf0fc64fb.css +0 -2
  160. /package/web-ui/web-ui/.next/static/{ez8SLWYcUCScGuFekZ-G_ → mAOCejrDqkMoDfzjWr6qu}/_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(),
@@ -18473,6 +18533,12 @@ var PolicyBundle = external_exports.object({
18473
18533
  // on-disk caches — that omit the field still parse; consumers read
18474
18534
  // `bundle.exceptions ?? []`.
18475
18535
  exceptions: external_exports.array(ExceptionBundleEntry).optional(),
18536
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
18537
+ // from a versioned installed pack. Optional so older backends — and older
18538
+ // on-disk caches — that omit the field still parse; consumers fall back to
18539
+ // the rule's own spec version. NOT the bundle version above — see
18540
+ // installedRuleset's ruleVersions for the source of truth.
18541
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
18476
18542
  customKeywords: external_exports.array(external_exports.string()),
18477
18543
  fetchedAt: external_exports.iso.datetime()
18478
18544
  }).meta({ id: "PolicyBundle" });
@@ -19272,6 +19338,15 @@ function groupActions(g) {
19272
19338
  actionsCache.set(g, actions);
19273
19339
  return actions;
19274
19340
  }
19341
+ function countInstancesByStatus(statusInputs, statuses) {
19342
+ const statusSet = new Set(statuses);
19343
+ let sum = 0;
19344
+ for (const input of statusInputs) {
19345
+ if (input.count === void 0) return null;
19346
+ if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
19347
+ }
19348
+ return sum;
19349
+ }
19275
19350
  function applyFindingFilters(groups, opts) {
19276
19351
  let filtered = groups;
19277
19352
  if (opts.severity && opts.severity.length > 0) {
@@ -19290,6 +19365,10 @@ function applyFindingFilters(groups, opts) {
19290
19365
  const subtypeSet = new Set(opts.subtype);
19291
19366
  filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
19292
19367
  }
19368
+ if (opts.statuses && opts.statuses.length > 0) {
19369
+ const statusSet = new Set(opts.statuses);
19370
+ filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
19371
+ }
19293
19372
  if (opts.q) {
19294
19373
  const q = opts.q.toLowerCase();
19295
19374
  filtered = filtered.filter((g) => groupHaystack(g).includes(q));
@@ -19311,6 +19390,7 @@ function computeFindingFacets(allGroups, opts) {
19311
19390
  const forSeverity = applyFindingFilters(allGroups, {
19312
19391
  providers: opts.providers,
19313
19392
  actions: opts.actions,
19393
+ statuses: opts.statuses,
19314
19394
  q: opts.q,
19315
19395
  subtype: opts.subtype
19316
19396
  });
@@ -19320,6 +19400,7 @@ function computeFindingFacets(allGroups, opts) {
19320
19400
  }
19321
19401
  const forProvider = applyFindingFilters(allGroups, {
19322
19402
  actions: opts.actions,
19403
+ statuses: opts.statuses,
19323
19404
  q: opts.q,
19324
19405
  subtype: opts.subtype,
19325
19406
  severity: opts.severity
@@ -19330,6 +19411,7 @@ function computeFindingFacets(allGroups, opts) {
19330
19411
  }
19331
19412
  const forAction = applyFindingFilters(allGroups, {
19332
19413
  providers: opts.providers,
19414
+ statuses: opts.statuses,
19333
19415
  q: opts.q,
19334
19416
  subtype: opts.subtype,
19335
19417
  severity: opts.severity
@@ -19341,17 +19423,30 @@ function computeFindingFacets(allGroups, opts) {
19341
19423
  const forSubtype = applyFindingFilters(allGroups, {
19342
19424
  providers: opts.providers,
19343
19425
  actions: opts.actions,
19426
+ statuses: opts.statuses,
19344
19427
  q: opts.q,
19345
19428
  severity: opts.severity
19346
19429
  });
19347
19430
  const subtypeMap = /* @__PURE__ */ new Map();
19348
19431
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
19432
+ const forStatus = applyFindingFilters(allGroups, {
19433
+ providers: opts.providers,
19434
+ actions: opts.actions,
19435
+ q: opts.q,
19436
+ subtype: opts.subtype,
19437
+ severity: opts.severity
19438
+ });
19439
+ const statusMap = /* @__PURE__ */ new Map();
19440
+ for (const g of forStatus) {
19441
+ if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
19442
+ }
19349
19443
  const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
19350
19444
  return {
19351
19445
  severity: toItems(severityMap),
19352
19446
  provider: toItems(providerMap),
19353
19447
  action: toItems(actionMap),
19354
- subtype: toItems(subtypeMap)
19448
+ subtype: toItems(subtypeMap),
19449
+ status: toItems(statusMap)
19355
19450
  };
19356
19451
  }
19357
19452
 
@@ -19386,10 +19481,14 @@ var PatchInstalledPackRequest = external_exports.object({
19386
19481
  }).meta({ id: "PatchInstalledPackRequest" });
19387
19482
 
19388
19483
  // ../packages/schema/src/zod/local.ts
19389
- var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
19484
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
19390
19485
  var RunMode = external_exports.enum(["standalone"]);
19391
19486
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
19392
19487
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
19488
+ var ModelJudgeConsent = external_exports.object({
19489
+ acknowledgedAt: external_exports.iso.datetime(),
19490
+ payloadVersion: external_exports.number().int().positive()
19491
+ });
19393
19492
  var WorkspaceSettings = external_exports.object({
19394
19493
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
19395
19494
  // Settings files written by earlier releases may carry the retired 'attached'
@@ -19405,37 +19504,16 @@ var WorkspaceSettings = external_exports.object({
19405
19504
  // Shares writes.
19406
19505
  dataSharesInPlace: external_exports.boolean().default(true),
19407
19506
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
19408
- onboardedAt: external_exports.iso.datetime().optional()
19507
+ onboardedAt: external_exports.iso.datetime().optional(),
19508
+ // Records that the user consented to sending findings to the model API for
19509
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
19510
+ // Absent until granted; a stale payloadVersion means the consent no longer
19511
+ // covers the current payload and must be re-granted.
19512
+ modelJudgeConsent: ModelJudgeConsent.optional()
19409
19513
  });
19410
19514
  function defaultWorkspaceSettings() {
19411
19515
  return WorkspaceSettings.parse({});
19412
19516
  }
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
19517
  function toInventoryRow(input, id, now) {
19440
19518
  return {
19441
19519
  id,
@@ -19505,7 +19583,42 @@ function toInspectionFindingRow(input) {
19505
19583
  spanEnd: input.span.end,
19506
19584
  maskedMatch: input.maskedMatch,
19507
19585
  actionTaken: input.actionTaken,
19508
- confidence: input.confidence
19586
+ confidence: input.confidence,
19587
+ findingKey: input.findingKey ?? null,
19588
+ firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
19589
+ };
19590
+ }
19591
+ function toCaptureAttributes(event) {
19592
+ const metadata = event.metadata;
19593
+ return {
19594
+ source_tool: event.sourceTool,
19595
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
19596
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
19597
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
19598
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
19599
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
19600
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
19601
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
19602
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
19603
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
19604
+ // has ever populated either), but every legacy metadata key still rides
19605
+ // the bag rather than being silently dropped — CaptureAttributes'
19606
+ // `.catchall(z.unknown())` carries the long tail.
19607
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
19608
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
19609
+ };
19610
+ }
19611
+ function captureDefinitionVersion(finding) {
19612
+ return `capture/${finding.category}/${finding.severity}`;
19613
+ }
19614
+ function toCaptureDefinitionInput(finding) {
19615
+ return {
19616
+ ruleId: finding.ruleId,
19617
+ version: captureDefinitionVersion(finding),
19618
+ name: finding.ruleId,
19619
+ category: finding.category,
19620
+ severity: finding.severity,
19621
+ definition: JSON.stringify({ ruleId: finding.ruleId })
19509
19622
  };
19510
19623
  }
19511
19624
 
@@ -19936,6 +20049,37 @@ function reviewSeverityRank(reasons) {
19936
20049
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
19937
20050
  }
19938
20051
 
20052
+ // ../packages/persistence/src/ids.ts
20053
+ import { createHash } from "crypto";
20054
+ function sha256Hex(input) {
20055
+ return createHash("sha256").update(input).digest("hex");
20056
+ }
20057
+ function inventoryId(objectType, identityKey) {
20058
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
20059
+ }
20060
+ function sourceProjectId(url2) {
20061
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
20062
+ }
20063
+ function classifiedDataId(cls) {
20064
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
20065
+ }
20066
+ function inspectionDefinitionId(ruleId, version2) {
20067
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
20068
+ }
20069
+ function llmCallId(sessionId, messageId) {
20070
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
20071
+ }
20072
+ function toolCallId(sessionId, toolUseId) {
20073
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
20074
+ }
20075
+ var NO_SESSION = "no_session";
20076
+ var NO_PATH = "no_path";
20077
+ function captureId(sessionId, contentHash, filePath = null) {
20078
+ return sha256Hex(
20079
+ canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
20080
+ );
20081
+ }
20082
+
19939
20083
  // ../packages/persistence/src/internal/sql-text.ts
19940
20084
  function escapeLikePattern(s) {
19941
20085
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -20032,28 +20176,98 @@ function evidenceExists(db, object2) {
20032
20176
  return schemaObjectExists(db, "table", object2.name);
20033
20177
  }
20034
20178
 
20035
- // ../packages/persistence/src/ids.ts
20036
- import { createHash } from "crypto";
20037
- function sha256Hex(input) {
20038
- return createHash("sha256").update(input).digest("hex");
20179
+ // ../packages/persistence/src/internal/rows.ts
20180
+ function allRows(stmt, params) {
20181
+ if (params === void 0) return stmt.all();
20182
+ if (Array.isArray(params)) return stmt.all(...params);
20183
+ return stmt.all(params);
20039
20184
  }
20040
- function inventoryId(objectType, identityKey) {
20041
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
20185
+ function getRow(stmt, params) {
20186
+ if (params === void 0) return stmt.get();
20187
+ if (Array.isArray(params)) return stmt.get(...params);
20188
+ return stmt.get(params);
20042
20189
  }
20043
- function sourceProjectId(url2) {
20044
- return sha256Hex(canonicalIdentity(["source_project", url2]));
20190
+ function intToBool(raw) {
20191
+ return raw === 1 || raw === true;
20045
20192
  }
20046
- function classifiedDataId(cls) {
20047
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
20193
+ function boolToInt(b) {
20194
+ return b ? 1 : 0;
20048
20195
  }
20049
- function inspectionDefinitionId(ruleId, version2) {
20050
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
20196
+ function bindParams(row) {
20197
+ const out = {};
20198
+ for (const [key, value] of Object.entries(row)) {
20199
+ out[key] = value === void 0 ? null : value;
20200
+ }
20201
+ return out;
20051
20202
  }
20052
- function llmCallId(sessionId, messageId) {
20053
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
20203
+ function countScalar(db, sql, params) {
20204
+ return getRow(db.prepare(sql), params)?.n ?? 0;
20054
20205
  }
20055
- function toolCallId(sessionId, toolUseId) {
20056
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
20206
+ function countBy(db, sql, params) {
20207
+ const map2 = /* @__PURE__ */ new Map();
20208
+ for (const row of allRows(db.prepare(sql), params)) {
20209
+ map2.set(row.k, row.n);
20210
+ }
20211
+ return map2;
20212
+ }
20213
+ function mapRowsTolerant(rows, map2) {
20214
+ const out = [];
20215
+ for (const row of rows) {
20216
+ try {
20217
+ out.push(map2(row));
20218
+ } catch {
20219
+ }
20220
+ }
20221
+ return out;
20222
+ }
20223
+
20224
+ // ../packages/persistence/src/paths.ts
20225
+ import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
20226
+ var DATA_DIR_MODE = 448;
20227
+ var DATA_FILE_MODE = 384;
20228
+ var DB_FILENAME = "aka.db";
20229
+ function chmodBestEffort(path, mode) {
20230
+ try {
20231
+ chmodSync(path, mode);
20232
+ } catch {
20233
+ }
20234
+ }
20235
+ function tightenDir(dir) {
20236
+ chmodBestEffort(dir, DATA_DIR_MODE);
20237
+ }
20238
+ function ensureDataDirSync(dir) {
20239
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
20240
+ tightenDir(dir);
20241
+ }
20242
+ function dbSidecars(file2) {
20243
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
20244
+ }
20245
+ function tightenFile(file2) {
20246
+ try {
20247
+ if (lstatSync(file2).isSymbolicLink()) return;
20248
+ } catch {
20249
+ }
20250
+ chmodBestEffort(file2, DATA_FILE_MODE);
20251
+ }
20252
+ function tightenPerms(file2) {
20253
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
20254
+ }
20255
+ function writeOwnerOnlyFileSync(file2, data) {
20256
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
20257
+ try {
20258
+ rmSync(tmp, { force: true });
20259
+ } catch {
20260
+ }
20261
+ try {
20262
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
20263
+ renameSync(tmp, file2);
20264
+ } finally {
20265
+ try {
20266
+ rmSync(tmp, { force: true });
20267
+ } catch {
20268
+ }
20269
+ }
20270
+ tightenFile(file2);
20057
20271
  }
20058
20272
 
20059
20273
  // ../packages/persistence/src/migrations.ts
@@ -20067,7 +20281,8 @@ function createdIndexName(statement) {
20067
20281
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
20068
20282
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
20069
20283
  }
20070
- function applyMigrations(db) {
20284
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
20285
+ function applyMigrations(db, file2) {
20071
20286
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
20072
20287
  db.exec(
20073
20288
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -20081,6 +20296,7 @@ function applyMigrations(db) {
20081
20296
  );
20082
20297
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
20083
20298
  if (applied.has(migration.tag)) continue;
20299
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
20084
20300
  const evidence = evidenceObjects(migration.sql);
20085
20301
  const present = evidence.filter((o) => evidenceExists(db, o));
20086
20302
  if (present.length > 0 && present.length < evidence.length) {
@@ -20125,7 +20341,6 @@ function applyMigrations(db) {
20125
20341
  if (legacyCount < SQLITE_MIGRATIONS.length) {
20126
20342
  db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
20127
20343
  }
20128
- ensureSyncedAtColumn(db, "events");
20129
20344
  ensureSyncedAtColumn(db, "audit_events");
20130
20345
  ensureScanLedgerTable(db);
20131
20346
  ensureBlockedDetectionsTable(db);
@@ -20133,6 +20348,47 @@ function applyMigrations(db) {
20133
20348
  ensureWriteGateTrigger(db);
20134
20349
  ensureTokenUsageColumns(db);
20135
20350
  reconcileSourceProjectIds(db);
20351
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
20352
+ const drained = runLegacyHistoryBackfill(db);
20353
+ if (drained) applyLegacyDropMigration(db, file2);
20354
+ }
20355
+ }
20356
+ function applyLegacyDropMigration(db, file2) {
20357
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
20358
+ if (!migration) return;
20359
+ if (file2) {
20360
+ try {
20361
+ backupBeforeLegacyDrop(db, file2);
20362
+ } catch (error51) {
20363
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
20364
+ return;
20365
+ }
20366
+ }
20367
+ try {
20368
+ withTransaction(
20369
+ db,
20370
+ () => {
20371
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
20372
+ if (alreadyDropped) return;
20373
+ for (const statement of splitStatements(migration.sql)) {
20374
+ db.exec(statement);
20375
+ }
20376
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
20377
+ migration.tag,
20378
+ Date.now()
20379
+ );
20380
+ },
20381
+ "IMMEDIATE"
20382
+ );
20383
+ } catch (error51) {
20384
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
20385
+ }
20386
+ }
20387
+ function backupBeforeLegacyDrop(db, file2) {
20388
+ const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
20389
+ db.prepare("VACUUM INTO ?").run(backup);
20390
+ tightenFile(backup);
20391
+ return backup;
20136
20392
  }
20137
20393
  var TOKEN_USAGE_COLUMNS = [
20138
20394
  {
@@ -20161,6 +20417,7 @@ var TOKEN_USAGE_COLUMNS = [
20161
20417
  }
20162
20418
  ];
20163
20419
  function ensureTokenUsageColumns(db) {
20420
+ if (!schemaObjectExists(db, "table", "audit_events")) return;
20164
20421
  const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
20165
20422
  for (const column of TOKEN_USAGE_COLUMNS) {
20166
20423
  if (!existing.has(column.name)) {
@@ -20226,11 +20483,187 @@ function reconcileSourceProjectIds(db) {
20226
20483
  akaWarn(`source_project id reconcile failed: ${String(error51)}`);
20227
20484
  }
20228
20485
  }
20486
+ var LEGACY_BACKFILL_BATCH_SIZE = 200;
20487
+ var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
20488
+ function getLegacyCopyWatermark(db, source) {
20489
+ const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
20490
+ return row?.lastRowid ?? 0;
20491
+ }
20492
+ function setLegacyCopyWatermark(db, source, lastRowid) {
20493
+ db.prepare(
20494
+ `INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
20495
+ ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
20496
+ ).run(source, lastRowid);
20497
+ }
20498
+ function drainLegacyTable(db, source, selectStmt, handleRows) {
20499
+ let watermark = getLegacyCopyWatermark(db, source);
20500
+ let processed = 0;
20501
+ while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
20502
+ const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
20503
+ if (rows.length === 0) return true;
20504
+ withTransaction(
20505
+ db,
20506
+ () => {
20507
+ handleRows(rows);
20508
+ watermark = rows[rows.length - 1]?.rowid ?? watermark;
20509
+ setLegacyCopyWatermark(db, source, watermark);
20510
+ },
20511
+ "IMMEDIATE"
20512
+ );
20513
+ processed += rows.length;
20514
+ if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
20515
+ }
20516
+ return false;
20517
+ }
20518
+ function parseLegacyEventMetadata(raw) {
20519
+ if (raw === null) return void 0;
20520
+ try {
20521
+ return JSON.parse(raw);
20522
+ } catch {
20523
+ return void 0;
20524
+ }
20525
+ }
20526
+ function toLegacyAuditAttributesJson(row) {
20527
+ return JSON.stringify(
20528
+ toCaptureAttributes({
20529
+ id: row.id,
20530
+ sourceTool: row.sourceTool,
20531
+ kind: row.kind,
20532
+ occurredAt: new Date(row.occurredAt).toISOString(),
20533
+ contentHash: row.contentHash,
20534
+ content: row.content,
20535
+ metadata: row.metadata
20536
+ })
20537
+ );
20538
+ }
20539
+ function copyLegacyEvents(db) {
20540
+ const selectStmt = db.prepare(
20541
+ `SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
20542
+ content_hash AS contentHash, content, metadata
20543
+ FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
20544
+ );
20545
+ const insertStmt = db.prepare(
20546
+ `INSERT OR IGNORE INTO audit_events
20547
+ (id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
20548
+ VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
20549
+ );
20550
+ const stubRootStmt = db.prepare(
20551
+ `INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
20552
+ );
20553
+ return drainLegacyTable(
20554
+ db,
20555
+ "events",
20556
+ selectStmt,
20557
+ (rows) => {
20558
+ for (const row of rows) {
20559
+ const metadata = parseLegacyEventMetadata(row.metadata);
20560
+ const sessionId = metadata?.sessionId ?? null;
20561
+ if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
20562
+ insertStmt.run(
20563
+ bindParams({
20564
+ id: row.id,
20565
+ parentId: sessionId,
20566
+ rootSessionId: sessionId,
20567
+ eventType: row.kind,
20568
+ startedAt: row.occurredAt,
20569
+ content: row.content,
20570
+ contentHash: row.contentHash,
20571
+ attributes: toLegacyAuditAttributesJson({ ...row, metadata })
20572
+ })
20573
+ );
20574
+ }
20575
+ }
20576
+ );
20577
+ }
20578
+ function copyLegacyFindings(db) {
20579
+ const selectStmt = db.prepare(
20580
+ `SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
20581
+ span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
20582
+ action_taken AS actionTaken, confidence, finding_key AS findingKey,
20583
+ first_detected_at AS firstDetectedAt
20584
+ FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
20585
+ );
20586
+ const definitionStmt = db.prepare(
20587
+ `INSERT OR IGNORE INTO inspection_definitions
20588
+ (id, rule_id, name, category, severity, definition, version)
20589
+ VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
20590
+ );
20591
+ const findingStmt = db.prepare(
20592
+ `INSERT INTO inspection_findings
20593
+ (id, audit_event_id, inspection_definition_id, classified_data_id,
20594
+ span_start, span_end, masked_match, action_taken, confidence,
20595
+ finding_key, first_detected_at)
20596
+ VALUES
20597
+ (:id, :auditEventId, :inspectionDefinitionId, NULL,
20598
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
20599
+ :findingKey, :firstDetectedAt)
20600
+ ON CONFLICT(id) DO NOTHING
20601
+ ON CONFLICT (finding_key) DO UPDATE SET
20602
+ first_detected_at = CASE
20603
+ WHEN first_detected_at IS NULL THEN excluded.first_detected_at
20604
+ WHEN excluded.first_detected_at IS NULL THEN first_detected_at
20605
+ ELSE min(first_detected_at, excluded.first_detected_at)
20606
+ END`
20607
+ );
20608
+ return drainLegacyTable(
20609
+ db,
20610
+ "findings",
20611
+ selectStmt,
20612
+ (rows) => {
20613
+ const definitionIds = /* @__PURE__ */ new Map();
20614
+ for (const row of rows) {
20615
+ const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
20616
+ let definitionId = definitionIds.get(tupleKey);
20617
+ if (definitionId === void 0) {
20618
+ const version2 = `unmigrated/${row.category}/${row.severity}`;
20619
+ definitionId = inspectionDefinitionId(row.ruleId, version2);
20620
+ definitionStmt.run(
20621
+ bindParams({
20622
+ id: definitionId,
20623
+ ruleId: row.ruleId,
20624
+ name: row.ruleId,
20625
+ category: row.category,
20626
+ severity: row.severity,
20627
+ definition: "",
20628
+ version: version2
20629
+ })
20630
+ );
20631
+ definitionIds.set(tupleKey, definitionId);
20632
+ }
20633
+ findingStmt.run(
20634
+ bindParams({
20635
+ id: row.id,
20636
+ auditEventId: row.eventId,
20637
+ inspectionDefinitionId: definitionId,
20638
+ spanStart: row.spanStart,
20639
+ spanEnd: row.spanEnd,
20640
+ maskedMatch: row.maskedMatch,
20641
+ actionTaken: row.actionTaken,
20642
+ confidence: row.confidence,
20643
+ findingKey: row.findingKey,
20644
+ firstDetectedAt: row.firstDetectedAt
20645
+ })
20646
+ );
20647
+ }
20648
+ }
20649
+ );
20650
+ }
20651
+ function runLegacyHistoryBackfill(db) {
20652
+ try {
20653
+ const eventsCaughtUp = copyLegacyEvents(db);
20654
+ if (!eventsCaughtUp) return false;
20655
+ return copyLegacyFindings(db);
20656
+ } catch (error51) {
20657
+ akaWarn(`legacy history backfill failed: ${String(error51)}`);
20658
+ return false;
20659
+ }
20660
+ }
20229
20661
  function isForeignSqliteLineage(db) {
20230
20662
  if (schemaObjectExists(db, "table", "tenants")) return true;
20231
20663
  return columnNames(db, "events").includes("tenant_id");
20232
20664
  }
20233
20665
  function ensureSyncedAtColumn(db, table) {
20666
+ if (!schemaObjectExists(db, "table", table)) return;
20234
20667
  if (!columnNames(db, table).includes("synced_at")) {
20235
20668
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
20236
20669
  }
@@ -20251,6 +20684,7 @@ function ensureWriteGateTrigger(db) {
20251
20684
  CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
20252
20685
  )`);
20253
20686
  db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
20687
+ if (!schemaObjectExists(db, "table", "installed_packs")) return;
20254
20688
  db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
20255
20689
  BEFORE UPDATE OF version, name, rules_json ON installed_packs
20256
20690
  WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
@@ -20278,30 +20712,6 @@ function ensureRuleProbeCacheTable(db) {
20278
20712
  )`);
20279
20713
  }
20280
20714
 
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
20715
  // ../packages/persistence/src/internal/json.ts
20306
20716
  function safeJson(s, fallback) {
20307
20717
  if (s == null) return fallback;
@@ -20321,51 +20731,6 @@ function parseJsonObject(s) {
20321
20731
  return void 0;
20322
20732
  }
20323
20733
 
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
20734
  // ../packages/persistence/src/repositories/activity.ts
20370
20735
  var DAY_MS = 864e5;
20371
20736
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -20982,6 +21347,21 @@ var SqliteAuditEventsRepository = class {
20982
21347
  })
20983
21348
  );
20984
21349
  }
21350
+ // Idempotent stub of a session's structural root. Session-scoped leaves
21351
+ // (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
21352
+ // INSERT OR IGNORE does NOT suppress a foreign-key violation (only
21353
+ // UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
21354
+ // raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
21355
+ // dropping the write under failOpenTransaction. SessionStart's own root write
21356
+ // is itself fail-open and marks "attempted", not "succeeded", so a session
21357
+ // with no root row yet is a real, permanent condition, not a transient race.
21358
+ // The stub carries no dimensions/attributes; an authoritative root
21359
+ // (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
21360
+ // on the id PK, so the stub never shadows real data. This is the single named
21361
+ // home for that FK invariant — call it before writing any session-scoped row.
21362
+ ensureSessionRoot(sessionId, startedAt) {
21363
+ this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
21364
+ }
20985
21365
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
20986
21366
  // (which takes a caller-supplied random id), the id here is MINTED internally
20987
21367
  // from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
@@ -21443,8 +21823,14 @@ var SqliteDetectionsRepository = class {
21443
21823
  )
21444
21824
  );
21445
21825
  }
21446
- // Findings whose parent event occurred in the last 30 days and whose rule_id is
21447
- // in the given set. Mirrors the security repo's findings⋈events window join.
21826
+ // Findings whose parent audit event occurred in the last 30 days, is one of
21827
+ // the four capture kinds, and whose definition's rule_id is in the given set.
21828
+ // Mirrors the security repo's inspection_findings⋈audit_events window join.
21829
+ // rule_id lives on inspection_definitions, not the finding row, so the join
21830
+ // chains through it. audit_events also holds structural rows (session, run,
21831
+ // tool_call, llm_call, source_lookup, config_scan) that never had a legacy
21832
+ // events counterpart, so the event_type predicate keeps this count identical
21833
+ // to the old findings⋈events one.
21448
21834
  countFindingsLast30d(ruleIds) {
21449
21835
  if (ruleIds.length === 0) return 0;
21450
21836
  const since = this.now() - 30 * DAY_MS2;
@@ -21452,8 +21838,12 @@ var SqliteDetectionsRepository = class {
21452
21838
  return countScalar(
21453
21839
  this.db,
21454
21840
  `SELECT count(*) AS n
21455
- FROM findings f JOIN events e ON e.id = f.event_id
21456
- WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
21841
+ FROM inspection_findings f
21842
+ JOIN audit_events e ON e.id = f.audit_event_id
21843
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21844
+ WHERE e.started_at >= ?
21845
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
21846
+ AND d.rule_id IN (${inClause})`,
21457
21847
  [since, ...ruleIds]
21458
21848
  );
21459
21849
  }
@@ -21463,35 +21853,24 @@ var SqliteDetectionsRepository = class {
21463
21853
  var SqliteEventsRepository = class {
21464
21854
  constructor(db) {
21465
21855
  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
21856
  }
21471
21857
  db;
21472
- insertStmt;
21473
- insertEvent(event) {
21474
- const row = toEventRow(event);
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.
21858
+ // Every recorded capture's content hash — the historical backfill loads this
21859
+ // once to skip transcript messages it has already stored, so re-running the
21860
+ // scan never duplicates findings.
21490
21861
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
21491
21862
  // async EventsReadPort contract.
21863
+ //
21864
+ // audit_events also holds structural rows (session, run, tool_call, llm_call,
21865
+ // source_lookup, config_scan) with a NULL content_hash, so the capture-kind
21866
+ // predicate isn't load-bearing here — it documents intent and keeps the scan
21867
+ // index-friendly rather than walking rows that can never match.
21492
21868
  contentHashes() {
21493
21869
  const rows = allRows(
21494
- this.db.prepare("SELECT content_hash FROM events")
21870
+ this.db.prepare(
21871
+ `SELECT content_hash FROM audit_events
21872
+ WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
21873
+ )
21495
21874
  );
21496
21875
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
21497
21876
  }
@@ -21827,17 +22206,20 @@ function parseExceptionRow(row) {
21827
22206
  }
21828
22207
 
21829
22208
  // ../packages/persistence/src/repositories/resolution-sql.ts
21830
- function latestResolutionStatusSql(findingsAlias) {
22209
+ function latestResolutionColumnSql(column, findingsAlias) {
21831
22210
  return `(
21832
- SELECT fr.status FROM finding_resolution fr
22211
+ SELECT fr.${column} FROM finding_resolution fr
21833
22212
  WHERE fr.finding_key = ${findingsAlias}.finding_key
21834
22213
  ORDER BY fr.created_at DESC, fr.rowid DESC
21835
22214
  LIMIT 1
21836
22215
  )`;
21837
22216
  }
22217
+ function latestResolutionStatusSql(findingsAlias) {
22218
+ return latestResolutionColumnSql("status", findingsAlias);
22219
+ }
21838
22220
  var LATEST_RESOLUTION_BY_KEY_SQL = `(
21839
- SELECT finding_key, status FROM (
21840
- SELECT fr.finding_key, fr.status,
22221
+ SELECT finding_key, status, method, resolved_at FROM (
22222
+ SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
21841
22223
  ROW_NUMBER() OVER (
21842
22224
  PARTITION BY fr.finding_key
21843
22225
  ORDER BY fr.created_at DESC, fr.rowid DESC
@@ -21864,68 +22246,21 @@ var DAY_MS3 = 864e5;
21864
22246
  var SqliteFindingsRepository = class {
21865
22247
  constructor(db) {
21866
22248
  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
22249
  }
21888
22250
  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
22251
  recentFindings(opts) {
21922
22252
  const limit = opts?.limit ?? 50;
21923
22253
  const rows = allRows(
21924
22254
  this.db.prepare(
21925
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
21926
- f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
21927
- FROM findings f JOIN events e ON e.id = f.event_id
21928
- ORDER BY e.occurred_at DESC, f.rowid DESC
22255
+ `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
22256
+ f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
22257
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
22258
+ e.event_type AS kind
22259
+ FROM inspection_findings f
22260
+ JOIN audit_events e ON e.id = f.audit_event_id
22261
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22262
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22263
+ ORDER BY e.started_at DESC, f.rowid DESC
21929
22264
  LIMIT :limit`
21930
22265
  ),
21931
22266
  { limit }
@@ -21947,25 +22282,34 @@ var SqliteFindingsRepository = class {
21947
22282
  );
21948
22283
  }
21949
22284
  /** Live-enforced findings recorded for one session — a bare COUNT over the
21950
- * session-stamped events (served by idx_events_session_id), so the Activity
22285
+ * session-stamped audit_events (served by idx_audit_session), so the Activity
21951
22286
  * page can label its findings link without the grouped pipeline. */
21952
22287
  sessionFindingsCount(sessionId) {
21953
22288
  if (!sessionId) return Promise.resolve(0);
21954
22289
  return Promise.resolve(
21955
22290
  countScalar(
21956
22291
  this.db,
21957
- `SELECT count(*) AS n FROM findings f
21958
- JOIN events e ON e.id = f.event_id
21959
- WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
22292
+ `SELECT count(*) AS n FROM inspection_findings f
22293
+ JOIN audit_events e ON e.id = f.audit_event_id
22294
+ WHERE e.root_session_id = :sessionId
22295
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
21960
22296
  { sessionId }
21961
22297
  )
21962
22298
  );
21963
22299
  }
21964
- /** Per-rule transcript firing tally for one session — reads the OTHER finding
21965
- * store (inspection_findings, keyed to audit_events): every detection the
21966
- * transcript pass recorded, counted per firing rather than per unique value.
21967
- * Rides on session-scoped grouped responses so the findings view can
21968
- * reconcile the Activity page's tally with the deduped groups it lists. */
22300
+ /** Per-rule transcript firing tally for one session — every detection the
22301
+ * transcript-reconciler pass recorded against the session's `tool_call` rows,
22302
+ * counted per firing rather than per unique value. Rides on session-scoped
22303
+ * grouped responses so the findings view can reconcile the Activity page's
22304
+ * tally with the deduped groups it lists.
22305
+ *
22306
+ * `inspection_findings`/`audit_events` are now the SAME physical tables the
22307
+ * rest of this class reads for the live-capture list above (they used to be
22308
+ * a separate store), so this excludes the four capture kinds those rows
22309
+ * already carry — without that exclusion, every live-capture finding in the
22310
+ * session would be tallied here too, double-counting against the grouped
22311
+ * list this response rides alongside. The reconciler attaches its findings
22312
+ * only to `tool_call` rows, which the exclusion leaves untouched. */
21969
22313
  sessionFirings(sessionId) {
21970
22314
  return Object.fromEntries(
21971
22315
  countBy(
@@ -21975,18 +22319,25 @@ var SqliteFindingsRepository = class {
21975
22319
  JOIN audit_events e ON e.id = f.audit_event_id
21976
22320
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21977
22321
  WHERE e.root_session_id = :sessionId
22322
+ AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
21978
22323
  GROUP BY d.rule_id`,
21979
22324
  { sessionId }
21980
22325
  )
21981
22326
  );
21982
22327
  }
21983
22328
  /**
21984
- * Grouped findings for the dashboard — joins findings⋈events (repo/file/
21985
- * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
21986
- * applies the requested filters, and sorts by severity then recency. Filtering
22329
+ * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
22330
+ * ⋈inspection_definitions (repo/file/toolName from the audit event's
22331
+ * attributes bag, rule_id/category/severity from the definition), scoped to
22332
+ * the four capture kinds (audit_events also holds structural/reconciler/scan
22333
+ * rows this list must never surface), groups by ruleId, computes
22334
+ * per-filter-excluded facets, applies the requested filters, and sorts by
22335
+ * severity then recency. Filtering
21987
22336
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
21988
22337
  * reflect the full filtered set; `items` is the requested
21989
- * page (default 50); no cursor (nextCursor is always null).
22338
+ * page (default 50); no cursor (nextCursor is always null). Under a `status`
22339
+ * filter, `totals.findings` counts only instances whose derived status was
22340
+ * requested, and each item's instance preview is narrowed the same way.
21990
22341
  *
21991
22342
  * Two reads, neither of which materializes a row per finding:
21992
22343
  * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
@@ -21999,10 +22350,11 @@ var SqliteFindingsRepository = class {
21999
22350
  * rule is ever restated in SQL.
22000
22351
  */
22001
22352
  listGroupedFindings(query) {
22002
- const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
22353
+ const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
22354
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
22003
22355
  const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
22004
22356
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
22005
- predicate: sessionPredicate,
22357
+ predicate,
22006
22358
  params: sessionParams
22007
22359
  });
22008
22360
  const rows = allRows(
@@ -22010,24 +22362,26 @@ var SqliteFindingsRepository = class {
22010
22362
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
22011
22363
  occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
22012
22364
  FROM (
22013
- SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
22014
- f.severity AS severity, f.masked_match AS masked_match,
22365
+ SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
22366
+ d.severity AS severity, f.masked_match AS masked_match,
22015
22367
  f.action_taken AS action_taken, f.confidence AS confidence,
22016
- e.occurred_at AS occurred_at, e.source_tool AS source_tool,
22017
- json_extract(e.metadata, '$.repo') AS repo,
22018
- json_extract(e.metadata, '$.filePath') AS file,
22019
- json_extract(e.metadata, '$.toolName') AS tool_name,
22020
- e.kind AS kind, f.finding_key AS finding_key,
22368
+ e.started_at AS occurred_at,
22369
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
22370
+ json_extract(e.attributes, '$.repo') AS repo,
22371
+ json_extract(e.attributes, '$.file_path') AS file,
22372
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
22373
+ e.event_type AS kind, f.finding_key AS finding_key,
22021
22374
  latest.status AS latest_status,
22022
22375
  ROW_NUMBER() OVER (
22023
- PARTITION BY f.rule_id
22024
- ORDER BY e.occurred_at DESC, f.id DESC
22376
+ PARTITION BY d.rule_id
22377
+ ORDER BY e.started_at DESC, f.id DESC
22025
22378
  ) AS rn
22026
- FROM findings f
22027
- JOIN events e ON e.id = f.event_id
22379
+ FROM inspection_findings f
22380
+ JOIN audit_events e ON e.id = f.audit_event_id
22381
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22028
22382
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22029
22383
  ON latest.finding_key = f.finding_key
22030
- ${sessionPredicate}
22384
+ ${predicate}
22031
22385
  )
22032
22386
  WHERE rn <= :cap
22033
22387
  ORDER BY occurred_at DESC, id DESC`
@@ -22054,17 +22408,29 @@ var SqliteFindingsRepository = class {
22054
22408
  severity: query.severity,
22055
22409
  providers: query.provider,
22056
22410
  actions: query.action,
22411
+ statuses: query.status,
22057
22412
  subtype: query.subtype,
22058
22413
  q: query.q
22059
22414
  };
22060
22415
  const facets = computeFindingFacets(allGroups, filterOpts);
22061
22416
  const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
22417
+ const statusFilter = query.status ?? [];
22062
22418
  const totals = {
22063
- findings: sorted.reduce((acc, g) => acc + g.instanceCount, 0),
22419
+ findings: sorted.reduce((acc, g) => {
22420
+ if (statusFilter.length === 0) return acc + g.instanceCount;
22421
+ const agg = aggregates.get(g.id);
22422
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
22423
+ }, 0),
22064
22424
  groups: sorted.length
22065
22425
  };
22066
22426
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
22067
- const items = sorted.slice(0, limit);
22427
+ const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
22428
+ const items = sorted.slice(0, limit).map(
22429
+ (g) => statusSet ? {
22430
+ ...g,
22431
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
22432
+ } : g
22433
+ );
22068
22434
  return Promise.resolve({
22069
22435
  totals,
22070
22436
  facets,
@@ -22078,45 +22444,62 @@ var SqliteFindingsRepository = class {
22078
22444
  * buildFindingGroups cannot recover from a preview. Bounded by the number of
22079
22445
  * distinct rule_ids (the installed packs' rules), not by the store's size.
22080
22446
  *
22081
- * The per-instance sets ride back as group_concat lists of RAW DB values —
22082
- * source_tool, action_taken, and the (kind, has-key, latest-status) triples
22083
- * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
22084
- * status keeps the classifier itself in @akasecurity/schema, where
22085
- * severitySummary's SQL and this query can't drift apart on what 'resolved'
22086
- * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
22447
+ * A single scan, folded in two levels: the inner SELECT groups by
22448
+ * (rule_id, status tuple) so each (kind, has-key, latest-status) combination
22449
+ * carries its instance count — countInstancesByStatus needs those counts for
22450
+ * status-scoped totals — and the outer SELECT folds the tuples back to one
22451
+ * row per rule. The per-instance sets ride back as group_concat lists of RAW
22452
+ * DB values — source_tool, action_taken, and the tuples deriveFindingStatus
22453
+ * consumes. Aggregating the status INPUTS rather than a status keeps the
22454
+ * classifier itself in @akasecurity/schema, where severitySummary's SQL and
22455
+ * this query can't drift apart on what 'resolved' means (see
22456
+ * resolution-sql.ts). The concat-of-concats can repeat a value across
22457
+ * tuples; the schema mappers dedupe, and each set is bounded by an enum, so
22087
22458
  * a group's row stays small however many findings it holds.
22088
22459
  *
22089
22460
  * `withSearchText` is the exception, and the one column here that does NOT
22090
- * stay small: the group's distinct repos/filePaths, whose size tracks how many
22091
- * distinct paths a rule fired across — for a rule hitting mostly-unique paths
22092
- * that is a string proportional to the store (~8MB over 200k distinct paths,
22093
- * and buildHaystack lowercases a second copy). It buys `q` the ability to
22094
- * match an instance outside the preview, which searching the preview alone
22095
- * would silently lose, so it is fetched only when the request actually
22096
- * carries a `q`.
22461
+ * stay small: the group's per-tuple-distinct repos/filePaths, whose size
22462
+ * tracks how many distinct paths a rule fired across — for a rule hitting
22463
+ * mostly-unique paths that is a string proportional to the store (~8MB over
22464
+ * 200k distinct paths, and buildHaystack lowercases a second copy). It buys
22465
+ * `q` the ability to match an instance outside the preview, which searching
22466
+ * the preview alone would silently lose, so it is fetched only when the
22467
+ * request actually carries a `q`. (Substring matching is unaffected by a
22468
+ * path repeating across tuples.)
22097
22469
  */
22098
22470
  groupAggregates(withSearchText, scope) {
22099
- const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
22100
- group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
22101
- group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
22471
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
22472
+ group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
22473
+ group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
22102
22474
  const rows = this.db.prepare(
22103
- `SELECT f.rule_id AS rule_id,
22104
- count(*) AS instance_count,
22105
- max(e.occurred_at) AS latest_at,
22106
- group_concat(DISTINCT e.source_tool) AS source_tools,
22107
- group_concat(DISTINCT f.action_taken) AS actions_taken,
22108
- group_concat(DISTINCT (
22109
- e.kind || '${TUPLE_SEP}' ||
22110
- (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
22111
- coalesce(latest.status, '')
22112
- )) AS status_inputs
22113
- ${searchTextColumns}
22114
- FROM findings f
22115
- JOIN events e ON e.id = f.event_id
22116
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22117
- ON latest.finding_key = f.finding_key
22118
- ${scope.predicate}
22119
- GROUP BY f.rule_id`
22475
+ `SELECT rule_id,
22476
+ sum(tuple_count) AS instance_count,
22477
+ max(latest_at) AS latest_at,
22478
+ group_concat(source_tools) AS source_tools,
22479
+ group_concat(actions_taken) AS actions_taken,
22480
+ group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
22481
+ group_concat(repos) AS repos,
22482
+ group_concat(files) AS files,
22483
+ group_concat(tool_names) AS tool_names
22484
+ FROM (
22485
+ SELECT d.rule_id AS rule_id,
22486
+ e.event_type || '${TUPLE_SEP}' ||
22487
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
22488
+ coalesce(latest.status, '') AS status_tuple,
22489
+ count(*) AS tuple_count,
22490
+ max(e.started_at) AS latest_at,
22491
+ group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
22492
+ group_concat(DISTINCT f.action_taken) AS actions_taken
22493
+ ${innerSearchColumns}
22494
+ FROM inspection_findings f
22495
+ JOIN audit_events e ON e.id = f.audit_event_id
22496
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22497
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22498
+ ON latest.finding_key = f.finding_key
22499
+ ${scope.predicate}
22500
+ GROUP BY d.rule_id, status_tuple
22501
+ )
22502
+ GROUP BY rule_id`
22120
22503
  ).all(scope.params);
22121
22504
  return new Map(
22122
22505
  rows.map((r) => [
@@ -22126,13 +22509,14 @@ var SqliteFindingsRepository = class {
22126
22509
  sourceTools: splitConcat(r.source_tools),
22127
22510
  actionsTaken: splitConcat(r.actions_taken),
22128
22511
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
22129
- const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
22512
+ const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
22130
22513
  return {
22131
22514
  // deriveFindingStatus only distinguishes null from non-null here,
22132
22515
  // so the marker stands in for the key itself (never rendered).
22133
22516
  kind,
22134
22517
  findingKey: keyMarker === "" ? null : keyMarker,
22135
- latestResolutionStatus: latestStatus === "" ? null : latestStatus
22518
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus,
22519
+ count: Number(count)
22136
22520
  };
22137
22521
  }),
22138
22522
  latestDetectedAt: epochMillisToIso(r.latest_at),
@@ -22149,10 +22533,21 @@ var SqliteFindingsRepository = class {
22149
22533
  );
22150
22534
  }
22151
22535
  healthSummary() {
22152
- const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
22536
+ const total = countScalar(
22537
+ this.db,
22538
+ `SELECT count(*) AS n FROM inspection_findings f
22539
+ JOIN audit_events e ON e.id = f.audit_event_id
22540
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
22541
+ );
22153
22542
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
22154
22543
  const grouped = allRows(
22155
- this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
22544
+ this.db.prepare(
22545
+ `SELECT f.action_taken AS action_taken, count(*) AS c
22546
+ FROM inspection_findings f
22547
+ JOIN audit_events e ON e.id = f.audit_event_id
22548
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22549
+ GROUP BY f.action_taken`
22550
+ )
22156
22551
  );
22157
22552
  for (const row of grouped) {
22158
22553
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
@@ -22160,12 +22555,15 @@ var SqliteFindingsRepository = class {
22160
22555
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
22161
22556
  const sevRows = allRows(
22162
22557
  this.db.prepare(
22163
- `SELECT f.severity AS severity, count(*) AS c
22164
- FROM findings f
22558
+ `SELECT d.severity AS severity, count(*) AS c
22559
+ FROM inspection_findings f
22560
+ JOIN audit_events e ON e.id = f.audit_event_id
22561
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22165
22562
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22166
22563
  ON latest.finding_key = f.finding_key
22167
- WHERE latest.status IS NULL OR latest.status != 'resolved'
22168
- GROUP BY f.severity`
22564
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22565
+ AND (latest.status IS NULL OR latest.status != 'resolved')
22566
+ GROUP BY d.severity`
22169
22567
  )
22170
22568
  );
22171
22569
  for (const row of sevRows) {
@@ -22186,9 +22584,11 @@ var SqliteFindingsRepository = class {
22186
22584
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
22187
22585
  const rows = allRows(
22188
22586
  this.db.prepare(
22189
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
22190
- FROM findings f JOIN events e ON e.id = f.event_id
22191
- WHERE e.occurred_at >= :since
22587
+ `SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
22588
+ FROM inspection_findings f
22589
+ JOIN audit_events e ON e.id = f.audit_event_id
22590
+ WHERE e.started_at >= :since
22591
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22192
22592
  GROUP BY day, f.action_taken`
22193
22593
  ),
22194
22594
  { since }
@@ -22253,15 +22653,59 @@ var SqliteInspectionFindingsRepository = class {
22253
22653
  this.insertStmt = db.prepare(
22254
22654
  `INSERT INTO inspection_findings
22255
22655
  (id, audit_event_id, inspection_definition_id, classified_data_id,
22256
- span_start, span_end, masked_match, action_taken, confidence)
22656
+ span_start, span_end, masked_match, action_taken, confidence,
22657
+ finding_key, first_detected_at)
22257
22658
  VALUES
22258
22659
  (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
22259
- :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence)
22260
- ON CONFLICT(id) DO NOTHING`
22660
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
22661
+ :findingKey,
22662
+ COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
22663
+ ON CONFLICT(id) DO UPDATE SET
22664
+ inspection_definition_id = excluded.inspection_definition_id
22665
+ ON CONFLICT (finding_key) DO UPDATE SET
22666
+ audit_event_id = excluded.audit_event_id,
22667
+ inspection_definition_id = excluded.inspection_definition_id,
22668
+ classified_data_id = excluded.classified_data_id,
22669
+ span_start = excluded.span_start,
22670
+ span_end = excluded.span_end,
22671
+ masked_match = excluded.masked_match,
22672
+ action_taken = excluded.action_taken,
22673
+ confidence = excluded.confidence`
22674
+ );
22675
+ this.sessionDupStmt = db.prepare(
22676
+ `SELECT 1 FROM inspection_findings f
22677
+ JOIN audit_events e ON e.id = f.audit_event_id
22678
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22679
+ WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
22680
+ AND e.root_session_id = :sessionId
22681
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22682
+ LIMIT 1`
22683
+ );
22684
+ this.eventDupStmt = db.prepare(
22685
+ `SELECT 1 FROM inspection_findings f
22686
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22687
+ WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
22688
+ AND f.masked_match = :maskedMatch
22689
+ AND f.span_start = :spanStart AND f.span_end = :spanEnd
22690
+ LIMIT 1`
22261
22691
  );
22262
22692
  }
22263
22693
  db;
22264
22694
  insertStmt;
22695
+ sessionDupStmt;
22696
+ eventDupStmt;
22697
+ // True when an earlier event in the same session already recorded a finding
22698
+ // with the same rule and masked value. The current event's own findings are
22699
+ // inserted one at a time in caller order, so an earlier finding in the SAME
22700
+ // recordCapture call is visible to a later duplicate check within it too.
22701
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
22702
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
22703
+ }
22704
+ // True when this exact detection (rule + masked value + span) is already
22705
+ // recorded against the given audit event.
22706
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
22707
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
22708
+ }
22265
22709
  insertFinding(input) {
22266
22710
  const row = toInspectionFindingRow(input);
22267
22711
  this.insertStmt.run(
@@ -22274,7 +22718,9 @@ var SqliteInspectionFindingsRepository = class {
22274
22718
  spanEnd: row.spanEnd,
22275
22719
  maskedMatch: row.maskedMatch,
22276
22720
  actionTaken: row.actionTaken,
22277
- confidence: row.confidence
22721
+ confidence: row.confidence,
22722
+ findingKey: row.findingKey,
22723
+ firstDetectedAt: row.firstDetectedAt
22278
22724
  })
22279
22725
  );
22280
22726
  }
@@ -22546,7 +22992,7 @@ var SqliteInstalledPacksRepository = class {
22546
22992
  installedRuleset() {
22547
22993
  const rows = allRows(
22548
22994
  this.db.prepare(
22549
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
22995
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
22550
22996
  )
22551
22997
  );
22552
22998
  const out = {
@@ -22554,7 +23000,8 @@ var SqliteInstalledPacksRepository = class {
22554
23000
  enabledPacks: 0,
22555
23001
  rules: [],
22556
23002
  invalidRules: 0,
22557
- ruleActions: /* @__PURE__ */ new Map()
23003
+ ruleActions: /* @__PURE__ */ new Map(),
23004
+ ruleVersions: /* @__PURE__ */ new Map()
22558
23005
  };
22559
23006
  for (const row of rows) {
22560
23007
  if (!intToBool(row.enabled)) continue;
@@ -22576,6 +23023,7 @@ var SqliteInstalledPacksRepository = class {
22576
23023
  if (parsed.success) {
22577
23024
  out.rules.push(parsed.data);
22578
23025
  out.ruleActions.set(parsed.data.id, action);
23026
+ out.ruleVersions.set(parsed.data.id, row.version);
22579
23027
  } else out.invalidRules += 1;
22580
23028
  }
22581
23029
  }
@@ -23750,19 +24198,19 @@ var SqliteResolutionsRepository = class {
23750
24198
  );
23751
24199
  this.openAtRestStmt = db.prepare(
23752
24200
  `SELECT DISTINCT f.finding_key AS finding_key
23753
- FROM findings f
23754
- JOIN events e ON e.id = f.event_id
23755
- WHERE e.kind = 'code_change'
23756
- AND json_extract(e.metadata, '$.filePath') = :path
24201
+ FROM inspection_findings f
24202
+ JOIN audit_events e ON e.id = f.audit_event_id
24203
+ WHERE e.event_type = 'code_change'
24204
+ AND json_extract(e.attributes, '$.file_path') = :path
23757
24205
  AND f.finding_key IS NOT NULL
23758
24206
  AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
23759
24207
  );
23760
24208
  this.resolvedAtRestStmt = db.prepare(
23761
24209
  `SELECT DISTINCT f.finding_key AS finding_key
23762
- FROM findings f
23763
- JOIN events e ON e.id = f.event_id
23764
- WHERE e.kind = 'code_change'
23765
- AND json_extract(e.metadata, '$.filePath') = :path
24210
+ FROM inspection_findings f
24211
+ JOIN audit_events e ON e.id = f.audit_event_id
24212
+ WHERE e.event_type = 'code_change'
24213
+ AND json_extract(e.attributes, '$.file_path') = :path
23766
24214
  AND f.finding_key IS NOT NULL
23767
24215
  AND ${latestResolutionStatusSql("f")} = 'resolved'`
23768
24216
  );
@@ -23985,25 +24433,27 @@ var SqliteSecurityRepository = class {
23985
24433
  severitySummary() {
23986
24434
  const rows = allRows(
23987
24435
  this.db.prepare(
23988
- `SELECT f.severity AS severity,
24436
+ `SELECT d.severity AS severity,
23989
24437
  COUNT(*) AS count,
23990
24438
  SUM(CASE
23991
- WHEN e.kind != 'code_change' THEN 1
24439
+ WHEN e.event_type != 'code_change' THEN 1
23992
24440
  WHEN f.finding_key IS NULL THEN 0
23993
24441
  WHEN latest.status = 'resolved' THEN 1
23994
24442
  ELSE 0
23995
24443
  END) AS caught,
23996
24444
  SUM(CASE
23997
- WHEN e.kind = 'code_change'
24445
+ WHEN e.event_type = 'code_change'
23998
24446
  AND f.finding_key IS NOT NULL
23999
24447
  AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
24000
24448
  ELSE 0
24001
24449
  END) AS open_at_rest
24002
- FROM findings f
24003
- JOIN events e ON e.id = f.event_id
24450
+ FROM inspection_findings f
24451
+ JOIN audit_events e ON e.id = f.audit_event_id
24452
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
24004
24453
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
24005
24454
  ON latest.finding_key = f.finding_key
24006
- GROUP BY f.severity`
24455
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
24456
+ GROUP BY d.severity`
24007
24457
  )
24008
24458
  );
24009
24459
  const byRow = new Map(rows.map((r) => [r.severity, r]));
@@ -24069,7 +24519,7 @@ var SqliteSecurityRepository = class {
24069
24519
  // Mean time-to-remediate per bucket, split by severity — a sibling of
24070
24520
  // findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
24071
24521
  // on a different timestamp: findingsTimeseries buckets by first-detection
24072
- // (events.occurred_at), this buckets by resolution time (the latest
24522
+ // (audit_events.started_at), this buckets by resolution time (the latest
24073
24523
  // finding_resolution row's resolved_at) — it's a "resolved in this bucket"
24074
24524
  // trend, not a "detected in this bucket" one. Only findings whose LATEST
24075
24525
  // resolution row (latest-resolution-wins, same correlated subquery as
@@ -24094,30 +24544,20 @@ var SqliteSecurityRepository = class {
24094
24544
  // first_detected_at is the PRESERVED first-detection time (set once on a
24095
24545
  // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
24096
24546
  // measures from first sighting — not the latest re-scan's event, whose
24097
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
24098
- // the parent event's occurred_at defends against any legacy/edge row the
24099
- // backfill left null.
24100
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
24101
- (
24102
- SELECT fr.status FROM finding_resolution fr
24103
- WHERE fr.finding_key = f.finding_key
24104
- ORDER BY fr.created_at DESC, fr.rowid DESC
24105
- LIMIT 1
24106
- ) AS latest_status,
24107
- (
24108
- SELECT fr.method FROM finding_resolution fr
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
24547
+ // started_at the upsert overwrites onto inspection_findings.audit_event_id.
24548
+ // COALESCE onto the parent event's started_at defends against any
24549
+ // legacy/edge row the backfill left null.
24550
+ `SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
24551
+ latest.status AS latest_status,
24552
+ latest.method AS latest_method,
24553
+ latest.resolved_at AS latest_resolved_at
24554
+ FROM inspection_findings f
24555
+ JOIN audit_events e ON e.id = f.audit_event_id
24556
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
24557
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
24558
+ ON latest.finding_key = f.finding_key
24120
24559
  WHERE f.finding_key IS NOT NULL
24560
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
24121
24561
  AND EXISTS (
24122
24562
  SELECT 1 FROM finding_resolution fr
24123
24563
  WHERE fr.finding_key = f.finding_key
@@ -24164,11 +24604,13 @@ var SqliteSecurityRepository = class {
24164
24604
  const from = now - RANGE_DAYS[range] * DAY_MS4;
24165
24605
  const rows = allRows(
24166
24606
  this.db.prepare(
24167
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
24168
- FROM findings f JOIN events e ON e.id = f.event_id
24169
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
24170
- AND json_extract(e.metadata, '$.repo') IS NOT NULL
24171
- AND json_extract(e.metadata, '$.repo') != ''
24607
+ `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
24608
+ FROM inspection_findings f
24609
+ JOIN audit_events e ON e.id = f.audit_event_id
24610
+ WHERE e.started_at >= :from AND e.started_at < :to
24611
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
24612
+ AND json_extract(e.attributes, '$.repo') IS NOT NULL
24613
+ AND json_extract(e.attributes, '$.repo') != ''
24172
24614
  GROUP BY repo
24173
24615
  ORDER BY c DESC, repo
24174
24616
  LIMIT :limit`
@@ -24192,44 +24634,28 @@ var SqliteSecurityRepository = class {
24192
24634
  // secret came back) is excluded — it is not currently resolved. Legacy
24193
24635
  // at-rest findings with finding_key IS NULL are excluded outright (the
24194
24636
  // resolution lifecycle can never attach to them). Path comes from the
24195
- // finding's parent event (kind 'code_change', metadata.filePath) — mirrors
24196
- // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
24197
- // capped at `limit`.
24637
+ // finding's parent event (event_type 'code_change', attributes.file_path) —
24638
+ // mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
24639
+ // DESC, capped at `limit`.
24198
24640
  recentlyResolved(limit = 20) {
24199
24641
  const rows = allRows(
24200
24642
  this.db.prepare(
24201
24643
  `SELECT f.finding_key AS finding_key,
24202
- f.rule_id AS rule_id,
24203
- f.severity AS severity,
24204
- json_extract(e.metadata, '$.filePath') AS path,
24205
- COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at,
24206
- (
24207
- SELECT fr.resolved_at FROM finding_resolution fr
24208
- WHERE fr.finding_key = f.finding_key
24209
- ORDER BY fr.created_at DESC, fr.rowid DESC
24210
- LIMIT 1
24211
- ) AS latest_resolved_at
24212
- FROM findings f JOIN events e ON e.id = f.event_id
24213
- WHERE e.kind = 'code_change'
24644
+ d.rule_id AS rule_id,
24645
+ d.severity AS severity,
24646
+ json_extract(e.attributes, '$.file_path') AS path,
24647
+ COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
24648
+ latest.resolved_at AS latest_resolved_at
24649
+ FROM inspection_findings f
24650
+ JOIN audit_events e ON e.id = f.audit_event_id
24651
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
24652
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
24653
+ ON latest.finding_key = f.finding_key
24654
+ WHERE e.event_type = 'code_change'
24214
24655
  AND f.finding_key IS NOT NULL
24215
- AND (
24216
- SELECT fr.status FROM finding_resolution fr
24217
- WHERE fr.finding_key = f.finding_key
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
24656
+ AND latest.status = 'resolved'
24657
+ AND latest.method = 'fixed-at-source'
24658
+ AND latest.resolved_at IS NOT NULL
24233
24659
  ORDER BY latest_resolved_at DESC
24234
24660
  LIMIT :limit`
24235
24661
  ),
@@ -24248,15 +24674,18 @@ var SqliteSecurityRepository = class {
24248
24674
  return Promise.resolve({ items });
24249
24675
  }
24250
24676
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
24251
- // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
24677
+ // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
24252
24678
  // numeric and the JS aggregations bucket/split on ms directly.
24253
24679
  findingsInRange(fromMs, toMs) {
24254
24680
  const rows = allRows(
24255
24681
  this.db.prepare(
24256
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
24257
- FROM findings f JOIN events e ON e.id = f.event_id
24258
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
24259
- ORDER BY e.occurred_at`
24682
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
24683
+ FROM inspection_findings f
24684
+ JOIN audit_events e ON e.id = f.audit_event_id
24685
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
24686
+ WHERE e.started_at >= :from AND e.started_at < :to
24687
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
24688
+ ORDER BY e.started_at`
24260
24689
  ),
24261
24690
  { from: fromMs, to: toMs }
24262
24691
  );
@@ -25080,9 +25509,10 @@ function openWithPragmas(file2) {
25080
25509
  }
25081
25510
  function backupLegacyStore(file2) {
25082
25511
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
25083
- renameSync(file2, backup);
25084
- for (const sidecar of walSidecars(file2)) {
25085
- if (existsSync2(sidecar)) rmSync(sidecar);
25512
+ renameSync2(file2, backup);
25513
+ tightenFile(backup);
25514
+ for (const sidecar of dbSidecars(file2)) {
25515
+ if (existsSync2(sidecar)) rmSync2(sidecar);
25086
25516
  }
25087
25517
  return backup;
25088
25518
  }
@@ -25098,7 +25528,7 @@ function openLocalDatabase(dir) {
25098
25528
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
25099
25529
  );
25100
25530
  }
25101
- applyMigrations(db);
25531
+ applyMigrations(db, file2);
25102
25532
  tightenPerms(file2);
25103
25533
  const events = new SqliteEventsRepository(db);
25104
25534
  const findings = new SqliteFindingsRepository(db);
@@ -25125,9 +25555,56 @@ function openLocalDatabase(dir) {
25125
25555
  policies.seedDefaults();
25126
25556
  function recordCapture(event, detected) {
25127
25557
  failOpenTransaction(db, () => {
25128
- events.insertEvent(event);
25129
25558
  const sessionId = event.metadata?.sessionId;
25130
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
25559
+ if (sessionId) {
25560
+ auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
25561
+ }
25562
+ const auditEventId = captureId(
25563
+ sessionId ?? null,
25564
+ event.contentHash,
25565
+ event.metadata?.filePath ?? null
25566
+ );
25567
+ auditEvents.insertAuditEvent({
25568
+ id: auditEventId,
25569
+ eventType: event.kind,
25570
+ startedAt: event.occurredAt,
25571
+ parentId: sessionId,
25572
+ rootSessionId: sessionId,
25573
+ content: event.content,
25574
+ contentHash: event.contentHash,
25575
+ attributes: toCaptureAttributes(event)
25576
+ });
25577
+ const definitionIds = /* @__PURE__ */ new Map();
25578
+ for (const finding of detected) {
25579
+ if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
25580
+ continue;
25581
+ }
25582
+ if (inspectionFindings.isEventDuplicate(
25583
+ auditEventId,
25584
+ finding.ruleId,
25585
+ finding.maskedMatch,
25586
+ finding.span.start,
25587
+ finding.span.end
25588
+ )) {
25589
+ continue;
25590
+ }
25591
+ const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
25592
+ let definitionId = definitionIds.get(key);
25593
+ if (!definitionId) {
25594
+ definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
25595
+ definitionIds.set(key, definitionId);
25596
+ }
25597
+ inspectionFindings.insertFinding({
25598
+ id: finding.id,
25599
+ auditEventId,
25600
+ inspectionDefinitionId: definitionId,
25601
+ span: finding.span,
25602
+ maskedMatch: finding.maskedMatch,
25603
+ actionTaken: finding.actionTaken,
25604
+ confidence: finding.confidence,
25605
+ findingKey: finding.findingKey ?? void 0
25606
+ });
25607
+ }
25131
25608
  });
25132
25609
  }
25133
25610
  function ensureInventory(ctx) {
@@ -25275,9 +25752,19 @@ function openLocalDatabase(dir) {
25275
25752
  };
25276
25753
  }
25277
25754
 
25755
+ // ../packages/persistence/src/finding-key.ts
25756
+ import { createHash as createHash3 } from "crypto";
25757
+ function normalizeFilePath(filePath) {
25758
+ return filePath.replaceAll("\\", "/");
25759
+ }
25760
+ function computeFindingKey(input) {
25761
+ const normalizedPath = normalizeFilePath(input.filePath);
25762
+ return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
25763
+ }
25764
+
25278
25765
  // ../packages/persistence/src/fingerprint.ts
25279
25766
  import { createHmac, randomBytes } from "crypto";
25280
- import { chmodSync as chmodSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync } from "fs";
25767
+ import { readFileSync as readFileSync2 } from "fs";
25281
25768
  import { join as join3 } from "path";
25282
25769
  var KEY_FILENAME = "exception.key";
25283
25770
  var KEY_MATERIAL_BYTES = 32;
@@ -25305,15 +25792,9 @@ function parseKeyFile(raw) {
25305
25792
  function writeKeyFile(dataDir2, key) {
25306
25793
  ensureDataDirSync(dataDir2);
25307
25794
  const file2 = keyFilePath(dataDir2);
25308
- const tmp = `${file2}.tmp`;
25309
25795
  const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
25310
- writeFileSync(tmp, `${body}
25311
- `, { mode: DATA_FILE_MODE });
25312
- renameSync2(tmp, file2);
25313
- try {
25314
- chmodSync2(file2, DATA_FILE_MODE);
25315
- } catch {
25316
- }
25796
+ writeOwnerOnlyFileSync(file2, `${body}
25797
+ `);
25317
25798
  return key;
25318
25799
  }
25319
25800
  function readFingerprintKey(dataDir2) {
@@ -25329,10 +25810,7 @@ function readFingerprintKey(dataDir2) {
25329
25810
  function loadOrCreateFingerprintKey(dataDir2) {
25330
25811
  const existing = readFingerprintKey(dataDir2);
25331
25812
  if (existing) {
25332
- try {
25333
- chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
25334
- } catch {
25335
- }
25813
+ tightenFile(keyFilePath(dataDir2));
25336
25814
  return existing;
25337
25815
  }
25338
25816
  return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
@@ -25349,8 +25827,8 @@ function fingerprintValue(key, raw) {
25349
25827
  }
25350
25828
 
25351
25829
  // ../packages/persistence/src/local-layout.ts
25352
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync2, renameSync as renameSync3 } from "fs";
25353
- import { chmod, mkdir } from "fs/promises";
25830
+ import { renameSync as renameSync3 } from "fs";
25831
+ import { mkdir } from "fs/promises";
25354
25832
  import { homedir as homedir3 } from "os";
25355
25833
  import { join as join4 } from "path";
25356
25834
  function defaultDataDir() {
@@ -25370,7 +25848,7 @@ function ensureLayoutDirSync(dir = defaultDataDir()) {
25370
25848
  }
25371
25849
 
25372
25850
  // ../packages/persistence/src/settings.ts
25373
- import { readFileSync as readFileSync3, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
25851
+ import { readFileSync as readFileSync3 } from "fs";
25374
25852
  import { join as join5 } from "path";
25375
25853
  function readWorkspaceSettings(base = defaultDataDir()) {
25376
25854
  const record2 = readJson(join5(settingsDir(base), "settings.json"));
@@ -25392,9 +25870,13 @@ function readJson(file2) {
25392
25870
  }
25393
25871
 
25394
25872
  // ../packages/persistence/src/warn-era-cap.ts
25395
- import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
25873
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
25396
25874
  import { join as join6 } from "path";
25397
25875
 
25876
+ // ../packages/plugin-sdk/src/config.ts
25877
+ import { existsSync as existsSync4 } from "fs";
25878
+ import { join as join7 } from "path";
25879
+
25398
25880
  // ../packages/plugin-sdk/src/provider-env.ts
25399
25881
  var booleanish = external_exports.string().optional().transform((v) => {
25400
25882
  if (v === void 0) return void 0;
@@ -25416,7 +25898,7 @@ var ProviderEnvSchema = external_exports.object(providerEnvShape);
25416
25898
  // ../packages/plugin-sdk/src/config-inventory.ts
25417
25899
  import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync2 } from "fs";
25418
25900
  import { homedir as homedir4 } from "os";
25419
- import { basename as basename2, join as join8 } from "path";
25901
+ import { basename as basename2, join as join9 } from "path";
25420
25902
 
25421
25903
  // ../rules/code-flaws/auth-jwt-no-verify.json
25422
25904
  var auth_jwt_no_verify_default = {
@@ -27451,8 +27933,8 @@ function bundledDetections() {
27451
27933
  }
27452
27934
 
27453
27935
  // ../packages/plugin-sdk/src/repo.ts
27454
- import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "fs";
27455
- import { basename, dirname as dirname2, isAbsolute, join as join7, sep as sep2 } from "path";
27936
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync } from "fs";
27937
+ import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep2 } from "path";
27456
27938
  function resolveRepoIdentity(cwd) {
27457
27939
  try {
27458
27940
  const root = findGitRoot(cwd);
@@ -27490,32 +27972,32 @@ function resolveHeadRoot(cwd) {
27490
27972
  function findGitRoot(start) {
27491
27973
  let dir = start;
27492
27974
  for (; ; ) {
27493
- if (existsSync4(join7(dir, ".git"))) return dir;
27975
+ if (existsSync5(join8(dir, ".git"))) return dir;
27494
27976
  const parent = dirname2(dir);
27495
27977
  if (parent === dir) return void 0;
27496
27978
  dir = parent;
27497
27979
  }
27498
27980
  }
27499
27981
  function resolveGitContext(root) {
27500
- const dotGit = join7(root, ".git");
27982
+ const dotGit = join8(root, ".git");
27501
27983
  try {
27502
27984
  if (statSync(dotGit).isDirectory()) {
27503
- return { configPath: join7(dotGit, "config"), headRoot: root };
27985
+ return { configPath: join8(dotGit, "config"), headRoot: root };
27504
27986
  }
27505
27987
  } catch {
27506
27988
  return void 0;
27507
27989
  }
27508
27990
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
27509
27991
  if (!target) return void 0;
27510
- const gitdir = isAbsolute(target) ? target : join7(root, target);
27511
- if (existsSync4(join7(gitdir, "config"))) {
27512
- return { configPath: join7(gitdir, "config"), headRoot: root };
27992
+ const gitdir = isAbsolute(target) ? target : join8(root, target);
27993
+ if (existsSync5(join8(gitdir, "config"))) {
27994
+ return { configPath: join8(gitdir, "config"), headRoot: root };
27513
27995
  }
27514
- const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
27996
+ const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
27515
27997
  if (!commonRaw) return void 0;
27516
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
27998
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
27517
27999
  const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
27518
- return { configPath: join7(commonGitDir, "config"), headRoot };
28000
+ return { configPath: join8(commonGitDir, "config"), headRoot };
27519
28001
  }
27520
28002
  function safeRead(path) {
27521
28003
  try {
@@ -27557,17 +28039,14 @@ function slugFromUrl(url2) {
27557
28039
  }
27558
28040
 
27559
28041
  // ../packages/plugin-sdk/src/events.ts
27560
- import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
27561
-
27562
- // ../packages/plugin-sdk/src/finding-key.ts
27563
- import { createHash as createHash4 } from "crypto";
28042
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
27564
28043
 
27565
28044
  // ../packages/plugin-sdk/src/inventory-resolver.ts
27566
28045
  import { arch, hostname as hostname3, platform, release } from "os";
27567
28046
 
27568
28047
  // ../packages/plugin-sdk/src/nudge.ts
27569
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
27570
- import { join as join9 } from "path";
28048
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
28049
+ import { join as join10 } from "path";
27571
28050
 
27572
28051
  // ../packages/plugin-sdk/src/paths.ts
27573
28052
  import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
@@ -27605,8 +28084,8 @@ function resolveNonGitProject(startDir, recognizeMarker) {
27605
28084
 
27606
28085
  // ../packages/plugin-sdk/src/project-files.ts
27607
28086
  var import_ignore = __toESM(require_ignore(), 1);
27608
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
27609
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
28087
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28088
+ import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
27610
28089
  var SKIP_DIRS = /* @__PURE__ */ new Set([
27611
28090
  ".git",
27612
28091
  "node_modules",
@@ -27626,7 +28105,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
27626
28105
  var MAX_FILES = 2e4;
27627
28106
  function readIgnoreLayer(dir) {
27628
28107
  try {
27629
- const content = readFileSync7(join10(dir, ".gitignore"), "utf8");
28108
+ const content = readFileSync7(join11(dir, ".gitignore"), "utf8");
27630
28109
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
27631
28110
  } catch {
27632
28111
  return void 0;
@@ -27704,10 +28183,10 @@ function resolveProjectFiles(cwd) {
27704
28183
  const layer = readIgnoreLayer(dir);
27705
28184
  const dirLayers = layer ? [...layers, layer] : layers;
27706
28185
  for (const entry of dirents) {
27707
- const fullPath = join10(dir, entry.name);
28186
+ const fullPath = join11(dir, entry.name);
27708
28187
  if (entry.isDirectory()) {
27709
28188
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
27710
- if (existsSync5(join10(fullPath, ".git"))) continue;
28189
+ if (existsSync6(join11(fullPath, ".git"))) continue;
27711
28190
  if (visit3(fullPath, dirLayers)) return true;
27712
28191
  continue;
27713
28192
  }
@@ -27748,8 +28227,8 @@ import { randomUUID as randomUUID10 } from "crypto";
27748
28227
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
27749
28228
 
27750
28229
  // ../packages/plugin-sdk/src/throttle.ts
27751
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
27752
- import { join as join11 } from "path";
28230
+ import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
28231
+ import { join as join12 } from "path";
27753
28232
 
27754
28233
  // ../packages/local-ops/src/egress-record.ts
27755
28234
  function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
@@ -27799,7 +28278,7 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
27799
28278
  // ../packages/local-ops/src/fs-scan.ts
27800
28279
  import { createHash as createHash5, randomUUID as randomUUID11 } from "crypto";
27801
28280
  import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
27802
- import { basename as basename5, extname, join as join12, relative as relative3 } from "path";
28281
+ import { basename as basename5, extname, join as join13, relative as relative3, resolve as resolve2 } from "path";
27803
28282
  var import_ignore2 = __toESM(require_ignore(), 1);
27804
28283
  var AKAIGNORE_FILENAME = ".akaignore";
27805
28284
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
@@ -27815,7 +28294,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
27815
28294
  var MAX_BYTES = 1e6;
27816
28295
  function readIgnoreLayer2(dir, filename) {
27817
28296
  try {
27818
- const content = readFileSync8(join12(dir, filename), "utf8");
28297
+ const content = readFileSync8(join13(dir, filename), "utf8");
27819
28298
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
27820
28299
  } catch {
27821
28300
  return void 0;
@@ -27851,7 +28330,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
27851
28330
  const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
27852
28331
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
27853
28332
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
27854
- const path = join12(dir, entry.name);
28333
+ const path = join13(dir, entry.name);
27855
28334
  if (entry.isDirectory()) {
27856
28335
  const skipState = evaluate(dirSkipLayers, path, true);
27857
28336
  if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
@@ -27890,7 +28369,18 @@ function scanPathIntoStore(db, target, opts = {}) {
27890
28369
  let findingCount = 0;
27891
28370
  const files = [];
27892
28371
  const egressFiles = [];
27893
- for (const { path: file2, gitignored } of collectFiles(target)) {
28372
+ let fingerprintKey;
28373
+ function resolveFingerprintKey() {
28374
+ if (fingerprintKey === void 0) {
28375
+ try {
28376
+ fingerprintKey = opts.dataDir ? loadOrCreateFingerprintKey(opts.dataDir) : null;
28377
+ } catch {
28378
+ fingerprintKey = null;
28379
+ }
28380
+ }
28381
+ return fingerprintKey;
28382
+ }
28383
+ for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
27894
28384
  let text;
27895
28385
  try {
27896
28386
  text = readFileSync8(file2, "utf8");
@@ -27915,19 +28405,28 @@ function scanPathIntoStore(db, target, opts = {}) {
27915
28405
  // store the REDACTED file, never the raw secret
27916
28406
  metadata
27917
28407
  };
27918
- const findings = matches.map((m) => ({
27919
- id: randomUUID11(),
27920
- eventId,
27921
- ruleId: m.ruleId,
27922
- category: m.category,
27923
- severity: m.severity,
27924
- span: m.span,
27925
- maskedMatch: maskMatch(m.rawMatch),
27926
- // Per-pack action (monitor-by-default) when the installed snapshot supplies
27927
- // one, else the per-category fallback — mirrors the live path's resolveAction.
27928
- actionTaken: opts.ruleActions?.get(m.ruleId) ?? DEFAULT_ACTIONS[m.category],
27929
- confidence: m.confidence
27930
- }));
28408
+ const findings = matches.map((m) => {
28409
+ const maskedMatch = maskMatch(m.rawMatch);
28410
+ const key = resolveFingerprintKey();
28411
+ const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
28412
+ return {
28413
+ id: randomUUID11(),
28414
+ eventId,
28415
+ ruleId: m.ruleId,
28416
+ category: m.category,
28417
+ severity: m.severity,
28418
+ span: m.span,
28419
+ maskedMatch,
28420
+ // Per-pack action (monitor-by-default) when the installed snapshot supplies
28421
+ // one, else the per-category fallback — mirrors the live path's resolveAction.
28422
+ actionTaken: opts.ruleActions?.get(m.ruleId) ?? DEFAULT_ACTIONS[m.category],
28423
+ confidence: m.confidence,
28424
+ // Every fs-scan finding is at-rest (kind: 'code_change' with a
28425
+ // filePath), unlike the plugin's in-flight captures, so — unlike
28426
+ // runtime.ts's isAtRest branch — a finding_key is unconditional here.
28427
+ findingKey: computeFindingKey({ ruleId: m.ruleId, filePath: file2, valueFingerprint })
28428
+ };
28429
+ });
27931
28430
  db.recordCapture(event, findings);
27932
28431
  files.push({ path: file2, gitignored, findings });
27933
28432
  findingCount += findings.length;
@@ -27937,10 +28436,10 @@ function scanPathIntoStore(db, target, opts = {}) {
27937
28436
 
27938
28437
  // ../packages/local-ops/src/project-inventory.ts
27939
28438
  import { statSync as statSync6 } from "fs";
27940
- import { resolve as resolve2 } from "path";
28439
+ import { resolve as resolve3 } from "path";
27941
28440
  function recordProjectInventory(db, target) {
27942
28441
  try {
27943
- const abs = resolve2(target);
28442
+ const abs = resolve3(target);
27944
28443
  statSync6(abs);
27945
28444
  const identity = resolveRepoIdentity(abs);
27946
28445
  if (!identity) return null;
@@ -27990,15 +28489,15 @@ function reinvokeArgv(subcommand, extraArgs = []) {
27990
28489
 
27991
28490
  // ../packages/local-ops/src/update-cache.ts
27992
28491
  import { spawn } from "child_process";
27993
- import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
27994
- import { join as join13 } from "path";
28492
+ import { existsSync as existsSync7, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
28493
+ import { join as join14 } from "path";
27995
28494
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
27996
28495
  function cachePath(home) {
27997
- return join13(dataDir(home), "update-check.json");
28496
+ return join14(dataDir(home), "update-check.json");
27998
28497
  }
27999
28498
  function readCache(home) {
28000
28499
  const path = cachePath(home);
28001
- if (!existsSync6(path)) return null;
28500
+ if (!existsSync7(path)) return null;
28002
28501
  try {
28003
28502
  const raw = JSON.parse(readFileSync9(path, "utf8"));
28004
28503
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
@@ -28008,9 +28507,9 @@ function readCache(home) {
28008
28507
  }
28009
28508
  }
28010
28509
  function writeCache(home, cache) {
28011
- if (!existsSync6(dataDir(home))) return;
28510
+ if (!existsSync7(dataDir(home))) return;
28012
28511
  try {
28013
- writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
28512
+ writeFileSync5(cachePath(home), `${JSON.stringify(cache, null, 2)}
28014
28513
  `);
28015
28514
  } catch {
28016
28515
  }
@@ -28020,7 +28519,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
28020
28519
  }
28021
28520
  function clearCache(home) {
28022
28521
  try {
28023
- rmSync2(cachePath(home), { force: true });
28522
+ rmSync3(cachePath(home), { force: true });
28024
28523
  } catch {
28025
28524
  }
28026
28525
  }
@@ -28173,7 +28672,7 @@ function commandsHelp() {
28173
28672
  }
28174
28673
 
28175
28674
  // src/commands/check-updates.ts
28176
- import { existsSync as existsSync7 } from "fs";
28675
+ import { existsSync as existsSync8 } from "fs";
28177
28676
  import { parseArgs } from "util";
28178
28677
 
28179
28678
  // src/lib/args.ts
@@ -28204,7 +28703,7 @@ function runCheckUpdates(argv) {
28204
28703
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
28205
28704
  `);
28206
28705
  }
28207
- if (existsSync7(dataDir(home))) {
28706
+ if (existsSync8(dataDir(home))) {
28208
28707
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
28209
28708
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
28210
28709
  }
@@ -28358,10 +28857,10 @@ function runCompletion(argv) {
28358
28857
 
28359
28858
  // src/commands/dashboard.ts
28360
28859
  import { spawn as spawn3 } from "child_process";
28361
- import { existsSync as existsSync8 } from "fs";
28860
+ import { existsSync as existsSync9 } from "fs";
28362
28861
  import { createRequire as createRequire2 } from "module";
28363
28862
  import { createServer } from "net";
28364
- import { dirname as dirname5, join as join14 } from "path";
28863
+ import { dirname as dirname5, join as join15 } from "path";
28365
28864
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
28366
28865
  import { parseArgs as parseArgs2 } from "util";
28367
28866
 
@@ -28380,21 +28879,22 @@ function openUrl(url2) {
28380
28879
  // src/commands/dashboard.ts
28381
28880
  var require3 = createRequire2(import.meta.url);
28382
28881
  var here = dirname5(fileURLToPath2(import.meta.url));
28882
+ var BIND_HOST = "127.0.0.1";
28383
28883
  function pkgBase() {
28384
- return isSea() ? dirname5(process.execPath) : join14(here, "..");
28884
+ return isSea() ? dirname5(process.execPath) : join15(here, "..");
28385
28885
  }
28386
28886
  function isPortFree(port) {
28387
- return new Promise((resolve3) => {
28887
+ return new Promise((resolve4) => {
28388
28888
  const probe = createServer();
28389
28889
  probe.once("error", () => {
28390
- resolve3(false);
28890
+ resolve4(false);
28391
28891
  });
28392
28892
  probe.once("listening", () => {
28393
28893
  probe.close(() => {
28394
- resolve3(true);
28894
+ resolve4(true);
28395
28895
  });
28396
28896
  });
28397
- probe.listen(port, "127.0.0.1");
28897
+ probe.listen(port, BIND_HOST);
28398
28898
  });
28399
28899
  }
28400
28900
  function refreshUpdateMirror(dir) {
@@ -28435,9 +28935,9 @@ async function runDashboard(argv) {
28435
28935
  refreshUpdateMirror(dataDir());
28436
28936
  const base = pkgBase();
28437
28937
  const bundledServer = [
28438
- join14(base, "web-ui", "server.js"),
28439
- join14(base, "web-ui", "web-ui", "server.js")
28440
- ].find((p) => existsSync8(p));
28938
+ join15(base, "web-ui", "server.js"),
28939
+ join15(base, "web-ui", "web-ui", "server.js")
28940
+ ].find((p) => existsSync9(p));
28441
28941
  if (bundledServer) {
28442
28942
  launchStandalone(bundledServer, port, shouldOpen, url2);
28443
28943
  return;
@@ -28447,7 +28947,7 @@ async function runDashboard(argv) {
28447
28947
  const startNext = () => {
28448
28948
  const child = spawn3(
28449
28949
  process.execPath,
28450
- [nextBin, "start", "--port", port, "--hostname", "127.0.0.1"],
28950
+ [nextBin, "start", "--port", port, "--hostname", BIND_HOST],
28451
28951
  {
28452
28952
  cwd: webUiDir,
28453
28953
  stdio: ["inherit", "pipe", "inherit"]
@@ -28456,7 +28956,7 @@ async function runDashboard(argv) {
28456
28956
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
28457
28957
  `);
28458
28958
  };
28459
- if (existsSync8(join14(webUiDir, ".next", "BUILD_ID"))) {
28959
+ if (existsSync9(join15(webUiDir, ".next", "BUILD_ID"))) {
28460
28960
  startNext();
28461
28961
  return;
28462
28962
  }
@@ -28484,7 +28984,7 @@ function launchStandalone(serverJs, port, shouldOpen, url2) {
28484
28984
  // Inherit the parent env so the child server sees PATH etc.; PORT/HOSTNAME tell
28485
28985
  // Next's standalone server where to bind (its only port input).
28486
28986
  // eslint-disable-next-line n/no-process-env
28487
- env: { ...process.env, PORT: port, HOSTNAME: "127.0.0.1" }
28987
+ env: { ...process.env, PORT: port, HOSTNAME: BIND_HOST }
28488
28988
  });
28489
28989
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
28490
28990
  `);
@@ -28698,11 +29198,11 @@ function terminalPrompter() {
28698
29198
  },
28699
29199
  isInteractive: process.stdin.isTTY && process.stdout.isTTY,
28700
29200
  ask(question) {
28701
- return new Promise((resolve3) => {
29201
+ return new Promise((resolve4) => {
28702
29202
  const rl = createInterface({ input: process.stdin, output: process.stdout });
28703
29203
  rl.question(question, (answer) => {
28704
29204
  rl.close();
28705
- resolve3(answer);
29205
+ resolve4(answer);
28706
29206
  });
28707
29207
  });
28708
29208
  },
@@ -28713,7 +29213,7 @@ function terminalPrompter() {
28713
29213
  // fatal signals: a security tool must never brick the terminal it was
28714
29214
  // invoked from.
28715
29215
  askHidden(question) {
28716
- return new Promise((resolve3, reject) => {
29216
+ return new Promise((resolve4, reject) => {
28717
29217
  const stdin = process.stdin;
28718
29218
  process.stdout.write(question);
28719
29219
  stdin.setRawMode(true);
@@ -28736,7 +29236,7 @@ function terminalPrompter() {
28736
29236
  restore();
28737
29237
  process.stdout.write("\n");
28738
29238
  if (err) reject(err);
28739
- else resolve3(state.value);
29239
+ else resolve4(state.value);
28740
29240
  };
28741
29241
  const onError = (err) => {
28742
29242
  finish(err);
@@ -29407,13 +29907,13 @@ async function runException(argv, io = terminalPrompter()) {
29407
29907
  }
29408
29908
 
29409
29909
  // src/commands/init.ts
29410
- import { existsSync as existsSync10, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
29411
- import { join as join15 } from "path";
29910
+ import { existsSync as existsSync11, statSync as statSync7 } from "fs";
29911
+ import { join as join16 } from "path";
29412
29912
  import * as readline from "readline/promises";
29413
29913
  import { parseArgs as parseArgs6 } from "util";
29414
29914
 
29415
29915
  // src/commands/plugins.ts
29416
- import { existsSync as existsSync9 } from "fs";
29916
+ import { existsSync as existsSync10 } from "fs";
29417
29917
  import { parseArgs as parseArgs5 } from "util";
29418
29918
  function runPlugins(argv) {
29419
29919
  const [sub, ...rest] = argv;
@@ -29430,7 +29930,7 @@ async function listPlugins(argv) {
29430
29930
  const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
29431
29931
  const home = homeBase(values.home);
29432
29932
  const active = /* @__PURE__ */ new Set();
29433
- if (existsSync9(dbPath(home))) {
29933
+ if (existsSync10(dbPath(home))) {
29434
29934
  const db = openLocalDatabase(dataDir(home));
29435
29935
  try {
29436
29936
  const recent = await db.findings.recentFindings({ limit: 1e3 });
@@ -29514,6 +30014,23 @@ Then run \`aka init\` to set up the local store.
29514
30014
 
29515
30015
  // src/commands/init.ts
29516
30016
  var PLUGIN_OFFER_IDENTITY = `${PRODUCT_NAME} \u2014 ${PRODUCT_TAGLINE}`;
30017
+ function looseStorePaths(home) {
30018
+ if (process.platform === "win32") return [];
30019
+ const targets = [
30020
+ home,
30021
+ settingsDir(home),
30022
+ dataDir(home),
30023
+ join16(settingsDir(home), "settings.json"),
30024
+ dbPath(home)
30025
+ ];
30026
+ return targets.filter((p) => {
30027
+ try {
30028
+ return (statSync7(p).mode & 63) !== 0;
30029
+ } catch {
30030
+ return false;
30031
+ }
30032
+ });
30033
+ }
29517
30034
  async function runInit(argv) {
29518
30035
  const { values } = parseArgs6({
29519
30036
  args: argv,
@@ -29523,16 +30040,16 @@ async function runInit(argv) {
29523
30040
  ensureLayoutDirSync(home);
29524
30041
  const settings = settingsDir(home);
29525
30042
  ensureLayoutDirSync(settings);
29526
- const settingsFile = join15(settings, "settings.json");
29527
- const settingsCreated = !existsSync10(settingsFile);
30043
+ const settingsFile = join16(settings, "settings.json");
30044
+ const settingsCreated = !existsSync11(settingsFile);
29528
30045
  if (settingsCreated) {
29529
- const tmp = `${settingsFile}.tmp`;
29530
- writeFileSync7(tmp, `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
29531
- `, {
29532
- mode: DATA_FILE_MODE
29533
- });
29534
- renameSync5(tmp, settingsFile);
30046
+ writeOwnerOnlyFileSync(
30047
+ settingsFile,
30048
+ `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
30049
+ `
30050
+ );
29535
30051
  }
30052
+ tightenFile(settingsFile);
29536
30053
  const db = openLocalDatabase(dataDir(home));
29537
30054
  let policyCount;
29538
30055
  let packCount;
@@ -29545,12 +30062,14 @@ async function runInit(argv) {
29545
30062
  } finally {
29546
30063
  db.close();
29547
30064
  }
30065
+ const loose = looseStorePaths(home);
29548
30066
  process.stdout.write(
29549
30067
  `\u2713 Initialized AKA at ${home}
29550
30068
  settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
29551
30069
  database: ${dbPath(home)}
29552
30070
  seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
29553
30071
  ` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
30072
+ ` : "") + (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
30073
  ` : "")
29555
30074
  );
29556
30075
  await offerPluginInstall(values.yes === true);
@@ -29596,7 +30115,7 @@ async function confirm(question) {
29596
30115
  }
29597
30116
 
29598
30117
  // src/commands/scan.ts
29599
- import { statSync as statSync7 } from "fs";
30118
+ import { statSync as statSync8 } from "fs";
29600
30119
  import { parseArgs as parseArgs7 } from "util";
29601
30120
  function severityRank(s) {
29602
30121
  return Severity.options.indexOf(s);
@@ -29643,7 +30162,7 @@ function runScan(argv) {
29643
30162
  failOn = parsed.data;
29644
30163
  }
29645
30164
  try {
29646
- statSync7(target);
30165
+ statSync8(target);
29647
30166
  } catch {
29648
30167
  process.stderr.write(`aka scan: no such file or directory: ${target}
29649
30168
  `);
@@ -29651,13 +30170,14 @@ function runScan(argv) {
29651
30170
  return;
29652
30171
  }
29653
30172
  registerBundledPacks();
29654
- const db = openLocalDatabase(dataDir(home));
30173
+ const storeDir = dataDir(home);
30174
+ const db = openLocalDatabase(storeDir);
29655
30175
  let result;
29656
30176
  let inventory;
29657
30177
  let egress;
29658
30178
  try {
29659
30179
  const { ruleActions } = db.installedPacks.installedRuleset();
29660
- result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli" });
30180
+ result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli", dataDir: storeDir });
29661
30181
  inventory = recordProjectInventory(db, target);
29662
30182
  egress = recordProjectEgress(db, target, result.egress, home);
29663
30183
  } finally {