@akasecurity/ai-tc-claude-code 0.9.0 → 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.
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
494
494
  // src/backfill.ts
495
495
  import { fileURLToPath } from "url";
496
496
 
497
+ // ../../packages/plugin-sdk/src/config.ts
498
+ import { existsSync as existsSync3 } from "fs";
499
+ import { join as join6 } from "path";
500
+
497
501
  // ../../packages/persistence/src/database.ts
498
502
  import { randomUUID as randomUUID8 } from "crypto";
499
- import { existsSync, renameSync, rmSync } from "fs";
503
+ import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
500
504
  import { join, sep } from "path";
501
505
  import { DatabaseSync } from "node:sqlite";
502
506
 
@@ -545,6 +549,22 @@ var SQLITE_MIGRATIONS = [
545
549
  {
546
550
  tag: "0010_events_session_expression_index",
547
551
  sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
552
+ },
553
+ {
554
+ tag: "0011_egress_writer",
555
+ 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'
556
+ },
557
+ {
558
+ tag: "0012_handy_the_captain",
559
+ 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`);"
560
+ },
561
+ {
562
+ tag: "0013_legacy_history_backfill_support",
563
+ 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"
564
+ },
565
+ {
566
+ tag: "0014_drop_legacy_events_findings",
567
+ 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"
548
568
  }
549
569
  ];
550
570
 
@@ -15375,7 +15395,12 @@ var FindingFacets = external_exports.object({
15375
15395
  severity: external_exports.array(FindingFacetItem),
15376
15396
  subtype: external_exports.array(FindingFacetItem),
15377
15397
  provider: external_exports.array(FindingFacetItem),
15378
- action: external_exports.array(FindingFacetItem)
15398
+ action: external_exports.array(FindingFacetItem),
15399
+ // Counts by the group's derived status. The SQLite store derives a status
15400
+ // for every instance, so every group lands in a bucket; a status-less
15401
+ // group (possible only for callers whose rows carry no statuses) is
15402
+ // counted under no value.
15403
+ status: external_exports.array(FindingFacetItem)
15379
15404
  }).meta({ id: "FindingFacets" });
15380
15405
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15381
15406
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15385,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
15385
15410
  subtype: external_exports.array(external_exports.string()).optional(),
15386
15411
  provider: external_exports.array(FindingProvider).optional(),
15387
15412
  action: external_exports.array(FindingAction).optional(),
15413
+ // Matches a group's DERIVED status (see FindingGroup.status), not its
15414
+ // individual instances' — so a filtered group's Status column always reads
15415
+ // one of the requested values.
15416
+ status: external_exports.array(FindingStatus).optional(),
15388
15417
  q: external_exports.string().optional(),
15389
15418
  // Scope to findings whose event carries this session id (the Activity page's
15390
15419
  // session → findings drilldown). Findings without a session never match.
@@ -15575,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
15575
15604
  parent_uuid: external_exports.string().optional(),
15576
15605
  run_key: external_exports.string().optional()
15577
15606
  }).catchall(external_exports.unknown());
15607
+ var CaptureAttributes = external_exports.object({
15608
+ // The harness/tool that produced the capture (`claude-code`, `cli`, …). A
15609
+ // column on the legacy `events` table; here it rides the bag because a
15610
+ // capture-typed audit row has no equivalent column of its own.
15611
+ source_tool: external_exports.string().optional(),
15612
+ file_path: external_exports.string().optional(),
15613
+ repo: external_exports.string().optional(),
15614
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
15615
+ // gives a non-file capture a display location ("via Bash") when file_path
15616
+ // is absent. The tool NAME only, never its arguments/output.
15617
+ tool_name: external_exports.string().optional(),
15618
+ // Presence-only provenance flag: set when the file is excluded by the
15619
+ // repo's .gitignore. Omitted (not false) for tracked files.
15620
+ gitignored: external_exports.boolean().optional(),
15621
+ // Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
15622
+ // reading from disk), never a partial fragment (a hook-captured edit).
15623
+ whole_file: external_exports.boolean().optional(),
15624
+ // Distributed-tracing correlation: `correlation_id` ties the capture back to
15625
+ // the request that produced it; `trace_id` is the originating span's W3C
15626
+ // trace id when telemetry is enabled.
15627
+ correlation_id: external_exports.uuid().optional(),
15628
+ trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
15629
+ // Ids of the detection exceptions that downgraded findings in this capture
15630
+ // to 'allow' — the enforcement audit trail's link back to the grant that
15631
+ // authorized the bypass.
15632
+ exception_ids: external_exports.array(external_exports.guid()).optional()
15633
+ }).catchall(external_exports.unknown());
15578
15634
  var ToolCallInspection = external_exports.object({
15579
15635
  ruleId: external_exports.string().min(1),
15580
15636
  ruleName: external_exports.string(),
@@ -15661,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
15661
15717
  span: Span,
15662
15718
  maskedMatch: external_exports.string(),
15663
15719
  actionTaken: ActionTaken,
15664
- confidence: external_exports.number().min(0).max(1)
15720
+ confidence: external_exports.number().min(0).max(1),
15721
+ // Stable, content-addressed key correlating this finding across re-detections
15722
+ // — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
15723
+ // its unique index). Optional: only an at-rest/re-scannable finding carries
15724
+ // one; an in-flight capture (prompt/response) has nothing to re-detect
15725
+ // against and leaves it unset, so every insert is a fresh row.
15726
+ findingKey: external_exports.string().optional(),
15727
+ // The ORIGINAL detection time, preserved across a later re-detection of the
15728
+ // same findingKey — mirrors the legacy `findings.first_detected_at`.
15729
+ // Optional: when omitted, the writer derives it from the referenced audit
15730
+ // event's startedAt on first insert (see SqliteInspectionFindingsRepository).
15731
+ firstDetectedAt: external_exports.iso.datetime().optional()
15665
15732
  });
15666
15733
  var InventoryContext = external_exports.object({
15667
15734
  host: InventoryInput.optional(),
@@ -15863,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
15863
15930
 
15864
15931
  // ../../packages/schema/src/zod/event.ts
15865
15932
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15933
+ var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15866
15934
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15867
15935
  var EventMetadata = external_exports.object({
15868
15936
  sessionId: external_exports.string().optional(),
@@ -16203,6 +16271,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16203
16271
 
16204
16272
  // ../../packages/schema/src/zod/rule.ts
16205
16273
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16274
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16206
16275
  var KeywordMatcher = external_exports.object({
16207
16276
  type: external_exports.literal("keyword"),
16208
16277
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16227,9 +16296,10 @@ function matchesEmptyString(pattern, flags) {
16227
16296
  return false;
16228
16297
  }
16229
16298
  }
16299
+ var MAX_PATTERN_LENGTH = 2e3;
16230
16300
  var RegexMatcher = external_exports.object({
16231
16301
  type: external_exports.literal("regex"),
16232
- pattern: external_exports.string(),
16302
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16233
16303
  flags: external_exports.string().default("gi"),
16234
16304
  captureGroup: external_exports.number().int().nonnegative().optional()
16235
16305
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16350,6 +16420,12 @@ var PolicyBundle = external_exports.object({
16350
16420
  // on-disk caches — that omit the field still parse; consumers read
16351
16421
  // `bundle.exceptions ?? []`.
16352
16422
  exceptions: external_exports.array(ExceptionBundleEntry).optional(),
16423
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
16424
+ // from a versioned installed pack. Optional so older backends — and older
16425
+ // on-disk caches — that omit the field still parse; consumers fall back to
16426
+ // the rule's own spec version. NOT the bundle version above — see
16427
+ // installedRuleset's ruleVersions for the source of truth.
16428
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
16353
16429
  customKeywords: external_exports.array(external_exports.string()),
16354
16430
  fetchedAt: external_exports.iso.datetime()
16355
16431
  }).meta({ id: "PolicyBundle" });
@@ -16796,6 +16872,212 @@ function buildDetectionsList(summaries, query) {
16796
16872
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16797
16873
  }
16798
16874
 
16875
+ // ../../packages/schema/src/zod/shares.ts
16876
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16877
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16878
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16879
+ var DATA_CLASS_ORDER = DataClass.options;
16880
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16881
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16882
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16883
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16884
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16885
+ var ReviewInfo = external_exports.object({
16886
+ needsReview: external_exports.boolean(),
16887
+ reasons: external_exports.array(ReviewReason)
16888
+ }).meta({ id: "ReviewInfo" });
16889
+ var DestinationNetwork = external_exports.object({
16890
+ port: external_exports.number().int().nullable(),
16891
+ geo: external_exports.string().nullable(),
16892
+ ptr: external_exports.string().nullable()
16893
+ }).meta({ id: "DestinationNetwork" });
16894
+ var EndpointSummary = external_exports.object({
16895
+ id: external_exports.string(),
16896
+ method: HttpMethod,
16897
+ transport: Transport,
16898
+ url: external_exports.string(),
16899
+ template: external_exports.boolean(),
16900
+ dataClass: DataClass,
16901
+ lastSeen: external_exports.iso.datetime(),
16902
+ callSiteCount: external_exports.number().int().nonnegative()
16903
+ }).meta({ id: "EndpointSummary" });
16904
+ var CallSite = external_exports.object({
16905
+ id: external_exports.string(),
16906
+ project: external_exports.string(),
16907
+ file: external_exports.string(),
16908
+ line: external_exports.number().int().nonnegative(),
16909
+ snippet: external_exports.string(),
16910
+ dynamic: external_exports.boolean(),
16911
+ vendored: external_exports.boolean(),
16912
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16913
+ projectId: external_exports.string().nullable()
16914
+ }).meta({ id: "CallSite" });
16915
+ var EndpointWithSites = EndpointSummary.extend({
16916
+ sites: external_exports.array(CallSite)
16917
+ }).meta({ id: "EndpointWithSites" });
16918
+ var ShareDestinationSummary = external_exports.object({
16919
+ id: external_exports.string(),
16920
+ kind: DestinationKind,
16921
+ name: external_exports.string(),
16922
+ host: external_exports.string(),
16923
+ category: external_exports.string(),
16924
+ trust: ShareTrustLevel,
16925
+ /** Effective state (decision applied over the trust default). */
16926
+ status: EgressStatus,
16927
+ /** True when an egress decision override differs from the trust default. */
16928
+ isCustom: external_exports.boolean(),
16929
+ lastSeen: external_exports.iso.datetime(),
16930
+ endpointCount: external_exports.number().int().nonnegative(),
16931
+ callSiteCount: external_exports.number().int().nonnegative(),
16932
+ transports: external_exports.array(Transport),
16933
+ /** Most-sensitive first. */
16934
+ dataClasses: external_exports.array(DataClass),
16935
+ review: ReviewInfo,
16936
+ /** Non-provider hosts only; null for providers. */
16937
+ network: DestinationNetwork.nullable(),
16938
+ /** Embedded for inline expansion — no call sites here. */
16939
+ endpoints: external_exports.array(EndpointSummary)
16940
+ }).meta({ id: "ShareDestinationSummary" });
16941
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16942
+ endpointCount: true,
16943
+ callSiteCount: true,
16944
+ endpoints: true
16945
+ }).extend({
16946
+ /** Ownership/geo rationale; null for providers. */
16947
+ note: external_exports.string().nullable(),
16948
+ endpoints: external_exports.array(EndpointWithSites)
16949
+ }).meta({ id: "ShareDestinationDetail" });
16950
+ var ReviewDestination = external_exports.object({
16951
+ id: external_exports.string(),
16952
+ kind: DestinationKind,
16953
+ name: external_exports.string(),
16954
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16955
+ host: external_exports.string(),
16956
+ trust: ShareTrustLevel,
16957
+ status: EgressStatus,
16958
+ review: ReviewInfo,
16959
+ topDataClass: DataClass,
16960
+ callSiteCount: external_exports.number().int().nonnegative(),
16961
+ lastSeen: external_exports.iso.datetime()
16962
+ }).meta({ id: "ReviewDestination" });
16963
+ var ShareDestinationGroup = external_exports.object({
16964
+ kind: DestinationKind,
16965
+ total: external_exports.number().int().nonnegative(),
16966
+ items: external_exports.array(ShareDestinationSummary)
16967
+ }).meta({ id: "ShareDestinationGroup" });
16968
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16969
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16970
+ var SharesStats = external_exports.object({
16971
+ destinations: external_exports.number().int().nonnegative(),
16972
+ endpoints: external_exports.number().int().nonnegative(),
16973
+ callSites: external_exports.number().int().nonnegative(),
16974
+ needsReview: external_exports.number().int().nonnegative(),
16975
+ insecure: external_exports.number().int().nonnegative(),
16976
+ byKind: external_exports.object({
16977
+ provider: external_exports.number().int().nonnegative(),
16978
+ internal: external_exports.number().int().nonnegative(),
16979
+ external: external_exports.number().int().nonnegative(),
16980
+ ip: external_exports.number().int().nonnegative()
16981
+ }),
16982
+ byTrust: external_exports.object({
16983
+ recognized: external_exports.number().int().nonnegative(),
16984
+ internal: external_exports.number().int().nonnegative(),
16985
+ unverified: external_exports.number().int().nonnegative(),
16986
+ ip: external_exports.number().int().nonnegative()
16987
+ })
16988
+ }).meta({ id: "SharesStats" });
16989
+ var SetEgressDecisionBody = external_exports.object({
16990
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16991
+ decision: EgressDecision.nullable()
16992
+ }).meta({ id: "SetEgressDecisionBody" });
16993
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16994
+ var ListShareDestinationsQuery = external_exports.object({
16995
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16996
+ q: external_exports.string().optional(),
16997
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16998
+ kind: external_exports.array(DestinationKind).optional(),
16999
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
17000
+ groupBy: external_exports.enum(["destination"]).default("destination"),
17001
+ /**
17002
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
17003
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17004
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17005
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17006
+ */
17007
+ review: external_exports.stringbool().default(false)
17008
+ });
17009
+ var ExportSharesQuery = external_exports.object({
17010
+ format: external_exports.enum(["csv", "json"]).default("csv"),
17011
+ q: external_exports.string().optional(),
17012
+ kind: external_exports.array(DestinationKind).optional()
17013
+ });
17014
+
17015
+ // ../../packages/schema/src/zod/egress-extraction.ts
17016
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
17017
+ var ProviderRegistryEntry = external_exports.object({
17018
+ id: external_exports.string(),
17019
+ name: external_exports.string(),
17020
+ category: external_exports.string(),
17021
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
17022
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
17023
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
17024
+ apiBase: external_exports.string(),
17025
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
17026
+ defaultDataClasses: external_exports.array(DataClass).min(1),
17027
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
17028
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
17029
+ }).meta({ id: "ProviderRegistryEntry" });
17030
+ var EgressCallSiteHit = external_exports.object({
17031
+ file: external_exports.string(),
17032
+ line: external_exports.number().int().positive(),
17033
+ snippet: external_exports.string(),
17034
+ dynamic: external_exports.boolean(),
17035
+ vendored: external_exports.boolean()
17036
+ }).meta({ id: "EgressCallSiteHit" });
17037
+ var ResolvedEgressHit = external_exports.object({
17038
+ host: external_exports.string(),
17039
+ kind: DestinationKind,
17040
+ name: external_exports.string(),
17041
+ category: external_exports.string(),
17042
+ trust: ShareTrustLevel,
17043
+ network: DestinationNetwork.nullable(),
17044
+ method: HttpMethod,
17045
+ transport: Transport,
17046
+ url: external_exports.string(),
17047
+ template: external_exports.boolean(),
17048
+ dataClass: DataClass,
17049
+ site: EgressCallSiteHit
17050
+ }).meta({ id: "ResolvedEgressHit" });
17051
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
17052
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
17053
+ external_exports.object({
17054
+ mode: external_exports.literal("ledger"),
17055
+ scannedFiles: external_exports.array(external_exports.string()),
17056
+ deletedFiles: external_exports.array(external_exports.string())
17057
+ })
17058
+ ]).meta({ id: "EgressReconcile" });
17059
+ var RecordProjectEgressInput = external_exports.object({
17060
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
17061
+ projectKey: external_exports.string().min(1),
17062
+ /** Display name only — never keys reconciliation. */
17063
+ project: external_exports.string(),
17064
+ projectId: external_exports.string().nullable(),
17065
+ reconcile: EgressReconcile,
17066
+ hits: external_exports.array(ResolvedEgressHit)
17067
+ }).meta({ id: "RecordProjectEgressInput" });
17068
+ var EgressWriteSummary = external_exports.object({
17069
+ destinations: external_exports.number().int().nonnegative(),
17070
+ endpoints: external_exports.number().int().nonnegative(),
17071
+ callSites: external_exports.number().int().nonnegative(),
17072
+ truncated: external_exports.boolean(),
17073
+ /**
17074
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17075
+ * ledger-keeping caller must withhold their ledger entries and read them
17076
+ * again next scan.
17077
+ */
17078
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17079
+ }).meta({ id: "EgressWriteSummary" });
17080
+
16799
17081
  // ../../packages/schema/src/zod/findings-group-build.ts
16800
17082
  function toApiAction(dbVal) {
16801
17083
  const map2 = {
@@ -16943,6 +17225,15 @@ function groupActions(g) {
16943
17225
  actionsCache.set(g, actions);
16944
17226
  return actions;
16945
17227
  }
17228
+ function countInstancesByStatus(statusInputs, statuses) {
17229
+ const statusSet = new Set(statuses);
17230
+ let sum = 0;
17231
+ for (const input of statusInputs) {
17232
+ if (input.count === void 0) return null;
17233
+ if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
17234
+ }
17235
+ return sum;
17236
+ }
16946
17237
  function applyFindingFilters(groups, opts) {
16947
17238
  let filtered = groups;
16948
17239
  if (opts.severity && opts.severity.length > 0) {
@@ -16961,6 +17252,10 @@ function applyFindingFilters(groups, opts) {
16961
17252
  const subtypeSet = new Set(opts.subtype);
16962
17253
  filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
16963
17254
  }
17255
+ if (opts.statuses && opts.statuses.length > 0) {
17256
+ const statusSet = new Set(opts.statuses);
17257
+ filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
17258
+ }
16964
17259
  if (opts.q) {
16965
17260
  const q = opts.q.toLowerCase();
16966
17261
  filtered = filtered.filter((g) => groupHaystack(g).includes(q));
@@ -16982,6 +17277,7 @@ function computeFindingFacets(allGroups, opts) {
16982
17277
  const forSeverity = applyFindingFilters(allGroups, {
16983
17278
  providers: opts.providers,
16984
17279
  actions: opts.actions,
17280
+ statuses: opts.statuses,
16985
17281
  q: opts.q,
16986
17282
  subtype: opts.subtype
16987
17283
  });
@@ -16991,6 +17287,7 @@ function computeFindingFacets(allGroups, opts) {
16991
17287
  }
16992
17288
  const forProvider = applyFindingFilters(allGroups, {
16993
17289
  actions: opts.actions,
17290
+ statuses: opts.statuses,
16994
17291
  q: opts.q,
16995
17292
  subtype: opts.subtype,
16996
17293
  severity: opts.severity
@@ -17001,6 +17298,7 @@ function computeFindingFacets(allGroups, opts) {
17001
17298
  }
17002
17299
  const forAction = applyFindingFilters(allGroups, {
17003
17300
  providers: opts.providers,
17301
+ statuses: opts.statuses,
17004
17302
  q: opts.q,
17005
17303
  subtype: opts.subtype,
17006
17304
  severity: opts.severity
@@ -17012,17 +17310,30 @@ function computeFindingFacets(allGroups, opts) {
17012
17310
  const forSubtype = applyFindingFilters(allGroups, {
17013
17311
  providers: opts.providers,
17014
17312
  actions: opts.actions,
17313
+ statuses: opts.statuses,
17015
17314
  q: opts.q,
17016
17315
  severity: opts.severity
17017
17316
  });
17018
17317
  const subtypeMap = /* @__PURE__ */ new Map();
17019
17318
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
17319
+ const forStatus = applyFindingFilters(allGroups, {
17320
+ providers: opts.providers,
17321
+ actions: opts.actions,
17322
+ q: opts.q,
17323
+ subtype: opts.subtype,
17324
+ severity: opts.severity
17325
+ });
17326
+ const statusMap = /* @__PURE__ */ new Map();
17327
+ for (const g of forStatus) {
17328
+ if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17329
+ }
17020
17330
  const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17021
17331
  return {
17022
17332
  severity: toItems(severityMap),
17023
17333
  provider: toItems(providerMap),
17024
17334
  action: toItems(actionMap),
17025
- subtype: toItems(subtypeMap)
17335
+ subtype: toItems(subtypeMap),
17336
+ status: toItems(statusMap)
17026
17337
  };
17027
17338
  }
17028
17339
 
@@ -17057,10 +17368,14 @@ var PatchInstalledPackRequest = external_exports.object({
17057
17368
  }).meta({ id: "PatchInstalledPackRequest" });
17058
17369
 
17059
17370
  // ../../packages/schema/src/zod/local.ts
17060
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17371
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17061
17372
  var RunMode = external_exports.enum(["standalone"]);
17062
17373
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17063
17374
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
17375
+ var ModelJudgeConsent = external_exports.object({
17376
+ acknowledgedAt: external_exports.iso.datetime(),
17377
+ payloadVersion: external_exports.number().int().positive()
17378
+ });
17064
17379
  var WorkspaceSettings = external_exports.object({
17065
17380
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
17066
17381
  // Settings files written by earlier releases may carry the retired 'attached'
@@ -17072,38 +17387,20 @@ var WorkspaceSettings = external_exports.object({
17072
17387
  policy: SimpleDetectionPolicy.default("redact"),
17073
17388
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17074
17389
  historicalAccess: HistoricalAccess.default("session-only"),
17390
+ // In-place egress extraction on the scan paths; disable to stop all Data
17391
+ // Shares writes.
17392
+ dataSharesInPlace: external_exports.boolean().default(true),
17075
17393
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17076
- onboardedAt: external_exports.iso.datetime().optional()
17394
+ onboardedAt: external_exports.iso.datetime().optional(),
17395
+ // Records that the user consented to sending findings to the model API for
17396
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
17397
+ // Absent until granted; a stale payloadVersion means the consent no longer
17398
+ // covers the current payload and must be re-granted.
17399
+ modelJudgeConsent: ModelJudgeConsent.optional()
17077
17400
  });
17078
17401
  function defaultWorkspaceSettings() {
17079
17402
  return WorkspaceSettings.parse({});
17080
17403
  }
17081
- function toEventRow(event) {
17082
- return {
17083
- id: event.id,
17084
- sourceTool: event.sourceTool,
17085
- kind: event.kind,
17086
- occurredAt: isoToEpochMillis(event.occurredAt),
17087
- contentHash: event.contentHash,
17088
- content: event.content,
17089
- metadata: event.metadata ? JSON.stringify(event.metadata) : null
17090
- };
17091
- }
17092
- function toFindingRow(finding) {
17093
- return {
17094
- id: finding.id,
17095
- eventId: finding.eventId,
17096
- ruleId: finding.ruleId,
17097
- category: finding.category,
17098
- severity: finding.severity,
17099
- spanStart: finding.span.start,
17100
- spanEnd: finding.span.end,
17101
- maskedMatch: finding.maskedMatch,
17102
- actionTaken: finding.actionTaken,
17103
- confidence: finding.confidence,
17104
- findingKey: finding.findingKey ?? null
17105
- };
17106
- }
17107
17404
  function toInventoryRow(input, id, now) {
17108
17405
  return {
17109
17406
  id,
@@ -17173,7 +17470,42 @@ function toInspectionFindingRow(input) {
17173
17470
  spanEnd: input.span.end,
17174
17471
  maskedMatch: input.maskedMatch,
17175
17472
  actionTaken: input.actionTaken,
17176
- confidence: input.confidence
17473
+ confidence: input.confidence,
17474
+ findingKey: input.findingKey ?? null,
17475
+ firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
17476
+ };
17477
+ }
17478
+ function toCaptureAttributes(event) {
17479
+ const metadata = event.metadata;
17480
+ return {
17481
+ source_tool: event.sourceTool,
17482
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
17483
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
17484
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
17485
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
17486
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
17487
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
17488
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
17489
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
17490
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
17491
+ // has ever populated either), but every legacy metadata key still rides
17492
+ // the bag rather than being silently dropped — CaptureAttributes'
17493
+ // `.catchall(z.unknown())` carries the long tail.
17494
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
17495
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
17496
+ };
17497
+ }
17498
+ function captureDefinitionVersion(finding) {
17499
+ return `capture/${finding.category}/${finding.severity}`;
17500
+ }
17501
+ function toCaptureDefinitionInput(finding) {
17502
+ return {
17503
+ ruleId: finding.ruleId,
17504
+ version: captureDefinitionVersion(finding),
17505
+ name: finding.ruleId,
17506
+ category: finding.category,
17507
+ severity: finding.severity,
17508
+ definition: JSON.stringify({ ruleId: finding.ruleId })
17177
17509
  };
17178
17510
  }
17179
17511
 
@@ -17555,145 +17887,6 @@ var SetupHandoffOffer = external_exports.object({
17555
17887
  path: ["liveKeys"]
17556
17888
  });
17557
17889
 
17558
- // ../../packages/schema/src/zod/shares.ts
17559
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17560
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17561
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17562
- var DATA_CLASS_ORDER = DataClass.options;
17563
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17564
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17565
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17566
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17567
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17568
- var ReviewInfo = external_exports.object({
17569
- needsReview: external_exports.boolean(),
17570
- reasons: external_exports.array(ReviewReason)
17571
- }).meta({ id: "ReviewInfo" });
17572
- var DestinationNetwork = external_exports.object({
17573
- port: external_exports.number().int().nullable(),
17574
- geo: external_exports.string().nullable(),
17575
- ptr: external_exports.string().nullable()
17576
- }).meta({ id: "DestinationNetwork" });
17577
- var EndpointSummary = external_exports.object({
17578
- id: external_exports.string(),
17579
- method: HttpMethod,
17580
- transport: Transport,
17581
- url: external_exports.string(),
17582
- template: external_exports.boolean(),
17583
- dataClass: DataClass,
17584
- lastSeen: external_exports.iso.datetime(),
17585
- callSiteCount: external_exports.number().int().nonnegative()
17586
- }).meta({ id: "EndpointSummary" });
17587
- var CallSite = external_exports.object({
17588
- id: external_exports.string(),
17589
- project: external_exports.string(),
17590
- file: external_exports.string(),
17591
- line: external_exports.number().int().nonnegative(),
17592
- snippet: external_exports.string(),
17593
- dynamic: external_exports.boolean(),
17594
- vendored: external_exports.boolean(),
17595
- /** Deep-link to the Inventory project, when the repo is governed there. */
17596
- projectId: external_exports.string().nullable()
17597
- }).meta({ id: "CallSite" });
17598
- var EndpointWithSites = EndpointSummary.extend({
17599
- sites: external_exports.array(CallSite)
17600
- }).meta({ id: "EndpointWithSites" });
17601
- var ShareDestinationSummary = external_exports.object({
17602
- id: external_exports.string(),
17603
- kind: DestinationKind,
17604
- name: external_exports.string(),
17605
- host: external_exports.string(),
17606
- category: external_exports.string(),
17607
- trust: ShareTrustLevel,
17608
- /** Effective state (decision applied over the trust default). */
17609
- status: EgressStatus,
17610
- /** True when an egress decision override differs from the trust default. */
17611
- isCustom: external_exports.boolean(),
17612
- lastSeen: external_exports.iso.datetime(),
17613
- endpointCount: external_exports.number().int().nonnegative(),
17614
- callSiteCount: external_exports.number().int().nonnegative(),
17615
- transports: external_exports.array(Transport),
17616
- /** Most-sensitive first. */
17617
- dataClasses: external_exports.array(DataClass),
17618
- review: ReviewInfo,
17619
- /** Non-provider hosts only; null for providers. */
17620
- network: DestinationNetwork.nullable(),
17621
- /** Embedded for inline expansion — no call sites here. */
17622
- endpoints: external_exports.array(EndpointSummary)
17623
- }).meta({ id: "ShareDestinationSummary" });
17624
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17625
- endpointCount: true,
17626
- callSiteCount: true,
17627
- endpoints: true
17628
- }).extend({
17629
- /** Ownership/geo rationale; null for providers. */
17630
- note: external_exports.string().nullable(),
17631
- endpoints: external_exports.array(EndpointWithSites)
17632
- }).meta({ id: "ShareDestinationDetail" });
17633
- var ReviewDestination = external_exports.object({
17634
- id: external_exports.string(),
17635
- kind: DestinationKind,
17636
- name: external_exports.string(),
17637
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17638
- host: external_exports.string(),
17639
- trust: ShareTrustLevel,
17640
- status: EgressStatus,
17641
- review: ReviewInfo,
17642
- topDataClass: DataClass,
17643
- callSiteCount: external_exports.number().int().nonnegative(),
17644
- lastSeen: external_exports.iso.datetime()
17645
- }).meta({ id: "ReviewDestination" });
17646
- var ShareDestinationGroup = external_exports.object({
17647
- kind: DestinationKind,
17648
- total: external_exports.number().int().nonnegative(),
17649
- items: external_exports.array(ShareDestinationSummary)
17650
- }).meta({ id: "ShareDestinationGroup" });
17651
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17652
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17653
- var SharesStats = external_exports.object({
17654
- destinations: external_exports.number().int().nonnegative(),
17655
- endpoints: external_exports.number().int().nonnegative(),
17656
- callSites: external_exports.number().int().nonnegative(),
17657
- needsReview: external_exports.number().int().nonnegative(),
17658
- insecure: external_exports.number().int().nonnegative(),
17659
- byKind: external_exports.object({
17660
- provider: external_exports.number().int().nonnegative(),
17661
- internal: external_exports.number().int().nonnegative(),
17662
- ip: external_exports.number().int().nonnegative()
17663
- }),
17664
- byTrust: external_exports.object({
17665
- recognized: external_exports.number().int().nonnegative(),
17666
- internal: external_exports.number().int().nonnegative(),
17667
- unverified: external_exports.number().int().nonnegative(),
17668
- ip: external_exports.number().int().nonnegative()
17669
- })
17670
- }).meta({ id: "SharesStats" });
17671
- var SetEgressDecisionBody = external_exports.object({
17672
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17673
- decision: EgressDecision.nullable()
17674
- }).meta({ id: "SetEgressDecisionBody" });
17675
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17676
- var ListShareDestinationsQuery = external_exports.object({
17677
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17678
- q: external_exports.string().optional(),
17679
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17680
- kind: external_exports.array(DestinationKind).optional(),
17681
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17682
- groupBy: external_exports.enum(["destination"]).default("destination"),
17683
- /**
17684
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17685
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17686
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17687
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17688
- */
17689
- review: external_exports.stringbool().default(false)
17690
- });
17691
- var ExportSharesQuery = external_exports.object({
17692
- format: external_exports.enum(["csv", "json"]).default("csv"),
17693
- q: external_exports.string().optional(),
17694
- kind: external_exports.array(DestinationKind).optional()
17695
- });
17696
-
17697
17890
  // ../../packages/schema/src/zod/shares-access.ts
17698
17891
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17699
17892
  function trustDefaultStatus(trust) {
@@ -17713,7 +17906,7 @@ function deriveReviewReasons(trust, transports) {
17713
17906
  const reasons = [];
17714
17907
  if (trust === "ip") reasons.push("raw_ip");
17715
17908
  if (trust === "unverified") reasons.push("unverified_domain");
17716
- if (transports.includes("http")) reasons.push("plaintext_transport");
17909
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17717
17910
  return reasons;
17718
17911
  }
17719
17912
  function buildReviewInfo(trust, transports) {
@@ -17740,6 +17933,48 @@ function reviewSeverityRank(reasons) {
17740
17933
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17741
17934
  }
17742
17935
 
17936
+ // ../../packages/persistence/src/ids.ts
17937
+ import { createHash } from "crypto";
17938
+ function sha256Hex(input) {
17939
+ return createHash("sha256").update(input).digest("hex");
17940
+ }
17941
+ function inventoryId(objectType, identityKey) {
17942
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
17943
+ }
17944
+ function sourceProjectId(url2) {
17945
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
17946
+ }
17947
+ function classifiedDataId(cls) {
17948
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
17949
+ }
17950
+ function inspectionDefinitionId(ruleId, version2) {
17951
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
17952
+ }
17953
+ function llmCallId(sessionId, messageId) {
17954
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
17955
+ }
17956
+ function toolCallId(sessionId, toolUseId) {
17957
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
17958
+ }
17959
+ function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
17960
+ return sha256Hex(
17961
+ canonicalIdentity([
17962
+ "inspection_finding",
17963
+ auditEventId,
17964
+ ruleId,
17965
+ String(spanStart),
17966
+ String(spanEnd)
17967
+ ])
17968
+ );
17969
+ }
17970
+ var NO_SESSION = "no_session";
17971
+ var NO_PATH = "no_path";
17972
+ function captureId(sessionId, contentHash, filePath = null) {
17973
+ return sha256Hex(
17974
+ canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
17975
+ );
17976
+ }
17977
+
17743
17978
  // ../../packages/persistence/src/internal/sql-text.ts
17744
17979
  function escapeLikePattern(s) {
17745
17980
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -17836,39 +18071,98 @@ function evidenceExists(db, object2) {
17836
18071
  return schemaObjectExists(db, "table", object2.name);
17837
18072
  }
17838
18073
 
17839
- // ../../packages/persistence/src/ids.ts
17840
- import { createHash } from "crypto";
17841
- function sha256Hex(input) {
17842
- return createHash("sha256").update(input).digest("hex");
18074
+ // ../../packages/persistence/src/internal/rows.ts
18075
+ function allRows(stmt, params) {
18076
+ if (params === void 0) return stmt.all();
18077
+ if (Array.isArray(params)) return stmt.all(...params);
18078
+ return stmt.all(params);
17843
18079
  }
17844
- function inventoryId(objectType, identityKey) {
17845
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18080
+ function getRow(stmt, params) {
18081
+ if (params === void 0) return stmt.get();
18082
+ if (Array.isArray(params)) return stmt.get(...params);
18083
+ return stmt.get(params);
17846
18084
  }
17847
- function sourceProjectId(url2) {
17848
- return sha256Hex(canonicalIdentity(["source_project", url2]));
18085
+ function intToBool(raw) {
18086
+ return raw === 1 || raw === true;
17849
18087
  }
17850
- function classifiedDataId(cls) {
17851
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
18088
+ function boolToInt(b) {
18089
+ return b ? 1 : 0;
17852
18090
  }
17853
- function inspectionDefinitionId(ruleId, version2) {
17854
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18091
+ function bindParams(row) {
18092
+ const out = {};
18093
+ for (const [key, value] of Object.entries(row)) {
18094
+ out[key] = value === void 0 ? null : value;
18095
+ }
18096
+ return out;
17855
18097
  }
17856
- function llmCallId(sessionId, messageId) {
17857
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18098
+ function countScalar(db, sql, params) {
18099
+ return getRow(db.prepare(sql), params)?.n ?? 0;
17858
18100
  }
17859
- function toolCallId(sessionId, toolUseId) {
17860
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18101
+ function countBy(db, sql, params) {
18102
+ const map2 = /* @__PURE__ */ new Map();
18103
+ for (const row of allRows(db.prepare(sql), params)) {
18104
+ map2.set(row.k, row.n);
18105
+ }
18106
+ return map2;
17861
18107
  }
17862
- function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17863
- return sha256Hex(
17864
- canonicalIdentity([
17865
- "inspection_finding",
17866
- auditEventId,
17867
- definitionId,
17868
- String(spanStart),
17869
- String(spanEnd)
17870
- ])
17871
- );
18108
+ function mapRowsTolerant(rows, map2) {
18109
+ const out = [];
18110
+ for (const row of rows) {
18111
+ try {
18112
+ out.push(map2(row));
18113
+ } catch {
18114
+ }
18115
+ }
18116
+ return out;
18117
+ }
18118
+
18119
+ // ../../packages/persistence/src/paths.ts
18120
+ import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18121
+ var DATA_DIR_MODE = 448;
18122
+ var DATA_FILE_MODE = 384;
18123
+ var DB_FILENAME = "aka.db";
18124
+ function chmodBestEffort(path, mode) {
18125
+ try {
18126
+ chmodSync(path, mode);
18127
+ } catch {
18128
+ }
18129
+ }
18130
+ function tightenDir(dir) {
18131
+ chmodBestEffort(dir, DATA_DIR_MODE);
18132
+ }
18133
+ function ensureDataDirSync(dir) {
18134
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18135
+ tightenDir(dir);
18136
+ }
18137
+ function dbSidecars(file2) {
18138
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18139
+ }
18140
+ function tightenFile(file2) {
18141
+ try {
18142
+ if (lstatSync(file2).isSymbolicLink()) return;
18143
+ } catch {
18144
+ }
18145
+ chmodBestEffort(file2, DATA_FILE_MODE);
18146
+ }
18147
+ function tightenPerms(file2) {
18148
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18149
+ }
18150
+ function writeOwnerOnlyFileSync(file2, data) {
18151
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
18152
+ try {
18153
+ rmSync(tmp, { force: true });
18154
+ } catch {
18155
+ }
18156
+ try {
18157
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18158
+ renameSync(tmp, file2);
18159
+ } finally {
18160
+ try {
18161
+ rmSync(tmp, { force: true });
18162
+ } catch {
18163
+ }
18164
+ }
18165
+ tightenFile(file2);
17872
18166
  }
17873
18167
 
17874
18168
  // ../../packages/persistence/src/migrations.ts
@@ -17882,7 +18176,8 @@ function createdIndexName(statement) {
17882
18176
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17883
18177
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17884
18178
  }
17885
- function applyMigrations(db) {
18179
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
18180
+ function applyMigrations(db, file2) {
17886
18181
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17887
18182
  db.exec(
17888
18183
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -17896,6 +18191,7 @@ function applyMigrations(db) {
17896
18191
  );
17897
18192
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
17898
18193
  if (applied.has(migration.tag)) continue;
18194
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
17899
18195
  const evidence = evidenceObjects(migration.sql);
17900
18196
  const present = evidence.filter((o) => evidenceExists(db, o));
17901
18197
  if (present.length > 0 && present.length < evidence.length) {
@@ -17940,13 +18236,54 @@ function applyMigrations(db) {
17940
18236
  if (legacyCount < SQLITE_MIGRATIONS.length) {
17941
18237
  db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
17942
18238
  }
17943
- ensureSyncedAtColumn(db, "events");
17944
18239
  ensureSyncedAtColumn(db, "audit_events");
17945
18240
  ensureScanLedgerTable(db);
17946
18241
  ensureBlockedDetectionsTable(db);
18242
+ ensureRuleProbeCacheTable(db);
17947
18243
  ensureWriteGateTrigger(db);
17948
18244
  ensureTokenUsageColumns(db);
17949
18245
  reconcileSourceProjectIds(db);
18246
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
18247
+ const drained = runLegacyHistoryBackfill(db);
18248
+ if (drained) applyLegacyDropMigration(db, file2);
18249
+ }
18250
+ }
18251
+ function applyLegacyDropMigration(db, file2) {
18252
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
18253
+ if (!migration) return;
18254
+ if (file2) {
18255
+ try {
18256
+ backupBeforeLegacyDrop(db, file2);
18257
+ } catch (error51) {
18258
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
18259
+ return;
18260
+ }
18261
+ }
18262
+ try {
18263
+ withTransaction(
18264
+ db,
18265
+ () => {
18266
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
18267
+ if (alreadyDropped) return;
18268
+ for (const statement of splitStatements(migration.sql)) {
18269
+ db.exec(statement);
18270
+ }
18271
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
18272
+ migration.tag,
18273
+ Date.now()
18274
+ );
18275
+ },
18276
+ "IMMEDIATE"
18277
+ );
18278
+ } catch (error51) {
18279
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
18280
+ }
18281
+ }
18282
+ function backupBeforeLegacyDrop(db, file2) {
18283
+ const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18284
+ db.prepare("VACUUM INTO ?").run(backup);
18285
+ tightenFile(backup);
18286
+ return backup;
17950
18287
  }
17951
18288
  var TOKEN_USAGE_COLUMNS = [
17952
18289
  {
@@ -17975,6 +18312,7 @@ var TOKEN_USAGE_COLUMNS = [
17975
18312
  }
17976
18313
  ];
17977
18314
  function ensureTokenUsageColumns(db) {
18315
+ if (!schemaObjectExists(db, "table", "audit_events")) return;
17978
18316
  const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17979
18317
  for (const column of TOKEN_USAGE_COLUMNS) {
17980
18318
  if (!existing.has(column.name)) {
@@ -18037,7 +18375,182 @@ function reconcileSourceProjectIds(db) {
18037
18375
  "IMMEDIATE"
18038
18376
  );
18039
18377
  } catch (error51) {
18040
- akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18378
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18379
+ }
18380
+ }
18381
+ var LEGACY_BACKFILL_BATCH_SIZE = 200;
18382
+ var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
18383
+ function getLegacyCopyWatermark(db, source) {
18384
+ const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
18385
+ return row?.lastRowid ?? 0;
18386
+ }
18387
+ function setLegacyCopyWatermark(db, source, lastRowid) {
18388
+ db.prepare(
18389
+ `INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
18390
+ ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
18391
+ ).run(source, lastRowid);
18392
+ }
18393
+ function drainLegacyTable(db, source, selectStmt, handleRows) {
18394
+ let watermark = getLegacyCopyWatermark(db, source);
18395
+ let processed = 0;
18396
+ while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
18397
+ const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
18398
+ if (rows.length === 0) return true;
18399
+ withTransaction(
18400
+ db,
18401
+ () => {
18402
+ handleRows(rows);
18403
+ watermark = rows[rows.length - 1]?.rowid ?? watermark;
18404
+ setLegacyCopyWatermark(db, source, watermark);
18405
+ },
18406
+ "IMMEDIATE"
18407
+ );
18408
+ processed += rows.length;
18409
+ if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
18410
+ }
18411
+ return false;
18412
+ }
18413
+ function parseLegacyEventMetadata(raw) {
18414
+ if (raw === null) return void 0;
18415
+ try {
18416
+ return JSON.parse(raw);
18417
+ } catch {
18418
+ return void 0;
18419
+ }
18420
+ }
18421
+ function toLegacyAuditAttributesJson(row) {
18422
+ return JSON.stringify(
18423
+ toCaptureAttributes({
18424
+ id: row.id,
18425
+ sourceTool: row.sourceTool,
18426
+ kind: row.kind,
18427
+ occurredAt: new Date(row.occurredAt).toISOString(),
18428
+ contentHash: row.contentHash,
18429
+ content: row.content,
18430
+ metadata: row.metadata
18431
+ })
18432
+ );
18433
+ }
18434
+ function copyLegacyEvents(db) {
18435
+ const selectStmt = db.prepare(
18436
+ `SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
18437
+ content_hash AS contentHash, content, metadata
18438
+ FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
18439
+ );
18440
+ const insertStmt = db.prepare(
18441
+ `INSERT OR IGNORE INTO audit_events
18442
+ (id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
18443
+ VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
18444
+ );
18445
+ const stubRootStmt = db.prepare(
18446
+ `INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
18447
+ );
18448
+ return drainLegacyTable(
18449
+ db,
18450
+ "events",
18451
+ selectStmt,
18452
+ (rows) => {
18453
+ for (const row of rows) {
18454
+ const metadata = parseLegacyEventMetadata(row.metadata);
18455
+ const sessionId = metadata?.sessionId ?? null;
18456
+ if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
18457
+ insertStmt.run(
18458
+ bindParams({
18459
+ id: row.id,
18460
+ parentId: sessionId,
18461
+ rootSessionId: sessionId,
18462
+ eventType: row.kind,
18463
+ startedAt: row.occurredAt,
18464
+ content: row.content,
18465
+ contentHash: row.contentHash,
18466
+ attributes: toLegacyAuditAttributesJson({ ...row, metadata })
18467
+ })
18468
+ );
18469
+ }
18470
+ }
18471
+ );
18472
+ }
18473
+ function copyLegacyFindings(db) {
18474
+ const selectStmt = db.prepare(
18475
+ `SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
18476
+ span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
18477
+ action_taken AS actionTaken, confidence, finding_key AS findingKey,
18478
+ first_detected_at AS firstDetectedAt
18479
+ FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
18480
+ );
18481
+ const definitionStmt = db.prepare(
18482
+ `INSERT OR IGNORE INTO inspection_definitions
18483
+ (id, rule_id, name, category, severity, definition, version)
18484
+ VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
18485
+ );
18486
+ const findingStmt = db.prepare(
18487
+ `INSERT INTO inspection_findings
18488
+ (id, audit_event_id, inspection_definition_id, classified_data_id,
18489
+ span_start, span_end, masked_match, action_taken, confidence,
18490
+ finding_key, first_detected_at)
18491
+ VALUES
18492
+ (:id, :auditEventId, :inspectionDefinitionId, NULL,
18493
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
18494
+ :findingKey, :firstDetectedAt)
18495
+ ON CONFLICT(id) DO NOTHING
18496
+ ON CONFLICT (finding_key) DO UPDATE SET
18497
+ first_detected_at = CASE
18498
+ WHEN first_detected_at IS NULL THEN excluded.first_detected_at
18499
+ WHEN excluded.first_detected_at IS NULL THEN first_detected_at
18500
+ ELSE min(first_detected_at, excluded.first_detected_at)
18501
+ END`
18502
+ );
18503
+ return drainLegacyTable(
18504
+ db,
18505
+ "findings",
18506
+ selectStmt,
18507
+ (rows) => {
18508
+ const definitionIds = /* @__PURE__ */ new Map();
18509
+ for (const row of rows) {
18510
+ const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
18511
+ let definitionId = definitionIds.get(tupleKey);
18512
+ if (definitionId === void 0) {
18513
+ const version2 = `unmigrated/${row.category}/${row.severity}`;
18514
+ definitionId = inspectionDefinitionId(row.ruleId, version2);
18515
+ definitionStmt.run(
18516
+ bindParams({
18517
+ id: definitionId,
18518
+ ruleId: row.ruleId,
18519
+ name: row.ruleId,
18520
+ category: row.category,
18521
+ severity: row.severity,
18522
+ definition: "",
18523
+ version: version2
18524
+ })
18525
+ );
18526
+ definitionIds.set(tupleKey, definitionId);
18527
+ }
18528
+ findingStmt.run(
18529
+ bindParams({
18530
+ id: row.id,
18531
+ auditEventId: row.eventId,
18532
+ inspectionDefinitionId: definitionId,
18533
+ spanStart: row.spanStart,
18534
+ spanEnd: row.spanEnd,
18535
+ maskedMatch: row.maskedMatch,
18536
+ actionTaken: row.actionTaken,
18537
+ confidence: row.confidence,
18538
+ findingKey: row.findingKey,
18539
+ firstDetectedAt: row.firstDetectedAt
18540
+ })
18541
+ );
18542
+ }
18543
+ }
18544
+ );
18545
+ }
18546
+ function runLegacyHistoryBackfill(db) {
18547
+ try {
18548
+ const eventsCaughtUp = copyLegacyEvents(db);
18549
+ if (!eventsCaughtUp) return false;
18550
+ return copyLegacyFindings(db);
18551
+ } catch (error51) {
18552
+ akaWarn(`legacy history backfill failed: ${String(error51)}`);
18553
+ return false;
18041
18554
  }
18042
18555
  }
18043
18556
  function isForeignSqliteLineage(db) {
@@ -18045,6 +18558,7 @@ function isForeignSqliteLineage(db) {
18045
18558
  return columnNames(db, "events").includes("tenant_id");
18046
18559
  }
18047
18560
  function ensureSyncedAtColumn(db, table) {
18561
+ if (!schemaObjectExists(db, "table", table)) return;
18048
18562
  if (!columnNames(db, table).includes("synced_at")) {
18049
18563
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
18050
18564
  }
@@ -18065,6 +18579,7 @@ function ensureWriteGateTrigger(db) {
18065
18579
  CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
18066
18580
  )`);
18067
18581
  db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
18582
+ if (!schemaObjectExists(db, "table", "installed_packs")) return;
18068
18583
  db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
18069
18584
  BEFORE UPDATE OF version, name, rules_json ON installed_packs
18070
18585
  WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
@@ -18083,29 +18598,13 @@ function ensureBlockedDetectionsTable(db) {
18083
18598
  blocked_at INTEGER NOT NULL
18084
18599
  )`);
18085
18600
  }
18086
-
18087
- // ../../packages/persistence/src/paths.ts
18088
- import { chmodSync, mkdirSync } from "fs";
18089
- var DATA_DIR_MODE = 448;
18090
- var DATA_FILE_MODE = 384;
18091
- var DB_FILENAME = "aka.db";
18092
- function ensureDataDirSync(dir) {
18093
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18094
- try {
18095
- chmodSync(dir, DATA_DIR_MODE);
18096
- } catch {
18097
- }
18098
- }
18099
- function walSidecars(file2) {
18100
- return [`${file2}-wal`, `${file2}-shm`];
18101
- }
18102
- function tightenPerms(file2) {
18103
- for (const path of [file2, ...walSidecars(file2)]) {
18104
- try {
18105
- chmodSync(path, DATA_FILE_MODE);
18106
- } catch {
18107
- }
18108
- }
18601
+ function ensureRuleProbeCacheTable(db) {
18602
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18603
+ rule_key TEXT PRIMARY KEY,
18604
+ verdict TEXT NOT NULL,
18605
+ worst_probe_ms REAL NOT NULL,
18606
+ checked_at INTEGER NOT NULL
18607
+ )`);
18109
18608
  }
18110
18609
 
18111
18610
  // ../../packages/persistence/src/internal/json.ts
@@ -18127,51 +18626,6 @@ function parseJsonObject(s) {
18127
18626
  return void 0;
18128
18627
  }
18129
18628
 
18130
- // ../../packages/persistence/src/internal/rows.ts
18131
- function allRows(stmt, params) {
18132
- if (params === void 0) return stmt.all();
18133
- if (Array.isArray(params)) return stmt.all(...params);
18134
- return stmt.all(params);
18135
- }
18136
- function getRow(stmt, params) {
18137
- if (params === void 0) return stmt.get();
18138
- if (Array.isArray(params)) return stmt.get(...params);
18139
- return stmt.get(params);
18140
- }
18141
- function intToBool(raw) {
18142
- return raw === 1 || raw === true;
18143
- }
18144
- function boolToInt(b) {
18145
- return b ? 1 : 0;
18146
- }
18147
- function bindParams(row) {
18148
- const out = {};
18149
- for (const [key, value] of Object.entries(row)) {
18150
- out[key] = value === void 0 ? null : value;
18151
- }
18152
- return out;
18153
- }
18154
- function countScalar(db, sql, params) {
18155
- return getRow(db.prepare(sql), params)?.n ?? 0;
18156
- }
18157
- function countBy(db, sql, params) {
18158
- const map2 = /* @__PURE__ */ new Map();
18159
- for (const row of allRows(db.prepare(sql), params)) {
18160
- map2.set(row.k, row.n);
18161
- }
18162
- return map2;
18163
- }
18164
- function mapRowsTolerant(rows, map2) {
18165
- const out = [];
18166
- for (const row of rows) {
18167
- try {
18168
- out.push(map2(row));
18169
- } catch {
18170
- }
18171
- }
18172
- return out;
18173
- }
18174
-
18175
18629
  // ../../packages/persistence/src/repositories/activity.ts
18176
18630
  var DAY_MS = 864e5;
18177
18631
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18788,6 +19242,21 @@ var SqliteAuditEventsRepository = class {
18788
19242
  })
18789
19243
  );
18790
19244
  }
19245
+ // Idempotent stub of a session's structural root. Session-scoped leaves
19246
+ // (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
19247
+ // INSERT OR IGNORE does NOT suppress a foreign-key violation (only
19248
+ // UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
19249
+ // raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
19250
+ // dropping the write under failOpenTransaction. SessionStart's own root write
19251
+ // is itself fail-open and marks "attempted", not "succeeded", so a session
19252
+ // with no root row yet is a real, permanent condition, not a transient race.
19253
+ // The stub carries no dimensions/attributes; an authoritative root
19254
+ // (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
19255
+ // on the id PK, so the stub never shadows real data. This is the single named
19256
+ // home for that FK invariant — call it before writing any session-scoped row.
19257
+ ensureSessionRoot(sessionId, startedAt) {
19258
+ this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
19259
+ }
18791
19260
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18792
19261
  // (which takes a caller-supplied random id), the id here is MINTED internally
18793
19262
  // from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
@@ -19249,8 +19718,14 @@ var SqliteDetectionsRepository = class {
19249
19718
  )
19250
19719
  );
19251
19720
  }
19252
- // Findings whose parent event occurred in the last 30 days and whose rule_id is
19253
- // in the given set. Mirrors the security repo's findings⋈events window join.
19721
+ // Findings whose parent audit event occurred in the last 30 days, is one of
19722
+ // the four capture kinds, and whose definition's rule_id is in the given set.
19723
+ // Mirrors the security repo's inspection_findings⋈audit_events window join.
19724
+ // rule_id lives on inspection_definitions, not the finding row, so the join
19725
+ // chains through it. audit_events also holds structural rows (session, run,
19726
+ // tool_call, llm_call, source_lookup, config_scan) that never had a legacy
19727
+ // events counterpart, so the event_type predicate keeps this count identical
19728
+ // to the old findings⋈events one.
19254
19729
  countFindingsLast30d(ruleIds) {
19255
19730
  if (ruleIds.length === 0) return 0;
19256
19731
  const since = this.now() - 30 * DAY_MS2;
@@ -19258,8 +19733,12 @@ var SqliteDetectionsRepository = class {
19258
19733
  return countScalar(
19259
19734
  this.db,
19260
19735
  `SELECT count(*) AS n
19261
- FROM findings f JOIN events e ON e.id = f.event_id
19262
- WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19736
+ FROM inspection_findings f
19737
+ JOIN audit_events e ON e.id = f.audit_event_id
19738
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19739
+ WHERE e.started_at >= ?
19740
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
19741
+ AND d.rule_id IN (${inClause})`,
19263
19742
  [since, ...ruleIds]
19264
19743
  );
19265
19744
  }
@@ -19269,35 +19748,24 @@ var SqliteDetectionsRepository = class {
19269
19748
  var SqliteEventsRepository = class {
19270
19749
  constructor(db) {
19271
19750
  this.db = db;
19272
- this.insertStmt = db.prepare(
19273
- `INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
19274
- VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
19275
- );
19276
19751
  }
19277
19752
  db;
19278
- insertStmt;
19279
- insertEvent(event) {
19280
- const row = toEventRow(event);
19281
- this.insertStmt.run(
19282
- bindParams({
19283
- id: row.id,
19284
- sourceTool: row.sourceTool,
19285
- kind: row.kind,
19286
- occurredAt: row.occurredAt,
19287
- contentHash: row.contentHash,
19288
- content: row.content,
19289
- metadata: row.metadata
19290
- })
19291
- );
19292
- }
19293
- // Every recorded event's content hash — the historical backfill loads this once
19294
- // to skip transcript messages it has already stored, so re-running the scan
19295
- // never duplicates findings.
19753
+ // Every recorded capture's content hash — the historical backfill loads this
19754
+ // once to skip transcript messages it has already stored, so re-running the
19755
+ // scan never duplicates findings.
19296
19756
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
19297
19757
  // async EventsReadPort contract.
19758
+ //
19759
+ // audit_events also holds structural rows (session, run, tool_call, llm_call,
19760
+ // source_lookup, config_scan) with a NULL content_hash, so the capture-kind
19761
+ // predicate isn't load-bearing here — it documents intent and keeps the scan
19762
+ // index-friendly rather than walking rows that can never match.
19298
19763
  contentHashes() {
19299
19764
  const rows = allRows(
19300
- this.db.prepare("SELECT content_hash FROM events")
19765
+ this.db.prepare(
19766
+ `SELECT content_hash FROM audit_events
19767
+ WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
19768
+ )
19301
19769
  );
19302
19770
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
19303
19771
  }
@@ -19633,17 +20101,20 @@ function parseExceptionRow(row) {
19633
20101
  }
19634
20102
 
19635
20103
  // ../../packages/persistence/src/repositories/resolution-sql.ts
19636
- function latestResolutionStatusSql(findingsAlias) {
20104
+ function latestResolutionColumnSql(column, findingsAlias) {
19637
20105
  return `(
19638
- SELECT fr.status FROM finding_resolution fr
20106
+ SELECT fr.${column} FROM finding_resolution fr
19639
20107
  WHERE fr.finding_key = ${findingsAlias}.finding_key
19640
20108
  ORDER BY fr.created_at DESC, fr.rowid DESC
19641
20109
  LIMIT 1
19642
20110
  )`;
19643
20111
  }
20112
+ function latestResolutionStatusSql(findingsAlias) {
20113
+ return latestResolutionColumnSql("status", findingsAlias);
20114
+ }
19644
20115
  var LATEST_RESOLUTION_BY_KEY_SQL = `(
19645
- SELECT finding_key, status FROM (
19646
- SELECT fr.finding_key, fr.status,
20116
+ SELECT finding_key, status, method, resolved_at FROM (
20117
+ SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
19647
20118
  ROW_NUMBER() OVER (
19648
20119
  PARTITION BY fr.finding_key
19649
20120
  ORDER BY fr.created_at DESC, fr.rowid DESC
@@ -19670,68 +20141,21 @@ var DAY_MS3 = 864e5;
19670
20141
  var SqliteFindingsRepository = class {
19671
20142
  constructor(db) {
19672
20143
  this.db = db;
19673
- this.insertStmt = db.prepare(
19674
- `INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
19675
- VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
19676
- (SELECT occurred_at FROM events WHERE id = :eventId))
19677
- ON CONFLICT (finding_key) DO UPDATE SET
19678
- event_id = excluded.event_id,
19679
- category = excluded.category,
19680
- severity = excluded.severity,
19681
- span_start = excluded.span_start,
19682
- span_end = excluded.span_end,
19683
- masked_match = excluded.masked_match,
19684
- action_taken = excluded.action_taken,
19685
- confidence = excluded.confidence`
19686
- );
19687
- this.sessionDupStmt = db.prepare(
19688
- `SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
19689
- WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
19690
- AND json_extract(e.metadata, '$.sessionId') = :sessionId
19691
- LIMIT 1`
19692
- );
19693
20144
  }
19694
20145
  db;
19695
- insertStmt;
19696
- sessionDupStmt;
19697
- insertFindings(findings, scope = {}) {
19698
- for (const finding of findings) {
19699
- if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
19700
- const row = toFindingRow(finding);
19701
- this.insertStmt.run({
19702
- id: row.id,
19703
- eventId: row.eventId,
19704
- ruleId: row.ruleId,
19705
- category: row.category,
19706
- severity: row.severity,
19707
- spanStart: row.spanStart,
19708
- spanEnd: row.spanEnd,
19709
- maskedMatch: row.maskedMatch,
19710
- actionTaken: row.actionTaken,
19711
- confidence: row.confidence,
19712
- findingKey: row.findingKey ?? null
19713
- });
19714
- }
19715
- }
19716
- // True when an earlier event in the same session already recorded a finding
19717
- // with the same rule and masked value. The current event is inserted before
19718
- // its findings, but carries no findings yet, so this never self-matches.
19719
- isSessionDuplicate(finding, sessionId) {
19720
- const hit = this.sessionDupStmt.get({
19721
- ruleId: finding.ruleId,
19722
- maskedMatch: finding.maskedMatch,
19723
- sessionId
19724
- });
19725
- return hit !== void 0;
19726
- }
19727
20146
  recentFindings(opts) {
19728
20147
  const limit = opts?.limit ?? 50;
19729
20148
  const rows = allRows(
19730
20149
  this.db.prepare(
19731
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19732
- f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19733
- FROM findings f JOIN events e ON e.id = f.event_id
19734
- ORDER BY e.occurred_at DESC, f.rowid DESC
20150
+ `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
20151
+ f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
20152
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20153
+ e.event_type AS kind
20154
+ FROM inspection_findings f
20155
+ JOIN audit_events e ON e.id = f.audit_event_id
20156
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20157
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20158
+ ORDER BY e.started_at DESC, f.rowid DESC
19735
20159
  LIMIT :limit`
19736
20160
  ),
19737
20161
  { limit }
@@ -19753,25 +20177,34 @@ var SqliteFindingsRepository = class {
19753
20177
  );
19754
20178
  }
19755
20179
  /** Live-enforced findings recorded for one session — a bare COUNT over the
19756
- * session-stamped events (served by idx_events_session_id), so the Activity
20180
+ * session-stamped audit_events (served by idx_audit_session), so the Activity
19757
20181
  * page can label its findings link without the grouped pipeline. */
19758
20182
  sessionFindingsCount(sessionId) {
19759
20183
  if (!sessionId) return Promise.resolve(0);
19760
20184
  return Promise.resolve(
19761
20185
  countScalar(
19762
20186
  this.db,
19763
- `SELECT count(*) AS n FROM findings f
19764
- JOIN events e ON e.id = f.event_id
19765
- WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
20187
+ `SELECT count(*) AS n FROM inspection_findings f
20188
+ JOIN audit_events e ON e.id = f.audit_event_id
20189
+ WHERE e.root_session_id = :sessionId
20190
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
19766
20191
  { sessionId }
19767
20192
  )
19768
20193
  );
19769
20194
  }
19770
- /** Per-rule transcript firing tally for one session — reads the OTHER finding
19771
- * store (inspection_findings, keyed to audit_events): every detection the
19772
- * transcript pass recorded, counted per firing rather than per unique value.
19773
- * Rides on session-scoped grouped responses so the findings view can
19774
- * reconcile the Activity page's tally with the deduped groups it lists. */
20195
+ /** Per-rule transcript firing tally for one session — every detection the
20196
+ * transcript-reconciler pass recorded against the session's `tool_call` rows,
20197
+ * counted per firing rather than per unique value. Rides on session-scoped
20198
+ * grouped responses so the findings view can reconcile the Activity page's
20199
+ * tally with the deduped groups it lists.
20200
+ *
20201
+ * `inspection_findings`/`audit_events` are now the SAME physical tables the
20202
+ * rest of this class reads for the live-capture list above (they used to be
20203
+ * a separate store), so this excludes the four capture kinds those rows
20204
+ * already carry — without that exclusion, every live-capture finding in the
20205
+ * session would be tallied here too, double-counting against the grouped
20206
+ * list this response rides alongside. The reconciler attaches its findings
20207
+ * only to `tool_call` rows, which the exclusion leaves untouched. */
19775
20208
  sessionFirings(sessionId) {
19776
20209
  return Object.fromEntries(
19777
20210
  countBy(
@@ -19781,18 +20214,25 @@ var SqliteFindingsRepository = class {
19781
20214
  JOIN audit_events e ON e.id = f.audit_event_id
19782
20215
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19783
20216
  WHERE e.root_session_id = :sessionId
20217
+ AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
19784
20218
  GROUP BY d.rule_id`,
19785
20219
  { sessionId }
19786
20220
  )
19787
20221
  );
19788
20222
  }
19789
20223
  /**
19790
- * Grouped findings for the dashboard — joins findingsevents (repo/file/
19791
- * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19792
- * applies the requested filters, and sorts by severity then recency. Filtering
20224
+ * Grouped findings for the dashboard — joins inspection_findingsaudit_events
20225
+ * ⋈inspection_definitions (repo/file/toolName from the audit event's
20226
+ * attributes bag, rule_id/category/severity from the definition), scoped to
20227
+ * the four capture kinds (audit_events also holds structural/reconciler/scan
20228
+ * rows this list must never surface), groups by ruleId, computes
20229
+ * per-filter-excluded facets, applies the requested filters, and sorts by
20230
+ * severity then recency. Filtering
19793
20231
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19794
20232
  * reflect the full filtered set; `items` is the requested
19795
- * page (default 50); no cursor (nextCursor is always null).
20233
+ * page (default 50); no cursor (nextCursor is always null). Under a `status`
20234
+ * filter, `totals.findings` counts only instances whose derived status was
20235
+ * requested, and each item's instance preview is narrowed the same way.
19796
20236
  *
19797
20237
  * Two reads, neither of which materializes a row per finding:
19798
20238
  * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
@@ -19805,10 +20245,11 @@ var SqliteFindingsRepository = class {
19805
20245
  * rule is ever restated in SQL.
19806
20246
  */
19807
20247
  listGroupedFindings(query) {
19808
- const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
20248
+ const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20249
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
19809
20250
  const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19810
20251
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19811
- predicate: sessionPredicate,
20252
+ predicate,
19812
20253
  params: sessionParams
19813
20254
  });
19814
20255
  const rows = allRows(
@@ -19816,24 +20257,26 @@ var SqliteFindingsRepository = class {
19816
20257
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19817
20258
  occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19818
20259
  FROM (
19819
- SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19820
- f.severity AS severity, f.masked_match AS masked_match,
20260
+ SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20261
+ d.severity AS severity, f.masked_match AS masked_match,
19821
20262
  f.action_taken AS action_taken, f.confidence AS confidence,
19822
- e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19823
- json_extract(e.metadata, '$.repo') AS repo,
19824
- json_extract(e.metadata, '$.filePath') AS file,
19825
- json_extract(e.metadata, '$.toolName') AS tool_name,
19826
- e.kind AS kind, f.finding_key AS finding_key,
20263
+ e.started_at AS occurred_at,
20264
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20265
+ json_extract(e.attributes, '$.repo') AS repo,
20266
+ json_extract(e.attributes, '$.file_path') AS file,
20267
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
20268
+ e.event_type AS kind, f.finding_key AS finding_key,
19827
20269
  latest.status AS latest_status,
19828
20270
  ROW_NUMBER() OVER (
19829
- PARTITION BY f.rule_id
19830
- ORDER BY e.occurred_at DESC, f.id DESC
20271
+ PARTITION BY d.rule_id
20272
+ ORDER BY e.started_at DESC, f.id DESC
19831
20273
  ) AS rn
19832
- FROM findings f
19833
- JOIN events e ON e.id = f.event_id
20274
+ FROM inspection_findings f
20275
+ JOIN audit_events e ON e.id = f.audit_event_id
20276
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19834
20277
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19835
20278
  ON latest.finding_key = f.finding_key
19836
- ${sessionPredicate}
20279
+ ${predicate}
19837
20280
  )
19838
20281
  WHERE rn <= :cap
19839
20282
  ORDER BY occurred_at DESC, id DESC`
@@ -19860,17 +20303,29 @@ var SqliteFindingsRepository = class {
19860
20303
  severity: query.severity,
19861
20304
  providers: query.provider,
19862
20305
  actions: query.action,
20306
+ statuses: query.status,
19863
20307
  subtype: query.subtype,
19864
20308
  q: query.q
19865
20309
  };
19866
20310
  const facets = computeFindingFacets(allGroups, filterOpts);
19867
20311
  const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
20312
+ const statusFilter = query.status ?? [];
19868
20313
  const totals = {
19869
- findings: sorted.reduce((acc, g) => acc + g.instanceCount, 0),
20314
+ findings: sorted.reduce((acc, g) => {
20315
+ if (statusFilter.length === 0) return acc + g.instanceCount;
20316
+ const agg = aggregates.get(g.id);
20317
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
20318
+ }, 0),
19870
20319
  groups: sorted.length
19871
20320
  };
19872
20321
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19873
- const items = sorted.slice(0, limit);
20322
+ const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20323
+ const items = sorted.slice(0, limit).map(
20324
+ (g) => statusSet ? {
20325
+ ...g,
20326
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20327
+ } : g
20328
+ );
19874
20329
  return Promise.resolve({
19875
20330
  totals,
19876
20331
  facets,
@@ -19884,45 +20339,62 @@ var SqliteFindingsRepository = class {
19884
20339
  * buildFindingGroups cannot recover from a preview. Bounded by the number of
19885
20340
  * distinct rule_ids (the installed packs' rules), not by the store's size.
19886
20341
  *
19887
- * The per-instance sets ride back as group_concat lists of RAW DB values —
19888
- * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19889
- * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19890
- * status keeps the classifier itself in @akasecurity/schema, where
19891
- * severitySummary's SQL and this query can't drift apart on what 'resolved'
19892
- * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
20342
+ * A single scan, folded in two levels: the inner SELECT groups by
20343
+ * (rule_id, status tuple) so each (kind, has-key, latest-status) combination
20344
+ * carries its instance count countInstancesByStatus needs those counts for
20345
+ * status-scoped totals — and the outer SELECT folds the tuples back to one
20346
+ * row per rule. The per-instance sets ride back as group_concat lists of RAW
20347
+ * DB values source_tool, action_taken, and the tuples deriveFindingStatus
20348
+ * consumes. Aggregating the status INPUTS rather than a status keeps the
20349
+ * classifier itself in @akasecurity/schema, where severitySummary's SQL and
20350
+ * this query can't drift apart on what 'resolved' means (see
20351
+ * resolution-sql.ts). The concat-of-concats can repeat a value across
20352
+ * tuples; the schema mappers dedupe, and each set is bounded by an enum, so
19893
20353
  * a group's row stays small however many findings it holds.
19894
20354
  *
19895
20355
  * `withSearchText` is the exception, and the one column here that does NOT
19896
- * stay small: the group's distinct repos/filePaths, whose size tracks how many
19897
- * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19898
- * that is a string proportional to the store (~8MB over 200k distinct paths,
19899
- * and buildHaystack lowercases a second copy). It buys `q` the ability to
19900
- * match an instance outside the preview, which searching the preview alone
19901
- * would silently lose, so it is fetched only when the request actually
19902
- * carries a `q`.
20356
+ * stay small: the group's per-tuple-distinct repos/filePaths, whose size
20357
+ * tracks how many distinct paths a rule fired across — for a rule hitting
20358
+ * mostly-unique paths that is a string proportional to the store (~8MB over
20359
+ * 200k distinct paths, and buildHaystack lowercases a second copy). It buys
20360
+ * `q` the ability to match an instance outside the preview, which searching
20361
+ * the preview alone would silently lose, so it is fetched only when the
20362
+ * request actually carries a `q`. (Substring matching is unaffected by a
20363
+ * path repeating across tuples.)
19903
20364
  */
19904
20365
  groupAggregates(withSearchText, scope) {
19905
- const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19906
- group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
19907
- group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20366
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20367
+ group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
20368
+ group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
19908
20369
  const rows = this.db.prepare(
19909
- `SELECT f.rule_id AS rule_id,
19910
- count(*) AS instance_count,
19911
- max(e.occurred_at) AS latest_at,
19912
- group_concat(DISTINCT e.source_tool) AS source_tools,
19913
- group_concat(DISTINCT f.action_taken) AS actions_taken,
19914
- group_concat(DISTINCT (
19915
- e.kind || '${TUPLE_SEP}' ||
19916
- (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19917
- coalesce(latest.status, '')
19918
- )) AS status_inputs
19919
- ${searchTextColumns}
19920
- FROM findings f
19921
- JOIN events e ON e.id = f.event_id
19922
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19923
- ON latest.finding_key = f.finding_key
19924
- ${scope.predicate}
19925
- GROUP BY f.rule_id`
20370
+ `SELECT rule_id,
20371
+ sum(tuple_count) AS instance_count,
20372
+ max(latest_at) AS latest_at,
20373
+ group_concat(source_tools) AS source_tools,
20374
+ group_concat(actions_taken) AS actions_taken,
20375
+ group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
20376
+ group_concat(repos) AS repos,
20377
+ group_concat(files) AS files,
20378
+ group_concat(tool_names) AS tool_names
20379
+ FROM (
20380
+ SELECT d.rule_id AS rule_id,
20381
+ e.event_type || '${TUPLE_SEP}' ||
20382
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20383
+ coalesce(latest.status, '') AS status_tuple,
20384
+ count(*) AS tuple_count,
20385
+ max(e.started_at) AS latest_at,
20386
+ group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
20387
+ group_concat(DISTINCT f.action_taken) AS actions_taken
20388
+ ${innerSearchColumns}
20389
+ FROM inspection_findings f
20390
+ JOIN audit_events e ON e.id = f.audit_event_id
20391
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20392
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20393
+ ON latest.finding_key = f.finding_key
20394
+ ${scope.predicate}
20395
+ GROUP BY d.rule_id, status_tuple
20396
+ )
20397
+ GROUP BY rule_id`
19926
20398
  ).all(scope.params);
19927
20399
  return new Map(
19928
20400
  rows.map((r) => [
@@ -19932,13 +20404,14 @@ var SqliteFindingsRepository = class {
19932
20404
  sourceTools: splitConcat(r.source_tools),
19933
20405
  actionsTaken: splitConcat(r.actions_taken),
19934
20406
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19935
- const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
20407
+ const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
19936
20408
  return {
19937
20409
  // deriveFindingStatus only distinguishes null from non-null here,
19938
20410
  // so the marker stands in for the key itself (never rendered).
19939
20411
  kind,
19940
20412
  findingKey: keyMarker === "" ? null : keyMarker,
19941
- latestResolutionStatus: latestStatus === "" ? null : latestStatus
20413
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus,
20414
+ count: Number(count)
19942
20415
  };
19943
20416
  }),
19944
20417
  latestDetectedAt: epochMillisToIso(r.latest_at),
@@ -19955,10 +20428,21 @@ var SqliteFindingsRepository = class {
19955
20428
  );
19956
20429
  }
19957
20430
  healthSummary() {
19958
- const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
20431
+ const total = countScalar(
20432
+ this.db,
20433
+ `SELECT count(*) AS n FROM inspection_findings f
20434
+ JOIN audit_events e ON e.id = f.audit_event_id
20435
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
20436
+ );
19959
20437
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19960
20438
  const grouped = allRows(
19961
- this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20439
+ this.db.prepare(
20440
+ `SELECT f.action_taken AS action_taken, count(*) AS c
20441
+ FROM inspection_findings f
20442
+ JOIN audit_events e ON e.id = f.audit_event_id
20443
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20444
+ GROUP BY f.action_taken`
20445
+ )
19962
20446
  );
19963
20447
  for (const row of grouped) {
19964
20448
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
@@ -19966,12 +20450,15 @@ var SqliteFindingsRepository = class {
19966
20450
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19967
20451
  const sevRows = allRows(
19968
20452
  this.db.prepare(
19969
- `SELECT f.severity AS severity, count(*) AS c
19970
- FROM findings f
20453
+ `SELECT d.severity AS severity, count(*) AS c
20454
+ FROM inspection_findings f
20455
+ JOIN audit_events e ON e.id = f.audit_event_id
20456
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19971
20457
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19972
20458
  ON latest.finding_key = f.finding_key
19973
- WHERE latest.status IS NULL OR latest.status != 'resolved'
19974
- GROUP BY f.severity`
20459
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20460
+ AND (latest.status IS NULL OR latest.status != 'resolved')
20461
+ GROUP BY d.severity`
19975
20462
  )
19976
20463
  );
19977
20464
  for (const row of sevRows) {
@@ -19992,9 +20479,11 @@ var SqliteFindingsRepository = class {
19992
20479
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19993
20480
  const rows = allRows(
19994
20481
  this.db.prepare(
19995
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19996
- FROM findings f JOIN events e ON e.id = f.event_id
19997
- WHERE e.occurred_at >= :since
20482
+ `SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20483
+ FROM inspection_findings f
20484
+ JOIN audit_events e ON e.id = f.audit_event_id
20485
+ WHERE e.started_at >= :since
20486
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
19998
20487
  GROUP BY day, f.action_taken`
19999
20488
  ),
20000
20489
  { since }
@@ -20059,15 +20548,59 @@ var SqliteInspectionFindingsRepository = class {
20059
20548
  this.insertStmt = db.prepare(
20060
20549
  `INSERT INTO inspection_findings
20061
20550
  (id, audit_event_id, inspection_definition_id, classified_data_id,
20062
- span_start, span_end, masked_match, action_taken, confidence)
20551
+ span_start, span_end, masked_match, action_taken, confidence,
20552
+ finding_key, first_detected_at)
20063
20553
  VALUES
20064
20554
  (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
20065
- :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence)
20066
- ON CONFLICT(id) DO NOTHING`
20555
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
20556
+ :findingKey,
20557
+ COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
20558
+ ON CONFLICT(id) DO UPDATE SET
20559
+ inspection_definition_id = excluded.inspection_definition_id
20560
+ ON CONFLICT (finding_key) DO UPDATE SET
20561
+ audit_event_id = excluded.audit_event_id,
20562
+ inspection_definition_id = excluded.inspection_definition_id,
20563
+ classified_data_id = excluded.classified_data_id,
20564
+ span_start = excluded.span_start,
20565
+ span_end = excluded.span_end,
20566
+ masked_match = excluded.masked_match,
20567
+ action_taken = excluded.action_taken,
20568
+ confidence = excluded.confidence`
20569
+ );
20570
+ this.sessionDupStmt = db.prepare(
20571
+ `SELECT 1 FROM inspection_findings f
20572
+ JOIN audit_events e ON e.id = f.audit_event_id
20573
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20574
+ WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
20575
+ AND e.root_session_id = :sessionId
20576
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20577
+ LIMIT 1`
20578
+ );
20579
+ this.eventDupStmt = db.prepare(
20580
+ `SELECT 1 FROM inspection_findings f
20581
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20582
+ WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
20583
+ AND f.masked_match = :maskedMatch
20584
+ AND f.span_start = :spanStart AND f.span_end = :spanEnd
20585
+ LIMIT 1`
20067
20586
  );
20068
20587
  }
20069
20588
  db;
20070
20589
  insertStmt;
20590
+ sessionDupStmt;
20591
+ eventDupStmt;
20592
+ // True when an earlier event in the same session already recorded a finding
20593
+ // with the same rule and masked value. The current event's own findings are
20594
+ // inserted one at a time in caller order, so an earlier finding in the SAME
20595
+ // recordCapture call is visible to a later duplicate check within it too.
20596
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
20597
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
20598
+ }
20599
+ // True when this exact detection (rule + masked value + span) is already
20600
+ // recorded against the given audit event.
20601
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
20602
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
20603
+ }
20071
20604
  insertFinding(input) {
20072
20605
  const row = toInspectionFindingRow(input);
20073
20606
  this.insertStmt.run(
@@ -20080,7 +20613,9 @@ var SqliteInspectionFindingsRepository = class {
20080
20613
  spanEnd: row.spanEnd,
20081
20614
  maskedMatch: row.maskedMatch,
20082
20615
  actionTaken: row.actionTaken,
20083
- confidence: row.confidence
20616
+ confidence: row.confidence,
20617
+ findingKey: row.findingKey,
20618
+ firstDetectedAt: row.firstDetectedAt
20084
20619
  })
20085
20620
  );
20086
20621
  }
@@ -20352,7 +20887,7 @@ var SqliteInstalledPacksRepository = class {
20352
20887
  installedRuleset() {
20353
20888
  const rows = allRows(
20354
20889
  this.db.prepare(
20355
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20890
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
20356
20891
  )
20357
20892
  );
20358
20893
  const out = {
@@ -20360,7 +20895,8 @@ var SqliteInstalledPacksRepository = class {
20360
20895
  enabledPacks: 0,
20361
20896
  rules: [],
20362
20897
  invalidRules: 0,
20363
- ruleActions: /* @__PURE__ */ new Map()
20898
+ ruleActions: /* @__PURE__ */ new Map(),
20899
+ ruleVersions: /* @__PURE__ */ new Map()
20364
20900
  };
20365
20901
  for (const row of rows) {
20366
20902
  if (!intToBool(row.enabled)) continue;
@@ -20382,6 +20918,7 @@ var SqliteInstalledPacksRepository = class {
20382
20918
  if (parsed.success) {
20383
20919
  out.rules.push(parsed.data);
20384
20920
  out.ruleActions.set(parsed.data.id, action);
20921
+ out.ruleVersions.set(parsed.data.id, row.version);
20385
20922
  } else out.invalidRules += 1;
20386
20923
  }
20387
20924
  }
@@ -21556,19 +22093,19 @@ var SqliteResolutionsRepository = class {
21556
22093
  );
21557
22094
  this.openAtRestStmt = db.prepare(
21558
22095
  `SELECT DISTINCT f.finding_key AS finding_key
21559
- FROM findings f
21560
- JOIN events e ON e.id = f.event_id
21561
- WHERE e.kind = 'code_change'
21562
- AND json_extract(e.metadata, '$.filePath') = :path
22096
+ FROM inspection_findings f
22097
+ JOIN audit_events e ON e.id = f.audit_event_id
22098
+ WHERE e.event_type = 'code_change'
22099
+ AND json_extract(e.attributes, '$.file_path') = :path
21563
22100
  AND f.finding_key IS NOT NULL
21564
22101
  AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
21565
22102
  );
21566
22103
  this.resolvedAtRestStmt = db.prepare(
21567
22104
  `SELECT DISTINCT f.finding_key AS finding_key
21568
- FROM findings f
21569
- JOIN events e ON e.id = f.event_id
21570
- WHERE e.kind = 'code_change'
21571
- AND json_extract(e.metadata, '$.filePath') = :path
22105
+ FROM inspection_findings f
22106
+ JOIN audit_events e ON e.id = f.audit_event_id
22107
+ WHERE e.event_type = 'code_change'
22108
+ AND json_extract(e.attributes, '$.file_path') = :path
21572
22109
  AND f.finding_key IS NOT NULL
21573
22110
  AND ${latestResolutionStatusSql("f")} = 'resolved'`
21574
22111
  );
@@ -21636,6 +22173,35 @@ var SqliteResolutionsRepository = class {
21636
22173
  }
21637
22174
  };
21638
22175
 
22176
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
22177
+ var SqliteRuleProbeCacheRepository = class {
22178
+ constructor(db) {
22179
+ this.db = db;
22180
+ this.upsertStmt = db.prepare(
22181
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
22182
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
22183
+ ON CONFLICT (rule_key) DO UPDATE SET
22184
+ verdict = excluded.verdict,
22185
+ worst_probe_ms = excluded.worst_probe_ms,
22186
+ checked_at = excluded.checked_at`
22187
+ );
22188
+ this.readStmt = db.prepare(
22189
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22190
+ );
22191
+ }
22192
+ db;
22193
+ upsertStmt;
22194
+ readStmt;
22195
+ getVerdict(ruleKey) {
22196
+ return getRow(this.readStmt, { ruleKey });
22197
+ }
22198
+ setVerdict(ruleKey, verdict, worstProbeMs2) {
22199
+ failOpenTransaction(this.db, () => {
22200
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
22201
+ });
22202
+ }
22203
+ };
22204
+
21639
22205
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21640
22206
  var SqliteScanLedgerRepository = class {
21641
22207
  constructor(db) {
@@ -21762,25 +22328,27 @@ var SqliteSecurityRepository = class {
21762
22328
  severitySummary() {
21763
22329
  const rows = allRows(
21764
22330
  this.db.prepare(
21765
- `SELECT f.severity AS severity,
22331
+ `SELECT d.severity AS severity,
21766
22332
  COUNT(*) AS count,
21767
22333
  SUM(CASE
21768
- WHEN e.kind != 'code_change' THEN 1
22334
+ WHEN e.event_type != 'code_change' THEN 1
21769
22335
  WHEN f.finding_key IS NULL THEN 0
21770
22336
  WHEN latest.status = 'resolved' THEN 1
21771
22337
  ELSE 0
21772
22338
  END) AS caught,
21773
22339
  SUM(CASE
21774
- WHEN e.kind = 'code_change'
22340
+ WHEN e.event_type = 'code_change'
21775
22341
  AND f.finding_key IS NOT NULL
21776
22342
  AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
21777
22343
  ELSE 0
21778
22344
  END) AS open_at_rest
21779
- FROM findings f
21780
- JOIN events e ON e.id = f.event_id
22345
+ FROM inspection_findings f
22346
+ JOIN audit_events e ON e.id = f.audit_event_id
22347
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21781
22348
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21782
22349
  ON latest.finding_key = f.finding_key
21783
- GROUP BY f.severity`
22350
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22351
+ GROUP BY d.severity`
21784
22352
  )
21785
22353
  );
21786
22354
  const byRow = new Map(rows.map((r) => [r.severity, r]));
@@ -21846,7 +22414,7 @@ var SqliteSecurityRepository = class {
21846
22414
  // Mean time-to-remediate per bucket, split by severity — a sibling of
21847
22415
  // findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
21848
22416
  // on a different timestamp: findingsTimeseries buckets by first-detection
21849
- // (events.occurred_at), this buckets by resolution time (the latest
22417
+ // (audit_events.started_at), this buckets by resolution time (the latest
21850
22418
  // finding_resolution row's resolved_at) — it's a "resolved in this bucket"
21851
22419
  // trend, not a "detected in this bucket" one. Only findings whose LATEST
21852
22420
  // resolution row (latest-resolution-wins, same correlated subquery as
@@ -21871,30 +22439,20 @@ var SqliteSecurityRepository = class {
21871
22439
  // first_detected_at is the PRESERVED first-detection time (set once on a
21872
22440
  // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21873
22441
  // measures from first sighting — not the latest re-scan's event, whose
21874
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21875
- // the parent event's occurred_at defends against any legacy/edge row the
21876
- // backfill left null.
21877
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21878
- (
21879
- SELECT fr.status FROM finding_resolution fr
21880
- WHERE fr.finding_key = f.finding_key
21881
- ORDER BY fr.created_at DESC, fr.rowid DESC
21882
- LIMIT 1
21883
- ) AS latest_status,
21884
- (
21885
- SELECT fr.method FROM finding_resolution fr
21886
- WHERE fr.finding_key = f.finding_key
21887
- ORDER BY fr.created_at DESC, fr.rowid DESC
21888
- LIMIT 1
21889
- ) AS latest_method,
21890
- (
21891
- SELECT fr.resolved_at FROM finding_resolution fr
21892
- WHERE fr.finding_key = f.finding_key
21893
- ORDER BY fr.created_at DESC, fr.rowid DESC
21894
- LIMIT 1
21895
- ) AS latest_resolved_at
21896
- FROM findings f JOIN events e ON e.id = f.event_id
22442
+ // started_at the upsert overwrites onto inspection_findings.audit_event_id.
22443
+ // COALESCE onto the parent event's started_at defends against any
22444
+ // legacy/edge row the backfill left null.
22445
+ `SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
22446
+ latest.status AS latest_status,
22447
+ latest.method AS latest_method,
22448
+ latest.resolved_at AS latest_resolved_at
22449
+ FROM inspection_findings f
22450
+ JOIN audit_events e ON e.id = f.audit_event_id
22451
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22452
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22453
+ ON latest.finding_key = f.finding_key
21897
22454
  WHERE f.finding_key IS NOT NULL
22455
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
21898
22456
  AND EXISTS (
21899
22457
  SELECT 1 FROM finding_resolution fr
21900
22458
  WHERE fr.finding_key = f.finding_key
@@ -21941,11 +22499,13 @@ var SqliteSecurityRepository = class {
21941
22499
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21942
22500
  const rows = allRows(
21943
22501
  this.db.prepare(
21944
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21945
- FROM findings f JOIN events e ON e.id = f.event_id
21946
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
21947
- AND json_extract(e.metadata, '$.repo') IS NOT NULL
21948
- AND json_extract(e.metadata, '$.repo') != ''
22502
+ `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
22503
+ FROM inspection_findings f
22504
+ JOIN audit_events e ON e.id = f.audit_event_id
22505
+ WHERE e.started_at >= :from AND e.started_at < :to
22506
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22507
+ AND json_extract(e.attributes, '$.repo') IS NOT NULL
22508
+ AND json_extract(e.attributes, '$.repo') != ''
21949
22509
  GROUP BY repo
21950
22510
  ORDER BY c DESC, repo
21951
22511
  LIMIT :limit`
@@ -21969,44 +22529,28 @@ var SqliteSecurityRepository = class {
21969
22529
  // secret came back) is excluded — it is not currently resolved. Legacy
21970
22530
  // at-rest findings with finding_key IS NULL are excluded outright (the
21971
22531
  // resolution lifecycle can never attach to them). Path comes from the
21972
- // finding's parent event (kind 'code_change', metadata.filePath) — mirrors
21973
- // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21974
- // capped at `limit`.
22532
+ // finding's parent event (event_type 'code_change', attributes.file_path) —
22533
+ // mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
22534
+ // DESC, capped at `limit`.
21975
22535
  recentlyResolved(limit = 20) {
21976
22536
  const rows = allRows(
21977
22537
  this.db.prepare(
21978
22538
  `SELECT f.finding_key AS finding_key,
21979
- f.rule_id AS rule_id,
21980
- f.severity AS severity,
21981
- json_extract(e.metadata, '$.filePath') AS path,
21982
- COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at,
21983
- (
21984
- SELECT fr.resolved_at FROM finding_resolution fr
21985
- WHERE fr.finding_key = f.finding_key
21986
- ORDER BY fr.created_at DESC, fr.rowid DESC
21987
- LIMIT 1
21988
- ) AS latest_resolved_at
21989
- FROM findings f JOIN events e ON e.id = f.event_id
21990
- WHERE e.kind = 'code_change'
22539
+ d.rule_id AS rule_id,
22540
+ d.severity AS severity,
22541
+ json_extract(e.attributes, '$.file_path') AS path,
22542
+ COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
22543
+ latest.resolved_at AS latest_resolved_at
22544
+ FROM inspection_findings f
22545
+ JOIN audit_events e ON e.id = f.audit_event_id
22546
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22547
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22548
+ ON latest.finding_key = f.finding_key
22549
+ WHERE e.event_type = 'code_change'
21991
22550
  AND f.finding_key IS NOT NULL
21992
- AND (
21993
- SELECT fr.status FROM finding_resolution fr
21994
- WHERE fr.finding_key = f.finding_key
21995
- ORDER BY fr.created_at DESC, fr.rowid DESC
21996
- LIMIT 1
21997
- ) = 'resolved'
21998
- AND (
21999
- SELECT fr.method FROM finding_resolution fr
22000
- WHERE fr.finding_key = f.finding_key
22001
- ORDER BY fr.created_at DESC, fr.rowid DESC
22002
- LIMIT 1
22003
- ) = 'fixed-at-source'
22004
- AND (
22005
- SELECT fr.resolved_at FROM finding_resolution fr
22006
- WHERE fr.finding_key = f.finding_key
22007
- ORDER BY fr.created_at DESC, fr.rowid DESC
22008
- LIMIT 1
22009
- ) IS NOT NULL
22551
+ AND latest.status = 'resolved'
22552
+ AND latest.method = 'fixed-at-source'
22553
+ AND latest.resolved_at IS NOT NULL
22010
22554
  ORDER BY latest_resolved_at DESC
22011
22555
  LIMIT :limit`
22012
22556
  ),
@@ -22025,15 +22569,18 @@ var SqliteSecurityRepository = class {
22025
22569
  return Promise.resolve({ items });
22026
22570
  }
22027
22571
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
22028
- // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
22572
+ // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
22029
22573
  // numeric and the JS aggregations bucket/split on ms directly.
22030
22574
  findingsInRange(fromMs, toMs) {
22031
22575
  const rows = allRows(
22032
22576
  this.db.prepare(
22033
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22034
- FROM findings f JOIN events e ON e.id = f.event_id
22035
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
22036
- ORDER BY e.occurred_at`
22577
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
22578
+ FROM inspection_findings f
22579
+ JOIN audit_events e ON e.id = f.audit_event_id
22580
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22581
+ WHERE e.started_at >= :from AND e.started_at < :to
22582
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22583
+ ORDER BY e.started_at`
22037
22584
  ),
22038
22585
  { from: fromMs, to: toMs }
22039
22586
  );
@@ -22047,11 +22594,50 @@ var SqliteSecurityRepository = class {
22047
22594
 
22048
22595
  // ../../packages/persistence/src/repositories/shares.ts
22049
22596
  import { randomUUID as randomUUID7 } from "crypto";
22050
- var KIND_ORDER = ["provider", "internal", "ip"];
22597
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22598
+ var IN_CHUNK = 500;
22599
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22600
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22601
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22602
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22051
22603
  var CALL_SITE_EMBED_CAP = 200;
22052
22604
  function parseNetwork(networkJson) {
22053
22605
  return safeJson(networkJson, null);
22054
22606
  }
22607
+ function capHits(all, mode) {
22608
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22609
+ return { hits: [...all], droppedFiles: [], truncated: false };
22610
+ }
22611
+ if (mode === "walk") {
22612
+ return {
22613
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22614
+ droppedFiles: [],
22615
+ truncated: true
22616
+ };
22617
+ }
22618
+ const byFile = /* @__PURE__ */ new Map();
22619
+ for (const hit of all) {
22620
+ const bucket = byFile.get(hit.site.file);
22621
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22622
+ else bucket.push(hit);
22623
+ }
22624
+ const hits = [];
22625
+ const droppedFiles = [];
22626
+ for (const [file2, bucket] of byFile) {
22627
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22628
+ else hits.push(...bucket);
22629
+ }
22630
+ return { hits, droppedFiles, truncated: true };
22631
+ }
22632
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22633
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22634
+ const dropped = new Set(droppedFiles);
22635
+ return {
22636
+ mode: "ledger",
22637
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22638
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22639
+ };
22640
+ }
22055
22641
  function toEndpointSummary(row) {
22056
22642
  return {
22057
22643
  id: row.id,
@@ -22142,13 +22728,15 @@ var SqliteSharesRepository = class {
22142
22728
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22143
22729
  const insecure = countScalar(
22144
22730
  this.db,
22145
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22731
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22732
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22146
22733
  );
22147
22734
  const needsReview = countScalar(
22148
22735
  this.db,
22149
22736
  `SELECT count(DISTINCT d.id) AS n
22150
22737
  FROM share_destination d
22151
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22738
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22739
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22152
22740
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22153
22741
  );
22154
22742
  const kindCounts = countBy(
@@ -22158,6 +22746,7 @@ var SqliteSharesRepository = class {
22158
22746
  const byKind = {
22159
22747
  provider: kindCounts.get("provider") ?? 0,
22160
22748
  internal: kindCounts.get("internal") ?? 0,
22749
+ external: kindCounts.get("external") ?? 0,
22161
22750
  ip: kindCounts.get("ip") ?? 0
22162
22751
  };
22163
22752
  const trustCounts = countBy(
@@ -22220,36 +22809,329 @@ var SqliteSharesRepository = class {
22220
22809
  });
22221
22810
  return Promise.resolve({ items });
22222
22811
  }
22223
- getDestination(destinationId) {
22224
- const dest = this.fetchDestinationById(destinationId);
22225
- if (!dest) return Promise.resolve(null);
22226
- const endpoints = this.fetchEndpoints([dest.id]);
22227
- const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
22228
- return Promise.resolve(buildDetail(dest, endpoints, callSites));
22812
+ getDestination(destinationId) {
22813
+ const dest = this.fetchDestinationById(destinationId);
22814
+ if (!dest) return Promise.resolve(null);
22815
+ const endpoints = this.fetchEndpoints([dest.id]);
22816
+ const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
22817
+ return Promise.resolve(buildDetail(dest, endpoints, callSites));
22818
+ }
22819
+ // ─── Writes ────────────────────────────────────────────────────────────────
22820
+ // Driven by a web-ui Server Action, not the hook path — errors surface to the
22821
+ // caller. Returns whether the destination existed, so the caller can tell a
22822
+ // real edit from a no-such-destination.
22823
+ /**
22824
+ * Set (decision) or clear (null) the egress decision override for a destination.
22825
+ * `null` deletes the override rows → reverts to the trust default.
22826
+ *
22827
+ * The written row carries both the destination id and its host, so the
22828
+ * decision re-attaches by host after the destination is pruned and
22829
+ * re-detected under a fresh id. Rows written before the host column existed
22830
+ * (host NULL, matched by destination id) are replaced rather than left to
22831
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22832
+ * would otherwise race a concurrent prune.
22833
+ */
22834
+ setEgressDecision(destinationId, decision) {
22835
+ let existed = false;
22836
+ withTransaction(
22837
+ this.db,
22838
+ () => {
22839
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22840
+ if (dest === void 0) return;
22841
+ existed = true;
22842
+ this.db.prepare(
22843
+ `DELETE FROM egress_decision_override
22844
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22845
+ ).run({ host: dest.host, destinationId });
22846
+ if (decision === null) return;
22847
+ this.db.prepare(
22848
+ `INSERT INTO egress_decision_override
22849
+ (id, destination_id, host, decision, created_at, updated_at)
22850
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22851
+ ).run({
22852
+ id: randomUUID7(),
22853
+ destinationId,
22854
+ host: dest.host,
22855
+ decision,
22856
+ now: Date.now()
22857
+ });
22858
+ },
22859
+ "IMMEDIATE"
22860
+ );
22861
+ return existed;
22862
+ }
22863
+ /**
22864
+ * Record one project's statically-extracted egress: reconcile the previously
22865
+ * stored call sites against this scan, upsert destination → endpoint → call
22866
+ * site for every hit, confirm `last_seen` on everything the project still
22867
+ * references, and drop what no longer has evidence.
22868
+ *
22869
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22870
+ * display payload and never scope a delete. The whole write is one
22871
+ * transaction: a failure leaves the project's previous inventory exactly as
22872
+ * it was, and THROWS rather than reporting a partial write — callers decide
22873
+ * their own fail-open behavior, and the scanner additionally withholds its
22874
+ * ledger commit so the next scan retries.
22875
+ *
22876
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22877
+ * their hits are both excluded from the reconcile delete and named in
22878
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22879
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22880
+ * caller withholds the ledger entry so the next scan reads it again.
22881
+ */
22882
+ recordProjectEgress(input) {
22883
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22884
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22885
+ const now = Date.now();
22886
+ let summary = {
22887
+ destinations: 0,
22888
+ endpoints: 0,
22889
+ callSites: 0,
22890
+ truncated,
22891
+ droppedFiles
22892
+ };
22893
+ withTransaction(
22894
+ this.db,
22895
+ () => {
22896
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22897
+ this.reconcileCallSites(input.projectKey, reconcile);
22898
+ this.upsertHits(input, hits, projectId, now);
22899
+ this.confirmLastSeen(input.projectKey, now);
22900
+ this.pruneOrphans();
22901
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22902
+ },
22903
+ "IMMEDIATE"
22904
+ );
22905
+ return summary;
22906
+ }
22907
+ // ─── Egress write internals ──────────────────────────────────────────────────
22908
+ /**
22909
+ * Clear the stored call sites this scan is responsible for re-creating.
22910
+ *
22911
+ * Each pipeline may only delete rows its own walker could have produced. The
22912
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22913
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22914
+ * reconcile, and deleting them here would make the two pipelines erase each
22915
+ * other's rows on every alternating scan. 'ledger' mode names its files
22916
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22917
+ * the scanner skips (vendored, oversize) survive it.
22918
+ */
22919
+ reconcileCallSites(projectKey, reconcile) {
22920
+ if (reconcile.mode === "walk") {
22921
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22922
+ this.db.prepare(
22923
+ `DELETE FROM share_call_site
22924
+ WHERE project_key = :key
22925
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22926
+ AND file NOT LIKE '.%'
22927
+ AND file NOT LIKE '%/.%'`
22928
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22929
+ return;
22930
+ }
22931
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22932
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22933
+ const chunk = files.slice(i, i + IN_CHUNK);
22934
+ this.db.prepare(
22935
+ `DELETE FROM share_call_site
22936
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22937
+ ).run(projectKey, ...chunk);
22938
+ }
22939
+ }
22940
+ /**
22941
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22942
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22943
+ * projects; only the call site carries `project_key`. A destination's `note`
22944
+ * is user-owned and never overwritten. The id caches keep one upsert per
22945
+ * distinct host and endpoint, so the first hit for a host supplies its
22946
+ * classification for this batch.
22947
+ */
22948
+ upsertHits(input, hits, projectId, now) {
22949
+ if (hits.length === 0) return;
22950
+ const destStmt = this.db.prepare(
22951
+ `INSERT INTO share_destination
22952
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22953
+ created_at, updated_at)
22954
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22955
+ ON CONFLICT (host) DO UPDATE SET
22956
+ kind = excluded.kind,
22957
+ name = excluded.name,
22958
+ category = excluded.category,
22959
+ trust = excluded.trust,
22960
+ network_json = excluded.network_json,
22961
+ last_seen = excluded.last_seen,
22962
+ updated_at = excluded.updated_at`
22963
+ );
22964
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22965
+ const endpointStmt = this.db.prepare(
22966
+ `INSERT INTO share_endpoint
22967
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22968
+ created_at, updated_at)
22969
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22970
+ :now, :now)
22971
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22972
+ transport = excluded.transport,
22973
+ template = excluded.template,
22974
+ data_class = excluded.data_class,
22975
+ last_seen = excluded.last_seen,
22976
+ updated_at = excluded.updated_at`
22977
+ );
22978
+ const endpointIdStmt = this.db.prepare(
22979
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22980
+ );
22981
+ const siteStmt = this.db.prepare(
22982
+ `INSERT INTO share_call_site
22983
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22984
+ project_id, created_at, updated_at)
22985
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22986
+ :vendored, :projectId, :now, :now)
22987
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22988
+ snippet = excluded.snippet,
22989
+ dynamic = excluded.dynamic,
22990
+ vendored = excluded.vendored,
22991
+ project = excluded.project,
22992
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22993
+ updated_at = excluded.updated_at`
22994
+ );
22995
+ const destIds = /* @__PURE__ */ new Map();
22996
+ const endpointIds = /* @__PURE__ */ new Map();
22997
+ for (const hit of hits) {
22998
+ let destinationId = destIds.get(hit.host);
22999
+ if (destinationId === void 0) {
23000
+ destStmt.run({
23001
+ id: randomUUID7(),
23002
+ kind: hit.kind,
23003
+ name: hit.name,
23004
+ host: hit.host,
23005
+ category: hit.category,
23006
+ trust: hit.trust,
23007
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
23008
+ now
23009
+ });
23010
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
23011
+ destIds.set(hit.host, destinationId);
23012
+ }
23013
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
23014
+ let endpointId = endpointIds.get(endpointKey);
23015
+ if (endpointId === void 0) {
23016
+ endpointStmt.run({
23017
+ id: randomUUID7(),
23018
+ destinationId,
23019
+ method: hit.method,
23020
+ transport: hit.transport,
23021
+ url: hit.url,
23022
+ template: boolToInt(hit.template),
23023
+ dataClass: hit.dataClass,
23024
+ now
23025
+ });
23026
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
23027
+ endpointIds.set(endpointKey, endpointId);
23028
+ }
23029
+ siteStmt.run({
23030
+ id: randomUUID7(),
23031
+ endpointId,
23032
+ project: input.project,
23033
+ projectKey: input.projectKey,
23034
+ file: hit.site.file,
23035
+ line: hit.site.line,
23036
+ snippet: hit.site.snippet,
23037
+ dynamic: boolToInt(hit.site.dynamic),
23038
+ vendored: boolToInt(hit.site.vendored),
23039
+ projectId,
23040
+ now
23041
+ });
23042
+ }
23043
+ }
23044
+ /**
23045
+ * The source-project id this project's stored call sites already carry, if
23046
+ * any. Only the pipeline that resolves a source project supplies one; the
23047
+ * other passes null and inherits this, so the link stops flapping between a
23048
+ * real id and NULL depending on which pipeline ran last. The value is a
23049
+ * per-project attribute stored redundantly on each row, so any row's is
23050
+ * representative.
23051
+ */
23052
+ knownProjectId(projectKey) {
23053
+ return getRow(
23054
+ this.db.prepare(
23055
+ `SELECT project_id AS projectId FROM share_call_site
23056
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
23057
+ ),
23058
+ [projectKey]
23059
+ )?.projectId ?? null;
22229
23060
  }
22230
- // ─── Writes ────────────────────────────────────────────────────────────────
22231
- // Driven by a web-ui Server Action, not the hook path — errors surface to the
22232
- // caller. Returns whether the destination existed, so the caller can tell a
22233
- // real edit from a no-such-destination.
22234
23061
  /**
22235
- * Set (decision) or clear (null) the egress decision override for a destination.
22236
- * `null` deletes the override row reverts to the trust default.
23062
+ * Stamp `last_seen` on every endpoint and destination this project still
23063
+ * references including rows the scan preserved rather than re-wrote, so a
23064
+ * ledger-skipped file's references don't decay into "stale" on the page.
22237
23065
  */
22238
- setEgressDecision(destinationId, decision) {
22239
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22240
- if (exists === void 0) return false;
22241
- if (decision === null) {
22242
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22243
- return true;
22244
- }
23066
+ confirmLastSeen(projectKey, now) {
22245
23067
  this.db.prepare(
22246
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22247
- VALUES (:id, :destinationId, :decision, :now, :now)
22248
- ON CONFLICT (destination_id) DO UPDATE SET
22249
- decision = excluded.decision,
22250
- updated_at = excluded.updated_at`
22251
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22252
- return true;
23068
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
23069
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
23070
+ ).run({ now, key: projectKey });
23071
+ this.db.prepare(
23072
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
23073
+ WHERE id IN (SELECT DISTINCT e.destination_id
23074
+ FROM share_endpoint e
23075
+ JOIN share_call_site c ON c.endpoint_id = e.id
23076
+ WHERE c.project_key = :key)`
23077
+ ).run({ now, key: projectKey });
23078
+ }
23079
+ /**
23080
+ * Drop rows left without evidence: endpoints with no call site, then
23081
+ * destinations with no endpoint. Call sites are the only evidence either one
23082
+ * has, so a row that lost its last one belongs to no project any more.
23083
+ *
23084
+ * Overrides are deleted between the two steps, and only the ones written
23085
+ * before the host column existed. Those match a destination by id alone;
23086
+ * because the id link is released on delete rather than cascading, leaving
23087
+ * them would accumulate rows that match neither join arm and that nothing can
23088
+ * reach again. Host-bearing rows deliberately survive — the host is what
23089
+ * re-attaches a user's decision when the destination comes back.
23090
+ */
23091
+ pruneOrphans() {
23092
+ this.db.exec(
23093
+ `DELETE FROM share_endpoint
23094
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
23095
+ );
23096
+ this.db.exec(
23097
+ `DELETE FROM egress_decision_override
23098
+ WHERE host IS NULL
23099
+ AND destination_id IN (
23100
+ SELECT d.id FROM share_destination d
23101
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
23102
+ );
23103
+ this.db.exec(
23104
+ `DELETE FROM share_destination
23105
+ WHERE NOT EXISTS (
23106
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
23107
+ );
23108
+ }
23109
+ /**
23110
+ * Live totals for one project. Destinations and endpoints are shared across
23111
+ * projects and carry no project column, so both are counted through the call
23112
+ * sites that reference them.
23113
+ */
23114
+ projectTotals(projectKey) {
23115
+ return {
23116
+ destinations: countScalar(
23117
+ this.db,
23118
+ `SELECT count(DISTINCT e.destination_id) AS n
23119
+ FROM share_endpoint e
23120
+ JOIN share_call_site c ON c.endpoint_id = e.id
23121
+ WHERE c.project_key = ?`,
23122
+ [projectKey]
23123
+ ),
23124
+ endpoints: countScalar(
23125
+ this.db,
23126
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
23127
+ [projectKey]
23128
+ ),
23129
+ callSites: countScalar(
23130
+ this.db,
23131
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
23132
+ [projectKey]
23133
+ )
23134
+ };
22253
23135
  }
22254
23136
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22255
23137
  mapDestRow(r) {
@@ -22269,7 +23151,8 @@ var SqliteSharesRepository = class {
22269
23151
  fetchDestinations(q, kinds, reviewOnly = false) {
22270
23152
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22271
23153
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22272
- d.created_at AS createdAt, o.decision AS overrideDecision`;
23154
+ d.created_at AS createdAt,
23155
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22273
23156
  const conditions = [];
22274
23157
  const params = [];
22275
23158
  if (kinds && kinds.length > 0) {
@@ -22280,7 +23163,8 @@ var SqliteSharesRepository = class {
22280
23163
  conditions.push(
22281
23164
  `(d.trust IN ('unverified', 'ip')
22282
23165
  OR EXISTS (SELECT 1 FROM share_endpoint re
22283
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
23166
+ WHERE re.destination_id = d.id
23167
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22284
23168
  );
22285
23169
  }
22286
23170
  let sql;
@@ -22293,7 +23177,7 @@ var SqliteSharesRepository = class {
22293
23177
  params.push(pattern, pattern, pattern, pattern, pattern);
22294
23178
  sql = `SELECT DISTINCT ${cols}
22295
23179
  FROM share_destination d
22296
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23180
+ ${OVERRIDE_JOIN}
22297
23181
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22298
23182
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22299
23183
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22301,7 +23185,7 @@ var SqliteSharesRepository = class {
22301
23185
  } else {
22302
23186
  sql = `SELECT ${cols}
22303
23187
  FROM share_destination d
22304
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23188
+ ${OVERRIDE_JOIN}
22305
23189
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22306
23190
  ORDER BY d.created_at ASC, d.id ASC`;
22307
23191
  }
@@ -22316,9 +23200,9 @@ var SqliteSharesRepository = class {
22316
23200
  this.db.prepare(
22317
23201
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22318
23202
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22319
- o.decision AS overrideDecision
23203
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22320
23204
  FROM share_destination d
22321
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23205
+ ${OVERRIDE_JOIN}
22322
23206
  WHERE d.id = ?`
22323
23207
  ),
22324
23208
  [destinationId]
@@ -22520,9 +23404,10 @@ function openWithPragmas(file2) {
22520
23404
  }
22521
23405
  function backupLegacyStore(file2) {
22522
23406
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
22523
- renameSync(file2, backup);
22524
- for (const sidecar of walSidecars(file2)) {
22525
- if (existsSync(sidecar)) rmSync(sidecar);
23407
+ renameSync2(file2, backup);
23408
+ tightenFile(backup);
23409
+ for (const sidecar of dbSidecars(file2)) {
23410
+ if (existsSync(sidecar)) rmSync2(sidecar);
22526
23411
  }
22527
23412
  return backup;
22528
23413
  }
@@ -22538,7 +23423,7 @@ function openLocalDatabase(dir) {
22538
23423
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
22539
23424
  );
22540
23425
  }
22541
- applyMigrations(db);
23426
+ applyMigrations(db, file2);
22542
23427
  tightenPerms(file2);
22543
23428
  const events = new SqliteEventsRepository(db);
22544
23429
  const findings = new SqliteFindingsRepository(db);
@@ -22547,6 +23432,7 @@ function openLocalDatabase(dir) {
22547
23432
  const scanLedger = new SqliteScanLedgerRepository(db);
22548
23433
  const exceptions = new SqliteExceptionsRepository(db);
22549
23434
  const resolutions = new SqliteResolutionsRepository(db);
23435
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22550
23436
  const security = new SqliteSecurityRepository(db);
22551
23437
  const detections = new SqliteDetectionsRepository(db);
22552
23438
  const shares = new SqliteSharesRepository(db);
@@ -22564,9 +23450,56 @@ function openLocalDatabase(dir) {
22564
23450
  policies.seedDefaults();
22565
23451
  function recordCapture(event, detected) {
22566
23452
  failOpenTransaction(db, () => {
22567
- events.insertEvent(event);
22568
23453
  const sessionId = event.metadata?.sessionId;
22569
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
23454
+ if (sessionId) {
23455
+ auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
23456
+ }
23457
+ const auditEventId = captureId(
23458
+ sessionId ?? null,
23459
+ event.contentHash,
23460
+ event.metadata?.filePath ?? null
23461
+ );
23462
+ auditEvents.insertAuditEvent({
23463
+ id: auditEventId,
23464
+ eventType: event.kind,
23465
+ startedAt: event.occurredAt,
23466
+ parentId: sessionId,
23467
+ rootSessionId: sessionId,
23468
+ content: event.content,
23469
+ contentHash: event.contentHash,
23470
+ attributes: toCaptureAttributes(event)
23471
+ });
23472
+ const definitionIds = /* @__PURE__ */ new Map();
23473
+ for (const finding of detected) {
23474
+ if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
23475
+ continue;
23476
+ }
23477
+ if (inspectionFindings.isEventDuplicate(
23478
+ auditEventId,
23479
+ finding.ruleId,
23480
+ finding.maskedMatch,
23481
+ finding.span.start,
23482
+ finding.span.end
23483
+ )) {
23484
+ continue;
23485
+ }
23486
+ const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
23487
+ let definitionId = definitionIds.get(key);
23488
+ if (!definitionId) {
23489
+ definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
23490
+ definitionIds.set(key, definitionId);
23491
+ }
23492
+ inspectionFindings.insertFinding({
23493
+ id: finding.id,
23494
+ auditEventId,
23495
+ inspectionDefinitionId: definitionId,
23496
+ span: finding.span,
23497
+ maskedMatch: finding.maskedMatch,
23498
+ actionTaken: finding.actionTaken,
23499
+ confidence: finding.confidence,
23500
+ findingKey: finding.findingKey ?? void 0
23501
+ });
23502
+ }
22570
23503
  });
22571
23504
  }
22572
23505
  function ensureInventory(ctx) {
@@ -22684,6 +23617,7 @@ function openLocalDatabase(dir) {
22684
23617
  scanLedger,
22685
23618
  exceptions,
22686
23619
  resolutions,
23620
+ ruleProbeCache,
22687
23621
  security,
22688
23622
  detections,
22689
23623
  shares,
@@ -22713,9 +23647,19 @@ function openLocalDatabase(dir) {
22713
23647
  };
22714
23648
  }
22715
23649
 
23650
+ // ../../packages/persistence/src/finding-key.ts
23651
+ import { createHash as createHash3 } from "crypto";
23652
+ function normalizeFilePath(filePath) {
23653
+ return filePath.replaceAll("\\", "/");
23654
+ }
23655
+ function computeFindingKey(input) {
23656
+ const normalizedPath = normalizeFilePath(input.filePath);
23657
+ return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
23658
+ }
23659
+
22716
23660
  // ../../packages/persistence/src/fingerprint.ts
22717
23661
  import { createHmac, randomBytes } from "crypto";
22718
- import { chmodSync as chmodSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
23662
+ import { readFileSync } from "fs";
22719
23663
  import { join as join2 } from "path";
22720
23664
  var KEY_FILENAME = "exception.key";
22721
23665
  var KEY_MATERIAL_BYTES = 32;
@@ -22743,15 +23687,9 @@ function parseKeyFile(raw) {
22743
23687
  function writeKeyFile(dataDir2, key) {
22744
23688
  ensureDataDirSync(dataDir2);
22745
23689
  const file2 = keyFilePath(dataDir2);
22746
- const tmp = `${file2}.tmp`;
22747
23690
  const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
22748
- writeFileSync(tmp, `${body}
22749
- `, { mode: DATA_FILE_MODE });
22750
- renameSync2(tmp, file2);
22751
- try {
22752
- chmodSync2(file2, DATA_FILE_MODE);
22753
- } catch {
22754
- }
23691
+ writeOwnerOnlyFileSync(file2, `${body}
23692
+ `);
22755
23693
  return key;
22756
23694
  }
22757
23695
  function readFingerprintKey(dataDir2) {
@@ -22767,10 +23705,7 @@ function readFingerprintKey(dataDir2) {
22767
23705
  function loadOrCreateFingerprintKey(dataDir2) {
22768
23706
  const existing = readFingerprintKey(dataDir2);
22769
23707
  if (existing) {
22770
- try {
22771
- chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
22772
- } catch {
22773
- }
23708
+ tightenFile(keyFilePath(dataDir2));
22774
23709
  return existing;
22775
23710
  }
22776
23711
  return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
@@ -22780,8 +23715,8 @@ function fingerprintValue(key, raw) {
22780
23715
  }
22781
23716
 
22782
23717
  // ../../packages/persistence/src/local-layout.ts
22783
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync2, renameSync as renameSync3 } from "fs";
22784
- import { chmod, mkdir } from "fs/promises";
23718
+ import { renameSync as renameSync3 } from "fs";
23719
+ import { mkdir } from "fs/promises";
22785
23720
  import { homedir } from "os";
22786
23721
  import { join as join3 } from "path";
22787
23722
  function defaultDataDir() {
@@ -22796,6 +23731,9 @@ function dataDir(base = defaultDataDir()) {
22796
23731
  function dbPath(base = defaultDataDir()) {
22797
23732
  return join3(dataDir(base), "aka.db");
22798
23733
  }
23734
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
23735
+ ensureDataDirSync(dir);
23736
+ }
22799
23737
  function migrateLegacyLayout(base = defaultDataDir()) {
22800
23738
  const moves = [
22801
23739
  { name: "config.json", dest: settingsDir(base) },
@@ -22803,19 +23741,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
22803
23741
  ];
22804
23742
  for (const { name, dest } of moves) {
22805
23743
  try {
22806
- mkdirSync2(dest, { recursive: true, mode: DATA_DIR_MODE });
22807
- try {
22808
- chmodSync3(dest, DATA_DIR_MODE);
22809
- } catch {
22810
- }
22811
- renameSync3(join3(base, name), join3(dest, name));
23744
+ ensureDataDirSync(dest);
23745
+ const moved = join3(dest, name);
23746
+ renameSync3(join3(base, name), moved);
23747
+ tightenFile(moved);
22812
23748
  } catch {
22813
23749
  }
22814
23750
  }
22815
23751
  }
22816
23752
 
22817
23753
  // ../../packages/persistence/src/settings.ts
22818
- import { readFileSync as readFileSync2, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
23754
+ import { readFileSync as readFileSync2 } from "fs";
22819
23755
  import { join as join4 } from "path";
22820
23756
  function readWorkspaceSettings(base = defaultDataDir()) {
22821
23757
  const record2 = readJson(join4(settingsDir(base), "settings.json"));
@@ -22837,7 +23773,7 @@ function readJson(file2) {
22837
23773
  }
22838
23774
 
22839
23775
  // ../../packages/persistence/src/warn-era-cap.ts
22840
- import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
23776
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
22841
23777
  import { join as join5 } from "path";
22842
23778
  var MARKER = "warn-era-capped";
22843
23779
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
@@ -22845,7 +23781,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22845
23781
  const marker = join5(dataDir2, MARKER);
22846
23782
  if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22847
23783
  const capped = db.policies.capCategoryActions();
22848
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
23784
+ writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
22849
23785
  `, { mode: DATA_FILE_MODE });
22850
23786
  return { capped };
22851
23787
  }
@@ -22910,6 +23846,12 @@ function providerFromModelId(modelId) {
22910
23846
 
22911
23847
  // ../../packages/plugin-sdk/src/config.ts
22912
23848
  function loadConfig(base = defaultDataDir()) {
23849
+ try {
23850
+ ensureLayoutDirSync(base);
23851
+ const settingsFile = join6(settingsDir(base), "settings.json");
23852
+ if (existsSync3(settingsFile)) tightenFile(settingsFile);
23853
+ } catch {
23854
+ }
22913
23855
  migrateLegacyLayout(base);
22914
23856
  const settings = readWorkspaceSettings(base);
22915
23857
  return {
@@ -22932,15 +23874,583 @@ function resolveProviderSafe() {
22932
23874
  // ../../packages/plugin-sdk/src/config-inventory.ts
22933
23875
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22934
23876
  import { homedir as homedir2 } from "os";
22935
- import { basename as basename2, join as join7 } from "path";
23877
+ import { basename as basename2, join as join8 } from "path";
23878
+
23879
+ // ../../packages/detections/src/egress/registry.ts
23880
+ var EXTRACTOR_VERSION = "1";
23881
+ var PROVIDER_REGISTRY = [
23882
+ {
23883
+ id: "stripe",
23884
+ name: "Stripe",
23885
+ category: "Payments",
23886
+ hostSuffixes: ["stripe.com"],
23887
+ apiBase: "https://api.stripe.com",
23888
+ defaultDataClasses: ["pii", "customer"],
23889
+ sdks: {
23890
+ npm: ["stripe"],
23891
+ pypi: ["stripe"],
23892
+ go: ["github.com/stripe/stripe-go"],
23893
+ maven: ["com.stripe"],
23894
+ rubygems: ["stripe"],
23895
+ composer: ["stripe/stripe-php"],
23896
+ nuget: ["Stripe.net"]
23897
+ }
23898
+ },
23899
+ {
23900
+ id: "datadog",
23901
+ name: "Datadog",
23902
+ category: "Observability",
23903
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23904
+ apiBase: "https://api.datadoghq.com",
23905
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23906
+ sdks: {
23907
+ npm: ["dd-trace", "@datadog/browser-logs"],
23908
+ pypi: ["datadog", "ddtrace"],
23909
+ go: ["github.com/DataDog/dd-trace-go"],
23910
+ maven: ["com.datadoghq"],
23911
+ rubygems: ["ddtrace", "dogapi"],
23912
+ nuget: ["Datadog.Trace"]
23913
+ }
23914
+ },
23915
+ {
23916
+ id: "newrelic",
23917
+ name: "New Relic",
23918
+ category: "Observability",
23919
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23920
+ apiBase: "https://api.newrelic.com",
23921
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23922
+ sdks: {
23923
+ npm: ["newrelic"],
23924
+ pypi: ["newrelic"],
23925
+ go: ["github.com/newrelic/go-agent"],
23926
+ maven: ["com.newrelic.agent.java"],
23927
+ rubygems: ["newrelic_rpm"],
23928
+ nuget: ["NewRelic.Agent"]
23929
+ }
23930
+ },
23931
+ {
23932
+ id: "sentry",
23933
+ name: "Sentry",
23934
+ category: "Error tracking",
23935
+ hostSuffixes: ["sentry.io"],
23936
+ apiBase: "https://sentry.io",
23937
+ defaultDataClasses: ["source", "telemetry"],
23938
+ sdks: {
23939
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23940
+ pypi: ["sentry-sdk"],
23941
+ go: ["github.com/getsentry/sentry-go"],
23942
+ maven: ["io.sentry"],
23943
+ rubygems: ["sentry-ruby"],
23944
+ cargo: ["sentry"],
23945
+ composer: ["sentry/sentry"],
23946
+ nuget: ["Sentry"]
23947
+ }
23948
+ },
23949
+ {
23950
+ id: "openai",
23951
+ name: "OpenAI",
23952
+ category: "LLM provider",
23953
+ hostSuffixes: ["openai.com"],
23954
+ apiBase: "https://api.openai.com",
23955
+ defaultDataClasses: ["pii", "source"],
23956
+ sdks: {
23957
+ npm: ["openai"],
23958
+ pypi: ["openai"],
23959
+ go: ["github.com/sashabaranov/go-openai"],
23960
+ maven: ["com.openai"],
23961
+ rubygems: ["ruby-openai"],
23962
+ cargo: ["async-openai"],
23963
+ composer: ["openai-php/client"],
23964
+ nuget: ["OpenAI"]
23965
+ }
23966
+ },
23967
+ {
23968
+ id: "anthropic",
23969
+ name: "Anthropic",
23970
+ category: "LLM provider",
23971
+ hostSuffixes: ["anthropic.com"],
23972
+ apiBase: "https://api.anthropic.com",
23973
+ defaultDataClasses: ["pii", "source"],
23974
+ sdks: {
23975
+ npm: ["@anthropic-ai/sdk"],
23976
+ pypi: ["anthropic"],
23977
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23978
+ nuget: ["Anthropic.SDK"]
23979
+ }
23980
+ },
23981
+ {
23982
+ id: "aws",
23983
+ name: "Amazon Web Services",
23984
+ category: "Cloud platform",
23985
+ hostSuffixes: ["amazonaws.com"],
23986
+ apiBase: "https://s3.amazonaws.com",
23987
+ defaultDataClasses: ["secrets", "customer"],
23988
+ sdks: {
23989
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23990
+ pypi: ["boto3"],
23991
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23992
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23993
+ rubygems: ["aws-sdk-s3"],
23994
+ cargo: ["aws-sdk-s3"],
23995
+ nuget: ["AWSSDK.S3"]
23996
+ }
23997
+ },
23998
+ {
23999
+ id: "gcp",
24000
+ name: "Google Cloud",
24001
+ category: "Cloud platform",
24002
+ hostSuffixes: ["googleapis.com"],
24003
+ apiBase: "https://storage.googleapis.com",
24004
+ defaultDataClasses: ["customer", "logs"],
24005
+ sdks: {
24006
+ npm: ["@google-cloud/storage"],
24007
+ pypi: ["google-cloud-storage"],
24008
+ go: ["cloud.google.com/go"],
24009
+ maven: ["com.google.cloud"],
24010
+ rubygems: ["google-cloud-storage"],
24011
+ nuget: ["Google.Cloud.Storage.V1"]
24012
+ }
24013
+ },
24014
+ {
24015
+ id: "azure",
24016
+ name: "Microsoft Azure",
24017
+ category: "Cloud platform",
24018
+ hostSuffixes: ["azure.com", "windows.net"],
24019
+ apiBase: "https://management.azure.com",
24020
+ defaultDataClasses: ["customer", "logs"],
24021
+ sdks: {
24022
+ npm: ["@azure/storage-blob"],
24023
+ pypi: ["azure-storage-blob"],
24024
+ go: ["github.com/Azure/azure-sdk-for-go"],
24025
+ maven: ["com.azure"],
24026
+ rubygems: ["azure-storage-blob"],
24027
+ nuget: ["Azure.Storage.Blobs"]
24028
+ }
24029
+ },
24030
+ {
24031
+ id: "slack",
24032
+ name: "Slack",
24033
+ category: "Notifications",
24034
+ hostSuffixes: ["slack.com"],
24035
+ apiBase: "https://slack.com/api",
24036
+ defaultDataClasses: ["logs"],
24037
+ sdks: {
24038
+ npm: ["@slack/web-api"],
24039
+ pypi: ["slack-sdk"],
24040
+ go: ["github.com/slack-go/slack"],
24041
+ maven: ["com.slack.api"],
24042
+ rubygems: ["slack-ruby-client"],
24043
+ composer: ["slack-php/slack-api"],
24044
+ nuget: ["SlackNet"]
24045
+ }
24046
+ },
24047
+ {
24048
+ id: "segment",
24049
+ name: "Segment",
24050
+ category: "Analytics",
24051
+ hostSuffixes: ["segment.io", "segment.com"],
24052
+ apiBase: "https://api.segment.io",
24053
+ defaultDataClasses: ["customer"],
24054
+ sdks: {
24055
+ npm: ["@segment/analytics-node", "analytics-node"],
24056
+ pypi: ["segment-analytics-python"],
24057
+ go: ["github.com/segmentio/analytics-go"],
24058
+ maven: ["com.segment.analytics.java"],
24059
+ rubygems: ["analytics-ruby"],
24060
+ nuget: ["Analytics"]
24061
+ }
24062
+ },
24063
+ {
24064
+ id: "twilio",
24065
+ name: "Twilio",
24066
+ category: "Communications",
24067
+ hostSuffixes: ["twilio.com"],
24068
+ apiBase: "https://api.twilio.com",
24069
+ defaultDataClasses: ["pii", "customer"],
24070
+ sdks: {
24071
+ npm: ["twilio"],
24072
+ pypi: ["twilio"],
24073
+ go: ["github.com/twilio/twilio-go"],
24074
+ maven: ["com.twilio.sdk"],
24075
+ rubygems: ["twilio-ruby"],
24076
+ composer: ["twilio/sdk"],
24077
+ nuget: ["Twilio"]
24078
+ }
24079
+ },
24080
+ {
24081
+ id: "sendgrid",
24082
+ name: "SendGrid",
24083
+ category: "Email",
24084
+ hostSuffixes: ["sendgrid.com"],
24085
+ apiBase: "https://api.sendgrid.com",
24086
+ defaultDataClasses: ["pii"],
24087
+ sdks: {
24088
+ npm: ["@sendgrid/mail"],
24089
+ pypi: ["sendgrid"],
24090
+ go: ["github.com/sendgrid/sendgrid-go"],
24091
+ maven: ["com.sendgrid"],
24092
+ rubygems: ["sendgrid-ruby"],
24093
+ composer: ["sendgrid/sendgrid"],
24094
+ nuget: ["SendGrid"]
24095
+ }
24096
+ },
24097
+ {
24098
+ id: "mailgun",
24099
+ name: "Mailgun",
24100
+ category: "Email",
24101
+ hostSuffixes: ["mailgun.net"],
24102
+ apiBase: "https://api.mailgun.net",
24103
+ defaultDataClasses: ["pii"],
24104
+ sdks: {
24105
+ npm: ["mailgun.js"],
24106
+ pypi: ["mailgun"],
24107
+ rubygems: ["mailgun-ruby"],
24108
+ composer: ["mailgun/mailgun-php"],
24109
+ nuget: ["Mailgun"]
24110
+ }
24111
+ },
24112
+ {
24113
+ id: "mixpanel",
24114
+ name: "Mixpanel",
24115
+ category: "Analytics",
24116
+ hostSuffixes: ["mixpanel.com"],
24117
+ apiBase: "https://api.mixpanel.com",
24118
+ defaultDataClasses: ["customer", "telemetry"],
24119
+ sdks: {
24120
+ npm: ["mixpanel"],
24121
+ pypi: ["mixpanel"],
24122
+ rubygems: ["mixpanel-ruby"],
24123
+ nuget: ["Mixpanel"]
24124
+ }
24125
+ },
24126
+ {
24127
+ id: "amplitude",
24128
+ name: "Amplitude",
24129
+ category: "Analytics",
24130
+ hostSuffixes: ["amplitude.com"],
24131
+ apiBase: "https://api2.amplitude.com",
24132
+ defaultDataClasses: ["customer", "telemetry"],
24133
+ sdks: {
24134
+ npm: ["@amplitude/analytics-node"],
24135
+ pypi: ["amplitude-analytics"],
24136
+ nuget: ["Amplitude"]
24137
+ }
24138
+ },
24139
+ {
24140
+ id: "posthog",
24141
+ name: "PostHog",
24142
+ category: "Analytics",
24143
+ hostSuffixes: ["posthog.com"],
24144
+ apiBase: "https://us.i.posthog.com",
24145
+ defaultDataClasses: ["customer", "telemetry"],
24146
+ sdks: {
24147
+ npm: ["posthog-node", "posthog-js"],
24148
+ pypi: ["posthog"],
24149
+ go: ["github.com/posthog/posthog-go"],
24150
+ rubygems: ["posthog-ruby"],
24151
+ composer: ["posthog/posthog-php"],
24152
+ nuget: ["PostHog"]
24153
+ }
24154
+ },
24155
+ {
24156
+ id: "honeycomb",
24157
+ name: "Honeycomb",
24158
+ category: "Observability",
24159
+ hostSuffixes: ["honeycomb.io"],
24160
+ apiBase: "https://api.honeycomb.io",
24161
+ defaultDataClasses: ["telemetry", "metrics"],
24162
+ sdks: {
24163
+ npm: ["libhoney"],
24164
+ pypi: ["libhoney"],
24165
+ go: ["github.com/honeycombio/libhoney-go"],
24166
+ rubygems: ["libhoney"]
24167
+ }
24168
+ },
24169
+ {
24170
+ id: "grafana",
24171
+ name: "Grafana Cloud",
24172
+ category: "Observability",
24173
+ hostSuffixes: ["grafana.net"],
24174
+ apiBase: "https://grafana.net",
24175
+ defaultDataClasses: ["logs", "metrics"],
24176
+ sdks: {
24177
+ npm: ["@grafana/faro-web-sdk"]
24178
+ }
24179
+ },
24180
+ {
24181
+ id: "splunk",
24182
+ name: "Splunk",
24183
+ category: "Observability",
24184
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
24185
+ apiBase: "https://http-inputs.splunkcloud.com",
24186
+ defaultDataClasses: ["logs"],
24187
+ sdks: {
24188
+ npm: ["splunk-logging"],
24189
+ pypi: ["splunk-sdk"],
24190
+ maven: ["com.splunk"],
24191
+ nuget: ["Splunk.Logging.Common"]
24192
+ }
24193
+ },
24194
+ {
24195
+ id: "pagerduty",
24196
+ name: "PagerDuty",
24197
+ category: "Incident response",
24198
+ hostSuffixes: ["pagerduty.com"],
24199
+ apiBase: "https://api.pagerduty.com",
24200
+ defaultDataClasses: ["logs"],
24201
+ sdks: {
24202
+ npm: ["@pagerduty/pdjs"],
24203
+ pypi: ["pdpyras"],
24204
+ go: ["github.com/PagerDuty/go-pagerduty"],
24205
+ rubygems: ["pagerduty"]
24206
+ }
24207
+ },
24208
+ {
24209
+ id: "github",
24210
+ name: "GitHub",
24211
+ category: "Developer platform",
24212
+ hostSuffixes: ["github.com", "githubusercontent.com"],
24213
+ apiBase: "https://api.github.com",
24214
+ defaultDataClasses: ["source"],
24215
+ sdks: {
24216
+ npm: ["@octokit/rest", "octokit"],
24217
+ pypi: ["pygithub"],
24218
+ go: ["github.com/google/go-github"],
24219
+ maven: ["org.kohsuke.github-api"],
24220
+ rubygems: ["octokit"],
24221
+ cargo: ["octocrab"],
24222
+ composer: ["knplabs/github-api"],
24223
+ nuget: ["Octokit"]
24224
+ }
24225
+ },
24226
+ {
24227
+ id: "gitlab",
24228
+ name: "GitLab",
24229
+ category: "Developer platform",
24230
+ hostSuffixes: ["gitlab.com"],
24231
+ apiBase: "https://gitlab.com/api",
24232
+ defaultDataClasses: ["source"],
24233
+ sdks: {
24234
+ npm: ["@gitbeaker/rest"],
24235
+ pypi: ["python-gitlab"],
24236
+ go: ["gitlab.com/gitlab-org/api/client-go"],
24237
+ rubygems: ["gitlab"],
24238
+ nuget: ["GitLabApiClient"]
24239
+ }
24240
+ },
24241
+ {
24242
+ id: "auth0",
24243
+ name: "Auth0",
24244
+ category: "Identity",
24245
+ hostSuffixes: ["auth0.com"],
24246
+ apiBase: "https://login.auth0.com",
24247
+ defaultDataClasses: ["pii"],
24248
+ sdks: {
24249
+ npm: ["auth0"],
24250
+ pypi: ["auth0-python"],
24251
+ go: ["github.com/auth0/go-auth0"],
24252
+ maven: ["com.auth0"],
24253
+ rubygems: ["auth0"],
24254
+ composer: ["auth0/auth0-php"],
24255
+ nuget: ["Auth0.ManagementApi"]
24256
+ }
24257
+ },
24258
+ {
24259
+ id: "okta",
24260
+ name: "Okta",
24261
+ category: "Identity",
24262
+ hostSuffixes: ["okta.com", "oktapreview.com"],
24263
+ apiBase: "https://login.okta.com",
24264
+ defaultDataClasses: ["pii"],
24265
+ sdks: {
24266
+ npm: ["@okta/okta-sdk-nodejs"],
24267
+ pypi: ["okta"],
24268
+ go: ["github.com/okta/okta-sdk-golang"],
24269
+ maven: ["com.okta.sdk"],
24270
+ nuget: ["Okta.Sdk"]
24271
+ }
24272
+ },
24273
+ {
24274
+ id: "clerk",
24275
+ name: "Clerk",
24276
+ category: "Identity",
24277
+ hostSuffixes: ["clerk.com", "clerk.dev"],
24278
+ apiBase: "https://api.clerk.com",
24279
+ defaultDataClasses: ["pii"],
24280
+ sdks: {
24281
+ npm: ["@clerk/backend", "@clerk/nextjs"],
24282
+ pypi: ["clerk-backend-api"],
24283
+ go: ["github.com/clerk/clerk-sdk-go"]
24284
+ }
24285
+ },
24286
+ {
24287
+ id: "supabase",
24288
+ name: "Supabase",
24289
+ category: "Backend platform",
24290
+ hostSuffixes: ["supabase.co", "supabase.com"],
24291
+ apiBase: "https://api.supabase.com",
24292
+ defaultDataClasses: ["pii", "customer"],
24293
+ sdks: {
24294
+ npm: ["@supabase/supabase-js"],
24295
+ pypi: ["supabase"],
24296
+ cargo: ["postgrest"]
24297
+ }
24298
+ },
24299
+ {
24300
+ id: "firebase",
24301
+ name: "Firebase",
24302
+ category: "Backend platform",
24303
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
24304
+ apiBase: "https://firebaseio.com",
24305
+ defaultDataClasses: ["customer"],
24306
+ sdks: {
24307
+ npm: ["firebase", "firebase-admin"],
24308
+ pypi: ["firebase-admin"],
24309
+ go: ["firebase.google.com/go"],
24310
+ maven: ["com.google.firebase"]
24311
+ }
24312
+ },
24313
+ {
24314
+ id: "mongodb-atlas",
24315
+ name: "MongoDB Atlas",
24316
+ category: "Database SaaS",
24317
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
24318
+ apiBase: "https://cloud.mongodb.com",
24319
+ defaultDataClasses: ["customer"],
24320
+ sdks: {
24321
+ npm: ["mongodb"],
24322
+ pypi: ["pymongo"],
24323
+ go: ["go.mongodb.org/mongo-driver"],
24324
+ maven: ["org.mongodb"],
24325
+ rubygems: ["mongo"],
24326
+ cargo: ["mongodb"],
24327
+ nuget: ["MongoDB.Driver"]
24328
+ }
24329
+ },
24330
+ {
24331
+ id: "planetscale",
24332
+ name: "PlanetScale",
24333
+ category: "Database SaaS",
24334
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
24335
+ apiBase: "https://api.planetscale.com",
24336
+ defaultDataClasses: ["customer"],
24337
+ sdks: {
24338
+ npm: ["@planetscale/database"],
24339
+ go: ["github.com/planetscale/planetscale-go"]
24340
+ }
24341
+ },
24342
+ {
24343
+ id: "algolia",
24344
+ name: "Algolia",
24345
+ category: "Search SaaS",
24346
+ hostSuffixes: ["algolia.net", "algolianet.com"],
24347
+ apiBase: "https://algolia.net",
24348
+ defaultDataClasses: ["customer"],
24349
+ sdks: {
24350
+ npm: ["algoliasearch"],
24351
+ pypi: ["algoliasearch"],
24352
+ go: ["github.com/algolia/algoliasearch-client-go"],
24353
+ maven: ["com.algolia"],
24354
+ rubygems: ["algolia"],
24355
+ composer: ["algolia/algoliasearch-client-php"],
24356
+ nuget: ["Algolia.Search"]
24357
+ }
24358
+ },
24359
+ {
24360
+ id: "cloudflare",
24361
+ name: "Cloudflare",
24362
+ category: "CDN / edge",
24363
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
24364
+ apiBase: "https://api.cloudflare.com",
24365
+ defaultDataClasses: ["logs"],
24366
+ sdks: {
24367
+ npm: ["cloudflare"],
24368
+ pypi: ["cloudflare"],
24369
+ go: ["github.com/cloudflare/cloudflare-go"],
24370
+ nuget: ["CloudFlare.Client"]
24371
+ }
24372
+ },
24373
+ {
24374
+ id: "huggingface",
24375
+ name: "Hugging Face",
24376
+ category: "LLM provider",
24377
+ hostSuffixes: ["huggingface.co"],
24378
+ apiBase: "https://api-inference.huggingface.co",
24379
+ defaultDataClasses: ["source"],
24380
+ sdks: {
24381
+ npm: ["@huggingface/inference"],
24382
+ pypi: ["huggingface-hub", "transformers"],
24383
+ rubygems: ["hugging-face"]
24384
+ }
24385
+ },
24386
+ {
24387
+ id: "cohere",
24388
+ name: "Cohere",
24389
+ category: "LLM provider",
24390
+ hostSuffixes: ["cohere.com", "cohere.ai"],
24391
+ apiBase: "https://api.cohere.com",
24392
+ defaultDataClasses: ["pii", "source"],
24393
+ sdks: {
24394
+ npm: ["cohere-ai"],
24395
+ pypi: ["cohere"],
24396
+ go: ["github.com/cohere-ai/cohere-go"]
24397
+ }
24398
+ },
24399
+ {
24400
+ id: "mistral",
24401
+ name: "Mistral AI",
24402
+ category: "LLM provider",
24403
+ hostSuffixes: ["mistral.ai"],
24404
+ apiBase: "https://api.mistral.ai",
24405
+ defaultDataClasses: ["pii", "source"],
24406
+ sdks: {
24407
+ npm: ["@mistralai/mistralai"],
24408
+ pypi: ["mistralai"],
24409
+ go: ["github.com/gage-technologies/mistral-go"]
24410
+ }
24411
+ }
24412
+ ];
24413
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
24414
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
24415
+
24416
+ // ../../packages/detections/src/egress/extract.ts
24417
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
24418
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
24419
+ var SECRET_VALUE = new RegExp(
24420
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
24421
+ "gi"
24422
+ );
24423
+ var AUTH_SCHEME_VALUE = new RegExp(
24424
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
24425
+ "gi"
24426
+ );
24427
+ var WEBHOOK_SECRET_PATHS = [
24428
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
24429
+ {
24430
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
24431
+ prefix: "/api/webhooks/"
24432
+ },
24433
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
24434
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
24435
+ ];
24436
+ function escapeRegExp(literal2) {
24437
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24438
+ }
24439
+ var WEBHOOK_URL = new RegExp(
24440
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
24441
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
24442
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
24443
+ "gi"
24444
+ );
22936
24445
 
22937
24446
  // ../../packages/detections/src/escape-regexp.ts
22938
- function escapeRegExp(value) {
24447
+ function escapeRegExp2(value) {
22939
24448
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22940
24449
  }
22941
24450
 
22942
24451
  // ../../packages/detections/src/matchers/limits.ts
22943
24452
  var MAX_MATCHES_PER_RULE = 1e4;
24453
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22944
24454
 
22945
24455
  // ../../packages/detections/src/matchers/keyword.ts
22946
24456
  var KeywordMatcher2 = class {
@@ -22951,7 +24461,7 @@ var KeywordMatcher2 = class {
22951
24461
  for (const kw of keywords) {
22952
24462
  if (kw.length === 0) continue;
22953
24463
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22954
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
24464
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22955
24465
  let m;
22956
24466
  while ((m = re.exec(text)) !== null) {
22957
24467
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22968,9 +24478,13 @@ var RegexMatcher2 = class {
22968
24478
  if (rule.matcher.type !== "regex") return [];
22969
24479
  const { pattern, flags, captureGroup } = rule.matcher;
22970
24480
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
24481
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22971
24482
  const spans = [];
22972
24483
  let m;
22973
- while ((m = re.exec(text)) !== null) {
24484
+ const maxIterations = scanText2.length + 1;
24485
+ let iterations = 0;
24486
+ while ((m = re.exec(scanText2)) !== null) {
24487
+ if (++iterations > maxIterations) break;
22974
24488
  const group = captureGroup != null ? m[captureGroup] : m[0];
22975
24489
  if (m[0].length === 0) re.lastIndex++;
22976
24490
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23074,7 +24588,7 @@ function isCorroborated(candidate, candidates, text) {
23074
24588
  for (const label of labels) {
23075
24589
  const trimmed = label.trim();
23076
24590
  if (trimmed.length === 0) continue;
23077
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24591
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23078
24592
  if (re.test(haystack)) return true;
23079
24593
  }
23080
24594
  }
@@ -23239,6 +24753,112 @@ var CONFIG_POSTURE_RULES = [
23239
24753
  }
23240
24754
  ];
23241
24755
 
24756
+ // ../../packages/detections/src/security/redos-probe.ts
24757
+ var BUDGET_MS = 100;
24758
+ var EXPONENTIAL_UNITS = [
24759
+ "a",
24760
+ "0",
24761
+ " ",
24762
+ "x",
24763
+ "ab",
24764
+ "a.",
24765
+ "a-",
24766
+ "a_",
24767
+ "a@",
24768
+ "a/",
24769
+ "a:",
24770
+ "a=",
24771
+ "a;",
24772
+ "aA0",
24773
+ " "
24774
+ ];
24775
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24776
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24777
+ );
24778
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24779
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24780
+ );
24781
+ function literalPrefix(pattern) {
24782
+ let prefix = "";
24783
+ let i = 0;
24784
+ if (pattern[i] === "^") i++;
24785
+ while (i < pattern.length) {
24786
+ const c = pattern[i];
24787
+ if (c === void 0) break;
24788
+ if (c === "\\") {
24789
+ const next = pattern[i + 1];
24790
+ if (next === "b" || next === "B") {
24791
+ i += 2;
24792
+ continue;
24793
+ }
24794
+ if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
24795
+ prefix += next;
24796
+ i += 2;
24797
+ continue;
24798
+ }
24799
+ if ("([{.*+?|)]}^$".includes(c)) break;
24800
+ prefix += c;
24801
+ i++;
24802
+ }
24803
+ return prefix;
24804
+ }
24805
+ function fuelChars(pattern) {
24806
+ const fuel = /* @__PURE__ */ new Set();
24807
+ for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
24808
+ const body = m[1];
24809
+ if (body === void 0) continue;
24810
+ const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
24811
+ const rangeStart = range?.[1];
24812
+ if (rangeStart !== void 0) fuel.add(rangeStart);
24813
+ else {
24814
+ const literal2 = body.replace(/\\/g, "")[0];
24815
+ if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
24816
+ }
24817
+ }
24818
+ if (pattern.includes("\\w")) fuel.add("a");
24819
+ if (pattern.includes("\\d")) fuel.add("0");
24820
+ if (pattern.includes("\\s")) fuel.add(" ");
24821
+ if (/(?<!\\)\./.test(pattern)) fuel.add("a");
24822
+ if (fuel.size === 0) fuel.add("a");
24823
+ return [...fuel];
24824
+ }
24825
+ function derivedProbes(pattern) {
24826
+ const prefix = literalPrefix(pattern);
24827
+ const fuel = fuelChars(pattern);
24828
+ const terminators = ["!", "#", "~", "\n"];
24829
+ const probes = [];
24830
+ for (const f of fuel) {
24831
+ for (const term of terminators) {
24832
+ if (term === f) continue;
24833
+ for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
24834
+ }
24835
+ }
24836
+ return probes;
24837
+ }
24838
+ function probesFor(rule) {
24839
+ const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
24840
+ return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
24841
+ }
24842
+ function worstProbeMs(rule) {
24843
+ let ms = 0;
24844
+ let probe = "";
24845
+ for (const text of probesFor(rule)) {
24846
+ const start = performance.now();
24847
+ scan(text, [rule]);
24848
+ const elapsed = performance.now() - start;
24849
+ if (elapsed > ms) {
24850
+ ms = elapsed;
24851
+ probe = text;
24852
+ }
24853
+ if (ms >= BUDGET_MS) break;
24854
+ }
24855
+ return { ms, probe };
24856
+ }
24857
+ function checkRuleTiming(rule) {
24858
+ const { ms, probe } = worstProbeMs(rule);
24859
+ return { safe: ms < BUDGET_MS, worstMs: ms, probe };
24860
+ }
24861
+
23242
24862
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23243
24863
  var auth_jwt_no_verify_default = {
23244
24864
  specVersion: 1,
@@ -25286,7 +26906,7 @@ function ensureBundledPacks() {
25286
26906
  return false;
25287
26907
  }
25288
26908
  }
25289
- function scanText(text) {
26909
+ function scanText(text, ruleVersions) {
25290
26910
  if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
25291
26911
  try {
25292
26912
  const rules = getLoadedRules();
@@ -25298,7 +26918,7 @@ function scanText(text) {
25298
26918
  return {
25299
26919
  ruleId: m.ruleId,
25300
26920
  ruleName: rule?.name ?? m.ruleId,
25301
- ruleVersion: String(rule?.specVersion ?? 1),
26921
+ ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
25302
26922
  category: m.category,
25303
26923
  severity: m.severity,
25304
26924
  span: m.span,
@@ -25313,8 +26933,8 @@ function scanText(text) {
25313
26933
  }
25314
26934
 
25315
26935
  // ../../packages/plugin-sdk/src/repo.ts
25316
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25317
- import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
26936
+ import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
26937
+ import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
25318
26938
  function resolveRepoIdentity(cwd) {
25319
26939
  try {
25320
26940
  const root = findGitRoot(cwd);
@@ -25347,32 +26967,32 @@ function resolveRepoNwo(cwd) {
25347
26967
  function findGitRoot(start) {
25348
26968
  let dir = start;
25349
26969
  for (; ; ) {
25350
- if (existsSync3(join6(dir, ".git"))) return dir;
26970
+ if (existsSync4(join7(dir, ".git"))) return dir;
25351
26971
  const parent = dirname(dir);
25352
26972
  if (parent === dir) return void 0;
25353
26973
  dir = parent;
25354
26974
  }
25355
26975
  }
25356
26976
  function resolveGitContext(root) {
25357
- const dotGit = join6(root, ".git");
26977
+ const dotGit = join7(root, ".git");
25358
26978
  try {
25359
26979
  if (statSync(dotGit).isDirectory()) {
25360
- return { configPath: join6(dotGit, "config"), headRoot: root };
26980
+ return { configPath: join7(dotGit, "config"), headRoot: root };
25361
26981
  }
25362
26982
  } catch {
25363
26983
  return void 0;
25364
26984
  }
25365
26985
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
25366
26986
  if (!target) return void 0;
25367
- const gitdir = isAbsolute(target) ? target : join6(root, target);
25368
- if (existsSync3(join6(gitdir, "config"))) {
25369
- return { configPath: join6(gitdir, "config"), headRoot: root };
26987
+ const gitdir = isAbsolute(target) ? target : join7(root, target);
26988
+ if (existsSync4(join7(gitdir, "config"))) {
26989
+ return { configPath: join7(gitdir, "config"), headRoot: root };
25370
26990
  }
25371
- const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
26991
+ const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
25372
26992
  if (!commonRaw) return void 0;
25373
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
26993
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
25374
26994
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
25375
- return { configPath: join6(commonGitDir, "config"), headRoot };
26995
+ return { configPath: join7(commonGitDir, "config"), headRoot };
25376
26996
  }
25377
26997
  function safeRead(path) {
25378
26998
  try {
@@ -25427,9 +27047,9 @@ function nwoFromUrl(url2) {
25427
27047
  }
25428
27048
 
25429
27049
  // ../../packages/plugin-sdk/src/events.ts
25430
- import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
27050
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
25431
27051
  function contentHashOf(text) {
25432
- return createHash3("sha256").update(text).digest("hex");
27052
+ return createHash4("sha256").update(text).digest("hex");
25433
27053
  }
25434
27054
  function buildIngestEvent(input) {
25435
27055
  return {
@@ -25449,16 +27069,6 @@ function buildIngestEvent(input) {
25449
27069
  };
25450
27070
  }
25451
27071
 
25452
- // ../../packages/plugin-sdk/src/finding-key.ts
25453
- import { createHash as createHash4 } from "crypto";
25454
- function normalizeFilePath(filePath) {
25455
- return filePath.replaceAll("\\", "/");
25456
- }
25457
- function computeFindingKey(input) {
25458
- const normalizedPath = normalizeFilePath(input.filePath);
25459
- return createHash4("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
25460
- }
25461
-
25462
27072
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
25463
27073
  import { arch, hostname as hostname3, platform, release } from "os";
25464
27074
  function resolveInventoryContext(input) {
@@ -25489,13 +27099,17 @@ function resolveInventoryContext(input) {
25489
27099
  }
25490
27100
 
25491
27101
  // ../../packages/plugin-sdk/src/nudge.ts
25492
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25493
- import { join as join8 } from "path";
27102
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
27103
+ import { join as join9 } from "path";
27104
+
27105
+ // ../../packages/plugin-sdk/src/paths.ts
27106
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
27107
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
25494
27108
 
25495
27109
  // ../../packages/plugin-sdk/src/project-files.ts
25496
27110
  var import_ignore = __toESM(require_ignore(), 1);
25497
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25498
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
27111
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27112
+ import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
25499
27113
 
25500
27114
  // ../../packages/plugin-sdk/src/raw-egress.ts
25501
27115
  var RawEgressError = class extends Error {
@@ -25537,6 +27151,59 @@ function safeMaskedMatch(rawMatch) {
25537
27151
  return masked;
25538
27152
  }
25539
27153
 
27154
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
27155
+ var PASS_BUDGET_MS = 2e3;
27156
+ function ruleProbeKey(rule) {
27157
+ if (rule.matcher.type !== "regex") return void 0;
27158
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
27159
+ }
27160
+ function warnQuarantined(rule, worstMs) {
27161
+ const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
27162
+ process.stderr.write(
27163
+ `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
27164
+ `
27165
+ );
27166
+ }
27167
+ async function filterUnsafeRules(rules, gateway, opts) {
27168
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
27169
+ const passStart = performance.now();
27170
+ const safe = [];
27171
+ for (const rule of rules) {
27172
+ const key = ruleProbeKey(rule);
27173
+ if (key === void 0) {
27174
+ safe.push(rule);
27175
+ continue;
27176
+ }
27177
+ let cached2;
27178
+ try {
27179
+ cached2 = await gateway.getRuleProbeVerdict(key);
27180
+ } catch {
27181
+ cached2 = void 0;
27182
+ }
27183
+ if (cached2) {
27184
+ if (cached2.verdict === "safe") safe.push(rule);
27185
+ else warnQuarantined(rule, cached2.worstProbeMs);
27186
+ continue;
27187
+ }
27188
+ if (performance.now() - passStart >= passBudgetMs) {
27189
+ warnQuarantined(rule, void 0);
27190
+ continue;
27191
+ }
27192
+ let isSafe;
27193
+ let worstMs;
27194
+ try {
27195
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
27196
+ } catch {
27197
+ isSafe = false;
27198
+ worstMs = Number.POSITIVE_INFINITY;
27199
+ }
27200
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
27201
+ if (isSafe) safe.push(rule);
27202
+ else warnQuarantined(rule, worstMs);
27203
+ }
27204
+ return safe;
27205
+ }
27206
+
25540
27207
  // ../../packages/plugin-sdk/src/runtime.ts
25541
27208
  import { randomUUID as randomUUID10 } from "crypto";
25542
27209
  var ENFORCEMENT_CEILING_ENABLED = false;
@@ -25579,7 +27246,17 @@ function createPluginRuntime(gateway, settings, opts) {
25579
27246
  categoryActionIndex.set(p.target.category, p.action);
25580
27247
  }
25581
27248
  }
25582
- rules = bundle.rulesComplete ? bundle.rules ?? [] : [...getLoadedRules(), ...bundle.rules ?? []];
27249
+ const bundledProbeKeys = new Set(
27250
+ getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
27251
+ );
27252
+ const incoming = bundle.rules ?? [];
27253
+ const ciVerified = incoming.filter((rule) => {
27254
+ const key = ruleProbeKey(rule);
27255
+ return key !== void 0 && bundledProbeKeys.has(key);
27256
+ });
27257
+ const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
27258
+ const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
27259
+ rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
25583
27260
  bundleExceptions = bundle.exceptions ?? [];
25584
27261
  initialized = true;
25585
27262
  }
@@ -25822,8 +27499,8 @@ function createPluginRuntime(gateway, settings, opts) {
25822
27499
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25823
27500
 
25824
27501
  // ../../packages/plugin-sdk/src/throttle.ts
25825
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25826
- import { join as join10 } from "path";
27502
+ import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27503
+ import { join as join11 } from "path";
25827
27504
 
25828
27505
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
25829
27506
  import { randomUUID as randomUUID11 } from "crypto";
@@ -25854,7 +27531,8 @@ var StandaloneDataGateway = class {
25854
27531
  }
25855
27532
  // The id is minted inside the repository from the natural key — the plugin can't
25856
27533
  // import @akasecurity/persistence to compute it, so the gateway is the boundary that
25857
- // hands the natural key across. INSERT OR IGNORE → idempotent re-reads.
27534
+ // hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
27535
+ // converge a streaming partial/final split (see insertLlmCall).
25858
27536
  recordLlmCall(input) {
25859
27537
  this.db.auditEvents.insertLlmCall(input);
25860
27538
  return Promise.resolve();
@@ -25896,7 +27574,9 @@ var StandaloneDataGateway = class {
25896
27574
  // caller's transaction (Layer 2b). The audit-event id the findings FK into is the
25897
27575
  // SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
25898
27576
  // idempotently. Definitions/classified-data are idempotent upserts; findings are
25899
- // content-addressed INSERT OR IGNORE.
27577
+ // content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
27578
+ // so a re-detection under a bumped rule version repoints the definition FK rather
27579
+ // than no-opping.
25900
27580
  writeToolCall(input) {
25901
27581
  this.db.auditEvents.insertToolCall(input);
25902
27582
  if (input.inspections.length === 0) return;
@@ -25916,7 +27596,7 @@ var StandaloneDataGateway = class {
25916
27596
  });
25917
27597
  const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
25918
27598
  this.db.inspectionFindings.insertFinding({
25919
- id: inspectionFindingId(auditEventId, definitionId, insp.span.start, insp.span.end),
27599
+ id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
25920
27600
  auditEventId,
25921
27601
  inspectionDefinitionId: definitionId,
25922
27602
  classifiedDataId: classifiedDataId2,
@@ -25965,10 +27645,17 @@ var StandaloneDataGateway = class {
25965
27645
  try {
25966
27646
  const snapshot = this.db.installedPacks.installedRuleset();
25967
27647
  if (snapshot.installedPacks === 0) return void 0;
25968
- if (snapshot.enabledPacks === 0) return { rules: [], ruleActions: /* @__PURE__ */ new Map(), complete: true };
27648
+ if (snapshot.enabledPacks === 0) {
27649
+ return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
27650
+ }
25969
27651
  if (snapshot.invalidRules > 0) return void 0;
25970
27652
  if (snapshot.rules.length === 0) return void 0;
25971
- return { rules: snapshot.rules, ruleActions: snapshot.ruleActions, complete: true };
27653
+ return {
27654
+ rules: snapshot.rules,
27655
+ ruleActions: snapshot.ruleActions,
27656
+ ruleVersions: snapshot.ruleVersions,
27657
+ complete: true
27658
+ };
25972
27659
  } catch {
25973
27660
  return void 0;
25974
27661
  }
@@ -25996,6 +27683,7 @@ var StandaloneDataGateway = class {
25996
27683
  policies: [...policies, ...rulePolicies],
25997
27684
  rules: installed ? installed.rules : [],
25998
27685
  ...installed ? { rulesComplete: true } : {},
27686
+ ...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
25999
27687
  ...exceptions !== void 0 ? { exceptions } : {},
26000
27688
  customKeywords,
26001
27689
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -26094,6 +27782,13 @@ var StandaloneDataGateway = class {
26094
27782
  this.db.scanLedger.upsertEntries(entries);
26095
27783
  return Promise.resolve();
26096
27784
  }
27785
+ getRuleProbeVerdict(ruleKey) {
27786
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
27787
+ }
27788
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
27789
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
27790
+ return Promise.resolve();
27791
+ }
26097
27792
  openAtRestKeysForPath(path) {
26098
27793
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
26099
27794
  }
@@ -26104,6 +27799,12 @@ var StandaloneDataGateway = class {
26104
27799
  this.db.resolutions.insertResolution(input);
26105
27800
  return Promise.resolve();
26106
27801
  }
27802
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
27803
+ // enforced by the caller, which already holds the parsed workspace
27804
+ // settings; this class only ever sees `dataDir`, not the settings base.
27805
+ recordProjectEgress(input) {
27806
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
27807
+ }
26107
27808
  close() {
26108
27809
  this.db.close();
26109
27810
  return Promise.resolve();
@@ -26121,11 +27822,11 @@ import { randomUUID as randomUUID12 } from "crypto";
26121
27822
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
26122
27823
 
26123
27824
  // src/history/transcripts.ts
26124
- import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
27825
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
26125
27826
  import { homedir as homedir3 } from "os";
26126
- import { join as join11 } from "path";
27827
+ import { join as join12 } from "path";
26127
27828
  function transcriptsDir(home) {
26128
- return join11(home ?? homedir3(), ".claude", "projects");
27829
+ return join12(home ?? homedir3(), ".claude", "projects");
26129
27830
  }
26130
27831
  function isRecord(value) {
26131
27832
  return typeof value === "object" && value !== null;
@@ -26193,9 +27894,9 @@ function parseTranscriptUsage(jsonl, sinceMs = 0) {
26193
27894
  if (sinceMs > 0 && Date.parse(optString(rec.timestamp) ?? "") < sinceMs) continue;
26194
27895
  if (rec.type === "user") {
26195
27896
  const uuid5 = optString(rec.uuid);
26196
- const promptId = optString(rec.promptId);
26197
- if (uuid5 === void 0 || promptId === void 0) continue;
26198
- out.push({ kind: "user", uuid: uuid5, promptId });
27897
+ const promptId2 = optString(rec.promptId);
27898
+ if (uuid5 === void 0 || promptId2 === void 0) continue;
27899
+ out.push({ kind: "user", uuid: uuid5, promptId: promptId2 });
26199
27900
  continue;
26200
27901
  }
26201
27902
  if (rec.type !== "assistant") continue;
@@ -26368,22 +28069,22 @@ var DAY_MS5 = 24 * 60 * 60 * 1e3;
26368
28069
  function* iterateFileContents(dir, excludeSessionId) {
26369
28070
  let projects;
26370
28071
  try {
26371
- projects = readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
28072
+ projects = readdirSync4(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
26372
28073
  } catch {
26373
28074
  return;
26374
28075
  }
26375
28076
  for (const project of projects) {
26376
- const projectDir = join11(dir, project);
28077
+ const projectDir = join12(dir, project);
26377
28078
  let files;
26378
28079
  try {
26379
- files = readdirSync3(projectDir).filter((name) => name.endsWith(".jsonl"));
28080
+ files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
26380
28081
  } catch {
26381
28082
  continue;
26382
28083
  }
26383
28084
  for (const file2 of files) {
26384
28085
  if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
26385
28086
  continue;
26386
- const filePath = join11(projectDir, file2);
28087
+ const filePath = join12(projectDir, file2);
26387
28088
  let content;
26388
28089
  try {
26389
28090
  content = readFileSync7(filePath, "utf8");
@@ -26504,13 +28205,13 @@ import { createHash as createHash5 } from "crypto";
26504
28205
  import {
26505
28206
  closeSync,
26506
28207
  fstatSync,
26507
- mkdirSync as mkdirSync5,
28208
+ mkdirSync as mkdirSync4,
26508
28209
  openSync,
26509
28210
  readFileSync as readFileSync8,
26510
28211
  readSync,
26511
- writeFileSync as writeFileSync6
28212
+ writeFileSync as writeFileSync5
26512
28213
  } from "fs";
26513
- import { join as join12 } from "path";
28214
+ import { join as join13 } from "path";
26514
28215
 
26515
28216
  // src/history/usage.ts
26516
28217
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
@@ -26561,12 +28262,18 @@ async function reconcileSessionToolCalls(gateway, sessionId, toolCalls, usageRec
26561
28262
  if (toolCalls.length === 0) return 0;
26562
28263
  const promptIdByUuid = /* @__PURE__ */ new Map();
26563
28264
  for (const r of usageRecords) if (r.kind === "user") promptIdByUuid.set(r.uuid, r.promptId);
28265
+ let ruleVersions;
28266
+ try {
28267
+ ruleVersions = (await gateway.getPolicyBundle()).ruleVersions;
28268
+ } catch {
28269
+ ruleVersions = void 0;
28270
+ }
26564
28271
  const inputs = toolCalls.map((tc) => {
26565
28272
  const runKey = (tc.parentUuid !== void 0 ? promptIdByUuid.get(tc.parentUuid) : void 0) ?? opts.seedPromptId;
26566
28273
  const attributes = { tool_name: tc.toolName, tool_use_id: tc.toolUseId };
26567
28274
  let inspections = [];
26568
28275
  if (tc.target !== void 0) {
26569
- const { masked, findings } = scanText(tc.target);
28276
+ const { masked, findings } = scanText(tc.target, ruleVersions);
26570
28277
  if (masked !== "") attributes.target = truncateTarget(masked);
26571
28278
  inspections = findings.map((f) => ({
26572
28279
  ruleId: f.ruleId,