@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.
@@ -491,9 +491,13 @@ var require_ignore = __commonJS({
491
491
  }
492
492
  });
493
493
 
494
+ // ../../packages/plugin-sdk/src/config.ts
495
+ import { existsSync as existsSync3 } from "fs";
496
+ import { join as join6 } from "path";
497
+
494
498
  // ../../packages/persistence/src/database.ts
495
499
  import { randomUUID as randomUUID8 } from "crypto";
496
- import { existsSync, renameSync, rmSync } from "fs";
500
+ import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
497
501
  import { join, sep } from "path";
498
502
  import { DatabaseSync } from "node:sqlite";
499
503
 
@@ -542,6 +546,22 @@ var SQLITE_MIGRATIONS = [
542
546
  {
543
547
  tag: "0010_events_session_expression_index",
544
548
  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"
549
+ },
550
+ {
551
+ tag: "0011_egress_writer",
552
+ 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'
553
+ },
554
+ {
555
+ tag: "0012_handy_the_captain",
556
+ 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`);"
557
+ },
558
+ {
559
+ tag: "0013_legacy_history_backfill_support",
560
+ 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"
561
+ },
562
+ {
563
+ tag: "0014_drop_legacy_events_findings",
564
+ 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"
545
565
  }
546
566
  ];
547
567
 
@@ -15372,7 +15392,12 @@ var FindingFacets = external_exports.object({
15372
15392
  severity: external_exports.array(FindingFacetItem),
15373
15393
  subtype: external_exports.array(FindingFacetItem),
15374
15394
  provider: external_exports.array(FindingFacetItem),
15375
- action: external_exports.array(FindingFacetItem)
15395
+ action: external_exports.array(FindingFacetItem),
15396
+ // Counts by the group's derived status. The SQLite store derives a status
15397
+ // for every instance, so every group lands in a bucket; a status-less
15398
+ // group (possible only for callers whose rows carry no statuses) is
15399
+ // counted under no value.
15400
+ status: external_exports.array(FindingFacetItem)
15376
15401
  }).meta({ id: "FindingFacets" });
15377
15402
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15378
15403
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15382,6 +15407,10 @@ var ListGroupedFindingsQuery = external_exports.object({
15382
15407
  subtype: external_exports.array(external_exports.string()).optional(),
15383
15408
  provider: external_exports.array(FindingProvider).optional(),
15384
15409
  action: external_exports.array(FindingAction).optional(),
15410
+ // Matches a group's DERIVED status (see FindingGroup.status), not its
15411
+ // individual instances' — so a filtered group's Status column always reads
15412
+ // one of the requested values.
15413
+ status: external_exports.array(FindingStatus).optional(),
15385
15414
  q: external_exports.string().optional(),
15386
15415
  // Scope to findings whose event carries this session id (the Activity page's
15387
15416
  // session → findings drilldown). Findings without a session never match.
@@ -15569,6 +15598,33 @@ var ToolCallAttributes = external_exports.object({
15569
15598
  parent_uuid: external_exports.string().optional(),
15570
15599
  run_key: external_exports.string().optional()
15571
15600
  }).catchall(external_exports.unknown());
15601
+ var CaptureAttributes = external_exports.object({
15602
+ // The harness/tool that produced the capture (`claude-code`, `cli`, …). A
15603
+ // column on the legacy `events` table; here it rides the bag because a
15604
+ // capture-typed audit row has no equivalent column of its own.
15605
+ source_tool: external_exports.string().optional(),
15606
+ file_path: external_exports.string().optional(),
15607
+ repo: external_exports.string().optional(),
15608
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
15609
+ // gives a non-file capture a display location ("via Bash") when file_path
15610
+ // is absent. The tool NAME only, never its arguments/output.
15611
+ tool_name: external_exports.string().optional(),
15612
+ // Presence-only provenance flag: set when the file is excluded by the
15613
+ // repo's .gitignore. Omitted (not false) for tracked files.
15614
+ gitignored: external_exports.boolean().optional(),
15615
+ // Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
15616
+ // reading from disk), never a partial fragment (a hook-captured edit).
15617
+ whole_file: external_exports.boolean().optional(),
15618
+ // Distributed-tracing correlation: `correlation_id` ties the capture back to
15619
+ // the request that produced it; `trace_id` is the originating span's W3C
15620
+ // trace id when telemetry is enabled.
15621
+ correlation_id: external_exports.uuid().optional(),
15622
+ trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
15623
+ // Ids of the detection exceptions that downgraded findings in this capture
15624
+ // to 'allow' — the enforcement audit trail's link back to the grant that
15625
+ // authorized the bypass.
15626
+ exception_ids: external_exports.array(external_exports.guid()).optional()
15627
+ }).catchall(external_exports.unknown());
15572
15628
  var ToolCallInspection = external_exports.object({
15573
15629
  ruleId: external_exports.string().min(1),
15574
15630
  ruleName: external_exports.string(),
@@ -15655,7 +15711,18 @@ var InspectionFindingInput = external_exports.object({
15655
15711
  span: Span,
15656
15712
  maskedMatch: external_exports.string(),
15657
15713
  actionTaken: ActionTaken,
15658
- confidence: external_exports.number().min(0).max(1)
15714
+ confidence: external_exports.number().min(0).max(1),
15715
+ // Stable, content-addressed key correlating this finding across re-detections
15716
+ // — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
15717
+ // its unique index). Optional: only an at-rest/re-scannable finding carries
15718
+ // one; an in-flight capture (prompt/response) has nothing to re-detect
15719
+ // against and leaves it unset, so every insert is a fresh row.
15720
+ findingKey: external_exports.string().optional(),
15721
+ // The ORIGINAL detection time, preserved across a later re-detection of the
15722
+ // same findingKey — mirrors the legacy `findings.first_detected_at`.
15723
+ // Optional: when omitted, the writer derives it from the referenced audit
15724
+ // event's startedAt on first insert (see SqliteInspectionFindingsRepository).
15725
+ firstDetectedAt: external_exports.iso.datetime().optional()
15659
15726
  });
15660
15727
  var InventoryContext = external_exports.object({
15661
15728
  host: InventoryInput.optional(),
@@ -15857,6 +15924,7 @@ var ActivityOverviewResponse = external_exports.object({
15857
15924
 
15858
15925
  // ../../packages/schema/src/zod/event.ts
15859
15926
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15927
+ var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15860
15928
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15861
15929
  var EventMetadata = external_exports.object({
15862
15930
  sessionId: external_exports.string().optional(),
@@ -16197,6 +16265,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16197
16265
 
16198
16266
  // ../../packages/schema/src/zod/rule.ts
16199
16267
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16268
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16200
16269
  var KeywordMatcher = external_exports.object({
16201
16270
  type: external_exports.literal("keyword"),
16202
16271
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16221,9 +16290,10 @@ function matchesEmptyString(pattern, flags) {
16221
16290
  return false;
16222
16291
  }
16223
16292
  }
16293
+ var MAX_PATTERN_LENGTH = 2e3;
16224
16294
  var RegexMatcher = external_exports.object({
16225
16295
  type: external_exports.literal("regex"),
16226
- pattern: external_exports.string(),
16296
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16227
16297
  flags: external_exports.string().default("gi"),
16228
16298
  captureGroup: external_exports.number().int().nonnegative().optional()
16229
16299
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16344,6 +16414,12 @@ var PolicyBundle = external_exports.object({
16344
16414
  // on-disk caches — that omit the field still parse; consumers read
16345
16415
  // `bundle.exceptions ?? []`.
16346
16416
  exceptions: external_exports.array(ExceptionBundleEntry).optional(),
16417
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
16418
+ // from a versioned installed pack. Optional so older backends — and older
16419
+ // on-disk caches — that omit the field still parse; consumers fall back to
16420
+ // the rule's own spec version. NOT the bundle version above — see
16421
+ // installedRuleset's ruleVersions for the source of truth.
16422
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
16347
16423
  customKeywords: external_exports.array(external_exports.string()),
16348
16424
  fetchedAt: external_exports.iso.datetime()
16349
16425
  }).meta({ id: "PolicyBundle" });
@@ -16790,6 +16866,212 @@ function buildDetectionsList(summaries, query) {
16790
16866
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16791
16867
  }
16792
16868
 
16869
+ // ../../packages/schema/src/zod/shares.ts
16870
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16871
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16872
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16873
+ var DATA_CLASS_ORDER = DataClass.options;
16874
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16875
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16876
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16877
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16878
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16879
+ var ReviewInfo = external_exports.object({
16880
+ needsReview: external_exports.boolean(),
16881
+ reasons: external_exports.array(ReviewReason)
16882
+ }).meta({ id: "ReviewInfo" });
16883
+ var DestinationNetwork = external_exports.object({
16884
+ port: external_exports.number().int().nullable(),
16885
+ geo: external_exports.string().nullable(),
16886
+ ptr: external_exports.string().nullable()
16887
+ }).meta({ id: "DestinationNetwork" });
16888
+ var EndpointSummary = external_exports.object({
16889
+ id: external_exports.string(),
16890
+ method: HttpMethod,
16891
+ transport: Transport,
16892
+ url: external_exports.string(),
16893
+ template: external_exports.boolean(),
16894
+ dataClass: DataClass,
16895
+ lastSeen: external_exports.iso.datetime(),
16896
+ callSiteCount: external_exports.number().int().nonnegative()
16897
+ }).meta({ id: "EndpointSummary" });
16898
+ var CallSite = external_exports.object({
16899
+ id: external_exports.string(),
16900
+ project: external_exports.string(),
16901
+ file: external_exports.string(),
16902
+ line: external_exports.number().int().nonnegative(),
16903
+ snippet: external_exports.string(),
16904
+ dynamic: external_exports.boolean(),
16905
+ vendored: external_exports.boolean(),
16906
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16907
+ projectId: external_exports.string().nullable()
16908
+ }).meta({ id: "CallSite" });
16909
+ var EndpointWithSites = EndpointSummary.extend({
16910
+ sites: external_exports.array(CallSite)
16911
+ }).meta({ id: "EndpointWithSites" });
16912
+ var ShareDestinationSummary = external_exports.object({
16913
+ id: external_exports.string(),
16914
+ kind: DestinationKind,
16915
+ name: external_exports.string(),
16916
+ host: external_exports.string(),
16917
+ category: external_exports.string(),
16918
+ trust: ShareTrustLevel,
16919
+ /** Effective state (decision applied over the trust default). */
16920
+ status: EgressStatus,
16921
+ /** True when an egress decision override differs from the trust default. */
16922
+ isCustom: external_exports.boolean(),
16923
+ lastSeen: external_exports.iso.datetime(),
16924
+ endpointCount: external_exports.number().int().nonnegative(),
16925
+ callSiteCount: external_exports.number().int().nonnegative(),
16926
+ transports: external_exports.array(Transport),
16927
+ /** Most-sensitive first. */
16928
+ dataClasses: external_exports.array(DataClass),
16929
+ review: ReviewInfo,
16930
+ /** Non-provider hosts only; null for providers. */
16931
+ network: DestinationNetwork.nullable(),
16932
+ /** Embedded for inline expansion — no call sites here. */
16933
+ endpoints: external_exports.array(EndpointSummary)
16934
+ }).meta({ id: "ShareDestinationSummary" });
16935
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16936
+ endpointCount: true,
16937
+ callSiteCount: true,
16938
+ endpoints: true
16939
+ }).extend({
16940
+ /** Ownership/geo rationale; null for providers. */
16941
+ note: external_exports.string().nullable(),
16942
+ endpoints: external_exports.array(EndpointWithSites)
16943
+ }).meta({ id: "ShareDestinationDetail" });
16944
+ var ReviewDestination = external_exports.object({
16945
+ id: external_exports.string(),
16946
+ kind: DestinationKind,
16947
+ name: external_exports.string(),
16948
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16949
+ host: external_exports.string(),
16950
+ trust: ShareTrustLevel,
16951
+ status: EgressStatus,
16952
+ review: ReviewInfo,
16953
+ topDataClass: DataClass,
16954
+ callSiteCount: external_exports.number().int().nonnegative(),
16955
+ lastSeen: external_exports.iso.datetime()
16956
+ }).meta({ id: "ReviewDestination" });
16957
+ var ShareDestinationGroup = external_exports.object({
16958
+ kind: DestinationKind,
16959
+ total: external_exports.number().int().nonnegative(),
16960
+ items: external_exports.array(ShareDestinationSummary)
16961
+ }).meta({ id: "ShareDestinationGroup" });
16962
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16963
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16964
+ var SharesStats = external_exports.object({
16965
+ destinations: external_exports.number().int().nonnegative(),
16966
+ endpoints: external_exports.number().int().nonnegative(),
16967
+ callSites: external_exports.number().int().nonnegative(),
16968
+ needsReview: external_exports.number().int().nonnegative(),
16969
+ insecure: external_exports.number().int().nonnegative(),
16970
+ byKind: external_exports.object({
16971
+ provider: external_exports.number().int().nonnegative(),
16972
+ internal: external_exports.number().int().nonnegative(),
16973
+ external: external_exports.number().int().nonnegative(),
16974
+ ip: external_exports.number().int().nonnegative()
16975
+ }),
16976
+ byTrust: external_exports.object({
16977
+ recognized: external_exports.number().int().nonnegative(),
16978
+ internal: external_exports.number().int().nonnegative(),
16979
+ unverified: external_exports.number().int().nonnegative(),
16980
+ ip: external_exports.number().int().nonnegative()
16981
+ })
16982
+ }).meta({ id: "SharesStats" });
16983
+ var SetEgressDecisionBody = external_exports.object({
16984
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16985
+ decision: EgressDecision.nullable()
16986
+ }).meta({ id: "SetEgressDecisionBody" });
16987
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16988
+ var ListShareDestinationsQuery = external_exports.object({
16989
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16990
+ q: external_exports.string().optional(),
16991
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16992
+ kind: external_exports.array(DestinationKind).optional(),
16993
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16994
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16995
+ /**
16996
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16997
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16998
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16999
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17000
+ */
17001
+ review: external_exports.stringbool().default(false)
17002
+ });
17003
+ var ExportSharesQuery = external_exports.object({
17004
+ format: external_exports.enum(["csv", "json"]).default("csv"),
17005
+ q: external_exports.string().optional(),
17006
+ kind: external_exports.array(DestinationKind).optional()
17007
+ });
17008
+
17009
+ // ../../packages/schema/src/zod/egress-extraction.ts
17010
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
17011
+ var ProviderRegistryEntry = external_exports.object({
17012
+ id: external_exports.string(),
17013
+ name: external_exports.string(),
17014
+ category: external_exports.string(),
17015
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
17016
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
17017
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
17018
+ apiBase: external_exports.string(),
17019
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
17020
+ defaultDataClasses: external_exports.array(DataClass).min(1),
17021
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
17022
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
17023
+ }).meta({ id: "ProviderRegistryEntry" });
17024
+ var EgressCallSiteHit = external_exports.object({
17025
+ file: external_exports.string(),
17026
+ line: external_exports.number().int().positive(),
17027
+ snippet: external_exports.string(),
17028
+ dynamic: external_exports.boolean(),
17029
+ vendored: external_exports.boolean()
17030
+ }).meta({ id: "EgressCallSiteHit" });
17031
+ var ResolvedEgressHit = external_exports.object({
17032
+ host: external_exports.string(),
17033
+ kind: DestinationKind,
17034
+ name: external_exports.string(),
17035
+ category: external_exports.string(),
17036
+ trust: ShareTrustLevel,
17037
+ network: DestinationNetwork.nullable(),
17038
+ method: HttpMethod,
17039
+ transport: Transport,
17040
+ url: external_exports.string(),
17041
+ template: external_exports.boolean(),
17042
+ dataClass: DataClass,
17043
+ site: EgressCallSiteHit
17044
+ }).meta({ id: "ResolvedEgressHit" });
17045
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
17046
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
17047
+ external_exports.object({
17048
+ mode: external_exports.literal("ledger"),
17049
+ scannedFiles: external_exports.array(external_exports.string()),
17050
+ deletedFiles: external_exports.array(external_exports.string())
17051
+ })
17052
+ ]).meta({ id: "EgressReconcile" });
17053
+ var RecordProjectEgressInput = external_exports.object({
17054
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
17055
+ projectKey: external_exports.string().min(1),
17056
+ /** Display name only — never keys reconciliation. */
17057
+ project: external_exports.string(),
17058
+ projectId: external_exports.string().nullable(),
17059
+ reconcile: EgressReconcile,
17060
+ hits: external_exports.array(ResolvedEgressHit)
17061
+ }).meta({ id: "RecordProjectEgressInput" });
17062
+ var EgressWriteSummary = external_exports.object({
17063
+ destinations: external_exports.number().int().nonnegative(),
17064
+ endpoints: external_exports.number().int().nonnegative(),
17065
+ callSites: external_exports.number().int().nonnegative(),
17066
+ truncated: external_exports.boolean(),
17067
+ /**
17068
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17069
+ * ledger-keeping caller must withhold their ledger entries and read them
17070
+ * again next scan.
17071
+ */
17072
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17073
+ }).meta({ id: "EgressWriteSummary" });
17074
+
16793
17075
  // ../../packages/schema/src/zod/findings-group-build.ts
16794
17076
  function toApiAction(dbVal) {
16795
17077
  const map2 = {
@@ -16937,6 +17219,15 @@ function groupActions(g) {
16937
17219
  actionsCache.set(g, actions);
16938
17220
  return actions;
16939
17221
  }
17222
+ function countInstancesByStatus(statusInputs, statuses) {
17223
+ const statusSet = new Set(statuses);
17224
+ let sum = 0;
17225
+ for (const input of statusInputs) {
17226
+ if (input.count === void 0) return null;
17227
+ if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
17228
+ }
17229
+ return sum;
17230
+ }
16940
17231
  function applyFindingFilters(groups, opts) {
16941
17232
  let filtered = groups;
16942
17233
  if (opts.severity && opts.severity.length > 0) {
@@ -16955,6 +17246,10 @@ function applyFindingFilters(groups, opts) {
16955
17246
  const subtypeSet = new Set(opts.subtype);
16956
17247
  filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
16957
17248
  }
17249
+ if (opts.statuses && opts.statuses.length > 0) {
17250
+ const statusSet = new Set(opts.statuses);
17251
+ filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
17252
+ }
16958
17253
  if (opts.q) {
16959
17254
  const q = opts.q.toLowerCase();
16960
17255
  filtered = filtered.filter((g) => groupHaystack(g).includes(q));
@@ -16976,6 +17271,7 @@ function computeFindingFacets(allGroups, opts) {
16976
17271
  const forSeverity = applyFindingFilters(allGroups, {
16977
17272
  providers: opts.providers,
16978
17273
  actions: opts.actions,
17274
+ statuses: opts.statuses,
16979
17275
  q: opts.q,
16980
17276
  subtype: opts.subtype
16981
17277
  });
@@ -16985,6 +17281,7 @@ function computeFindingFacets(allGroups, opts) {
16985
17281
  }
16986
17282
  const forProvider = applyFindingFilters(allGroups, {
16987
17283
  actions: opts.actions,
17284
+ statuses: opts.statuses,
16988
17285
  q: opts.q,
16989
17286
  subtype: opts.subtype,
16990
17287
  severity: opts.severity
@@ -16995,6 +17292,7 @@ function computeFindingFacets(allGroups, opts) {
16995
17292
  }
16996
17293
  const forAction = applyFindingFilters(allGroups, {
16997
17294
  providers: opts.providers,
17295
+ statuses: opts.statuses,
16998
17296
  q: opts.q,
16999
17297
  subtype: opts.subtype,
17000
17298
  severity: opts.severity
@@ -17006,17 +17304,30 @@ function computeFindingFacets(allGroups, opts) {
17006
17304
  const forSubtype = applyFindingFilters(allGroups, {
17007
17305
  providers: opts.providers,
17008
17306
  actions: opts.actions,
17307
+ statuses: opts.statuses,
17009
17308
  q: opts.q,
17010
17309
  severity: opts.severity
17011
17310
  });
17012
17311
  const subtypeMap = /* @__PURE__ */ new Map();
17013
17312
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
17313
+ const forStatus = applyFindingFilters(allGroups, {
17314
+ providers: opts.providers,
17315
+ actions: opts.actions,
17316
+ q: opts.q,
17317
+ subtype: opts.subtype,
17318
+ severity: opts.severity
17319
+ });
17320
+ const statusMap = /* @__PURE__ */ new Map();
17321
+ for (const g of forStatus) {
17322
+ if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17323
+ }
17014
17324
  const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17015
17325
  return {
17016
17326
  severity: toItems(severityMap),
17017
17327
  provider: toItems(providerMap),
17018
17328
  action: toItems(actionMap),
17019
- subtype: toItems(subtypeMap)
17329
+ subtype: toItems(subtypeMap),
17330
+ status: toItems(statusMap)
17020
17331
  };
17021
17332
  }
17022
17333
 
@@ -17051,10 +17362,14 @@ var PatchInstalledPackRequest = external_exports.object({
17051
17362
  }).meta({ id: "PatchInstalledPackRequest" });
17052
17363
 
17053
17364
  // ../../packages/schema/src/zod/local.ts
17054
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17365
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17055
17366
  var RunMode = external_exports.enum(["standalone"]);
17056
17367
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17057
17368
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
17369
+ var ModelJudgeConsent = external_exports.object({
17370
+ acknowledgedAt: external_exports.iso.datetime(),
17371
+ payloadVersion: external_exports.number().int().positive()
17372
+ });
17058
17373
  var WorkspaceSettings = external_exports.object({
17059
17374
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
17060
17375
  // Settings files written by earlier releases may carry the retired 'attached'
@@ -17066,38 +17381,20 @@ var WorkspaceSettings = external_exports.object({
17066
17381
  policy: SimpleDetectionPolicy.default("redact"),
17067
17382
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17068
17383
  historicalAccess: HistoricalAccess.default("session-only"),
17384
+ // In-place egress extraction on the scan paths; disable to stop all Data
17385
+ // Shares writes.
17386
+ dataSharesInPlace: external_exports.boolean().default(true),
17069
17387
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17070
- onboardedAt: external_exports.iso.datetime().optional()
17388
+ onboardedAt: external_exports.iso.datetime().optional(),
17389
+ // Records that the user consented to sending findings to the model API for
17390
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
17391
+ // Absent until granted; a stale payloadVersion means the consent no longer
17392
+ // covers the current payload and must be re-granted.
17393
+ modelJudgeConsent: ModelJudgeConsent.optional()
17071
17394
  });
17072
17395
  function defaultWorkspaceSettings() {
17073
17396
  return WorkspaceSettings.parse({});
17074
17397
  }
17075
- function toEventRow(event) {
17076
- return {
17077
- id: event.id,
17078
- sourceTool: event.sourceTool,
17079
- kind: event.kind,
17080
- occurredAt: isoToEpochMillis(event.occurredAt),
17081
- contentHash: event.contentHash,
17082
- content: event.content,
17083
- metadata: event.metadata ? JSON.stringify(event.metadata) : null
17084
- };
17085
- }
17086
- function toFindingRow(finding) {
17087
- return {
17088
- id: finding.id,
17089
- eventId: finding.eventId,
17090
- ruleId: finding.ruleId,
17091
- category: finding.category,
17092
- severity: finding.severity,
17093
- spanStart: finding.span.start,
17094
- spanEnd: finding.span.end,
17095
- maskedMatch: finding.maskedMatch,
17096
- actionTaken: finding.actionTaken,
17097
- confidence: finding.confidence,
17098
- findingKey: finding.findingKey ?? null
17099
- };
17100
- }
17101
17398
  function toInventoryRow(input, id, now) {
17102
17399
  return {
17103
17400
  id,
@@ -17167,7 +17464,42 @@ function toInspectionFindingRow(input) {
17167
17464
  spanEnd: input.span.end,
17168
17465
  maskedMatch: input.maskedMatch,
17169
17466
  actionTaken: input.actionTaken,
17170
- confidence: input.confidence
17467
+ confidence: input.confidence,
17468
+ findingKey: input.findingKey ?? null,
17469
+ firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
17470
+ };
17471
+ }
17472
+ function toCaptureAttributes(event) {
17473
+ const metadata = event.metadata;
17474
+ return {
17475
+ source_tool: event.sourceTool,
17476
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
17477
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
17478
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
17479
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
17480
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
17481
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
17482
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
17483
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
17484
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
17485
+ // has ever populated either), but every legacy metadata key still rides
17486
+ // the bag rather than being silently dropped — CaptureAttributes'
17487
+ // `.catchall(z.unknown())` carries the long tail.
17488
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
17489
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
17490
+ };
17491
+ }
17492
+ function captureDefinitionVersion(finding) {
17493
+ return `capture/${finding.category}/${finding.severity}`;
17494
+ }
17495
+ function toCaptureDefinitionInput(finding) {
17496
+ return {
17497
+ ruleId: finding.ruleId,
17498
+ version: captureDefinitionVersion(finding),
17499
+ name: finding.ruleId,
17500
+ category: finding.category,
17501
+ severity: finding.severity,
17502
+ definition: JSON.stringify({ ruleId: finding.ruleId })
17171
17503
  };
17172
17504
  }
17173
17505
 
@@ -17549,145 +17881,6 @@ var SetupHandoffOffer = external_exports.object({
17549
17881
  path: ["liveKeys"]
17550
17882
  });
17551
17883
 
17552
- // ../../packages/schema/src/zod/shares.ts
17553
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17554
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17555
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17556
- var DATA_CLASS_ORDER = DataClass.options;
17557
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17558
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17559
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17560
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17561
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17562
- var ReviewInfo = external_exports.object({
17563
- needsReview: external_exports.boolean(),
17564
- reasons: external_exports.array(ReviewReason)
17565
- }).meta({ id: "ReviewInfo" });
17566
- var DestinationNetwork = external_exports.object({
17567
- port: external_exports.number().int().nullable(),
17568
- geo: external_exports.string().nullable(),
17569
- ptr: external_exports.string().nullable()
17570
- }).meta({ id: "DestinationNetwork" });
17571
- var EndpointSummary = external_exports.object({
17572
- id: external_exports.string(),
17573
- method: HttpMethod,
17574
- transport: Transport,
17575
- url: external_exports.string(),
17576
- template: external_exports.boolean(),
17577
- dataClass: DataClass,
17578
- lastSeen: external_exports.iso.datetime(),
17579
- callSiteCount: external_exports.number().int().nonnegative()
17580
- }).meta({ id: "EndpointSummary" });
17581
- var CallSite = external_exports.object({
17582
- id: external_exports.string(),
17583
- project: external_exports.string(),
17584
- file: external_exports.string(),
17585
- line: external_exports.number().int().nonnegative(),
17586
- snippet: external_exports.string(),
17587
- dynamic: external_exports.boolean(),
17588
- vendored: external_exports.boolean(),
17589
- /** Deep-link to the Inventory project, when the repo is governed there. */
17590
- projectId: external_exports.string().nullable()
17591
- }).meta({ id: "CallSite" });
17592
- var EndpointWithSites = EndpointSummary.extend({
17593
- sites: external_exports.array(CallSite)
17594
- }).meta({ id: "EndpointWithSites" });
17595
- var ShareDestinationSummary = external_exports.object({
17596
- id: external_exports.string(),
17597
- kind: DestinationKind,
17598
- name: external_exports.string(),
17599
- host: external_exports.string(),
17600
- category: external_exports.string(),
17601
- trust: ShareTrustLevel,
17602
- /** Effective state (decision applied over the trust default). */
17603
- status: EgressStatus,
17604
- /** True when an egress decision override differs from the trust default. */
17605
- isCustom: external_exports.boolean(),
17606
- lastSeen: external_exports.iso.datetime(),
17607
- endpointCount: external_exports.number().int().nonnegative(),
17608
- callSiteCount: external_exports.number().int().nonnegative(),
17609
- transports: external_exports.array(Transport),
17610
- /** Most-sensitive first. */
17611
- dataClasses: external_exports.array(DataClass),
17612
- review: ReviewInfo,
17613
- /** Non-provider hosts only; null for providers. */
17614
- network: DestinationNetwork.nullable(),
17615
- /** Embedded for inline expansion — no call sites here. */
17616
- endpoints: external_exports.array(EndpointSummary)
17617
- }).meta({ id: "ShareDestinationSummary" });
17618
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17619
- endpointCount: true,
17620
- callSiteCount: true,
17621
- endpoints: true
17622
- }).extend({
17623
- /** Ownership/geo rationale; null for providers. */
17624
- note: external_exports.string().nullable(),
17625
- endpoints: external_exports.array(EndpointWithSites)
17626
- }).meta({ id: "ShareDestinationDetail" });
17627
- var ReviewDestination = external_exports.object({
17628
- id: external_exports.string(),
17629
- kind: DestinationKind,
17630
- name: external_exports.string(),
17631
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17632
- host: external_exports.string(),
17633
- trust: ShareTrustLevel,
17634
- status: EgressStatus,
17635
- review: ReviewInfo,
17636
- topDataClass: DataClass,
17637
- callSiteCount: external_exports.number().int().nonnegative(),
17638
- lastSeen: external_exports.iso.datetime()
17639
- }).meta({ id: "ReviewDestination" });
17640
- var ShareDestinationGroup = external_exports.object({
17641
- kind: DestinationKind,
17642
- total: external_exports.number().int().nonnegative(),
17643
- items: external_exports.array(ShareDestinationSummary)
17644
- }).meta({ id: "ShareDestinationGroup" });
17645
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17646
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17647
- var SharesStats = external_exports.object({
17648
- destinations: external_exports.number().int().nonnegative(),
17649
- endpoints: external_exports.number().int().nonnegative(),
17650
- callSites: external_exports.number().int().nonnegative(),
17651
- needsReview: external_exports.number().int().nonnegative(),
17652
- insecure: external_exports.number().int().nonnegative(),
17653
- byKind: external_exports.object({
17654
- provider: external_exports.number().int().nonnegative(),
17655
- internal: external_exports.number().int().nonnegative(),
17656
- ip: external_exports.number().int().nonnegative()
17657
- }),
17658
- byTrust: external_exports.object({
17659
- recognized: external_exports.number().int().nonnegative(),
17660
- internal: external_exports.number().int().nonnegative(),
17661
- unverified: external_exports.number().int().nonnegative(),
17662
- ip: external_exports.number().int().nonnegative()
17663
- })
17664
- }).meta({ id: "SharesStats" });
17665
- var SetEgressDecisionBody = external_exports.object({
17666
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17667
- decision: EgressDecision.nullable()
17668
- }).meta({ id: "SetEgressDecisionBody" });
17669
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17670
- var ListShareDestinationsQuery = external_exports.object({
17671
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17672
- q: external_exports.string().optional(),
17673
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17674
- kind: external_exports.array(DestinationKind).optional(),
17675
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17676
- groupBy: external_exports.enum(["destination"]).default("destination"),
17677
- /**
17678
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17679
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17680
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17681
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17682
- */
17683
- review: external_exports.stringbool().default(false)
17684
- });
17685
- var ExportSharesQuery = external_exports.object({
17686
- format: external_exports.enum(["csv", "json"]).default("csv"),
17687
- q: external_exports.string().optional(),
17688
- kind: external_exports.array(DestinationKind).optional()
17689
- });
17690
-
17691
17884
  // ../../packages/schema/src/zod/shares-access.ts
17692
17885
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17693
17886
  function trustDefaultStatus(trust) {
@@ -17707,7 +17900,7 @@ function deriveReviewReasons(trust, transports) {
17707
17900
  const reasons = [];
17708
17901
  if (trust === "ip") reasons.push("raw_ip");
17709
17902
  if (trust === "unverified") reasons.push("unverified_domain");
17710
- if (transports.includes("http")) reasons.push("plaintext_transport");
17903
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17711
17904
  return reasons;
17712
17905
  }
17713
17906
  function buildReviewInfo(trust, transports) {
@@ -17734,6 +17927,48 @@ function reviewSeverityRank(reasons) {
17734
17927
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17735
17928
  }
17736
17929
 
17930
+ // ../../packages/persistence/src/ids.ts
17931
+ import { createHash } from "crypto";
17932
+ function sha256Hex(input) {
17933
+ return createHash("sha256").update(input).digest("hex");
17934
+ }
17935
+ function inventoryId(objectType, identityKey) {
17936
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
17937
+ }
17938
+ function sourceProjectId(url2) {
17939
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
17940
+ }
17941
+ function classifiedDataId(cls) {
17942
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
17943
+ }
17944
+ function inspectionDefinitionId(ruleId, version2) {
17945
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
17946
+ }
17947
+ function llmCallId(sessionId, messageId) {
17948
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
17949
+ }
17950
+ function toolCallId(sessionId, toolUseId) {
17951
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
17952
+ }
17953
+ function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
17954
+ return sha256Hex(
17955
+ canonicalIdentity([
17956
+ "inspection_finding",
17957
+ auditEventId,
17958
+ ruleId,
17959
+ String(spanStart),
17960
+ String(spanEnd)
17961
+ ])
17962
+ );
17963
+ }
17964
+ var NO_SESSION = "no_session";
17965
+ var NO_PATH = "no_path";
17966
+ function captureId(sessionId, contentHash, filePath = null) {
17967
+ return sha256Hex(
17968
+ canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
17969
+ );
17970
+ }
17971
+
17737
17972
  // ../../packages/persistence/src/internal/sql-text.ts
17738
17973
  function escapeLikePattern(s) {
17739
17974
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -17830,39 +18065,98 @@ function evidenceExists(db, object2) {
17830
18065
  return schemaObjectExists(db, "table", object2.name);
17831
18066
  }
17832
18067
 
17833
- // ../../packages/persistence/src/ids.ts
17834
- import { createHash } from "crypto";
17835
- function sha256Hex(input) {
17836
- return createHash("sha256").update(input).digest("hex");
18068
+ // ../../packages/persistence/src/internal/rows.ts
18069
+ function allRows(stmt, params) {
18070
+ if (params === void 0) return stmt.all();
18071
+ if (Array.isArray(params)) return stmt.all(...params);
18072
+ return stmt.all(params);
17837
18073
  }
17838
- function inventoryId(objectType, identityKey) {
17839
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18074
+ function getRow(stmt, params) {
18075
+ if (params === void 0) return stmt.get();
18076
+ if (Array.isArray(params)) return stmt.get(...params);
18077
+ return stmt.get(params);
17840
18078
  }
17841
- function sourceProjectId(url2) {
17842
- return sha256Hex(canonicalIdentity(["source_project", url2]));
18079
+ function intToBool(raw) {
18080
+ return raw === 1 || raw === true;
17843
18081
  }
17844
- function classifiedDataId(cls) {
17845
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
18082
+ function boolToInt(b) {
18083
+ return b ? 1 : 0;
17846
18084
  }
17847
- function inspectionDefinitionId(ruleId, version2) {
17848
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18085
+ function bindParams(row) {
18086
+ const out = {};
18087
+ for (const [key, value] of Object.entries(row)) {
18088
+ out[key] = value === void 0 ? null : value;
18089
+ }
18090
+ return out;
17849
18091
  }
17850
- function llmCallId(sessionId, messageId) {
17851
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18092
+ function countScalar(db, sql, params) {
18093
+ return getRow(db.prepare(sql), params)?.n ?? 0;
17852
18094
  }
17853
- function toolCallId(sessionId, toolUseId) {
17854
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18095
+ function countBy(db, sql, params) {
18096
+ const map2 = /* @__PURE__ */ new Map();
18097
+ for (const row of allRows(db.prepare(sql), params)) {
18098
+ map2.set(row.k, row.n);
18099
+ }
18100
+ return map2;
17855
18101
  }
17856
- function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17857
- return sha256Hex(
17858
- canonicalIdentity([
17859
- "inspection_finding",
17860
- auditEventId,
17861
- definitionId,
17862
- String(spanStart),
17863
- String(spanEnd)
17864
- ])
17865
- );
18102
+ function mapRowsTolerant(rows, map2) {
18103
+ const out = [];
18104
+ for (const row of rows) {
18105
+ try {
18106
+ out.push(map2(row));
18107
+ } catch {
18108
+ }
18109
+ }
18110
+ return out;
18111
+ }
18112
+
18113
+ // ../../packages/persistence/src/paths.ts
18114
+ import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18115
+ var DATA_DIR_MODE = 448;
18116
+ var DATA_FILE_MODE = 384;
18117
+ var DB_FILENAME = "aka.db";
18118
+ function chmodBestEffort(path, mode) {
18119
+ try {
18120
+ chmodSync(path, mode);
18121
+ } catch {
18122
+ }
18123
+ }
18124
+ function tightenDir(dir) {
18125
+ chmodBestEffort(dir, DATA_DIR_MODE);
18126
+ }
18127
+ function ensureDataDirSync(dir) {
18128
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18129
+ tightenDir(dir);
18130
+ }
18131
+ function dbSidecars(file2) {
18132
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18133
+ }
18134
+ function tightenFile(file2) {
18135
+ try {
18136
+ if (lstatSync(file2).isSymbolicLink()) return;
18137
+ } catch {
18138
+ }
18139
+ chmodBestEffort(file2, DATA_FILE_MODE);
18140
+ }
18141
+ function tightenPerms(file2) {
18142
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18143
+ }
18144
+ function writeOwnerOnlyFileSync(file2, data) {
18145
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
18146
+ try {
18147
+ rmSync(tmp, { force: true });
18148
+ } catch {
18149
+ }
18150
+ try {
18151
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18152
+ renameSync(tmp, file2);
18153
+ } finally {
18154
+ try {
18155
+ rmSync(tmp, { force: true });
18156
+ } catch {
18157
+ }
18158
+ }
18159
+ tightenFile(file2);
17866
18160
  }
17867
18161
 
17868
18162
  // ../../packages/persistence/src/migrations.ts
@@ -17876,7 +18170,8 @@ function createdIndexName(statement) {
17876
18170
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17877
18171
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17878
18172
  }
17879
- function applyMigrations(db) {
18173
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
18174
+ function applyMigrations(db, file2) {
17880
18175
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17881
18176
  db.exec(
17882
18177
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -17890,6 +18185,7 @@ function applyMigrations(db) {
17890
18185
  );
17891
18186
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
17892
18187
  if (applied.has(migration.tag)) continue;
18188
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
17893
18189
  const evidence = evidenceObjects(migration.sql);
17894
18190
  const present = evidence.filter((o) => evidenceExists(db, o));
17895
18191
  if (present.length > 0 && present.length < evidence.length) {
@@ -17934,13 +18230,54 @@ function applyMigrations(db) {
17934
18230
  if (legacyCount < SQLITE_MIGRATIONS.length) {
17935
18231
  db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
17936
18232
  }
17937
- ensureSyncedAtColumn(db, "events");
17938
18233
  ensureSyncedAtColumn(db, "audit_events");
17939
18234
  ensureScanLedgerTable(db);
17940
18235
  ensureBlockedDetectionsTable(db);
18236
+ ensureRuleProbeCacheTable(db);
17941
18237
  ensureWriteGateTrigger(db);
17942
18238
  ensureTokenUsageColumns(db);
17943
18239
  reconcileSourceProjectIds(db);
18240
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
18241
+ const drained = runLegacyHistoryBackfill(db);
18242
+ if (drained) applyLegacyDropMigration(db, file2);
18243
+ }
18244
+ }
18245
+ function applyLegacyDropMigration(db, file2) {
18246
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
18247
+ if (!migration) return;
18248
+ if (file2) {
18249
+ try {
18250
+ backupBeforeLegacyDrop(db, file2);
18251
+ } catch (error51) {
18252
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
18253
+ return;
18254
+ }
18255
+ }
18256
+ try {
18257
+ withTransaction(
18258
+ db,
18259
+ () => {
18260
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
18261
+ if (alreadyDropped) return;
18262
+ for (const statement of splitStatements(migration.sql)) {
18263
+ db.exec(statement);
18264
+ }
18265
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
18266
+ migration.tag,
18267
+ Date.now()
18268
+ );
18269
+ },
18270
+ "IMMEDIATE"
18271
+ );
18272
+ } catch (error51) {
18273
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
18274
+ }
18275
+ }
18276
+ function backupBeforeLegacyDrop(db, file2) {
18277
+ const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18278
+ db.prepare("VACUUM INTO ?").run(backup);
18279
+ tightenFile(backup);
18280
+ return backup;
17944
18281
  }
17945
18282
  var TOKEN_USAGE_COLUMNS = [
17946
18283
  {
@@ -17969,6 +18306,7 @@ var TOKEN_USAGE_COLUMNS = [
17969
18306
  }
17970
18307
  ];
17971
18308
  function ensureTokenUsageColumns(db) {
18309
+ if (!schemaObjectExists(db, "table", "audit_events")) return;
17972
18310
  const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17973
18311
  for (const column of TOKEN_USAGE_COLUMNS) {
17974
18312
  if (!existing.has(column.name)) {
@@ -18027,11 +18365,186 @@ function reconcileSourceProjectIds(db) {
18027
18365
  repointCallSite.run(canonicalId, row.id);
18028
18366
  deleteLegacy.run(row.id);
18029
18367
  }
18030
- },
18031
- "IMMEDIATE"
18032
- );
18368
+ },
18369
+ "IMMEDIATE"
18370
+ );
18371
+ } catch (error51) {
18372
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18373
+ }
18374
+ }
18375
+ var LEGACY_BACKFILL_BATCH_SIZE = 200;
18376
+ var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
18377
+ function getLegacyCopyWatermark(db, source) {
18378
+ const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
18379
+ return row?.lastRowid ?? 0;
18380
+ }
18381
+ function setLegacyCopyWatermark(db, source, lastRowid) {
18382
+ db.prepare(
18383
+ `INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
18384
+ ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
18385
+ ).run(source, lastRowid);
18386
+ }
18387
+ function drainLegacyTable(db, source, selectStmt, handleRows) {
18388
+ let watermark = getLegacyCopyWatermark(db, source);
18389
+ let processed = 0;
18390
+ while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
18391
+ const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
18392
+ if (rows.length === 0) return true;
18393
+ withTransaction(
18394
+ db,
18395
+ () => {
18396
+ handleRows(rows);
18397
+ watermark = rows[rows.length - 1]?.rowid ?? watermark;
18398
+ setLegacyCopyWatermark(db, source, watermark);
18399
+ },
18400
+ "IMMEDIATE"
18401
+ );
18402
+ processed += rows.length;
18403
+ if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
18404
+ }
18405
+ return false;
18406
+ }
18407
+ function parseLegacyEventMetadata(raw) {
18408
+ if (raw === null) return void 0;
18409
+ try {
18410
+ return JSON.parse(raw);
18411
+ } catch {
18412
+ return void 0;
18413
+ }
18414
+ }
18415
+ function toLegacyAuditAttributesJson(row) {
18416
+ return JSON.stringify(
18417
+ toCaptureAttributes({
18418
+ id: row.id,
18419
+ sourceTool: row.sourceTool,
18420
+ kind: row.kind,
18421
+ occurredAt: new Date(row.occurredAt).toISOString(),
18422
+ contentHash: row.contentHash,
18423
+ content: row.content,
18424
+ metadata: row.metadata
18425
+ })
18426
+ );
18427
+ }
18428
+ function copyLegacyEvents(db) {
18429
+ const selectStmt = db.prepare(
18430
+ `SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
18431
+ content_hash AS contentHash, content, metadata
18432
+ FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
18433
+ );
18434
+ const insertStmt = db.prepare(
18435
+ `INSERT OR IGNORE INTO audit_events
18436
+ (id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
18437
+ VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
18438
+ );
18439
+ const stubRootStmt = db.prepare(
18440
+ `INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
18441
+ );
18442
+ return drainLegacyTable(
18443
+ db,
18444
+ "events",
18445
+ selectStmt,
18446
+ (rows) => {
18447
+ for (const row of rows) {
18448
+ const metadata = parseLegacyEventMetadata(row.metadata);
18449
+ const sessionId = metadata?.sessionId ?? null;
18450
+ if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
18451
+ insertStmt.run(
18452
+ bindParams({
18453
+ id: row.id,
18454
+ parentId: sessionId,
18455
+ rootSessionId: sessionId,
18456
+ eventType: row.kind,
18457
+ startedAt: row.occurredAt,
18458
+ content: row.content,
18459
+ contentHash: row.contentHash,
18460
+ attributes: toLegacyAuditAttributesJson({ ...row, metadata })
18461
+ })
18462
+ );
18463
+ }
18464
+ }
18465
+ );
18466
+ }
18467
+ function copyLegacyFindings(db) {
18468
+ const selectStmt = db.prepare(
18469
+ `SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
18470
+ span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
18471
+ action_taken AS actionTaken, confidence, finding_key AS findingKey,
18472
+ first_detected_at AS firstDetectedAt
18473
+ FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
18474
+ );
18475
+ const definitionStmt = db.prepare(
18476
+ `INSERT OR IGNORE INTO inspection_definitions
18477
+ (id, rule_id, name, category, severity, definition, version)
18478
+ VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
18479
+ );
18480
+ const findingStmt = db.prepare(
18481
+ `INSERT INTO inspection_findings
18482
+ (id, audit_event_id, inspection_definition_id, classified_data_id,
18483
+ span_start, span_end, masked_match, action_taken, confidence,
18484
+ finding_key, first_detected_at)
18485
+ VALUES
18486
+ (:id, :auditEventId, :inspectionDefinitionId, NULL,
18487
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
18488
+ :findingKey, :firstDetectedAt)
18489
+ ON CONFLICT(id) DO NOTHING
18490
+ ON CONFLICT (finding_key) DO UPDATE SET
18491
+ first_detected_at = CASE
18492
+ WHEN first_detected_at IS NULL THEN excluded.first_detected_at
18493
+ WHEN excluded.first_detected_at IS NULL THEN first_detected_at
18494
+ ELSE min(first_detected_at, excluded.first_detected_at)
18495
+ END`
18496
+ );
18497
+ return drainLegacyTable(
18498
+ db,
18499
+ "findings",
18500
+ selectStmt,
18501
+ (rows) => {
18502
+ const definitionIds = /* @__PURE__ */ new Map();
18503
+ for (const row of rows) {
18504
+ const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
18505
+ let definitionId = definitionIds.get(tupleKey);
18506
+ if (definitionId === void 0) {
18507
+ const version2 = `unmigrated/${row.category}/${row.severity}`;
18508
+ definitionId = inspectionDefinitionId(row.ruleId, version2);
18509
+ definitionStmt.run(
18510
+ bindParams({
18511
+ id: definitionId,
18512
+ ruleId: row.ruleId,
18513
+ name: row.ruleId,
18514
+ category: row.category,
18515
+ severity: row.severity,
18516
+ definition: "",
18517
+ version: version2
18518
+ })
18519
+ );
18520
+ definitionIds.set(tupleKey, definitionId);
18521
+ }
18522
+ findingStmt.run(
18523
+ bindParams({
18524
+ id: row.id,
18525
+ auditEventId: row.eventId,
18526
+ inspectionDefinitionId: definitionId,
18527
+ spanStart: row.spanStart,
18528
+ spanEnd: row.spanEnd,
18529
+ maskedMatch: row.maskedMatch,
18530
+ actionTaken: row.actionTaken,
18531
+ confidence: row.confidence,
18532
+ findingKey: row.findingKey,
18533
+ firstDetectedAt: row.firstDetectedAt
18534
+ })
18535
+ );
18536
+ }
18537
+ }
18538
+ );
18539
+ }
18540
+ function runLegacyHistoryBackfill(db) {
18541
+ try {
18542
+ const eventsCaughtUp = copyLegacyEvents(db);
18543
+ if (!eventsCaughtUp) return false;
18544
+ return copyLegacyFindings(db);
18033
18545
  } catch (error51) {
18034
- akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18546
+ akaWarn(`legacy history backfill failed: ${String(error51)}`);
18547
+ return false;
18035
18548
  }
18036
18549
  }
18037
18550
  function isForeignSqliteLineage(db) {
@@ -18039,6 +18552,7 @@ function isForeignSqliteLineage(db) {
18039
18552
  return columnNames(db, "events").includes("tenant_id");
18040
18553
  }
18041
18554
  function ensureSyncedAtColumn(db, table) {
18555
+ if (!schemaObjectExists(db, "table", table)) return;
18042
18556
  if (!columnNames(db, table).includes("synced_at")) {
18043
18557
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
18044
18558
  }
@@ -18059,6 +18573,7 @@ function ensureWriteGateTrigger(db) {
18059
18573
  CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
18060
18574
  )`);
18061
18575
  db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
18576
+ if (!schemaObjectExists(db, "table", "installed_packs")) return;
18062
18577
  db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
18063
18578
  BEFORE UPDATE OF version, name, rules_json ON installed_packs
18064
18579
  WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
@@ -18077,29 +18592,13 @@ function ensureBlockedDetectionsTable(db) {
18077
18592
  blocked_at INTEGER NOT NULL
18078
18593
  )`);
18079
18594
  }
18080
-
18081
- // ../../packages/persistence/src/paths.ts
18082
- import { chmodSync, mkdirSync } from "fs";
18083
- var DATA_DIR_MODE = 448;
18084
- var DATA_FILE_MODE = 384;
18085
- var DB_FILENAME = "aka.db";
18086
- function ensureDataDirSync(dir) {
18087
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18088
- try {
18089
- chmodSync(dir, DATA_DIR_MODE);
18090
- } catch {
18091
- }
18092
- }
18093
- function walSidecars(file2) {
18094
- return [`${file2}-wal`, `${file2}-shm`];
18095
- }
18096
- function tightenPerms(file2) {
18097
- for (const path of [file2, ...walSidecars(file2)]) {
18098
- try {
18099
- chmodSync(path, DATA_FILE_MODE);
18100
- } catch {
18101
- }
18102
- }
18595
+ function ensureRuleProbeCacheTable(db) {
18596
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18597
+ rule_key TEXT PRIMARY KEY,
18598
+ verdict TEXT NOT NULL,
18599
+ worst_probe_ms REAL NOT NULL,
18600
+ checked_at INTEGER NOT NULL
18601
+ )`);
18103
18602
  }
18104
18603
 
18105
18604
  // ../../packages/persistence/src/internal/json.ts
@@ -18121,51 +18620,6 @@ function parseJsonObject(s) {
18121
18620
  return void 0;
18122
18621
  }
18123
18622
 
18124
- // ../../packages/persistence/src/internal/rows.ts
18125
- function allRows(stmt, params) {
18126
- if (params === void 0) return stmt.all();
18127
- if (Array.isArray(params)) return stmt.all(...params);
18128
- return stmt.all(params);
18129
- }
18130
- function getRow(stmt, params) {
18131
- if (params === void 0) return stmt.get();
18132
- if (Array.isArray(params)) return stmt.get(...params);
18133
- return stmt.get(params);
18134
- }
18135
- function intToBool(raw) {
18136
- return raw === 1 || raw === true;
18137
- }
18138
- function boolToInt(b) {
18139
- return b ? 1 : 0;
18140
- }
18141
- function bindParams(row) {
18142
- const out = {};
18143
- for (const [key, value] of Object.entries(row)) {
18144
- out[key] = value === void 0 ? null : value;
18145
- }
18146
- return out;
18147
- }
18148
- function countScalar(db, sql, params) {
18149
- return getRow(db.prepare(sql), params)?.n ?? 0;
18150
- }
18151
- function countBy(db, sql, params) {
18152
- const map2 = /* @__PURE__ */ new Map();
18153
- for (const row of allRows(db.prepare(sql), params)) {
18154
- map2.set(row.k, row.n);
18155
- }
18156
- return map2;
18157
- }
18158
- function mapRowsTolerant(rows, map2) {
18159
- const out = [];
18160
- for (const row of rows) {
18161
- try {
18162
- out.push(map2(row));
18163
- } catch {
18164
- }
18165
- }
18166
- return out;
18167
- }
18168
-
18169
18623
  // ../../packages/persistence/src/repositories/activity.ts
18170
18624
  var DAY_MS = 864e5;
18171
18625
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18782,6 +19236,21 @@ var SqliteAuditEventsRepository = class {
18782
19236
  })
18783
19237
  );
18784
19238
  }
19239
+ // Idempotent stub of a session's structural root. Session-scoped leaves
19240
+ // (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
19241
+ // INSERT OR IGNORE does NOT suppress a foreign-key violation (only
19242
+ // UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
19243
+ // raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
19244
+ // dropping the write under failOpenTransaction. SessionStart's own root write
19245
+ // is itself fail-open and marks "attempted", not "succeeded", so a session
19246
+ // with no root row yet is a real, permanent condition, not a transient race.
19247
+ // The stub carries no dimensions/attributes; an authoritative root
19248
+ // (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
19249
+ // on the id PK, so the stub never shadows real data. This is the single named
19250
+ // home for that FK invariant — call it before writing any session-scoped row.
19251
+ ensureSessionRoot(sessionId, startedAt) {
19252
+ this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
19253
+ }
18785
19254
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18786
19255
  // (which takes a caller-supplied random id), the id here is MINTED internally
18787
19256
  // from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
@@ -19243,8 +19712,14 @@ var SqliteDetectionsRepository = class {
19243
19712
  )
19244
19713
  );
19245
19714
  }
19246
- // Findings whose parent event occurred in the last 30 days and whose rule_id is
19247
- // in the given set. Mirrors the security repo's findings⋈events window join.
19715
+ // Findings whose parent audit event occurred in the last 30 days, is one of
19716
+ // the four capture kinds, and whose definition's rule_id is in the given set.
19717
+ // Mirrors the security repo's inspection_findings⋈audit_events window join.
19718
+ // rule_id lives on inspection_definitions, not the finding row, so the join
19719
+ // chains through it. audit_events also holds structural rows (session, run,
19720
+ // tool_call, llm_call, source_lookup, config_scan) that never had a legacy
19721
+ // events counterpart, so the event_type predicate keeps this count identical
19722
+ // to the old findings⋈events one.
19248
19723
  countFindingsLast30d(ruleIds) {
19249
19724
  if (ruleIds.length === 0) return 0;
19250
19725
  const since = this.now() - 30 * DAY_MS2;
@@ -19252,8 +19727,12 @@ var SqliteDetectionsRepository = class {
19252
19727
  return countScalar(
19253
19728
  this.db,
19254
19729
  `SELECT count(*) AS n
19255
- FROM findings f JOIN events e ON e.id = f.event_id
19256
- WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19730
+ FROM inspection_findings f
19731
+ JOIN audit_events e ON e.id = f.audit_event_id
19732
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19733
+ WHERE e.started_at >= ?
19734
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
19735
+ AND d.rule_id IN (${inClause})`,
19257
19736
  [since, ...ruleIds]
19258
19737
  );
19259
19738
  }
@@ -19263,35 +19742,24 @@ var SqliteDetectionsRepository = class {
19263
19742
  var SqliteEventsRepository = class {
19264
19743
  constructor(db) {
19265
19744
  this.db = db;
19266
- this.insertStmt = db.prepare(
19267
- `INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
19268
- VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
19269
- );
19270
19745
  }
19271
19746
  db;
19272
- insertStmt;
19273
- insertEvent(event) {
19274
- const row = toEventRow(event);
19275
- this.insertStmt.run(
19276
- bindParams({
19277
- id: row.id,
19278
- sourceTool: row.sourceTool,
19279
- kind: row.kind,
19280
- occurredAt: row.occurredAt,
19281
- contentHash: row.contentHash,
19282
- content: row.content,
19283
- metadata: row.metadata
19284
- })
19285
- );
19286
- }
19287
- // Every recorded event's content hash — the historical backfill loads this once
19288
- // to skip transcript messages it has already stored, so re-running the scan
19289
- // never duplicates findings.
19747
+ // Every recorded capture's content hash — the historical backfill loads this
19748
+ // once to skip transcript messages it has already stored, so re-running the
19749
+ // scan never duplicates findings.
19290
19750
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
19291
19751
  // async EventsReadPort contract.
19752
+ //
19753
+ // audit_events also holds structural rows (session, run, tool_call, llm_call,
19754
+ // source_lookup, config_scan) with a NULL content_hash, so the capture-kind
19755
+ // predicate isn't load-bearing here — it documents intent and keeps the scan
19756
+ // index-friendly rather than walking rows that can never match.
19292
19757
  contentHashes() {
19293
19758
  const rows = allRows(
19294
- this.db.prepare("SELECT content_hash FROM events")
19759
+ this.db.prepare(
19760
+ `SELECT content_hash FROM audit_events
19761
+ WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
19762
+ )
19295
19763
  );
19296
19764
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
19297
19765
  }
@@ -19627,17 +20095,20 @@ function parseExceptionRow(row) {
19627
20095
  }
19628
20096
 
19629
20097
  // ../../packages/persistence/src/repositories/resolution-sql.ts
19630
- function latestResolutionStatusSql(findingsAlias) {
20098
+ function latestResolutionColumnSql(column, findingsAlias) {
19631
20099
  return `(
19632
- SELECT fr.status FROM finding_resolution fr
20100
+ SELECT fr.${column} FROM finding_resolution fr
19633
20101
  WHERE fr.finding_key = ${findingsAlias}.finding_key
19634
20102
  ORDER BY fr.created_at DESC, fr.rowid DESC
19635
20103
  LIMIT 1
19636
20104
  )`;
19637
20105
  }
20106
+ function latestResolutionStatusSql(findingsAlias) {
20107
+ return latestResolutionColumnSql("status", findingsAlias);
20108
+ }
19638
20109
  var LATEST_RESOLUTION_BY_KEY_SQL = `(
19639
- SELECT finding_key, status FROM (
19640
- SELECT fr.finding_key, fr.status,
20110
+ SELECT finding_key, status, method, resolved_at FROM (
20111
+ SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
19641
20112
  ROW_NUMBER() OVER (
19642
20113
  PARTITION BY fr.finding_key
19643
20114
  ORDER BY fr.created_at DESC, fr.rowid DESC
@@ -19664,68 +20135,21 @@ var DAY_MS3 = 864e5;
19664
20135
  var SqliteFindingsRepository = class {
19665
20136
  constructor(db) {
19666
20137
  this.db = db;
19667
- this.insertStmt = db.prepare(
19668
- `INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
19669
- VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
19670
- (SELECT occurred_at FROM events WHERE id = :eventId))
19671
- ON CONFLICT (finding_key) DO UPDATE SET
19672
- event_id = excluded.event_id,
19673
- category = excluded.category,
19674
- severity = excluded.severity,
19675
- span_start = excluded.span_start,
19676
- span_end = excluded.span_end,
19677
- masked_match = excluded.masked_match,
19678
- action_taken = excluded.action_taken,
19679
- confidence = excluded.confidence`
19680
- );
19681
- this.sessionDupStmt = db.prepare(
19682
- `SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
19683
- WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
19684
- AND json_extract(e.metadata, '$.sessionId') = :sessionId
19685
- LIMIT 1`
19686
- );
19687
20138
  }
19688
20139
  db;
19689
- insertStmt;
19690
- sessionDupStmt;
19691
- insertFindings(findings, scope = {}) {
19692
- for (const finding of findings) {
19693
- if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
19694
- const row = toFindingRow(finding);
19695
- this.insertStmt.run({
19696
- id: row.id,
19697
- eventId: row.eventId,
19698
- ruleId: row.ruleId,
19699
- category: row.category,
19700
- severity: row.severity,
19701
- spanStart: row.spanStart,
19702
- spanEnd: row.spanEnd,
19703
- maskedMatch: row.maskedMatch,
19704
- actionTaken: row.actionTaken,
19705
- confidence: row.confidence,
19706
- findingKey: row.findingKey ?? null
19707
- });
19708
- }
19709
- }
19710
- // True when an earlier event in the same session already recorded a finding
19711
- // with the same rule and masked value. The current event is inserted before
19712
- // its findings, but carries no findings yet, so this never self-matches.
19713
- isSessionDuplicate(finding, sessionId) {
19714
- const hit = this.sessionDupStmt.get({
19715
- ruleId: finding.ruleId,
19716
- maskedMatch: finding.maskedMatch,
19717
- sessionId
19718
- });
19719
- return hit !== void 0;
19720
- }
19721
20140
  recentFindings(opts) {
19722
20141
  const limit = opts?.limit ?? 50;
19723
20142
  const rows = allRows(
19724
20143
  this.db.prepare(
19725
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19726
- f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19727
- FROM findings f JOIN events e ON e.id = f.event_id
19728
- ORDER BY e.occurred_at DESC, f.rowid DESC
20144
+ `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
20145
+ f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
20146
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20147
+ e.event_type AS kind
20148
+ FROM inspection_findings f
20149
+ JOIN audit_events e ON e.id = f.audit_event_id
20150
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20151
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20152
+ ORDER BY e.started_at DESC, f.rowid DESC
19729
20153
  LIMIT :limit`
19730
20154
  ),
19731
20155
  { limit }
@@ -19747,25 +20171,34 @@ var SqliteFindingsRepository = class {
19747
20171
  );
19748
20172
  }
19749
20173
  /** Live-enforced findings recorded for one session — a bare COUNT over the
19750
- * session-stamped events (served by idx_events_session_id), so the Activity
20174
+ * session-stamped audit_events (served by idx_audit_session), so the Activity
19751
20175
  * page can label its findings link without the grouped pipeline. */
19752
20176
  sessionFindingsCount(sessionId) {
19753
20177
  if (!sessionId) return Promise.resolve(0);
19754
20178
  return Promise.resolve(
19755
20179
  countScalar(
19756
20180
  this.db,
19757
- `SELECT count(*) AS n FROM findings f
19758
- JOIN events e ON e.id = f.event_id
19759
- WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
20181
+ `SELECT count(*) AS n FROM inspection_findings f
20182
+ JOIN audit_events e ON e.id = f.audit_event_id
20183
+ WHERE e.root_session_id = :sessionId
20184
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
19760
20185
  { sessionId }
19761
20186
  )
19762
20187
  );
19763
20188
  }
19764
- /** Per-rule transcript firing tally for one session — reads the OTHER finding
19765
- * store (inspection_findings, keyed to audit_events): every detection the
19766
- * transcript pass recorded, counted per firing rather than per unique value.
19767
- * Rides on session-scoped grouped responses so the findings view can
19768
- * reconcile the Activity page's tally with the deduped groups it lists. */
20189
+ /** Per-rule transcript firing tally for one session — every detection the
20190
+ * transcript-reconciler pass recorded against the session's `tool_call` rows,
20191
+ * counted per firing rather than per unique value. Rides on session-scoped
20192
+ * grouped responses so the findings view can reconcile the Activity page's
20193
+ * tally with the deduped groups it lists.
20194
+ *
20195
+ * `inspection_findings`/`audit_events` are now the SAME physical tables the
20196
+ * rest of this class reads for the live-capture list above (they used to be
20197
+ * a separate store), so this excludes the four capture kinds those rows
20198
+ * already carry — without that exclusion, every live-capture finding in the
20199
+ * session would be tallied here too, double-counting against the grouped
20200
+ * list this response rides alongside. The reconciler attaches its findings
20201
+ * only to `tool_call` rows, which the exclusion leaves untouched. */
19769
20202
  sessionFirings(sessionId) {
19770
20203
  return Object.fromEntries(
19771
20204
  countBy(
@@ -19775,18 +20208,25 @@ var SqliteFindingsRepository = class {
19775
20208
  JOIN audit_events e ON e.id = f.audit_event_id
19776
20209
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19777
20210
  WHERE e.root_session_id = :sessionId
20211
+ AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
19778
20212
  GROUP BY d.rule_id`,
19779
20213
  { sessionId }
19780
20214
  )
19781
20215
  );
19782
20216
  }
19783
20217
  /**
19784
- * Grouped findings for the dashboard — joins findingsevents (repo/file/
19785
- * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19786
- * applies the requested filters, and sorts by severity then recency. Filtering
20218
+ * Grouped findings for the dashboard — joins inspection_findingsaudit_events
20219
+ * ⋈inspection_definitions (repo/file/toolName from the audit event's
20220
+ * attributes bag, rule_id/category/severity from the definition), scoped to
20221
+ * the four capture kinds (audit_events also holds structural/reconciler/scan
20222
+ * rows this list must never surface), groups by ruleId, computes
20223
+ * per-filter-excluded facets, applies the requested filters, and sorts by
20224
+ * severity then recency. Filtering
19787
20225
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19788
20226
  * reflect the full filtered set; `items` is the requested
19789
- * page (default 50); no cursor (nextCursor is always null).
20227
+ * page (default 50); no cursor (nextCursor is always null). Under a `status`
20228
+ * filter, `totals.findings` counts only instances whose derived status was
20229
+ * requested, and each item's instance preview is narrowed the same way.
19790
20230
  *
19791
20231
  * Two reads, neither of which materializes a row per finding:
19792
20232
  * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
@@ -19799,10 +20239,11 @@ var SqliteFindingsRepository = class {
19799
20239
  * rule is ever restated in SQL.
19800
20240
  */
19801
20241
  listGroupedFindings(query) {
19802
- const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
20242
+ const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20243
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
19803
20244
  const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19804
20245
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19805
- predicate: sessionPredicate,
20246
+ predicate,
19806
20247
  params: sessionParams
19807
20248
  });
19808
20249
  const rows = allRows(
@@ -19810,24 +20251,26 @@ var SqliteFindingsRepository = class {
19810
20251
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19811
20252
  occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19812
20253
  FROM (
19813
- SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19814
- f.severity AS severity, f.masked_match AS masked_match,
20254
+ SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20255
+ d.severity AS severity, f.masked_match AS masked_match,
19815
20256
  f.action_taken AS action_taken, f.confidence AS confidence,
19816
- e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19817
- json_extract(e.metadata, '$.repo') AS repo,
19818
- json_extract(e.metadata, '$.filePath') AS file,
19819
- json_extract(e.metadata, '$.toolName') AS tool_name,
19820
- e.kind AS kind, f.finding_key AS finding_key,
20257
+ e.started_at AS occurred_at,
20258
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20259
+ json_extract(e.attributes, '$.repo') AS repo,
20260
+ json_extract(e.attributes, '$.file_path') AS file,
20261
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
20262
+ e.event_type AS kind, f.finding_key AS finding_key,
19821
20263
  latest.status AS latest_status,
19822
20264
  ROW_NUMBER() OVER (
19823
- PARTITION BY f.rule_id
19824
- ORDER BY e.occurred_at DESC, f.id DESC
20265
+ PARTITION BY d.rule_id
20266
+ ORDER BY e.started_at DESC, f.id DESC
19825
20267
  ) AS rn
19826
- FROM findings f
19827
- JOIN events e ON e.id = f.event_id
20268
+ FROM inspection_findings f
20269
+ JOIN audit_events e ON e.id = f.audit_event_id
20270
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19828
20271
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19829
20272
  ON latest.finding_key = f.finding_key
19830
- ${sessionPredicate}
20273
+ ${predicate}
19831
20274
  )
19832
20275
  WHERE rn <= :cap
19833
20276
  ORDER BY occurred_at DESC, id DESC`
@@ -19854,17 +20297,29 @@ var SqliteFindingsRepository = class {
19854
20297
  severity: query.severity,
19855
20298
  providers: query.provider,
19856
20299
  actions: query.action,
20300
+ statuses: query.status,
19857
20301
  subtype: query.subtype,
19858
20302
  q: query.q
19859
20303
  };
19860
20304
  const facets = computeFindingFacets(allGroups, filterOpts);
19861
20305
  const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
20306
+ const statusFilter = query.status ?? [];
19862
20307
  const totals = {
19863
- findings: sorted.reduce((acc, g) => acc + g.instanceCount, 0),
20308
+ findings: sorted.reduce((acc, g) => {
20309
+ if (statusFilter.length === 0) return acc + g.instanceCount;
20310
+ const agg = aggregates.get(g.id);
20311
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
20312
+ }, 0),
19864
20313
  groups: sorted.length
19865
20314
  };
19866
20315
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19867
- const items = sorted.slice(0, limit);
20316
+ const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20317
+ const items = sorted.slice(0, limit).map(
20318
+ (g) => statusSet ? {
20319
+ ...g,
20320
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20321
+ } : g
20322
+ );
19868
20323
  return Promise.resolve({
19869
20324
  totals,
19870
20325
  facets,
@@ -19878,45 +20333,62 @@ var SqliteFindingsRepository = class {
19878
20333
  * buildFindingGroups cannot recover from a preview. Bounded by the number of
19879
20334
  * distinct rule_ids (the installed packs' rules), not by the store's size.
19880
20335
  *
19881
- * The per-instance sets ride back as group_concat lists of RAW DB values —
19882
- * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19883
- * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19884
- * status keeps the classifier itself in @akasecurity/schema, where
19885
- * severitySummary's SQL and this query can't drift apart on what 'resolved'
19886
- * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
20336
+ * A single scan, folded in two levels: the inner SELECT groups by
20337
+ * (rule_id, status tuple) so each (kind, has-key, latest-status) combination
20338
+ * carries its instance count countInstancesByStatus needs those counts for
20339
+ * status-scoped totals — and the outer SELECT folds the tuples back to one
20340
+ * row per rule. The per-instance sets ride back as group_concat lists of RAW
20341
+ * DB values source_tool, action_taken, and the tuples deriveFindingStatus
20342
+ * consumes. Aggregating the status INPUTS rather than a status keeps the
20343
+ * classifier itself in @akasecurity/schema, where severitySummary's SQL and
20344
+ * this query can't drift apart on what 'resolved' means (see
20345
+ * resolution-sql.ts). The concat-of-concats can repeat a value across
20346
+ * tuples; the schema mappers dedupe, and each set is bounded by an enum, so
19887
20347
  * a group's row stays small however many findings it holds.
19888
20348
  *
19889
20349
  * `withSearchText` is the exception, and the one column here that does NOT
19890
- * stay small: the group's distinct repos/filePaths, whose size tracks how many
19891
- * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19892
- * that is a string proportional to the store (~8MB over 200k distinct paths,
19893
- * and buildHaystack lowercases a second copy). It buys `q` the ability to
19894
- * match an instance outside the preview, which searching the preview alone
19895
- * would silently lose, so it is fetched only when the request actually
19896
- * carries a `q`.
20350
+ * stay small: the group's per-tuple-distinct repos/filePaths, whose size
20351
+ * tracks how many distinct paths a rule fired across — for a rule hitting
20352
+ * mostly-unique paths that is a string proportional to the store (~8MB over
20353
+ * 200k distinct paths, and buildHaystack lowercases a second copy). It buys
20354
+ * `q` the ability to match an instance outside the preview, which searching
20355
+ * the preview alone would silently lose, so it is fetched only when the
20356
+ * request actually carries a `q`. (Substring matching is unaffected by a
20357
+ * path repeating across tuples.)
19897
20358
  */
19898
20359
  groupAggregates(withSearchText, scope) {
19899
- const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19900
- group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
19901
- group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20360
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20361
+ group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
20362
+ group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
19902
20363
  const rows = this.db.prepare(
19903
- `SELECT f.rule_id AS rule_id,
19904
- count(*) AS instance_count,
19905
- max(e.occurred_at) AS latest_at,
19906
- group_concat(DISTINCT e.source_tool) AS source_tools,
19907
- group_concat(DISTINCT f.action_taken) AS actions_taken,
19908
- group_concat(DISTINCT (
19909
- e.kind || '${TUPLE_SEP}' ||
19910
- (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19911
- coalesce(latest.status, '')
19912
- )) AS status_inputs
19913
- ${searchTextColumns}
19914
- FROM findings f
19915
- JOIN events e ON e.id = f.event_id
19916
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19917
- ON latest.finding_key = f.finding_key
19918
- ${scope.predicate}
19919
- GROUP BY f.rule_id`
20364
+ `SELECT rule_id,
20365
+ sum(tuple_count) AS instance_count,
20366
+ max(latest_at) AS latest_at,
20367
+ group_concat(source_tools) AS source_tools,
20368
+ group_concat(actions_taken) AS actions_taken,
20369
+ group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
20370
+ group_concat(repos) AS repos,
20371
+ group_concat(files) AS files,
20372
+ group_concat(tool_names) AS tool_names
20373
+ FROM (
20374
+ SELECT d.rule_id AS rule_id,
20375
+ e.event_type || '${TUPLE_SEP}' ||
20376
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20377
+ coalesce(latest.status, '') AS status_tuple,
20378
+ count(*) AS tuple_count,
20379
+ max(e.started_at) AS latest_at,
20380
+ group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
20381
+ group_concat(DISTINCT f.action_taken) AS actions_taken
20382
+ ${innerSearchColumns}
20383
+ FROM inspection_findings f
20384
+ JOIN audit_events e ON e.id = f.audit_event_id
20385
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20386
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20387
+ ON latest.finding_key = f.finding_key
20388
+ ${scope.predicate}
20389
+ GROUP BY d.rule_id, status_tuple
20390
+ )
20391
+ GROUP BY rule_id`
19920
20392
  ).all(scope.params);
19921
20393
  return new Map(
19922
20394
  rows.map((r) => [
@@ -19926,13 +20398,14 @@ var SqliteFindingsRepository = class {
19926
20398
  sourceTools: splitConcat(r.source_tools),
19927
20399
  actionsTaken: splitConcat(r.actions_taken),
19928
20400
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19929
- const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
20401
+ const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
19930
20402
  return {
19931
20403
  // deriveFindingStatus only distinguishes null from non-null here,
19932
20404
  // so the marker stands in for the key itself (never rendered).
19933
20405
  kind,
19934
20406
  findingKey: keyMarker === "" ? null : keyMarker,
19935
- latestResolutionStatus: latestStatus === "" ? null : latestStatus
20407
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus,
20408
+ count: Number(count)
19936
20409
  };
19937
20410
  }),
19938
20411
  latestDetectedAt: epochMillisToIso(r.latest_at),
@@ -19949,10 +20422,21 @@ var SqliteFindingsRepository = class {
19949
20422
  );
19950
20423
  }
19951
20424
  healthSummary() {
19952
- const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
20425
+ const total = countScalar(
20426
+ this.db,
20427
+ `SELECT count(*) AS n FROM inspection_findings f
20428
+ JOIN audit_events e ON e.id = f.audit_event_id
20429
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
20430
+ );
19953
20431
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19954
20432
  const grouped = allRows(
19955
- this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20433
+ this.db.prepare(
20434
+ `SELECT f.action_taken AS action_taken, count(*) AS c
20435
+ FROM inspection_findings f
20436
+ JOIN audit_events e ON e.id = f.audit_event_id
20437
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20438
+ GROUP BY f.action_taken`
20439
+ )
19956
20440
  );
19957
20441
  for (const row of grouped) {
19958
20442
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
@@ -19960,12 +20444,15 @@ var SqliteFindingsRepository = class {
19960
20444
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19961
20445
  const sevRows = allRows(
19962
20446
  this.db.prepare(
19963
- `SELECT f.severity AS severity, count(*) AS c
19964
- FROM findings f
20447
+ `SELECT d.severity AS severity, count(*) AS c
20448
+ FROM inspection_findings f
20449
+ JOIN audit_events e ON e.id = f.audit_event_id
20450
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19965
20451
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19966
20452
  ON latest.finding_key = f.finding_key
19967
- WHERE latest.status IS NULL OR latest.status != 'resolved'
19968
- GROUP BY f.severity`
20453
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20454
+ AND (latest.status IS NULL OR latest.status != 'resolved')
20455
+ GROUP BY d.severity`
19969
20456
  )
19970
20457
  );
19971
20458
  for (const row of sevRows) {
@@ -19986,9 +20473,11 @@ var SqliteFindingsRepository = class {
19986
20473
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19987
20474
  const rows = allRows(
19988
20475
  this.db.prepare(
19989
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19990
- FROM findings f JOIN events e ON e.id = f.event_id
19991
- WHERE e.occurred_at >= :since
20476
+ `SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20477
+ FROM inspection_findings f
20478
+ JOIN audit_events e ON e.id = f.audit_event_id
20479
+ WHERE e.started_at >= :since
20480
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
19992
20481
  GROUP BY day, f.action_taken`
19993
20482
  ),
19994
20483
  { since }
@@ -20053,15 +20542,59 @@ var SqliteInspectionFindingsRepository = class {
20053
20542
  this.insertStmt = db.prepare(
20054
20543
  `INSERT INTO inspection_findings
20055
20544
  (id, audit_event_id, inspection_definition_id, classified_data_id,
20056
- span_start, span_end, masked_match, action_taken, confidence)
20545
+ span_start, span_end, masked_match, action_taken, confidence,
20546
+ finding_key, first_detected_at)
20057
20547
  VALUES
20058
20548
  (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
20059
- :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence)
20060
- ON CONFLICT(id) DO NOTHING`
20549
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
20550
+ :findingKey,
20551
+ COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
20552
+ ON CONFLICT(id) DO UPDATE SET
20553
+ inspection_definition_id = excluded.inspection_definition_id
20554
+ ON CONFLICT (finding_key) DO UPDATE SET
20555
+ audit_event_id = excluded.audit_event_id,
20556
+ inspection_definition_id = excluded.inspection_definition_id,
20557
+ classified_data_id = excluded.classified_data_id,
20558
+ span_start = excluded.span_start,
20559
+ span_end = excluded.span_end,
20560
+ masked_match = excluded.masked_match,
20561
+ action_taken = excluded.action_taken,
20562
+ confidence = excluded.confidence`
20563
+ );
20564
+ this.sessionDupStmt = db.prepare(
20565
+ `SELECT 1 FROM inspection_findings f
20566
+ JOIN audit_events e ON e.id = f.audit_event_id
20567
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20568
+ WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
20569
+ AND e.root_session_id = :sessionId
20570
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20571
+ LIMIT 1`
20572
+ );
20573
+ this.eventDupStmt = db.prepare(
20574
+ `SELECT 1 FROM inspection_findings f
20575
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20576
+ WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
20577
+ AND f.masked_match = :maskedMatch
20578
+ AND f.span_start = :spanStart AND f.span_end = :spanEnd
20579
+ LIMIT 1`
20061
20580
  );
20062
20581
  }
20063
20582
  db;
20064
20583
  insertStmt;
20584
+ sessionDupStmt;
20585
+ eventDupStmt;
20586
+ // True when an earlier event in the same session already recorded a finding
20587
+ // with the same rule and masked value. The current event's own findings are
20588
+ // inserted one at a time in caller order, so an earlier finding in the SAME
20589
+ // recordCapture call is visible to a later duplicate check within it too.
20590
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
20591
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
20592
+ }
20593
+ // True when this exact detection (rule + masked value + span) is already
20594
+ // recorded against the given audit event.
20595
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
20596
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
20597
+ }
20065
20598
  insertFinding(input) {
20066
20599
  const row = toInspectionFindingRow(input);
20067
20600
  this.insertStmt.run(
@@ -20074,7 +20607,9 @@ var SqliteInspectionFindingsRepository = class {
20074
20607
  spanEnd: row.spanEnd,
20075
20608
  maskedMatch: row.maskedMatch,
20076
20609
  actionTaken: row.actionTaken,
20077
- confidence: row.confidence
20610
+ confidence: row.confidence,
20611
+ findingKey: row.findingKey,
20612
+ firstDetectedAt: row.firstDetectedAt
20078
20613
  })
20079
20614
  );
20080
20615
  }
@@ -20346,7 +20881,7 @@ var SqliteInstalledPacksRepository = class {
20346
20881
  installedRuleset() {
20347
20882
  const rows = allRows(
20348
20883
  this.db.prepare(
20349
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20884
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
20350
20885
  )
20351
20886
  );
20352
20887
  const out = {
@@ -20354,7 +20889,8 @@ var SqliteInstalledPacksRepository = class {
20354
20889
  enabledPacks: 0,
20355
20890
  rules: [],
20356
20891
  invalidRules: 0,
20357
- ruleActions: /* @__PURE__ */ new Map()
20892
+ ruleActions: /* @__PURE__ */ new Map(),
20893
+ ruleVersions: /* @__PURE__ */ new Map()
20358
20894
  };
20359
20895
  for (const row of rows) {
20360
20896
  if (!intToBool(row.enabled)) continue;
@@ -20376,6 +20912,7 @@ var SqliteInstalledPacksRepository = class {
20376
20912
  if (parsed.success) {
20377
20913
  out.rules.push(parsed.data);
20378
20914
  out.ruleActions.set(parsed.data.id, action);
20915
+ out.ruleVersions.set(parsed.data.id, row.version);
20379
20916
  } else out.invalidRules += 1;
20380
20917
  }
20381
20918
  }
@@ -21550,19 +22087,19 @@ var SqliteResolutionsRepository = class {
21550
22087
  );
21551
22088
  this.openAtRestStmt = db.prepare(
21552
22089
  `SELECT DISTINCT f.finding_key AS finding_key
21553
- FROM findings f
21554
- JOIN events e ON e.id = f.event_id
21555
- WHERE e.kind = 'code_change'
21556
- AND json_extract(e.metadata, '$.filePath') = :path
22090
+ FROM inspection_findings f
22091
+ JOIN audit_events e ON e.id = f.audit_event_id
22092
+ WHERE e.event_type = 'code_change'
22093
+ AND json_extract(e.attributes, '$.file_path') = :path
21557
22094
  AND f.finding_key IS NOT NULL
21558
22095
  AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
21559
22096
  );
21560
22097
  this.resolvedAtRestStmt = db.prepare(
21561
22098
  `SELECT DISTINCT f.finding_key AS finding_key
21562
- FROM findings f
21563
- JOIN events e ON e.id = f.event_id
21564
- WHERE e.kind = 'code_change'
21565
- AND json_extract(e.metadata, '$.filePath') = :path
22099
+ FROM inspection_findings f
22100
+ JOIN audit_events e ON e.id = f.audit_event_id
22101
+ WHERE e.event_type = 'code_change'
22102
+ AND json_extract(e.attributes, '$.file_path') = :path
21566
22103
  AND f.finding_key IS NOT NULL
21567
22104
  AND ${latestResolutionStatusSql("f")} = 'resolved'`
21568
22105
  );
@@ -21630,6 +22167,35 @@ var SqliteResolutionsRepository = class {
21630
22167
  }
21631
22168
  };
21632
22169
 
22170
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
22171
+ var SqliteRuleProbeCacheRepository = class {
22172
+ constructor(db) {
22173
+ this.db = db;
22174
+ this.upsertStmt = db.prepare(
22175
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
22176
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
22177
+ ON CONFLICT (rule_key) DO UPDATE SET
22178
+ verdict = excluded.verdict,
22179
+ worst_probe_ms = excluded.worst_probe_ms,
22180
+ checked_at = excluded.checked_at`
22181
+ );
22182
+ this.readStmt = db.prepare(
22183
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22184
+ );
22185
+ }
22186
+ db;
22187
+ upsertStmt;
22188
+ readStmt;
22189
+ getVerdict(ruleKey) {
22190
+ return getRow(this.readStmt, { ruleKey });
22191
+ }
22192
+ setVerdict(ruleKey, verdict, worstProbeMs2) {
22193
+ failOpenTransaction(this.db, () => {
22194
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
22195
+ });
22196
+ }
22197
+ };
22198
+
21633
22199
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21634
22200
  var SqliteScanLedgerRepository = class {
21635
22201
  constructor(db) {
@@ -21756,25 +22322,27 @@ var SqliteSecurityRepository = class {
21756
22322
  severitySummary() {
21757
22323
  const rows = allRows(
21758
22324
  this.db.prepare(
21759
- `SELECT f.severity AS severity,
22325
+ `SELECT d.severity AS severity,
21760
22326
  COUNT(*) AS count,
21761
22327
  SUM(CASE
21762
- WHEN e.kind != 'code_change' THEN 1
22328
+ WHEN e.event_type != 'code_change' THEN 1
21763
22329
  WHEN f.finding_key IS NULL THEN 0
21764
22330
  WHEN latest.status = 'resolved' THEN 1
21765
22331
  ELSE 0
21766
22332
  END) AS caught,
21767
22333
  SUM(CASE
21768
- WHEN e.kind = 'code_change'
22334
+ WHEN e.event_type = 'code_change'
21769
22335
  AND f.finding_key IS NOT NULL
21770
22336
  AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
21771
22337
  ELSE 0
21772
22338
  END) AS open_at_rest
21773
- FROM findings f
21774
- JOIN events e ON e.id = f.event_id
22339
+ FROM inspection_findings f
22340
+ JOIN audit_events e ON e.id = f.audit_event_id
22341
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21775
22342
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21776
22343
  ON latest.finding_key = f.finding_key
21777
- GROUP BY f.severity`
22344
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22345
+ GROUP BY d.severity`
21778
22346
  )
21779
22347
  );
21780
22348
  const byRow = new Map(rows.map((r) => [r.severity, r]));
@@ -21840,7 +22408,7 @@ var SqliteSecurityRepository = class {
21840
22408
  // Mean time-to-remediate per bucket, split by severity — a sibling of
21841
22409
  // findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
21842
22410
  // on a different timestamp: findingsTimeseries buckets by first-detection
21843
- // (events.occurred_at), this buckets by resolution time (the latest
22411
+ // (audit_events.started_at), this buckets by resolution time (the latest
21844
22412
  // finding_resolution row's resolved_at) — it's a "resolved in this bucket"
21845
22413
  // trend, not a "detected in this bucket" one. Only findings whose LATEST
21846
22414
  // resolution row (latest-resolution-wins, same correlated subquery as
@@ -21865,30 +22433,20 @@ var SqliteSecurityRepository = class {
21865
22433
  // first_detected_at is the PRESERVED first-detection time (set once on a
21866
22434
  // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21867
22435
  // measures from first sighting — not the latest re-scan's event, whose
21868
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21869
- // the parent event's occurred_at defends against any legacy/edge row the
21870
- // backfill left null.
21871
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21872
- (
21873
- SELECT fr.status FROM finding_resolution fr
21874
- WHERE fr.finding_key = f.finding_key
21875
- ORDER BY fr.created_at DESC, fr.rowid DESC
21876
- LIMIT 1
21877
- ) AS latest_status,
21878
- (
21879
- SELECT fr.method 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_method,
21884
- (
21885
- SELECT fr.resolved_at 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_resolved_at
21890
- FROM findings f JOIN events e ON e.id = f.event_id
22436
+ // started_at the upsert overwrites onto inspection_findings.audit_event_id.
22437
+ // COALESCE onto the parent event's started_at defends against any
22438
+ // legacy/edge row the backfill left null.
22439
+ `SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
22440
+ latest.status AS latest_status,
22441
+ latest.method AS latest_method,
22442
+ latest.resolved_at AS latest_resolved_at
22443
+ FROM inspection_findings f
22444
+ JOIN audit_events e ON e.id = f.audit_event_id
22445
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22446
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22447
+ ON latest.finding_key = f.finding_key
21891
22448
  WHERE f.finding_key IS NOT NULL
22449
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
21892
22450
  AND EXISTS (
21893
22451
  SELECT 1 FROM finding_resolution fr
21894
22452
  WHERE fr.finding_key = f.finding_key
@@ -21935,11 +22493,13 @@ var SqliteSecurityRepository = class {
21935
22493
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21936
22494
  const rows = allRows(
21937
22495
  this.db.prepare(
21938
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21939
- FROM findings f JOIN events e ON e.id = f.event_id
21940
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
21941
- AND json_extract(e.metadata, '$.repo') IS NOT NULL
21942
- AND json_extract(e.metadata, '$.repo') != ''
22496
+ `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
22497
+ FROM inspection_findings f
22498
+ JOIN audit_events e ON e.id = f.audit_event_id
22499
+ WHERE e.started_at >= :from AND e.started_at < :to
22500
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22501
+ AND json_extract(e.attributes, '$.repo') IS NOT NULL
22502
+ AND json_extract(e.attributes, '$.repo') != ''
21943
22503
  GROUP BY repo
21944
22504
  ORDER BY c DESC, repo
21945
22505
  LIMIT :limit`
@@ -21963,44 +22523,28 @@ var SqliteSecurityRepository = class {
21963
22523
  // secret came back) is excluded — it is not currently resolved. Legacy
21964
22524
  // at-rest findings with finding_key IS NULL are excluded outright (the
21965
22525
  // resolution lifecycle can never attach to them). Path comes from the
21966
- // finding's parent event (kind 'code_change', metadata.filePath) — mirrors
21967
- // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21968
- // capped at `limit`.
22526
+ // finding's parent event (event_type 'code_change', attributes.file_path) —
22527
+ // mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
22528
+ // DESC, capped at `limit`.
21969
22529
  recentlyResolved(limit = 20) {
21970
22530
  const rows = allRows(
21971
22531
  this.db.prepare(
21972
22532
  `SELECT f.finding_key AS finding_key,
21973
- f.rule_id AS rule_id,
21974
- f.severity AS severity,
21975
- json_extract(e.metadata, '$.filePath') AS path,
21976
- COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at,
21977
- (
21978
- SELECT fr.resolved_at FROM finding_resolution fr
21979
- WHERE fr.finding_key = f.finding_key
21980
- ORDER BY fr.created_at DESC, fr.rowid DESC
21981
- LIMIT 1
21982
- ) AS latest_resolved_at
21983
- FROM findings f JOIN events e ON e.id = f.event_id
21984
- WHERE e.kind = 'code_change'
22533
+ d.rule_id AS rule_id,
22534
+ d.severity AS severity,
22535
+ json_extract(e.attributes, '$.file_path') AS path,
22536
+ COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
22537
+ latest.resolved_at AS latest_resolved_at
22538
+ FROM inspection_findings f
22539
+ JOIN audit_events e ON e.id = f.audit_event_id
22540
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22541
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22542
+ ON latest.finding_key = f.finding_key
22543
+ WHERE e.event_type = 'code_change'
21985
22544
  AND f.finding_key IS NOT NULL
21986
- AND (
21987
- SELECT fr.status FROM finding_resolution fr
21988
- WHERE fr.finding_key = f.finding_key
21989
- ORDER BY fr.created_at DESC, fr.rowid DESC
21990
- LIMIT 1
21991
- ) = 'resolved'
21992
- AND (
21993
- SELECT fr.method 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
- ) = 'fixed-at-source'
21998
- AND (
21999
- SELECT fr.resolved_at 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
- ) IS NOT NULL
22545
+ AND latest.status = 'resolved'
22546
+ AND latest.method = 'fixed-at-source'
22547
+ AND latest.resolved_at IS NOT NULL
22004
22548
  ORDER BY latest_resolved_at DESC
22005
22549
  LIMIT :limit`
22006
22550
  ),
@@ -22019,15 +22563,18 @@ var SqliteSecurityRepository = class {
22019
22563
  return Promise.resolve({ items });
22020
22564
  }
22021
22565
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
22022
- // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
22566
+ // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
22023
22567
  // numeric and the JS aggregations bucket/split on ms directly.
22024
22568
  findingsInRange(fromMs, toMs) {
22025
22569
  const rows = allRows(
22026
22570
  this.db.prepare(
22027
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22028
- FROM findings f JOIN events e ON e.id = f.event_id
22029
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
22030
- ORDER BY e.occurred_at`
22571
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
22572
+ FROM inspection_findings f
22573
+ JOIN audit_events e ON e.id = f.audit_event_id
22574
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22575
+ WHERE e.started_at >= :from AND e.started_at < :to
22576
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22577
+ ORDER BY e.started_at`
22031
22578
  ),
22032
22579
  { from: fromMs, to: toMs }
22033
22580
  );
@@ -22041,11 +22588,50 @@ var SqliteSecurityRepository = class {
22041
22588
 
22042
22589
  // ../../packages/persistence/src/repositories/shares.ts
22043
22590
  import { randomUUID as randomUUID7 } from "crypto";
22044
- var KIND_ORDER = ["provider", "internal", "ip"];
22591
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22592
+ var IN_CHUNK = 500;
22593
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22594
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22595
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22596
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22045
22597
  var CALL_SITE_EMBED_CAP = 200;
22046
22598
  function parseNetwork(networkJson) {
22047
22599
  return safeJson(networkJson, null);
22048
22600
  }
22601
+ function capHits(all, mode) {
22602
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22603
+ return { hits: [...all], droppedFiles: [], truncated: false };
22604
+ }
22605
+ if (mode === "walk") {
22606
+ return {
22607
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22608
+ droppedFiles: [],
22609
+ truncated: true
22610
+ };
22611
+ }
22612
+ const byFile = /* @__PURE__ */ new Map();
22613
+ for (const hit of all) {
22614
+ const bucket = byFile.get(hit.site.file);
22615
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22616
+ else bucket.push(hit);
22617
+ }
22618
+ const hits = [];
22619
+ const droppedFiles = [];
22620
+ for (const [file2, bucket] of byFile) {
22621
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22622
+ else hits.push(...bucket);
22623
+ }
22624
+ return { hits, droppedFiles, truncated: true };
22625
+ }
22626
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22627
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22628
+ const dropped = new Set(droppedFiles);
22629
+ return {
22630
+ mode: "ledger",
22631
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22632
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22633
+ };
22634
+ }
22049
22635
  function toEndpointSummary(row) {
22050
22636
  return {
22051
22637
  id: row.id,
@@ -22136,13 +22722,15 @@ var SqliteSharesRepository = class {
22136
22722
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22137
22723
  const insecure = countScalar(
22138
22724
  this.db,
22139
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22725
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22726
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22140
22727
  );
22141
22728
  const needsReview = countScalar(
22142
22729
  this.db,
22143
22730
  `SELECT count(DISTINCT d.id) AS n
22144
22731
  FROM share_destination d
22145
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22732
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22733
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22146
22734
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22147
22735
  );
22148
22736
  const kindCounts = countBy(
@@ -22152,6 +22740,7 @@ var SqliteSharesRepository = class {
22152
22740
  const byKind = {
22153
22741
  provider: kindCounts.get("provider") ?? 0,
22154
22742
  internal: kindCounts.get("internal") ?? 0,
22743
+ external: kindCounts.get("external") ?? 0,
22155
22744
  ip: kindCounts.get("ip") ?? 0
22156
22745
  };
22157
22746
  const trustCounts = countBy(
@@ -22221,29 +22810,322 @@ var SqliteSharesRepository = class {
22221
22810
  const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
22222
22811
  return Promise.resolve(buildDetail(dest, endpoints, callSites));
22223
22812
  }
22224
- // ─── Writes ────────────────────────────────────────────────────────────────
22225
- // Driven by a web-ui Server Action, not the hook path — errors surface to the
22226
- // caller. Returns whether the destination existed, so the caller can tell a
22227
- // real edit from a no-such-destination.
22813
+ // ─── Writes ────────────────────────────────────────────────────────────────
22814
+ // Driven by a web-ui Server Action, not the hook path — errors surface to the
22815
+ // caller. Returns whether the destination existed, so the caller can tell a
22816
+ // real edit from a no-such-destination.
22817
+ /**
22818
+ * Set (decision) or clear (null) the egress decision override for a destination.
22819
+ * `null` deletes the override rows → reverts to the trust default.
22820
+ *
22821
+ * The written row carries both the destination id and its host, so the
22822
+ * decision re-attaches by host after the destination is pruned and
22823
+ * re-detected under a fresh id. Rows written before the host column existed
22824
+ * (host NULL, matched by destination id) are replaced rather than left to
22825
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22826
+ * would otherwise race a concurrent prune.
22827
+ */
22828
+ setEgressDecision(destinationId, decision) {
22829
+ let existed = false;
22830
+ withTransaction(
22831
+ this.db,
22832
+ () => {
22833
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22834
+ if (dest === void 0) return;
22835
+ existed = true;
22836
+ this.db.prepare(
22837
+ `DELETE FROM egress_decision_override
22838
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22839
+ ).run({ host: dest.host, destinationId });
22840
+ if (decision === null) return;
22841
+ this.db.prepare(
22842
+ `INSERT INTO egress_decision_override
22843
+ (id, destination_id, host, decision, created_at, updated_at)
22844
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22845
+ ).run({
22846
+ id: randomUUID7(),
22847
+ destinationId,
22848
+ host: dest.host,
22849
+ decision,
22850
+ now: Date.now()
22851
+ });
22852
+ },
22853
+ "IMMEDIATE"
22854
+ );
22855
+ return existed;
22856
+ }
22857
+ /**
22858
+ * Record one project's statically-extracted egress: reconcile the previously
22859
+ * stored call sites against this scan, upsert destination → endpoint → call
22860
+ * site for every hit, confirm `last_seen` on everything the project still
22861
+ * references, and drop what no longer has evidence.
22862
+ *
22863
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22864
+ * display payload and never scope a delete. The whole write is one
22865
+ * transaction: a failure leaves the project's previous inventory exactly as
22866
+ * it was, and THROWS rather than reporting a partial write — callers decide
22867
+ * their own fail-open behavior, and the scanner additionally withholds its
22868
+ * ledger commit so the next scan retries.
22869
+ *
22870
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22871
+ * their hits are both excluded from the reconcile delete and named in
22872
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22873
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22874
+ * caller withholds the ledger entry so the next scan reads it again.
22875
+ */
22876
+ recordProjectEgress(input) {
22877
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22878
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22879
+ const now = Date.now();
22880
+ let summary = {
22881
+ destinations: 0,
22882
+ endpoints: 0,
22883
+ callSites: 0,
22884
+ truncated,
22885
+ droppedFiles
22886
+ };
22887
+ withTransaction(
22888
+ this.db,
22889
+ () => {
22890
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22891
+ this.reconcileCallSites(input.projectKey, reconcile);
22892
+ this.upsertHits(input, hits, projectId, now);
22893
+ this.confirmLastSeen(input.projectKey, now);
22894
+ this.pruneOrphans();
22895
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22896
+ },
22897
+ "IMMEDIATE"
22898
+ );
22899
+ return summary;
22900
+ }
22901
+ // ─── Egress write internals ──────────────────────────────────────────────────
22902
+ /**
22903
+ * Clear the stored call sites this scan is responsible for re-creating.
22904
+ *
22905
+ * Each pipeline may only delete rows its own walker could have produced. The
22906
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22907
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22908
+ * reconcile, and deleting them here would make the two pipelines erase each
22909
+ * other's rows on every alternating scan. 'ledger' mode names its files
22910
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22911
+ * the scanner skips (vendored, oversize) survive it.
22912
+ */
22913
+ reconcileCallSites(projectKey, reconcile) {
22914
+ if (reconcile.mode === "walk") {
22915
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22916
+ this.db.prepare(
22917
+ `DELETE FROM share_call_site
22918
+ WHERE project_key = :key
22919
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22920
+ AND file NOT LIKE '.%'
22921
+ AND file NOT LIKE '%/.%'`
22922
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22923
+ return;
22924
+ }
22925
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22926
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22927
+ const chunk = files.slice(i, i + IN_CHUNK);
22928
+ this.db.prepare(
22929
+ `DELETE FROM share_call_site
22930
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22931
+ ).run(projectKey, ...chunk);
22932
+ }
22933
+ }
22934
+ /**
22935
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22936
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22937
+ * projects; only the call site carries `project_key`. A destination's `note`
22938
+ * is user-owned and never overwritten. The id caches keep one upsert per
22939
+ * distinct host and endpoint, so the first hit for a host supplies its
22940
+ * classification for this batch.
22941
+ */
22942
+ upsertHits(input, hits, projectId, now) {
22943
+ if (hits.length === 0) return;
22944
+ const destStmt = this.db.prepare(
22945
+ `INSERT INTO share_destination
22946
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22947
+ created_at, updated_at)
22948
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22949
+ ON CONFLICT (host) DO UPDATE SET
22950
+ kind = excluded.kind,
22951
+ name = excluded.name,
22952
+ category = excluded.category,
22953
+ trust = excluded.trust,
22954
+ network_json = excluded.network_json,
22955
+ last_seen = excluded.last_seen,
22956
+ updated_at = excluded.updated_at`
22957
+ );
22958
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22959
+ const endpointStmt = this.db.prepare(
22960
+ `INSERT INTO share_endpoint
22961
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22962
+ created_at, updated_at)
22963
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22964
+ :now, :now)
22965
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22966
+ transport = excluded.transport,
22967
+ template = excluded.template,
22968
+ data_class = excluded.data_class,
22969
+ last_seen = excluded.last_seen,
22970
+ updated_at = excluded.updated_at`
22971
+ );
22972
+ const endpointIdStmt = this.db.prepare(
22973
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22974
+ );
22975
+ const siteStmt = this.db.prepare(
22976
+ `INSERT INTO share_call_site
22977
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22978
+ project_id, created_at, updated_at)
22979
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22980
+ :vendored, :projectId, :now, :now)
22981
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22982
+ snippet = excluded.snippet,
22983
+ dynamic = excluded.dynamic,
22984
+ vendored = excluded.vendored,
22985
+ project = excluded.project,
22986
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22987
+ updated_at = excluded.updated_at`
22988
+ );
22989
+ const destIds = /* @__PURE__ */ new Map();
22990
+ const endpointIds = /* @__PURE__ */ new Map();
22991
+ for (const hit of hits) {
22992
+ let destinationId = destIds.get(hit.host);
22993
+ if (destinationId === void 0) {
22994
+ destStmt.run({
22995
+ id: randomUUID7(),
22996
+ kind: hit.kind,
22997
+ name: hit.name,
22998
+ host: hit.host,
22999
+ category: hit.category,
23000
+ trust: hit.trust,
23001
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
23002
+ now
23003
+ });
23004
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
23005
+ destIds.set(hit.host, destinationId);
23006
+ }
23007
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
23008
+ let endpointId = endpointIds.get(endpointKey);
23009
+ if (endpointId === void 0) {
23010
+ endpointStmt.run({
23011
+ id: randomUUID7(),
23012
+ destinationId,
23013
+ method: hit.method,
23014
+ transport: hit.transport,
23015
+ url: hit.url,
23016
+ template: boolToInt(hit.template),
23017
+ dataClass: hit.dataClass,
23018
+ now
23019
+ });
23020
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
23021
+ endpointIds.set(endpointKey, endpointId);
23022
+ }
23023
+ siteStmt.run({
23024
+ id: randomUUID7(),
23025
+ endpointId,
23026
+ project: input.project,
23027
+ projectKey: input.projectKey,
23028
+ file: hit.site.file,
23029
+ line: hit.site.line,
23030
+ snippet: hit.site.snippet,
23031
+ dynamic: boolToInt(hit.site.dynamic),
23032
+ vendored: boolToInt(hit.site.vendored),
23033
+ projectId,
23034
+ now
23035
+ });
23036
+ }
23037
+ }
22228
23038
  /**
22229
- * Set (decision) or clear (null) the egress decision override for a destination.
22230
- * `null` deletes the override row reverts to the trust default.
23039
+ * The source-project id this project's stored call sites already carry, if
23040
+ * any. Only the pipeline that resolves a source project supplies one; the
23041
+ * other passes null and inherits this, so the link stops flapping between a
23042
+ * real id and NULL depending on which pipeline ran last. The value is a
23043
+ * per-project attribute stored redundantly on each row, so any row's is
23044
+ * representative.
22231
23045
  */
22232
- setEgressDecision(destinationId, decision) {
22233
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22234
- if (exists === void 0) return false;
22235
- if (decision === null) {
22236
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22237
- return true;
22238
- }
23046
+ knownProjectId(projectKey) {
23047
+ return getRow(
23048
+ this.db.prepare(
23049
+ `SELECT project_id AS projectId FROM share_call_site
23050
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
23051
+ ),
23052
+ [projectKey]
23053
+ )?.projectId ?? null;
23054
+ }
23055
+ /**
23056
+ * Stamp `last_seen` on every endpoint and destination this project still
23057
+ * references — including rows the scan preserved rather than re-wrote, so a
23058
+ * ledger-skipped file's references don't decay into "stale" on the page.
23059
+ */
23060
+ confirmLastSeen(projectKey, now) {
22239
23061
  this.db.prepare(
22240
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22241
- VALUES (:id, :destinationId, :decision, :now, :now)
22242
- ON CONFLICT (destination_id) DO UPDATE SET
22243
- decision = excluded.decision,
22244
- updated_at = excluded.updated_at`
22245
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22246
- return true;
23062
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
23063
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
23064
+ ).run({ now, key: projectKey });
23065
+ this.db.prepare(
23066
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
23067
+ WHERE id IN (SELECT DISTINCT e.destination_id
23068
+ FROM share_endpoint e
23069
+ JOIN share_call_site c ON c.endpoint_id = e.id
23070
+ WHERE c.project_key = :key)`
23071
+ ).run({ now, key: projectKey });
23072
+ }
23073
+ /**
23074
+ * Drop rows left without evidence: endpoints with no call site, then
23075
+ * destinations with no endpoint. Call sites are the only evidence either one
23076
+ * has, so a row that lost its last one belongs to no project any more.
23077
+ *
23078
+ * Overrides are deleted between the two steps, and only the ones written
23079
+ * before the host column existed. Those match a destination by id alone;
23080
+ * because the id link is released on delete rather than cascading, leaving
23081
+ * them would accumulate rows that match neither join arm and that nothing can
23082
+ * reach again. Host-bearing rows deliberately survive — the host is what
23083
+ * re-attaches a user's decision when the destination comes back.
23084
+ */
23085
+ pruneOrphans() {
23086
+ this.db.exec(
23087
+ `DELETE FROM share_endpoint
23088
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
23089
+ );
23090
+ this.db.exec(
23091
+ `DELETE FROM egress_decision_override
23092
+ WHERE host IS NULL
23093
+ AND destination_id IN (
23094
+ SELECT d.id FROM share_destination d
23095
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
23096
+ );
23097
+ this.db.exec(
23098
+ `DELETE FROM share_destination
23099
+ WHERE NOT EXISTS (
23100
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
23101
+ );
23102
+ }
23103
+ /**
23104
+ * Live totals for one project. Destinations and endpoints are shared across
23105
+ * projects and carry no project column, so both are counted through the call
23106
+ * sites that reference them.
23107
+ */
23108
+ projectTotals(projectKey) {
23109
+ return {
23110
+ destinations: countScalar(
23111
+ this.db,
23112
+ `SELECT count(DISTINCT e.destination_id) AS n
23113
+ FROM share_endpoint e
23114
+ JOIN share_call_site c ON c.endpoint_id = e.id
23115
+ WHERE c.project_key = ?`,
23116
+ [projectKey]
23117
+ ),
23118
+ endpoints: countScalar(
23119
+ this.db,
23120
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
23121
+ [projectKey]
23122
+ ),
23123
+ callSites: countScalar(
23124
+ this.db,
23125
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
23126
+ [projectKey]
23127
+ )
23128
+ };
22247
23129
  }
22248
23130
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22249
23131
  mapDestRow(r) {
@@ -22263,7 +23145,8 @@ var SqliteSharesRepository = class {
22263
23145
  fetchDestinations(q, kinds, reviewOnly = false) {
22264
23146
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22265
23147
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22266
- d.created_at AS createdAt, o.decision AS overrideDecision`;
23148
+ d.created_at AS createdAt,
23149
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22267
23150
  const conditions = [];
22268
23151
  const params = [];
22269
23152
  if (kinds && kinds.length > 0) {
@@ -22274,7 +23157,8 @@ var SqliteSharesRepository = class {
22274
23157
  conditions.push(
22275
23158
  `(d.trust IN ('unverified', 'ip')
22276
23159
  OR EXISTS (SELECT 1 FROM share_endpoint re
22277
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
23160
+ WHERE re.destination_id = d.id
23161
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22278
23162
  );
22279
23163
  }
22280
23164
  let sql;
@@ -22287,7 +23171,7 @@ var SqliteSharesRepository = class {
22287
23171
  params.push(pattern, pattern, pattern, pattern, pattern);
22288
23172
  sql = `SELECT DISTINCT ${cols}
22289
23173
  FROM share_destination d
22290
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23174
+ ${OVERRIDE_JOIN}
22291
23175
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22292
23176
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22293
23177
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22295,7 +23179,7 @@ var SqliteSharesRepository = class {
22295
23179
  } else {
22296
23180
  sql = `SELECT ${cols}
22297
23181
  FROM share_destination d
22298
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23182
+ ${OVERRIDE_JOIN}
22299
23183
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22300
23184
  ORDER BY d.created_at ASC, d.id ASC`;
22301
23185
  }
@@ -22310,9 +23194,9 @@ var SqliteSharesRepository = class {
22310
23194
  this.db.prepare(
22311
23195
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22312
23196
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22313
- o.decision AS overrideDecision
23197
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22314
23198
  FROM share_destination d
22315
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23199
+ ${OVERRIDE_JOIN}
22316
23200
  WHERE d.id = ?`
22317
23201
  ),
22318
23202
  [destinationId]
@@ -22514,9 +23398,10 @@ function openWithPragmas(file2) {
22514
23398
  }
22515
23399
  function backupLegacyStore(file2) {
22516
23400
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
22517
- renameSync(file2, backup);
22518
- for (const sidecar of walSidecars(file2)) {
22519
- if (existsSync(sidecar)) rmSync(sidecar);
23401
+ renameSync2(file2, backup);
23402
+ tightenFile(backup);
23403
+ for (const sidecar of dbSidecars(file2)) {
23404
+ if (existsSync(sidecar)) rmSync2(sidecar);
22520
23405
  }
22521
23406
  return backup;
22522
23407
  }
@@ -22532,7 +23417,7 @@ function openLocalDatabase(dir) {
22532
23417
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
22533
23418
  );
22534
23419
  }
22535
- applyMigrations(db);
23420
+ applyMigrations(db, file2);
22536
23421
  tightenPerms(file2);
22537
23422
  const events = new SqliteEventsRepository(db);
22538
23423
  const findings = new SqliteFindingsRepository(db);
@@ -22541,6 +23426,7 @@ function openLocalDatabase(dir) {
22541
23426
  const scanLedger = new SqliteScanLedgerRepository(db);
22542
23427
  const exceptions = new SqliteExceptionsRepository(db);
22543
23428
  const resolutions = new SqliteResolutionsRepository(db);
23429
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22544
23430
  const security = new SqliteSecurityRepository(db);
22545
23431
  const detections = new SqliteDetectionsRepository(db);
22546
23432
  const shares = new SqliteSharesRepository(db);
@@ -22558,9 +23444,56 @@ function openLocalDatabase(dir) {
22558
23444
  policies.seedDefaults();
22559
23445
  function recordCapture(event, detected) {
22560
23446
  failOpenTransaction(db, () => {
22561
- events.insertEvent(event);
22562
23447
  const sessionId = event.metadata?.sessionId;
22563
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
23448
+ if (sessionId) {
23449
+ auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
23450
+ }
23451
+ const auditEventId = captureId(
23452
+ sessionId ?? null,
23453
+ event.contentHash,
23454
+ event.metadata?.filePath ?? null
23455
+ );
23456
+ auditEvents.insertAuditEvent({
23457
+ id: auditEventId,
23458
+ eventType: event.kind,
23459
+ startedAt: event.occurredAt,
23460
+ parentId: sessionId,
23461
+ rootSessionId: sessionId,
23462
+ content: event.content,
23463
+ contentHash: event.contentHash,
23464
+ attributes: toCaptureAttributes(event)
23465
+ });
23466
+ const definitionIds = /* @__PURE__ */ new Map();
23467
+ for (const finding of detected) {
23468
+ if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
23469
+ continue;
23470
+ }
23471
+ if (inspectionFindings.isEventDuplicate(
23472
+ auditEventId,
23473
+ finding.ruleId,
23474
+ finding.maskedMatch,
23475
+ finding.span.start,
23476
+ finding.span.end
23477
+ )) {
23478
+ continue;
23479
+ }
23480
+ const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
23481
+ let definitionId = definitionIds.get(key);
23482
+ if (!definitionId) {
23483
+ definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
23484
+ definitionIds.set(key, definitionId);
23485
+ }
23486
+ inspectionFindings.insertFinding({
23487
+ id: finding.id,
23488
+ auditEventId,
23489
+ inspectionDefinitionId: definitionId,
23490
+ span: finding.span,
23491
+ maskedMatch: finding.maskedMatch,
23492
+ actionTaken: finding.actionTaken,
23493
+ confidence: finding.confidence,
23494
+ findingKey: finding.findingKey ?? void 0
23495
+ });
23496
+ }
22564
23497
  });
22565
23498
  }
22566
23499
  function ensureInventory(ctx) {
@@ -22678,6 +23611,7 @@ function openLocalDatabase(dir) {
22678
23611
  scanLedger,
22679
23612
  exceptions,
22680
23613
  resolutions,
23614
+ ruleProbeCache,
22681
23615
  security,
22682
23616
  detections,
22683
23617
  shares,
@@ -22707,9 +23641,19 @@ function openLocalDatabase(dir) {
22707
23641
  };
22708
23642
  }
22709
23643
 
23644
+ // ../../packages/persistence/src/finding-key.ts
23645
+ import { createHash as createHash3 } from "crypto";
23646
+ function normalizeFilePath(filePath) {
23647
+ return filePath.replaceAll("\\", "/");
23648
+ }
23649
+ function computeFindingKey(input) {
23650
+ const normalizedPath = normalizeFilePath(input.filePath);
23651
+ return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
23652
+ }
23653
+
22710
23654
  // ../../packages/persistence/src/fingerprint.ts
22711
23655
  import { createHmac, randomBytes } from "crypto";
22712
- import { chmodSync as chmodSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
23656
+ import { readFileSync } from "fs";
22713
23657
  import { join as join2 } from "path";
22714
23658
  var KEY_FILENAME = "exception.key";
22715
23659
  var KEY_MATERIAL_BYTES = 32;
@@ -22737,15 +23681,9 @@ function parseKeyFile(raw) {
22737
23681
  function writeKeyFile(dataDir2, key) {
22738
23682
  ensureDataDirSync(dataDir2);
22739
23683
  const file2 = keyFilePath(dataDir2);
22740
- const tmp = `${file2}.tmp`;
22741
23684
  const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
22742
- writeFileSync(tmp, `${body}
22743
- `, { mode: DATA_FILE_MODE });
22744
- renameSync2(tmp, file2);
22745
- try {
22746
- chmodSync2(file2, DATA_FILE_MODE);
22747
- } catch {
22748
- }
23685
+ writeOwnerOnlyFileSync(file2, `${body}
23686
+ `);
22749
23687
  return key;
22750
23688
  }
22751
23689
  function readFingerprintKey(dataDir2) {
@@ -22761,10 +23699,7 @@ function readFingerprintKey(dataDir2) {
22761
23699
  function loadOrCreateFingerprintKey(dataDir2) {
22762
23700
  const existing = readFingerprintKey(dataDir2);
22763
23701
  if (existing) {
22764
- try {
22765
- chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
22766
- } catch {
22767
- }
23702
+ tightenFile(keyFilePath(dataDir2));
22768
23703
  return existing;
22769
23704
  }
22770
23705
  return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
@@ -22774,8 +23709,8 @@ function fingerprintValue(key, raw) {
22774
23709
  }
22775
23710
 
22776
23711
  // ../../packages/persistence/src/local-layout.ts
22777
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync2, renameSync as renameSync3 } from "fs";
22778
- import { chmod, mkdir } from "fs/promises";
23712
+ import { renameSync as renameSync3 } from "fs";
23713
+ import { mkdir } from "fs/promises";
22779
23714
  import { homedir } from "os";
22780
23715
  import { join as join3 } from "path";
22781
23716
  function defaultDataDir() {
@@ -22790,6 +23725,9 @@ function dataDir(base = defaultDataDir()) {
22790
23725
  function dbPath(base = defaultDataDir()) {
22791
23726
  return join3(dataDir(base), "aka.db");
22792
23727
  }
23728
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
23729
+ ensureDataDirSync(dir);
23730
+ }
22793
23731
  function migrateLegacyLayout(base = defaultDataDir()) {
22794
23732
  const moves = [
22795
23733
  { name: "config.json", dest: settingsDir(base) },
@@ -22797,19 +23735,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
22797
23735
  ];
22798
23736
  for (const { name, dest } of moves) {
22799
23737
  try {
22800
- mkdirSync2(dest, { recursive: true, mode: DATA_DIR_MODE });
22801
- try {
22802
- chmodSync3(dest, DATA_DIR_MODE);
22803
- } catch {
22804
- }
22805
- renameSync3(join3(base, name), join3(dest, name));
23738
+ ensureDataDirSync(dest);
23739
+ const moved = join3(dest, name);
23740
+ renameSync3(join3(base, name), moved);
23741
+ tightenFile(moved);
22806
23742
  } catch {
22807
23743
  }
22808
23744
  }
22809
23745
  }
22810
23746
 
22811
23747
  // ../../packages/persistence/src/settings.ts
22812
- import { readFileSync as readFileSync2, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
23748
+ import { readFileSync as readFileSync2 } from "fs";
22813
23749
  import { join as join4 } from "path";
22814
23750
  function readWorkspaceSettings(base = defaultDataDir()) {
22815
23751
  const record2 = readJson(join4(settingsDir(base), "settings.json"));
@@ -22831,7 +23767,7 @@ function readJson(file2) {
22831
23767
  }
22832
23768
 
22833
23769
  // ../../packages/persistence/src/warn-era-cap.ts
22834
- import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
23770
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
22835
23771
  import { join as join5 } from "path";
22836
23772
  var MARKER = "warn-era-capped";
22837
23773
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
@@ -22839,7 +23775,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22839
23775
  const marker = join5(dataDir2, MARKER);
22840
23776
  if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22841
23777
  const capped = db.policies.capCategoryActions();
22842
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
23778
+ writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
22843
23779
  `, { mode: DATA_FILE_MODE });
22844
23780
  return { capped };
22845
23781
  }
@@ -22894,6 +23830,12 @@ function resolveProvider() {
22894
23830
 
22895
23831
  // ../../packages/plugin-sdk/src/config.ts
22896
23832
  function loadConfig(base = defaultDataDir()) {
23833
+ try {
23834
+ ensureLayoutDirSync(base);
23835
+ const settingsFile = join6(settingsDir(base), "settings.json");
23836
+ if (existsSync3(settingsFile)) tightenFile(settingsFile);
23837
+ } catch {
23838
+ }
22897
23839
  migrateLegacyLayout(base);
22898
23840
  const settings = readWorkspaceSettings(base);
22899
23841
  return {
@@ -22916,15 +23858,583 @@ function resolveProviderSafe() {
22916
23858
  // ../../packages/plugin-sdk/src/config-inventory.ts
22917
23859
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22918
23860
  import { homedir as homedir2 } from "os";
22919
- import { basename as basename2, join as join7 } from "path";
23861
+ import { basename as basename2, join as join8 } from "path";
23862
+
23863
+ // ../../packages/detections/src/egress/registry.ts
23864
+ var EXTRACTOR_VERSION = "1";
23865
+ var PROVIDER_REGISTRY = [
23866
+ {
23867
+ id: "stripe",
23868
+ name: "Stripe",
23869
+ category: "Payments",
23870
+ hostSuffixes: ["stripe.com"],
23871
+ apiBase: "https://api.stripe.com",
23872
+ defaultDataClasses: ["pii", "customer"],
23873
+ sdks: {
23874
+ npm: ["stripe"],
23875
+ pypi: ["stripe"],
23876
+ go: ["github.com/stripe/stripe-go"],
23877
+ maven: ["com.stripe"],
23878
+ rubygems: ["stripe"],
23879
+ composer: ["stripe/stripe-php"],
23880
+ nuget: ["Stripe.net"]
23881
+ }
23882
+ },
23883
+ {
23884
+ id: "datadog",
23885
+ name: "Datadog",
23886
+ category: "Observability",
23887
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23888
+ apiBase: "https://api.datadoghq.com",
23889
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23890
+ sdks: {
23891
+ npm: ["dd-trace", "@datadog/browser-logs"],
23892
+ pypi: ["datadog", "ddtrace"],
23893
+ go: ["github.com/DataDog/dd-trace-go"],
23894
+ maven: ["com.datadoghq"],
23895
+ rubygems: ["ddtrace", "dogapi"],
23896
+ nuget: ["Datadog.Trace"]
23897
+ }
23898
+ },
23899
+ {
23900
+ id: "newrelic",
23901
+ name: "New Relic",
23902
+ category: "Observability",
23903
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23904
+ apiBase: "https://api.newrelic.com",
23905
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23906
+ sdks: {
23907
+ npm: ["newrelic"],
23908
+ pypi: ["newrelic"],
23909
+ go: ["github.com/newrelic/go-agent"],
23910
+ maven: ["com.newrelic.agent.java"],
23911
+ rubygems: ["newrelic_rpm"],
23912
+ nuget: ["NewRelic.Agent"]
23913
+ }
23914
+ },
23915
+ {
23916
+ id: "sentry",
23917
+ name: "Sentry",
23918
+ category: "Error tracking",
23919
+ hostSuffixes: ["sentry.io"],
23920
+ apiBase: "https://sentry.io",
23921
+ defaultDataClasses: ["source", "telemetry"],
23922
+ sdks: {
23923
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23924
+ pypi: ["sentry-sdk"],
23925
+ go: ["github.com/getsentry/sentry-go"],
23926
+ maven: ["io.sentry"],
23927
+ rubygems: ["sentry-ruby"],
23928
+ cargo: ["sentry"],
23929
+ composer: ["sentry/sentry"],
23930
+ nuget: ["Sentry"]
23931
+ }
23932
+ },
23933
+ {
23934
+ id: "openai",
23935
+ name: "OpenAI",
23936
+ category: "LLM provider",
23937
+ hostSuffixes: ["openai.com"],
23938
+ apiBase: "https://api.openai.com",
23939
+ defaultDataClasses: ["pii", "source"],
23940
+ sdks: {
23941
+ npm: ["openai"],
23942
+ pypi: ["openai"],
23943
+ go: ["github.com/sashabaranov/go-openai"],
23944
+ maven: ["com.openai"],
23945
+ rubygems: ["ruby-openai"],
23946
+ cargo: ["async-openai"],
23947
+ composer: ["openai-php/client"],
23948
+ nuget: ["OpenAI"]
23949
+ }
23950
+ },
23951
+ {
23952
+ id: "anthropic",
23953
+ name: "Anthropic",
23954
+ category: "LLM provider",
23955
+ hostSuffixes: ["anthropic.com"],
23956
+ apiBase: "https://api.anthropic.com",
23957
+ defaultDataClasses: ["pii", "source"],
23958
+ sdks: {
23959
+ npm: ["@anthropic-ai/sdk"],
23960
+ pypi: ["anthropic"],
23961
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23962
+ nuget: ["Anthropic.SDK"]
23963
+ }
23964
+ },
23965
+ {
23966
+ id: "aws",
23967
+ name: "Amazon Web Services",
23968
+ category: "Cloud platform",
23969
+ hostSuffixes: ["amazonaws.com"],
23970
+ apiBase: "https://s3.amazonaws.com",
23971
+ defaultDataClasses: ["secrets", "customer"],
23972
+ sdks: {
23973
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23974
+ pypi: ["boto3"],
23975
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23976
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23977
+ rubygems: ["aws-sdk-s3"],
23978
+ cargo: ["aws-sdk-s3"],
23979
+ nuget: ["AWSSDK.S3"]
23980
+ }
23981
+ },
23982
+ {
23983
+ id: "gcp",
23984
+ name: "Google Cloud",
23985
+ category: "Cloud platform",
23986
+ hostSuffixes: ["googleapis.com"],
23987
+ apiBase: "https://storage.googleapis.com",
23988
+ defaultDataClasses: ["customer", "logs"],
23989
+ sdks: {
23990
+ npm: ["@google-cloud/storage"],
23991
+ pypi: ["google-cloud-storage"],
23992
+ go: ["cloud.google.com/go"],
23993
+ maven: ["com.google.cloud"],
23994
+ rubygems: ["google-cloud-storage"],
23995
+ nuget: ["Google.Cloud.Storage.V1"]
23996
+ }
23997
+ },
23998
+ {
23999
+ id: "azure",
24000
+ name: "Microsoft Azure",
24001
+ category: "Cloud platform",
24002
+ hostSuffixes: ["azure.com", "windows.net"],
24003
+ apiBase: "https://management.azure.com",
24004
+ defaultDataClasses: ["customer", "logs"],
24005
+ sdks: {
24006
+ npm: ["@azure/storage-blob"],
24007
+ pypi: ["azure-storage-blob"],
24008
+ go: ["github.com/Azure/azure-sdk-for-go"],
24009
+ maven: ["com.azure"],
24010
+ rubygems: ["azure-storage-blob"],
24011
+ nuget: ["Azure.Storage.Blobs"]
24012
+ }
24013
+ },
24014
+ {
24015
+ id: "slack",
24016
+ name: "Slack",
24017
+ category: "Notifications",
24018
+ hostSuffixes: ["slack.com"],
24019
+ apiBase: "https://slack.com/api",
24020
+ defaultDataClasses: ["logs"],
24021
+ sdks: {
24022
+ npm: ["@slack/web-api"],
24023
+ pypi: ["slack-sdk"],
24024
+ go: ["github.com/slack-go/slack"],
24025
+ maven: ["com.slack.api"],
24026
+ rubygems: ["slack-ruby-client"],
24027
+ composer: ["slack-php/slack-api"],
24028
+ nuget: ["SlackNet"]
24029
+ }
24030
+ },
24031
+ {
24032
+ id: "segment",
24033
+ name: "Segment",
24034
+ category: "Analytics",
24035
+ hostSuffixes: ["segment.io", "segment.com"],
24036
+ apiBase: "https://api.segment.io",
24037
+ defaultDataClasses: ["customer"],
24038
+ sdks: {
24039
+ npm: ["@segment/analytics-node", "analytics-node"],
24040
+ pypi: ["segment-analytics-python"],
24041
+ go: ["github.com/segmentio/analytics-go"],
24042
+ maven: ["com.segment.analytics.java"],
24043
+ rubygems: ["analytics-ruby"],
24044
+ nuget: ["Analytics"]
24045
+ }
24046
+ },
24047
+ {
24048
+ id: "twilio",
24049
+ name: "Twilio",
24050
+ category: "Communications",
24051
+ hostSuffixes: ["twilio.com"],
24052
+ apiBase: "https://api.twilio.com",
24053
+ defaultDataClasses: ["pii", "customer"],
24054
+ sdks: {
24055
+ npm: ["twilio"],
24056
+ pypi: ["twilio"],
24057
+ go: ["github.com/twilio/twilio-go"],
24058
+ maven: ["com.twilio.sdk"],
24059
+ rubygems: ["twilio-ruby"],
24060
+ composer: ["twilio/sdk"],
24061
+ nuget: ["Twilio"]
24062
+ }
24063
+ },
24064
+ {
24065
+ id: "sendgrid",
24066
+ name: "SendGrid",
24067
+ category: "Email",
24068
+ hostSuffixes: ["sendgrid.com"],
24069
+ apiBase: "https://api.sendgrid.com",
24070
+ defaultDataClasses: ["pii"],
24071
+ sdks: {
24072
+ npm: ["@sendgrid/mail"],
24073
+ pypi: ["sendgrid"],
24074
+ go: ["github.com/sendgrid/sendgrid-go"],
24075
+ maven: ["com.sendgrid"],
24076
+ rubygems: ["sendgrid-ruby"],
24077
+ composer: ["sendgrid/sendgrid"],
24078
+ nuget: ["SendGrid"]
24079
+ }
24080
+ },
24081
+ {
24082
+ id: "mailgun",
24083
+ name: "Mailgun",
24084
+ category: "Email",
24085
+ hostSuffixes: ["mailgun.net"],
24086
+ apiBase: "https://api.mailgun.net",
24087
+ defaultDataClasses: ["pii"],
24088
+ sdks: {
24089
+ npm: ["mailgun.js"],
24090
+ pypi: ["mailgun"],
24091
+ rubygems: ["mailgun-ruby"],
24092
+ composer: ["mailgun/mailgun-php"],
24093
+ nuget: ["Mailgun"]
24094
+ }
24095
+ },
24096
+ {
24097
+ id: "mixpanel",
24098
+ name: "Mixpanel",
24099
+ category: "Analytics",
24100
+ hostSuffixes: ["mixpanel.com"],
24101
+ apiBase: "https://api.mixpanel.com",
24102
+ defaultDataClasses: ["customer", "telemetry"],
24103
+ sdks: {
24104
+ npm: ["mixpanel"],
24105
+ pypi: ["mixpanel"],
24106
+ rubygems: ["mixpanel-ruby"],
24107
+ nuget: ["Mixpanel"]
24108
+ }
24109
+ },
24110
+ {
24111
+ id: "amplitude",
24112
+ name: "Amplitude",
24113
+ category: "Analytics",
24114
+ hostSuffixes: ["amplitude.com"],
24115
+ apiBase: "https://api2.amplitude.com",
24116
+ defaultDataClasses: ["customer", "telemetry"],
24117
+ sdks: {
24118
+ npm: ["@amplitude/analytics-node"],
24119
+ pypi: ["amplitude-analytics"],
24120
+ nuget: ["Amplitude"]
24121
+ }
24122
+ },
24123
+ {
24124
+ id: "posthog",
24125
+ name: "PostHog",
24126
+ category: "Analytics",
24127
+ hostSuffixes: ["posthog.com"],
24128
+ apiBase: "https://us.i.posthog.com",
24129
+ defaultDataClasses: ["customer", "telemetry"],
24130
+ sdks: {
24131
+ npm: ["posthog-node", "posthog-js"],
24132
+ pypi: ["posthog"],
24133
+ go: ["github.com/posthog/posthog-go"],
24134
+ rubygems: ["posthog-ruby"],
24135
+ composer: ["posthog/posthog-php"],
24136
+ nuget: ["PostHog"]
24137
+ }
24138
+ },
24139
+ {
24140
+ id: "honeycomb",
24141
+ name: "Honeycomb",
24142
+ category: "Observability",
24143
+ hostSuffixes: ["honeycomb.io"],
24144
+ apiBase: "https://api.honeycomb.io",
24145
+ defaultDataClasses: ["telemetry", "metrics"],
24146
+ sdks: {
24147
+ npm: ["libhoney"],
24148
+ pypi: ["libhoney"],
24149
+ go: ["github.com/honeycombio/libhoney-go"],
24150
+ rubygems: ["libhoney"]
24151
+ }
24152
+ },
24153
+ {
24154
+ id: "grafana",
24155
+ name: "Grafana Cloud",
24156
+ category: "Observability",
24157
+ hostSuffixes: ["grafana.net"],
24158
+ apiBase: "https://grafana.net",
24159
+ defaultDataClasses: ["logs", "metrics"],
24160
+ sdks: {
24161
+ npm: ["@grafana/faro-web-sdk"]
24162
+ }
24163
+ },
24164
+ {
24165
+ id: "splunk",
24166
+ name: "Splunk",
24167
+ category: "Observability",
24168
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
24169
+ apiBase: "https://http-inputs.splunkcloud.com",
24170
+ defaultDataClasses: ["logs"],
24171
+ sdks: {
24172
+ npm: ["splunk-logging"],
24173
+ pypi: ["splunk-sdk"],
24174
+ maven: ["com.splunk"],
24175
+ nuget: ["Splunk.Logging.Common"]
24176
+ }
24177
+ },
24178
+ {
24179
+ id: "pagerduty",
24180
+ name: "PagerDuty",
24181
+ category: "Incident response",
24182
+ hostSuffixes: ["pagerduty.com"],
24183
+ apiBase: "https://api.pagerduty.com",
24184
+ defaultDataClasses: ["logs"],
24185
+ sdks: {
24186
+ npm: ["@pagerduty/pdjs"],
24187
+ pypi: ["pdpyras"],
24188
+ go: ["github.com/PagerDuty/go-pagerduty"],
24189
+ rubygems: ["pagerduty"]
24190
+ }
24191
+ },
24192
+ {
24193
+ id: "github",
24194
+ name: "GitHub",
24195
+ category: "Developer platform",
24196
+ hostSuffixes: ["github.com", "githubusercontent.com"],
24197
+ apiBase: "https://api.github.com",
24198
+ defaultDataClasses: ["source"],
24199
+ sdks: {
24200
+ npm: ["@octokit/rest", "octokit"],
24201
+ pypi: ["pygithub"],
24202
+ go: ["github.com/google/go-github"],
24203
+ maven: ["org.kohsuke.github-api"],
24204
+ rubygems: ["octokit"],
24205
+ cargo: ["octocrab"],
24206
+ composer: ["knplabs/github-api"],
24207
+ nuget: ["Octokit"]
24208
+ }
24209
+ },
24210
+ {
24211
+ id: "gitlab",
24212
+ name: "GitLab",
24213
+ category: "Developer platform",
24214
+ hostSuffixes: ["gitlab.com"],
24215
+ apiBase: "https://gitlab.com/api",
24216
+ defaultDataClasses: ["source"],
24217
+ sdks: {
24218
+ npm: ["@gitbeaker/rest"],
24219
+ pypi: ["python-gitlab"],
24220
+ go: ["gitlab.com/gitlab-org/api/client-go"],
24221
+ rubygems: ["gitlab"],
24222
+ nuget: ["GitLabApiClient"]
24223
+ }
24224
+ },
24225
+ {
24226
+ id: "auth0",
24227
+ name: "Auth0",
24228
+ category: "Identity",
24229
+ hostSuffixes: ["auth0.com"],
24230
+ apiBase: "https://login.auth0.com",
24231
+ defaultDataClasses: ["pii"],
24232
+ sdks: {
24233
+ npm: ["auth0"],
24234
+ pypi: ["auth0-python"],
24235
+ go: ["github.com/auth0/go-auth0"],
24236
+ maven: ["com.auth0"],
24237
+ rubygems: ["auth0"],
24238
+ composer: ["auth0/auth0-php"],
24239
+ nuget: ["Auth0.ManagementApi"]
24240
+ }
24241
+ },
24242
+ {
24243
+ id: "okta",
24244
+ name: "Okta",
24245
+ category: "Identity",
24246
+ hostSuffixes: ["okta.com", "oktapreview.com"],
24247
+ apiBase: "https://login.okta.com",
24248
+ defaultDataClasses: ["pii"],
24249
+ sdks: {
24250
+ npm: ["@okta/okta-sdk-nodejs"],
24251
+ pypi: ["okta"],
24252
+ go: ["github.com/okta/okta-sdk-golang"],
24253
+ maven: ["com.okta.sdk"],
24254
+ nuget: ["Okta.Sdk"]
24255
+ }
24256
+ },
24257
+ {
24258
+ id: "clerk",
24259
+ name: "Clerk",
24260
+ category: "Identity",
24261
+ hostSuffixes: ["clerk.com", "clerk.dev"],
24262
+ apiBase: "https://api.clerk.com",
24263
+ defaultDataClasses: ["pii"],
24264
+ sdks: {
24265
+ npm: ["@clerk/backend", "@clerk/nextjs"],
24266
+ pypi: ["clerk-backend-api"],
24267
+ go: ["github.com/clerk/clerk-sdk-go"]
24268
+ }
24269
+ },
24270
+ {
24271
+ id: "supabase",
24272
+ name: "Supabase",
24273
+ category: "Backend platform",
24274
+ hostSuffixes: ["supabase.co", "supabase.com"],
24275
+ apiBase: "https://api.supabase.com",
24276
+ defaultDataClasses: ["pii", "customer"],
24277
+ sdks: {
24278
+ npm: ["@supabase/supabase-js"],
24279
+ pypi: ["supabase"],
24280
+ cargo: ["postgrest"]
24281
+ }
24282
+ },
24283
+ {
24284
+ id: "firebase",
24285
+ name: "Firebase",
24286
+ category: "Backend platform",
24287
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
24288
+ apiBase: "https://firebaseio.com",
24289
+ defaultDataClasses: ["customer"],
24290
+ sdks: {
24291
+ npm: ["firebase", "firebase-admin"],
24292
+ pypi: ["firebase-admin"],
24293
+ go: ["firebase.google.com/go"],
24294
+ maven: ["com.google.firebase"]
24295
+ }
24296
+ },
24297
+ {
24298
+ id: "mongodb-atlas",
24299
+ name: "MongoDB Atlas",
24300
+ category: "Database SaaS",
24301
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
24302
+ apiBase: "https://cloud.mongodb.com",
24303
+ defaultDataClasses: ["customer"],
24304
+ sdks: {
24305
+ npm: ["mongodb"],
24306
+ pypi: ["pymongo"],
24307
+ go: ["go.mongodb.org/mongo-driver"],
24308
+ maven: ["org.mongodb"],
24309
+ rubygems: ["mongo"],
24310
+ cargo: ["mongodb"],
24311
+ nuget: ["MongoDB.Driver"]
24312
+ }
24313
+ },
24314
+ {
24315
+ id: "planetscale",
24316
+ name: "PlanetScale",
24317
+ category: "Database SaaS",
24318
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
24319
+ apiBase: "https://api.planetscale.com",
24320
+ defaultDataClasses: ["customer"],
24321
+ sdks: {
24322
+ npm: ["@planetscale/database"],
24323
+ go: ["github.com/planetscale/planetscale-go"]
24324
+ }
24325
+ },
24326
+ {
24327
+ id: "algolia",
24328
+ name: "Algolia",
24329
+ category: "Search SaaS",
24330
+ hostSuffixes: ["algolia.net", "algolianet.com"],
24331
+ apiBase: "https://algolia.net",
24332
+ defaultDataClasses: ["customer"],
24333
+ sdks: {
24334
+ npm: ["algoliasearch"],
24335
+ pypi: ["algoliasearch"],
24336
+ go: ["github.com/algolia/algoliasearch-client-go"],
24337
+ maven: ["com.algolia"],
24338
+ rubygems: ["algolia"],
24339
+ composer: ["algolia/algoliasearch-client-php"],
24340
+ nuget: ["Algolia.Search"]
24341
+ }
24342
+ },
24343
+ {
24344
+ id: "cloudflare",
24345
+ name: "Cloudflare",
24346
+ category: "CDN / edge",
24347
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
24348
+ apiBase: "https://api.cloudflare.com",
24349
+ defaultDataClasses: ["logs"],
24350
+ sdks: {
24351
+ npm: ["cloudflare"],
24352
+ pypi: ["cloudflare"],
24353
+ go: ["github.com/cloudflare/cloudflare-go"],
24354
+ nuget: ["CloudFlare.Client"]
24355
+ }
24356
+ },
24357
+ {
24358
+ id: "huggingface",
24359
+ name: "Hugging Face",
24360
+ category: "LLM provider",
24361
+ hostSuffixes: ["huggingface.co"],
24362
+ apiBase: "https://api-inference.huggingface.co",
24363
+ defaultDataClasses: ["source"],
24364
+ sdks: {
24365
+ npm: ["@huggingface/inference"],
24366
+ pypi: ["huggingface-hub", "transformers"],
24367
+ rubygems: ["hugging-face"]
24368
+ }
24369
+ },
24370
+ {
24371
+ id: "cohere",
24372
+ name: "Cohere",
24373
+ category: "LLM provider",
24374
+ hostSuffixes: ["cohere.com", "cohere.ai"],
24375
+ apiBase: "https://api.cohere.com",
24376
+ defaultDataClasses: ["pii", "source"],
24377
+ sdks: {
24378
+ npm: ["cohere-ai"],
24379
+ pypi: ["cohere"],
24380
+ go: ["github.com/cohere-ai/cohere-go"]
24381
+ }
24382
+ },
24383
+ {
24384
+ id: "mistral",
24385
+ name: "Mistral AI",
24386
+ category: "LLM provider",
24387
+ hostSuffixes: ["mistral.ai"],
24388
+ apiBase: "https://api.mistral.ai",
24389
+ defaultDataClasses: ["pii", "source"],
24390
+ sdks: {
24391
+ npm: ["@mistralai/mistralai"],
24392
+ pypi: ["mistralai"],
24393
+ go: ["github.com/gage-technologies/mistral-go"]
24394
+ }
24395
+ }
24396
+ ];
24397
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
24398
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
24399
+
24400
+ // ../../packages/detections/src/egress/extract.ts
24401
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
24402
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
24403
+ var SECRET_VALUE = new RegExp(
24404
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
24405
+ "gi"
24406
+ );
24407
+ var AUTH_SCHEME_VALUE = new RegExp(
24408
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
24409
+ "gi"
24410
+ );
24411
+ var WEBHOOK_SECRET_PATHS = [
24412
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
24413
+ {
24414
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
24415
+ prefix: "/api/webhooks/"
24416
+ },
24417
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
24418
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
24419
+ ];
24420
+ function escapeRegExp(literal2) {
24421
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24422
+ }
24423
+ var WEBHOOK_URL = new RegExp(
24424
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
24425
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
24426
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
24427
+ "gi"
24428
+ );
22920
24429
 
22921
24430
  // ../../packages/detections/src/escape-regexp.ts
22922
- function escapeRegExp(value) {
24431
+ function escapeRegExp2(value) {
22923
24432
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22924
24433
  }
22925
24434
 
22926
24435
  // ../../packages/detections/src/matchers/limits.ts
22927
24436
  var MAX_MATCHES_PER_RULE = 1e4;
24437
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22928
24438
 
22929
24439
  // ../../packages/detections/src/matchers/keyword.ts
22930
24440
  var KeywordMatcher2 = class {
@@ -22935,7 +24445,7 @@ var KeywordMatcher2 = class {
22935
24445
  for (const kw of keywords) {
22936
24446
  if (kw.length === 0) continue;
22937
24447
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22938
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
24448
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22939
24449
  let m;
22940
24450
  while ((m = re.exec(text)) !== null) {
22941
24451
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22952,9 +24462,13 @@ var RegexMatcher2 = class {
22952
24462
  if (rule.matcher.type !== "regex") return [];
22953
24463
  const { pattern, flags, captureGroup } = rule.matcher;
22954
24464
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
24465
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22955
24466
  const spans = [];
22956
24467
  let m;
22957
- while ((m = re.exec(text)) !== null) {
24468
+ const maxIterations = scanText2.length + 1;
24469
+ let iterations = 0;
24470
+ while ((m = re.exec(scanText2)) !== null) {
24471
+ if (++iterations > maxIterations) break;
22958
24472
  const group = captureGroup != null ? m[captureGroup] : m[0];
22959
24473
  if (m[0].length === 0) re.lastIndex++;
22960
24474
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23058,7 +24572,7 @@ function isCorroborated(candidate, candidates, text) {
23058
24572
  for (const label of labels) {
23059
24573
  const trimmed = label.trim();
23060
24574
  if (trimmed.length === 0) continue;
23061
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24575
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23062
24576
  if (re.test(haystack)) return true;
23063
24577
  }
23064
24578
  }
@@ -23223,6 +24737,112 @@ var CONFIG_POSTURE_RULES = [
23223
24737
  }
23224
24738
  ];
23225
24739
 
24740
+ // ../../packages/detections/src/security/redos-probe.ts
24741
+ var BUDGET_MS = 100;
24742
+ var EXPONENTIAL_UNITS = [
24743
+ "a",
24744
+ "0",
24745
+ " ",
24746
+ "x",
24747
+ "ab",
24748
+ "a.",
24749
+ "a-",
24750
+ "a_",
24751
+ "a@",
24752
+ "a/",
24753
+ "a:",
24754
+ "a=",
24755
+ "a;",
24756
+ "aA0",
24757
+ " "
24758
+ ];
24759
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24760
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24761
+ );
24762
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24763
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24764
+ );
24765
+ function literalPrefix(pattern) {
24766
+ let prefix = "";
24767
+ let i = 0;
24768
+ if (pattern[i] === "^") i++;
24769
+ while (i < pattern.length) {
24770
+ const c = pattern[i];
24771
+ if (c === void 0) break;
24772
+ if (c === "\\") {
24773
+ const next = pattern[i + 1];
24774
+ if (next === "b" || next === "B") {
24775
+ i += 2;
24776
+ continue;
24777
+ }
24778
+ if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
24779
+ prefix += next;
24780
+ i += 2;
24781
+ continue;
24782
+ }
24783
+ if ("([{.*+?|)]}^$".includes(c)) break;
24784
+ prefix += c;
24785
+ i++;
24786
+ }
24787
+ return prefix;
24788
+ }
24789
+ function fuelChars(pattern) {
24790
+ const fuel = /* @__PURE__ */ new Set();
24791
+ for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
24792
+ const body = m[1];
24793
+ if (body === void 0) continue;
24794
+ const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
24795
+ const rangeStart = range?.[1];
24796
+ if (rangeStart !== void 0) fuel.add(rangeStart);
24797
+ else {
24798
+ const literal2 = body.replace(/\\/g, "")[0];
24799
+ if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
24800
+ }
24801
+ }
24802
+ if (pattern.includes("\\w")) fuel.add("a");
24803
+ if (pattern.includes("\\d")) fuel.add("0");
24804
+ if (pattern.includes("\\s")) fuel.add(" ");
24805
+ if (/(?<!\\)\./.test(pattern)) fuel.add("a");
24806
+ if (fuel.size === 0) fuel.add("a");
24807
+ return [...fuel];
24808
+ }
24809
+ function derivedProbes(pattern) {
24810
+ const prefix = literalPrefix(pattern);
24811
+ const fuel = fuelChars(pattern);
24812
+ const terminators = ["!", "#", "~", "\n"];
24813
+ const probes = [];
24814
+ for (const f of fuel) {
24815
+ for (const term of terminators) {
24816
+ if (term === f) continue;
24817
+ for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
24818
+ }
24819
+ }
24820
+ return probes;
24821
+ }
24822
+ function probesFor(rule) {
24823
+ const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
24824
+ return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
24825
+ }
24826
+ function worstProbeMs(rule) {
24827
+ let ms = 0;
24828
+ let probe = "";
24829
+ for (const text of probesFor(rule)) {
24830
+ const start = performance.now();
24831
+ scan(text, [rule]);
24832
+ const elapsed = performance.now() - start;
24833
+ if (elapsed > ms) {
24834
+ ms = elapsed;
24835
+ probe = text;
24836
+ }
24837
+ if (ms >= BUDGET_MS) break;
24838
+ }
24839
+ return { ms, probe };
24840
+ }
24841
+ function checkRuleTiming(rule) {
24842
+ const { ms, probe } = worstProbeMs(rule);
24843
+ return { safe: ms < BUDGET_MS, worstMs: ms, probe };
24844
+ }
24845
+
23226
24846
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23227
24847
  var auth_jwt_no_verify_default = {
23228
24848
  specVersion: 1,
@@ -25259,8 +26879,8 @@ function uniqueRuleIds(findings) {
25259
26879
  }
25260
26880
 
25261
26881
  // ../../packages/plugin-sdk/src/repo.ts
25262
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25263
- import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
26882
+ import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
26883
+ import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
25264
26884
  function resolveRepo(cwd) {
25265
26885
  try {
25266
26886
  const root = findGitRoot(cwd);
@@ -25275,32 +26895,32 @@ function resolveRepo(cwd) {
25275
26895
  function findGitRoot(start) {
25276
26896
  let dir = start;
25277
26897
  for (; ; ) {
25278
- if (existsSync3(join6(dir, ".git"))) return dir;
26898
+ if (existsSync4(join7(dir, ".git"))) return dir;
25279
26899
  const parent = dirname(dir);
25280
26900
  if (parent === dir) return void 0;
25281
26901
  dir = parent;
25282
26902
  }
25283
26903
  }
25284
26904
  function resolveGitContext(root) {
25285
- const dotGit = join6(root, ".git");
26905
+ const dotGit = join7(root, ".git");
25286
26906
  try {
25287
26907
  if (statSync(dotGit).isDirectory()) {
25288
- return { configPath: join6(dotGit, "config"), headRoot: root };
26908
+ return { configPath: join7(dotGit, "config"), headRoot: root };
25289
26909
  }
25290
26910
  } catch {
25291
26911
  return void 0;
25292
26912
  }
25293
26913
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
25294
26914
  if (!target) return void 0;
25295
- const gitdir = isAbsolute(target) ? target : join6(root, target);
25296
- if (existsSync3(join6(gitdir, "config"))) {
25297
- return { configPath: join6(gitdir, "config"), headRoot: root };
26915
+ const gitdir = isAbsolute(target) ? target : join7(root, target);
26916
+ if (existsSync4(join7(gitdir, "config"))) {
26917
+ return { configPath: join7(gitdir, "config"), headRoot: root };
25298
26918
  }
25299
- const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
26919
+ const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
25300
26920
  if (!commonRaw) return void 0;
25301
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
26921
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
25302
26922
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
25303
- return { configPath: join6(commonGitDir, "config"), headRoot };
26923
+ return { configPath: join7(commonGitDir, "config"), headRoot };
25304
26924
  }
25305
26925
  function safeRead(path) {
25306
26926
  try {
@@ -25342,9 +26962,9 @@ function slugFromUrl(url2) {
25342
26962
  }
25343
26963
 
25344
26964
  // ../../packages/plugin-sdk/src/events.ts
25345
- import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
26965
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
25346
26966
  function contentHashOf(text) {
25347
- return createHash3("sha256").update(text).digest("hex");
26967
+ return createHash4("sha256").update(text).digest("hex");
25348
26968
  }
25349
26969
  function buildIngestEvent(input) {
25350
26970
  return {
@@ -25364,27 +26984,74 @@ function buildIngestEvent(input) {
25364
26984
  };
25365
26985
  }
25366
26986
 
25367
- // ../../packages/plugin-sdk/src/finding-key.ts
25368
- import { createHash as createHash4 } from "crypto";
25369
- function normalizeFilePath(filePath) {
25370
- return filePath.replaceAll("\\", "/");
25371
- }
25372
- function computeFindingKey(input) {
25373
- const normalizedPath = normalizeFilePath(input.filePath);
25374
- return createHash4("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
25375
- }
25376
-
25377
26987
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
25378
26988
  import { arch, hostname as hostname3, platform, release } from "os";
25379
26989
 
25380
26990
  // ../../packages/plugin-sdk/src/nudge.ts
25381
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25382
- import { join as join8 } from "path";
26991
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
26992
+ import { join as join9 } from "path";
26993
+
26994
+ // ../../packages/plugin-sdk/src/paths.ts
26995
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26996
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
25383
26997
 
25384
26998
  // ../../packages/plugin-sdk/src/project-files.ts
25385
26999
  var import_ignore = __toESM(require_ignore(), 1);
25386
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25387
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
27000
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27001
+ import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
27002
+
27003
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
27004
+ var PASS_BUDGET_MS = 2e3;
27005
+ function ruleProbeKey(rule) {
27006
+ if (rule.matcher.type !== "regex") return void 0;
27007
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
27008
+ }
27009
+ function warnQuarantined(rule, worstMs) {
27010
+ const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
27011
+ process.stderr.write(
27012
+ `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
27013
+ `
27014
+ );
27015
+ }
27016
+ async function filterUnsafeRules(rules, gateway, opts) {
27017
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
27018
+ const passStart = performance.now();
27019
+ const safe = [];
27020
+ for (const rule of rules) {
27021
+ const key = ruleProbeKey(rule);
27022
+ if (key === void 0) {
27023
+ safe.push(rule);
27024
+ continue;
27025
+ }
27026
+ let cached2;
27027
+ try {
27028
+ cached2 = await gateway.getRuleProbeVerdict(key);
27029
+ } catch {
27030
+ cached2 = void 0;
27031
+ }
27032
+ if (cached2) {
27033
+ if (cached2.verdict === "safe") safe.push(rule);
27034
+ else warnQuarantined(rule, cached2.worstProbeMs);
27035
+ continue;
27036
+ }
27037
+ if (performance.now() - passStart >= passBudgetMs) {
27038
+ warnQuarantined(rule, void 0);
27039
+ continue;
27040
+ }
27041
+ let isSafe;
27042
+ let worstMs;
27043
+ try {
27044
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
27045
+ } catch {
27046
+ isSafe = false;
27047
+ worstMs = Number.POSITIVE_INFINITY;
27048
+ }
27049
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
27050
+ if (isSafe) safe.push(rule);
27051
+ else warnQuarantined(rule, worstMs);
27052
+ }
27053
+ return safe;
27054
+ }
25388
27055
 
25389
27056
  // ../../packages/plugin-sdk/src/runtime.ts
25390
27057
  import { randomUUID as randomUUID10 } from "crypto";
@@ -25428,7 +27095,17 @@ function createPluginRuntime(gateway, settings, opts) {
25428
27095
  categoryActionIndex.set(p.target.category, p.action);
25429
27096
  }
25430
27097
  }
25431
- rules = bundle.rulesComplete ? bundle.rules ?? [] : [...getLoadedRules(), ...bundle.rules ?? []];
27098
+ const bundledProbeKeys = new Set(
27099
+ getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
27100
+ );
27101
+ const incoming = bundle.rules ?? [];
27102
+ const ciVerified = incoming.filter((rule) => {
27103
+ const key = ruleProbeKey(rule);
27104
+ return key !== void 0 && bundledProbeKeys.has(key);
27105
+ });
27106
+ const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
27107
+ const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
27108
+ rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
25432
27109
  bundleExceptions = bundle.exceptions ?? [];
25433
27110
  initialized = true;
25434
27111
  }
@@ -25671,8 +27348,8 @@ function createPluginRuntime(gateway, settings, opts) {
25671
27348
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25672
27349
 
25673
27350
  // ../../packages/plugin-sdk/src/throttle.ts
25674
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25675
- import { join as join10 } from "path";
27351
+ import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27352
+ import { join as join11 } from "path";
25676
27353
 
25677
27354
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
25678
27355
  import { randomUUID as randomUUID11 } from "crypto";
@@ -25703,7 +27380,8 @@ var StandaloneDataGateway = class {
25703
27380
  }
25704
27381
  // The id is minted inside the repository from the natural key — the plugin can't
25705
27382
  // import @akasecurity/persistence to compute it, so the gateway is the boundary that
25706
- // hands the natural key across. INSERT OR IGNORE → idempotent re-reads.
27383
+ // hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
27384
+ // converge a streaming partial/final split (see insertLlmCall).
25707
27385
  recordLlmCall(input) {
25708
27386
  this.db.auditEvents.insertLlmCall(input);
25709
27387
  return Promise.resolve();
@@ -25745,7 +27423,9 @@ var StandaloneDataGateway = class {
25745
27423
  // caller's transaction (Layer 2b). The audit-event id the findings FK into is the
25746
27424
  // SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
25747
27425
  // idempotently. Definitions/classified-data are idempotent upserts; findings are
25748
- // content-addressed INSERT OR IGNORE.
27426
+ // content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
27427
+ // so a re-detection under a bumped rule version repoints the definition FK rather
27428
+ // than no-opping.
25749
27429
  writeToolCall(input) {
25750
27430
  this.db.auditEvents.insertToolCall(input);
25751
27431
  if (input.inspections.length === 0) return;
@@ -25765,7 +27445,7 @@ var StandaloneDataGateway = class {
25765
27445
  });
25766
27446
  const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
25767
27447
  this.db.inspectionFindings.insertFinding({
25768
- id: inspectionFindingId(auditEventId, definitionId, insp.span.start, insp.span.end),
27448
+ id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
25769
27449
  auditEventId,
25770
27450
  inspectionDefinitionId: definitionId,
25771
27451
  classifiedDataId: classifiedDataId2,
@@ -25814,10 +27494,17 @@ var StandaloneDataGateway = class {
25814
27494
  try {
25815
27495
  const snapshot = this.db.installedPacks.installedRuleset();
25816
27496
  if (snapshot.installedPacks === 0) return void 0;
25817
- if (snapshot.enabledPacks === 0) return { rules: [], ruleActions: /* @__PURE__ */ new Map(), complete: true };
27497
+ if (snapshot.enabledPacks === 0) {
27498
+ return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
27499
+ }
25818
27500
  if (snapshot.invalidRules > 0) return void 0;
25819
27501
  if (snapshot.rules.length === 0) return void 0;
25820
- return { rules: snapshot.rules, ruleActions: snapshot.ruleActions, complete: true };
27502
+ return {
27503
+ rules: snapshot.rules,
27504
+ ruleActions: snapshot.ruleActions,
27505
+ ruleVersions: snapshot.ruleVersions,
27506
+ complete: true
27507
+ };
25821
27508
  } catch {
25822
27509
  return void 0;
25823
27510
  }
@@ -25845,6 +27532,7 @@ var StandaloneDataGateway = class {
25845
27532
  policies: [...policies, ...rulePolicies],
25846
27533
  rules: installed ? installed.rules : [],
25847
27534
  ...installed ? { rulesComplete: true } : {},
27535
+ ...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
25848
27536
  ...exceptions !== void 0 ? { exceptions } : {},
25849
27537
  customKeywords,
25850
27538
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -25943,6 +27631,13 @@ var StandaloneDataGateway = class {
25943
27631
  this.db.scanLedger.upsertEntries(entries);
25944
27632
  return Promise.resolve();
25945
27633
  }
27634
+ getRuleProbeVerdict(ruleKey) {
27635
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
27636
+ }
27637
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
27638
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
27639
+ return Promise.resolve();
27640
+ }
25946
27641
  openAtRestKeysForPath(path) {
25947
27642
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
25948
27643
  }
@@ -25953,6 +27648,12 @@ var StandaloneDataGateway = class {
25953
27648
  this.db.resolutions.insertResolution(input);
25954
27649
  return Promise.resolve();
25955
27650
  }
27651
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
27652
+ // enforced by the caller, which already holds the parsed workspace
27653
+ // settings; this class only ever sees `dataDir`, not the settings base.
27654
+ recordProjectEgress(input) {
27655
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
27656
+ }
25956
27657
  close() {
25957
27658
  this.db.close();
25958
27659
  return Promise.resolve();
@@ -26141,13 +27842,23 @@ function responseEmitPayload(toolName, outcome) {
26141
27842
  async function readStdin() {
26142
27843
  return new Promise((resolve) => {
26143
27844
  let data = "";
26144
- process.stdin.setEncoding("utf8");
26145
- process.stdin.on("data", (chunk) => {
26146
- data += chunk;
26147
- });
26148
- process.stdin.on("end", () => {
27845
+ let settled = false;
27846
+ const finish = () => {
27847
+ if (settled) return;
27848
+ settled = true;
27849
+ clearTimeout(timer);
27850
+ process.stdin.removeListener("data", onData);
27851
+ process.stdin.removeListener("end", finish);
26149
27852
  resolve(data);
26150
- });
27853
+ };
27854
+ const onData = (chunk) => {
27855
+ data += chunk;
27856
+ };
27857
+ const timer = setTimeout(finish, 5e3);
27858
+ process.stdin.setEncoding("utf8");
27859
+ process.stdin.on("data", onData);
27860
+ process.stdin.on("end", finish);
27861
+ process.stdin.on("error", finish);
26151
27862
  });
26152
27863
  }
26153
27864
  function parseJson(raw) {
@@ -26164,9 +27875,14 @@ function getString(record2, key) {
26164
27875
  }
26165
27876
  function emit(output) {
26166
27877
  return new Promise((resolve) => {
26167
- process.stdout.write(JSON.stringify(output), () => {
27878
+ let settled = false;
27879
+ const finish = () => {
27880
+ if (settled) return;
27881
+ settled = true;
26168
27882
  resolve();
26169
- });
27883
+ };
27884
+ process.stdout.on("error", finish);
27885
+ process.stdout.write(JSON.stringify(output), finish);
26170
27886
  });
26171
27887
  }
26172
27888
  function baseMetadata(input) {