@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/commands/setup.md +101 -19
- package/package.json +3 -3
- package/scripts/apply-suppressions.js +3192 -487
- package/scripts/backfill.js +951 -455
- package/scripts/filescan.js +939 -449
- package/scripts/firstrun.js +895 -413
- package/scripts/intro.js +91 -24
- package/scripts/onboard.js +920 -416
- package/scripts/post-tool-use.js +932 -442
- package/scripts/pre-tool-use.js +937 -447
- package/scripts/query.js +895 -413
- package/scripts/reconcile.js +924 -436
- package/scripts/remediate.js +942 -452
- package/scripts/session-start.js +957 -475
- package/scripts/start-light.js +91 -24
- package/scripts/statusline.js +895 -413
- package/scripts/stop.js +132 -38
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +940 -450
package/scripts/reconcile.js
CHANGED
|
@@ -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
|
|
|
@@ -546,6 +550,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
546
550
|
{
|
|
547
551
|
tag: "0011_egress_writer",
|
|
548
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"
|
|
549
565
|
}
|
|
550
566
|
];
|
|
551
567
|
|
|
@@ -15376,7 +15392,12 @@ var FindingFacets = external_exports.object({
|
|
|
15376
15392
|
severity: external_exports.array(FindingFacetItem),
|
|
15377
15393
|
subtype: external_exports.array(FindingFacetItem),
|
|
15378
15394
|
provider: external_exports.array(FindingFacetItem),
|
|
15379
|
-
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)
|
|
15380
15401
|
}).meta({ id: "FindingFacets" });
|
|
15381
15402
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15382
15403
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15386,6 +15407,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15386
15407
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15387
15408
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15388
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(),
|
|
15389
15414
|
q: external_exports.string().optional(),
|
|
15390
15415
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15391
15416
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15576,6 +15601,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15576
15601
|
parent_uuid: external_exports.string().optional(),
|
|
15577
15602
|
run_key: external_exports.string().optional()
|
|
15578
15603
|
}).catchall(external_exports.unknown());
|
|
15604
|
+
var CaptureAttributes = external_exports.object({
|
|
15605
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15606
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15607
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15608
|
+
source_tool: external_exports.string().optional(),
|
|
15609
|
+
file_path: external_exports.string().optional(),
|
|
15610
|
+
repo: external_exports.string().optional(),
|
|
15611
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15612
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15613
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15614
|
+
tool_name: external_exports.string().optional(),
|
|
15615
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15616
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15617
|
+
gitignored: external_exports.boolean().optional(),
|
|
15618
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15619
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15620
|
+
whole_file: external_exports.boolean().optional(),
|
|
15621
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15622
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15623
|
+
// trace id when telemetry is enabled.
|
|
15624
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15625
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15626
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15627
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15628
|
+
// authorized the bypass.
|
|
15629
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15630
|
+
}).catchall(external_exports.unknown());
|
|
15579
15631
|
var ToolCallInspection = external_exports.object({
|
|
15580
15632
|
ruleId: external_exports.string().min(1),
|
|
15581
15633
|
ruleName: external_exports.string(),
|
|
@@ -15662,7 +15714,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15662
15714
|
span: Span,
|
|
15663
15715
|
maskedMatch: external_exports.string(),
|
|
15664
15716
|
actionTaken: ActionTaken,
|
|
15665
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15717
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15718
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15719
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15720
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15721
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15722
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15723
|
+
findingKey: external_exports.string().optional(),
|
|
15724
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15725
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15726
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15727
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15728
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15666
15729
|
});
|
|
15667
15730
|
var InventoryContext = external_exports.object({
|
|
15668
15731
|
host: InventoryInput.optional(),
|
|
@@ -15864,6 +15927,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15864
15927
|
|
|
15865
15928
|
// ../../packages/schema/src/zod/event.ts
|
|
15866
15929
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15930
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15867
15931
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15868
15932
|
var EventMetadata = external_exports.object({
|
|
15869
15933
|
sessionId: external_exports.string().optional(),
|
|
@@ -16353,6 +16417,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16353
16417
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16354
16418
|
// `bundle.exceptions ?? []`.
|
|
16355
16419
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16420
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16421
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16422
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16423
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16424
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16425
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16356
16426
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16357
16427
|
fetchedAt: external_exports.iso.datetime()
|
|
16358
16428
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17152,6 +17222,15 @@ function groupActions(g) {
|
|
|
17152
17222
|
actionsCache.set(g, actions);
|
|
17153
17223
|
return actions;
|
|
17154
17224
|
}
|
|
17225
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17226
|
+
const statusSet = new Set(statuses);
|
|
17227
|
+
let sum = 0;
|
|
17228
|
+
for (const input of statusInputs) {
|
|
17229
|
+
if (input.count === void 0) return null;
|
|
17230
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17231
|
+
}
|
|
17232
|
+
return sum;
|
|
17233
|
+
}
|
|
17155
17234
|
function applyFindingFilters(groups, opts) {
|
|
17156
17235
|
let filtered = groups;
|
|
17157
17236
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17170,6 +17249,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17170
17249
|
const subtypeSet = new Set(opts.subtype);
|
|
17171
17250
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17172
17251
|
}
|
|
17252
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17253
|
+
const statusSet = new Set(opts.statuses);
|
|
17254
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17255
|
+
}
|
|
17173
17256
|
if (opts.q) {
|
|
17174
17257
|
const q = opts.q.toLowerCase();
|
|
17175
17258
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17191,6 +17274,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17191
17274
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17192
17275
|
providers: opts.providers,
|
|
17193
17276
|
actions: opts.actions,
|
|
17277
|
+
statuses: opts.statuses,
|
|
17194
17278
|
q: opts.q,
|
|
17195
17279
|
subtype: opts.subtype
|
|
17196
17280
|
});
|
|
@@ -17200,6 +17284,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17200
17284
|
}
|
|
17201
17285
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17202
17286
|
actions: opts.actions,
|
|
17287
|
+
statuses: opts.statuses,
|
|
17203
17288
|
q: opts.q,
|
|
17204
17289
|
subtype: opts.subtype,
|
|
17205
17290
|
severity: opts.severity
|
|
@@ -17210,6 +17295,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17210
17295
|
}
|
|
17211
17296
|
const forAction = applyFindingFilters(allGroups, {
|
|
17212
17297
|
providers: opts.providers,
|
|
17298
|
+
statuses: opts.statuses,
|
|
17213
17299
|
q: opts.q,
|
|
17214
17300
|
subtype: opts.subtype,
|
|
17215
17301
|
severity: opts.severity
|
|
@@ -17221,17 +17307,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17221
17307
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17222
17308
|
providers: opts.providers,
|
|
17223
17309
|
actions: opts.actions,
|
|
17310
|
+
statuses: opts.statuses,
|
|
17224
17311
|
q: opts.q,
|
|
17225
17312
|
severity: opts.severity
|
|
17226
17313
|
});
|
|
17227
17314
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17228
17315
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17316
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17317
|
+
providers: opts.providers,
|
|
17318
|
+
actions: opts.actions,
|
|
17319
|
+
q: opts.q,
|
|
17320
|
+
subtype: opts.subtype,
|
|
17321
|
+
severity: opts.severity
|
|
17322
|
+
});
|
|
17323
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17324
|
+
for (const g of forStatus) {
|
|
17325
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17326
|
+
}
|
|
17229
17327
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17230
17328
|
return {
|
|
17231
17329
|
severity: toItems(severityMap),
|
|
17232
17330
|
provider: toItems(providerMap),
|
|
17233
17331
|
action: toItems(actionMap),
|
|
17234
|
-
subtype: toItems(subtypeMap)
|
|
17332
|
+
subtype: toItems(subtypeMap),
|
|
17333
|
+
status: toItems(statusMap)
|
|
17235
17334
|
};
|
|
17236
17335
|
}
|
|
17237
17336
|
|
|
@@ -17266,10 +17365,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17266
17365
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17267
17366
|
|
|
17268
17367
|
// ../../packages/schema/src/zod/local.ts
|
|
17269
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17368
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17270
17369
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17271
17370
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17272
17371
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17372
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17373
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17374
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17375
|
+
});
|
|
17273
17376
|
var WorkspaceSettings = external_exports.object({
|
|
17274
17377
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17275
17378
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17285,37 +17388,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17285
17388
|
// Shares writes.
|
|
17286
17389
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17287
17390
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17288
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17391
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17392
|
+
// Records that the user consented to sending findings to the model API for
|
|
17393
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17394
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17395
|
+
// covers the current payload and must be re-granted.
|
|
17396
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17289
17397
|
});
|
|
17290
17398
|
function defaultWorkspaceSettings() {
|
|
17291
17399
|
return WorkspaceSettings.parse({});
|
|
17292
17400
|
}
|
|
17293
|
-
function toEventRow(event) {
|
|
17294
|
-
return {
|
|
17295
|
-
id: event.id,
|
|
17296
|
-
sourceTool: event.sourceTool,
|
|
17297
|
-
kind: event.kind,
|
|
17298
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17299
|
-
contentHash: event.contentHash,
|
|
17300
|
-
content: event.content,
|
|
17301
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17302
|
-
};
|
|
17303
|
-
}
|
|
17304
|
-
function toFindingRow(finding) {
|
|
17305
|
-
return {
|
|
17306
|
-
id: finding.id,
|
|
17307
|
-
eventId: finding.eventId,
|
|
17308
|
-
ruleId: finding.ruleId,
|
|
17309
|
-
category: finding.category,
|
|
17310
|
-
severity: finding.severity,
|
|
17311
|
-
spanStart: finding.span.start,
|
|
17312
|
-
spanEnd: finding.span.end,
|
|
17313
|
-
maskedMatch: finding.maskedMatch,
|
|
17314
|
-
actionTaken: finding.actionTaken,
|
|
17315
|
-
confidence: finding.confidence,
|
|
17316
|
-
findingKey: finding.findingKey ?? null
|
|
17317
|
-
};
|
|
17318
|
-
}
|
|
17319
17401
|
function toInventoryRow(input, id, now) {
|
|
17320
17402
|
return {
|
|
17321
17403
|
id,
|
|
@@ -17385,7 +17467,42 @@ function toInspectionFindingRow(input) {
|
|
|
17385
17467
|
spanEnd: input.span.end,
|
|
17386
17468
|
maskedMatch: input.maskedMatch,
|
|
17387
17469
|
actionTaken: input.actionTaken,
|
|
17388
|
-
confidence: input.confidence
|
|
17470
|
+
confidence: input.confidence,
|
|
17471
|
+
findingKey: input.findingKey ?? null,
|
|
17472
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17473
|
+
};
|
|
17474
|
+
}
|
|
17475
|
+
function toCaptureAttributes(event) {
|
|
17476
|
+
const metadata = event.metadata;
|
|
17477
|
+
return {
|
|
17478
|
+
source_tool: event.sourceTool,
|
|
17479
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17480
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17481
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17482
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17483
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17484
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17485
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17486
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17487
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17488
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17489
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17490
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17491
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17492
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17495
|
+
function captureDefinitionVersion(finding) {
|
|
17496
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17497
|
+
}
|
|
17498
|
+
function toCaptureDefinitionInput(finding) {
|
|
17499
|
+
return {
|
|
17500
|
+
ruleId: finding.ruleId,
|
|
17501
|
+
version: captureDefinitionVersion(finding),
|
|
17502
|
+
name: finding.ruleId,
|
|
17503
|
+
category: finding.category,
|
|
17504
|
+
severity: finding.severity,
|
|
17505
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17389
17506
|
};
|
|
17390
17507
|
}
|
|
17391
17508
|
|
|
@@ -17813,6 +17930,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17813
17930
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17814
17931
|
}
|
|
17815
17932
|
|
|
17933
|
+
// ../../packages/persistence/src/ids.ts
|
|
17934
|
+
import { createHash } from "crypto";
|
|
17935
|
+
function sha256Hex(input) {
|
|
17936
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17937
|
+
}
|
|
17938
|
+
function inventoryId(objectType, identityKey) {
|
|
17939
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17940
|
+
}
|
|
17941
|
+
function sourceProjectId(url2) {
|
|
17942
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17943
|
+
}
|
|
17944
|
+
function classifiedDataId(cls) {
|
|
17945
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17946
|
+
}
|
|
17947
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17948
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17949
|
+
}
|
|
17950
|
+
function llmCallId(sessionId, messageId) {
|
|
17951
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17952
|
+
}
|
|
17953
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17954
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17955
|
+
}
|
|
17956
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17957
|
+
return sha256Hex(
|
|
17958
|
+
canonicalIdentity([
|
|
17959
|
+
"inspection_finding",
|
|
17960
|
+
auditEventId,
|
|
17961
|
+
ruleId,
|
|
17962
|
+
String(spanStart),
|
|
17963
|
+
String(spanEnd)
|
|
17964
|
+
])
|
|
17965
|
+
);
|
|
17966
|
+
}
|
|
17967
|
+
var NO_SESSION = "no_session";
|
|
17968
|
+
var NO_PATH = "no_path";
|
|
17969
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17970
|
+
return sha256Hex(
|
|
17971
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17972
|
+
);
|
|
17973
|
+
}
|
|
17974
|
+
|
|
17816
17975
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17817
17976
|
function escapeLikePattern(s) {
|
|
17818
17977
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17909,39 +18068,81 @@ function evidenceExists(db, object2) {
|
|
|
17909
18068
|
return schemaObjectExists(db, "table", object2.name);
|
|
17910
18069
|
}
|
|
17911
18070
|
|
|
17912
|
-
// ../../packages/persistence/src/
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
18071
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18072
|
+
function allRows(stmt, params) {
|
|
18073
|
+
if (params === void 0) return stmt.all();
|
|
18074
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18075
|
+
return stmt.all(params);
|
|
17916
18076
|
}
|
|
17917
|
-
function
|
|
17918
|
-
|
|
18077
|
+
function getRow(stmt, params) {
|
|
18078
|
+
if (params === void 0) return stmt.get();
|
|
18079
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18080
|
+
return stmt.get(params);
|
|
17919
18081
|
}
|
|
17920
|
-
function
|
|
17921
|
-
return
|
|
18082
|
+
function intToBool(raw) {
|
|
18083
|
+
return raw === 1 || raw === true;
|
|
17922
18084
|
}
|
|
17923
|
-
function
|
|
17924
|
-
return
|
|
18085
|
+
function boolToInt(b) {
|
|
18086
|
+
return b ? 1 : 0;
|
|
17925
18087
|
}
|
|
17926
|
-
function
|
|
17927
|
-
|
|
18088
|
+
function bindParams(row) {
|
|
18089
|
+
const out = {};
|
|
18090
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18091
|
+
out[key] = value === void 0 ? null : value;
|
|
18092
|
+
}
|
|
18093
|
+
return out;
|
|
17928
18094
|
}
|
|
17929
|
-
function
|
|
17930
|
-
return
|
|
18095
|
+
function countScalar(db, sql, params) {
|
|
18096
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17931
18097
|
}
|
|
17932
|
-
function
|
|
17933
|
-
|
|
18098
|
+
function countBy(db, sql, params) {
|
|
18099
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18100
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18101
|
+
map2.set(row.k, row.n);
|
|
18102
|
+
}
|
|
18103
|
+
return map2;
|
|
17934
18104
|
}
|
|
17935
|
-
function
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
18105
|
+
function mapRowsTolerant(rows, map2) {
|
|
18106
|
+
const out = [];
|
|
18107
|
+
for (const row of rows) {
|
|
18108
|
+
try {
|
|
18109
|
+
out.push(map2(row));
|
|
18110
|
+
} catch {
|
|
18111
|
+
}
|
|
18112
|
+
}
|
|
18113
|
+
return out;
|
|
18114
|
+
}
|
|
18115
|
+
|
|
18116
|
+
// ../../packages/persistence/src/paths.ts
|
|
18117
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18118
|
+
var DATA_DIR_MODE = 448;
|
|
18119
|
+
var DATA_FILE_MODE = 384;
|
|
18120
|
+
var DB_FILENAME = "aka.db";
|
|
18121
|
+
function chmodBestEffort(path, mode) {
|
|
18122
|
+
try {
|
|
18123
|
+
chmodSync(path, mode);
|
|
18124
|
+
} catch {
|
|
18125
|
+
}
|
|
18126
|
+
}
|
|
18127
|
+
function tightenDir(dir) {
|
|
18128
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18129
|
+
}
|
|
18130
|
+
function ensureDataDirSync(dir) {
|
|
18131
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18132
|
+
tightenDir(dir);
|
|
18133
|
+
}
|
|
18134
|
+
function dbSidecars(file2) {
|
|
18135
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18136
|
+
}
|
|
18137
|
+
function tightenFile(file2) {
|
|
18138
|
+
try {
|
|
18139
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18140
|
+
} catch {
|
|
18141
|
+
}
|
|
18142
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18143
|
+
}
|
|
18144
|
+
function tightenPerms(file2) {
|
|
18145
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
17945
18146
|
}
|
|
17946
18147
|
|
|
17947
18148
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17955,7 +18156,8 @@ function createdIndexName(statement) {
|
|
|
17955
18156
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17956
18157
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17957
18158
|
}
|
|
17958
|
-
|
|
18159
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18160
|
+
function applyMigrations(db, file2) {
|
|
17959
18161
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17960
18162
|
db.exec(
|
|
17961
18163
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17969,6 +18171,7 @@ function applyMigrations(db) {
|
|
|
17969
18171
|
);
|
|
17970
18172
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17971
18173
|
if (applied.has(migration.tag)) continue;
|
|
18174
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17972
18175
|
const evidence = evidenceObjects(migration.sql);
|
|
17973
18176
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17974
18177
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18013,7 +18216,6 @@ function applyMigrations(db) {
|
|
|
18013
18216
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18014
18217
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18015
18218
|
}
|
|
18016
|
-
ensureSyncedAtColumn(db, "events");
|
|
18017
18219
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18018
18220
|
ensureScanLedgerTable(db);
|
|
18019
18221
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18021,6 +18223,47 @@ function applyMigrations(db) {
|
|
|
18021
18223
|
ensureWriteGateTrigger(db);
|
|
18022
18224
|
ensureTokenUsageColumns(db);
|
|
18023
18225
|
reconcileSourceProjectIds(db);
|
|
18226
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18227
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18228
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18229
|
+
}
|
|
18230
|
+
}
|
|
18231
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18232
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18233
|
+
if (!migration) return;
|
|
18234
|
+
if (file2) {
|
|
18235
|
+
try {
|
|
18236
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18237
|
+
} catch (error51) {
|
|
18238
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18239
|
+
return;
|
|
18240
|
+
}
|
|
18241
|
+
}
|
|
18242
|
+
try {
|
|
18243
|
+
withTransaction(
|
|
18244
|
+
db,
|
|
18245
|
+
() => {
|
|
18246
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18247
|
+
if (alreadyDropped) return;
|
|
18248
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18249
|
+
db.exec(statement);
|
|
18250
|
+
}
|
|
18251
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18252
|
+
migration.tag,
|
|
18253
|
+
Date.now()
|
|
18254
|
+
);
|
|
18255
|
+
},
|
|
18256
|
+
"IMMEDIATE"
|
|
18257
|
+
);
|
|
18258
|
+
} catch (error51) {
|
|
18259
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18260
|
+
}
|
|
18261
|
+
}
|
|
18262
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18263
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18264
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18265
|
+
tightenFile(backup);
|
|
18266
|
+
return backup;
|
|
18024
18267
|
}
|
|
18025
18268
|
var TOKEN_USAGE_COLUMNS = [
|
|
18026
18269
|
{
|
|
@@ -18049,6 +18292,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18049
18292
|
}
|
|
18050
18293
|
];
|
|
18051
18294
|
function ensureTokenUsageColumns(db) {
|
|
18295
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18052
18296
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18053
18297
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18054
18298
|
if (!existing.has(column.name)) {
|
|
@@ -18114,11 +18358,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18114
18358
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18115
18359
|
}
|
|
18116
18360
|
}
|
|
18361
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18362
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18363
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18364
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18365
|
+
return row?.lastRowid ?? 0;
|
|
18366
|
+
}
|
|
18367
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18368
|
+
db.prepare(
|
|
18369
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18370
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18371
|
+
).run(source, lastRowid);
|
|
18372
|
+
}
|
|
18373
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18374
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18375
|
+
let processed = 0;
|
|
18376
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18377
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18378
|
+
if (rows.length === 0) return true;
|
|
18379
|
+
withTransaction(
|
|
18380
|
+
db,
|
|
18381
|
+
() => {
|
|
18382
|
+
handleRows(rows);
|
|
18383
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18384
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18385
|
+
},
|
|
18386
|
+
"IMMEDIATE"
|
|
18387
|
+
);
|
|
18388
|
+
processed += rows.length;
|
|
18389
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18390
|
+
}
|
|
18391
|
+
return false;
|
|
18392
|
+
}
|
|
18393
|
+
function parseLegacyEventMetadata(raw) {
|
|
18394
|
+
if (raw === null) return void 0;
|
|
18395
|
+
try {
|
|
18396
|
+
return JSON.parse(raw);
|
|
18397
|
+
} catch {
|
|
18398
|
+
return void 0;
|
|
18399
|
+
}
|
|
18400
|
+
}
|
|
18401
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18402
|
+
return JSON.stringify(
|
|
18403
|
+
toCaptureAttributes({
|
|
18404
|
+
id: row.id,
|
|
18405
|
+
sourceTool: row.sourceTool,
|
|
18406
|
+
kind: row.kind,
|
|
18407
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18408
|
+
contentHash: row.contentHash,
|
|
18409
|
+
content: row.content,
|
|
18410
|
+
metadata: row.metadata
|
|
18411
|
+
})
|
|
18412
|
+
);
|
|
18413
|
+
}
|
|
18414
|
+
function copyLegacyEvents(db) {
|
|
18415
|
+
const selectStmt = db.prepare(
|
|
18416
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18417
|
+
content_hash AS contentHash, content, metadata
|
|
18418
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18419
|
+
);
|
|
18420
|
+
const insertStmt = db.prepare(
|
|
18421
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18422
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18423
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18424
|
+
);
|
|
18425
|
+
const stubRootStmt = db.prepare(
|
|
18426
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18427
|
+
);
|
|
18428
|
+
return drainLegacyTable(
|
|
18429
|
+
db,
|
|
18430
|
+
"events",
|
|
18431
|
+
selectStmt,
|
|
18432
|
+
(rows) => {
|
|
18433
|
+
for (const row of rows) {
|
|
18434
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18435
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18436
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18437
|
+
insertStmt.run(
|
|
18438
|
+
bindParams({
|
|
18439
|
+
id: row.id,
|
|
18440
|
+
parentId: sessionId,
|
|
18441
|
+
rootSessionId: sessionId,
|
|
18442
|
+
eventType: row.kind,
|
|
18443
|
+
startedAt: row.occurredAt,
|
|
18444
|
+
content: row.content,
|
|
18445
|
+
contentHash: row.contentHash,
|
|
18446
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18447
|
+
})
|
|
18448
|
+
);
|
|
18449
|
+
}
|
|
18450
|
+
}
|
|
18451
|
+
);
|
|
18452
|
+
}
|
|
18453
|
+
function copyLegacyFindings(db) {
|
|
18454
|
+
const selectStmt = db.prepare(
|
|
18455
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18456
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18457
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18458
|
+
first_detected_at AS firstDetectedAt
|
|
18459
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18460
|
+
);
|
|
18461
|
+
const definitionStmt = db.prepare(
|
|
18462
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18463
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18464
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18465
|
+
);
|
|
18466
|
+
const findingStmt = db.prepare(
|
|
18467
|
+
`INSERT INTO inspection_findings
|
|
18468
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18469
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18470
|
+
finding_key, first_detected_at)
|
|
18471
|
+
VALUES
|
|
18472
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18473
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18474
|
+
:findingKey, :firstDetectedAt)
|
|
18475
|
+
ON CONFLICT(id) DO NOTHING
|
|
18476
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18477
|
+
first_detected_at = CASE
|
|
18478
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18479
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18480
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18481
|
+
END`
|
|
18482
|
+
);
|
|
18483
|
+
return drainLegacyTable(
|
|
18484
|
+
db,
|
|
18485
|
+
"findings",
|
|
18486
|
+
selectStmt,
|
|
18487
|
+
(rows) => {
|
|
18488
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18489
|
+
for (const row of rows) {
|
|
18490
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18491
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18492
|
+
if (definitionId === void 0) {
|
|
18493
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18494
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18495
|
+
definitionStmt.run(
|
|
18496
|
+
bindParams({
|
|
18497
|
+
id: definitionId,
|
|
18498
|
+
ruleId: row.ruleId,
|
|
18499
|
+
name: row.ruleId,
|
|
18500
|
+
category: row.category,
|
|
18501
|
+
severity: row.severity,
|
|
18502
|
+
definition: "",
|
|
18503
|
+
version: version2
|
|
18504
|
+
})
|
|
18505
|
+
);
|
|
18506
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18507
|
+
}
|
|
18508
|
+
findingStmt.run(
|
|
18509
|
+
bindParams({
|
|
18510
|
+
id: row.id,
|
|
18511
|
+
auditEventId: row.eventId,
|
|
18512
|
+
inspectionDefinitionId: definitionId,
|
|
18513
|
+
spanStart: row.spanStart,
|
|
18514
|
+
spanEnd: row.spanEnd,
|
|
18515
|
+
maskedMatch: row.maskedMatch,
|
|
18516
|
+
actionTaken: row.actionTaken,
|
|
18517
|
+
confidence: row.confidence,
|
|
18518
|
+
findingKey: row.findingKey,
|
|
18519
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18520
|
+
})
|
|
18521
|
+
);
|
|
18522
|
+
}
|
|
18523
|
+
}
|
|
18524
|
+
);
|
|
18525
|
+
}
|
|
18526
|
+
function runLegacyHistoryBackfill(db) {
|
|
18527
|
+
try {
|
|
18528
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18529
|
+
if (!eventsCaughtUp) return false;
|
|
18530
|
+
return copyLegacyFindings(db);
|
|
18531
|
+
} catch (error51) {
|
|
18532
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18533
|
+
return false;
|
|
18534
|
+
}
|
|
18535
|
+
}
|
|
18117
18536
|
function isForeignSqliteLineage(db) {
|
|
18118
18537
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18119
18538
|
return columnNames(db, "events").includes("tenant_id");
|
|
18120
18539
|
}
|
|
18121
18540
|
function ensureSyncedAtColumn(db, table) {
|
|
18541
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
18122
18542
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
18123
18543
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18124
18544
|
}
|
|
@@ -18139,6 +18559,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18139
18559
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18140
18560
|
)`);
|
|
18141
18561
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18562
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18142
18563
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18143
18564
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18144
18565
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18166,30 +18587,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18166
18587
|
)`);
|
|
18167
18588
|
}
|
|
18168
18589
|
|
|
18169
|
-
// ../../packages/persistence/src/paths.ts
|
|
18170
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18171
|
-
var DATA_DIR_MODE = 448;
|
|
18172
|
-
var DATA_FILE_MODE = 384;
|
|
18173
|
-
var DB_FILENAME = "aka.db";
|
|
18174
|
-
function ensureDataDirSync(dir) {
|
|
18175
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18176
|
-
try {
|
|
18177
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18178
|
-
} catch {
|
|
18179
|
-
}
|
|
18180
|
-
}
|
|
18181
|
-
function walSidecars(file2) {
|
|
18182
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18183
|
-
}
|
|
18184
|
-
function tightenPerms(file2) {
|
|
18185
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18186
|
-
try {
|
|
18187
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18188
|
-
} catch {
|
|
18189
|
-
}
|
|
18190
|
-
}
|
|
18191
|
-
}
|
|
18192
|
-
|
|
18193
18590
|
// ../../packages/persistence/src/internal/json.ts
|
|
18194
18591
|
function safeJson(s, fallback) {
|
|
18195
18592
|
if (s == null) return fallback;
|
|
@@ -18209,51 +18606,6 @@ function parseJsonObject(s) {
|
|
|
18209
18606
|
return void 0;
|
|
18210
18607
|
}
|
|
18211
18608
|
|
|
18212
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18213
|
-
function allRows(stmt, params) {
|
|
18214
|
-
if (params === void 0) return stmt.all();
|
|
18215
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18216
|
-
return stmt.all(params);
|
|
18217
|
-
}
|
|
18218
|
-
function getRow(stmt, params) {
|
|
18219
|
-
if (params === void 0) return stmt.get();
|
|
18220
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18221
|
-
return stmt.get(params);
|
|
18222
|
-
}
|
|
18223
|
-
function intToBool(raw) {
|
|
18224
|
-
return raw === 1 || raw === true;
|
|
18225
|
-
}
|
|
18226
|
-
function boolToInt(b) {
|
|
18227
|
-
return b ? 1 : 0;
|
|
18228
|
-
}
|
|
18229
|
-
function bindParams(row) {
|
|
18230
|
-
const out = {};
|
|
18231
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18232
|
-
out[key] = value === void 0 ? null : value;
|
|
18233
|
-
}
|
|
18234
|
-
return out;
|
|
18235
|
-
}
|
|
18236
|
-
function countScalar(db, sql, params) {
|
|
18237
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18238
|
-
}
|
|
18239
|
-
function countBy(db, sql, params) {
|
|
18240
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18241
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18242
|
-
map2.set(row.k, row.n);
|
|
18243
|
-
}
|
|
18244
|
-
return map2;
|
|
18245
|
-
}
|
|
18246
|
-
function mapRowsTolerant(rows, map2) {
|
|
18247
|
-
const out = [];
|
|
18248
|
-
for (const row of rows) {
|
|
18249
|
-
try {
|
|
18250
|
-
out.push(map2(row));
|
|
18251
|
-
} catch {
|
|
18252
|
-
}
|
|
18253
|
-
}
|
|
18254
|
-
return out;
|
|
18255
|
-
}
|
|
18256
|
-
|
|
18257
18609
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18258
18610
|
var DAY_MS = 864e5;
|
|
18259
18611
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18870,6 +19222,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18870
19222
|
})
|
|
18871
19223
|
);
|
|
18872
19224
|
}
|
|
19225
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19226
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19227
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19228
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19229
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19230
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19231
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19232
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19233
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19234
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19235
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19236
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19237
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19238
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19239
|
+
}
|
|
18873
19240
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18874
19241
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18875
19242
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19331,8 +19698,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19331
19698
|
)
|
|
19332
19699
|
);
|
|
19333
19700
|
}
|
|
19334
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19335
|
-
//
|
|
19701
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19702
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19703
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19704
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19705
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19706
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19707
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19708
|
+
// to the old findings⋈events one.
|
|
19336
19709
|
countFindingsLast30d(ruleIds) {
|
|
19337
19710
|
if (ruleIds.length === 0) return 0;
|
|
19338
19711
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19340,8 +19713,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19340
19713
|
return countScalar(
|
|
19341
19714
|
this.db,
|
|
19342
19715
|
`SELECT count(*) AS n
|
|
19343
|
-
FROM
|
|
19344
|
-
|
|
19716
|
+
FROM inspection_findings f
|
|
19717
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19718
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19719
|
+
WHERE e.started_at >= ?
|
|
19720
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19721
|
+
AND d.rule_id IN (${inClause})`,
|
|
19345
19722
|
[since, ...ruleIds]
|
|
19346
19723
|
);
|
|
19347
19724
|
}
|
|
@@ -19351,35 +19728,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19351
19728
|
var SqliteEventsRepository = class {
|
|
19352
19729
|
constructor(db) {
|
|
19353
19730
|
this.db = db;
|
|
19354
|
-
this.insertStmt = db.prepare(
|
|
19355
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19356
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19357
|
-
);
|
|
19358
19731
|
}
|
|
19359
19732
|
db;
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
this.insertStmt.run(
|
|
19364
|
-
bindParams({
|
|
19365
|
-
id: row.id,
|
|
19366
|
-
sourceTool: row.sourceTool,
|
|
19367
|
-
kind: row.kind,
|
|
19368
|
-
occurredAt: row.occurredAt,
|
|
19369
|
-
contentHash: row.contentHash,
|
|
19370
|
-
content: row.content,
|
|
19371
|
-
metadata: row.metadata
|
|
19372
|
-
})
|
|
19373
|
-
);
|
|
19374
|
-
}
|
|
19375
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19376
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19377
|
-
// never duplicates findings.
|
|
19733
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19734
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19735
|
+
// scan never duplicates findings.
|
|
19378
19736
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19379
19737
|
// async EventsReadPort contract.
|
|
19738
|
+
//
|
|
19739
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19740
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19741
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19742
|
+
// index-friendly rather than walking rows that can never match.
|
|
19380
19743
|
contentHashes() {
|
|
19381
19744
|
const rows = allRows(
|
|
19382
|
-
this.db.prepare(
|
|
19745
|
+
this.db.prepare(
|
|
19746
|
+
`SELECT content_hash FROM audit_events
|
|
19747
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19748
|
+
)
|
|
19383
19749
|
);
|
|
19384
19750
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19385
19751
|
}
|
|
@@ -19715,17 +20081,20 @@ function parseExceptionRow(row) {
|
|
|
19715
20081
|
}
|
|
19716
20082
|
|
|
19717
20083
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19718
|
-
function
|
|
20084
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19719
20085
|
return `(
|
|
19720
|
-
SELECT fr
|
|
20086
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19721
20087
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19722
20088
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19723
20089
|
LIMIT 1
|
|
19724
20090
|
)`;
|
|
19725
20091
|
}
|
|
20092
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20093
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20094
|
+
}
|
|
19726
20095
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19727
|
-
SELECT finding_key, status FROM (
|
|
19728
|
-
SELECT fr.finding_key, fr.status,
|
|
20096
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20097
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19729
20098
|
ROW_NUMBER() OVER (
|
|
19730
20099
|
PARTITION BY fr.finding_key
|
|
19731
20100
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19752,68 +20121,21 @@ var DAY_MS3 = 864e5;
|
|
|
19752
20121
|
var SqliteFindingsRepository = class {
|
|
19753
20122
|
constructor(db) {
|
|
19754
20123
|
this.db = db;
|
|
19755
|
-
this.insertStmt = db.prepare(
|
|
19756
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19757
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19758
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19759
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19760
|
-
event_id = excluded.event_id,
|
|
19761
|
-
category = excluded.category,
|
|
19762
|
-
severity = excluded.severity,
|
|
19763
|
-
span_start = excluded.span_start,
|
|
19764
|
-
span_end = excluded.span_end,
|
|
19765
|
-
masked_match = excluded.masked_match,
|
|
19766
|
-
action_taken = excluded.action_taken,
|
|
19767
|
-
confidence = excluded.confidence`
|
|
19768
|
-
);
|
|
19769
|
-
this.sessionDupStmt = db.prepare(
|
|
19770
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19771
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19772
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19773
|
-
LIMIT 1`
|
|
19774
|
-
);
|
|
19775
20124
|
}
|
|
19776
20125
|
db;
|
|
19777
|
-
insertStmt;
|
|
19778
|
-
sessionDupStmt;
|
|
19779
|
-
insertFindings(findings, scope = {}) {
|
|
19780
|
-
for (const finding of findings) {
|
|
19781
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19782
|
-
const row = toFindingRow(finding);
|
|
19783
|
-
this.insertStmt.run({
|
|
19784
|
-
id: row.id,
|
|
19785
|
-
eventId: row.eventId,
|
|
19786
|
-
ruleId: row.ruleId,
|
|
19787
|
-
category: row.category,
|
|
19788
|
-
severity: row.severity,
|
|
19789
|
-
spanStart: row.spanStart,
|
|
19790
|
-
spanEnd: row.spanEnd,
|
|
19791
|
-
maskedMatch: row.maskedMatch,
|
|
19792
|
-
actionTaken: row.actionTaken,
|
|
19793
|
-
confidence: row.confidence,
|
|
19794
|
-
findingKey: row.findingKey ?? null
|
|
19795
|
-
});
|
|
19796
|
-
}
|
|
19797
|
-
}
|
|
19798
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19799
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19800
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19801
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19802
|
-
const hit = this.sessionDupStmt.get({
|
|
19803
|
-
ruleId: finding.ruleId,
|
|
19804
|
-
maskedMatch: finding.maskedMatch,
|
|
19805
|
-
sessionId
|
|
19806
|
-
});
|
|
19807
|
-
return hit !== void 0;
|
|
19808
|
-
}
|
|
19809
20126
|
recentFindings(opts) {
|
|
19810
20127
|
const limit = opts?.limit ?? 50;
|
|
19811
20128
|
const rows = allRows(
|
|
19812
20129
|
this.db.prepare(
|
|
19813
|
-
`SELECT f.id, f.event_id,
|
|
19814
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19815
|
-
|
|
19816
|
-
|
|
20130
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20131
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20132
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20133
|
+
e.event_type AS kind
|
|
20134
|
+
FROM inspection_findings f
|
|
20135
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20136
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20137
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20138
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19817
20139
|
LIMIT :limit`
|
|
19818
20140
|
),
|
|
19819
20141
|
{ limit }
|
|
@@ -19835,25 +20157,34 @@ var SqliteFindingsRepository = class {
|
|
|
19835
20157
|
);
|
|
19836
20158
|
}
|
|
19837
20159
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19838
|
-
* session-stamped
|
|
20160
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19839
20161
|
* page can label its findings link without the grouped pipeline. */
|
|
19840
20162
|
sessionFindingsCount(sessionId) {
|
|
19841
20163
|
if (!sessionId) return Promise.resolve(0);
|
|
19842
20164
|
return Promise.resolve(
|
|
19843
20165
|
countScalar(
|
|
19844
20166
|
this.db,
|
|
19845
|
-
`SELECT count(*) AS n FROM
|
|
19846
|
-
JOIN
|
|
19847
|
-
WHERE
|
|
20167
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20168
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20169
|
+
WHERE e.root_session_id = :sessionId
|
|
20170
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19848
20171
|
{ sessionId }
|
|
19849
20172
|
)
|
|
19850
20173
|
);
|
|
19851
20174
|
}
|
|
19852
|
-
/** Per-rule transcript firing tally for one session —
|
|
19853
|
-
*
|
|
19854
|
-
*
|
|
19855
|
-
*
|
|
19856
|
-
*
|
|
20175
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20176
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20177
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20178
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20179
|
+
* tally with the deduped groups it lists.
|
|
20180
|
+
*
|
|
20181
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20182
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20183
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20184
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20185
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20186
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20187
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19857
20188
|
sessionFirings(sessionId) {
|
|
19858
20189
|
return Object.fromEntries(
|
|
19859
20190
|
countBy(
|
|
@@ -19863,18 +20194,25 @@ var SqliteFindingsRepository = class {
|
|
|
19863
20194
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19864
20195
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19865
20196
|
WHERE e.root_session_id = :sessionId
|
|
20197
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19866
20198
|
GROUP BY d.rule_id`,
|
|
19867
20199
|
{ sessionId }
|
|
19868
20200
|
)
|
|
19869
20201
|
);
|
|
19870
20202
|
}
|
|
19871
20203
|
/**
|
|
19872
|
-
* Grouped findings for the dashboard — joins
|
|
19873
|
-
* toolName from
|
|
19874
|
-
*
|
|
20204
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20205
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20206
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20207
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20208
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20209
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20210
|
+
* severity then recency. Filtering
|
|
19875
20211
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19876
20212
|
* reflect the full filtered set; `items` is the requested
|
|
19877
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20213
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20214
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20215
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19878
20216
|
*
|
|
19879
20217
|
* Two reads, neither of which materializes a row per finding:
|
|
19880
20218
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19887,10 +20225,11 @@ var SqliteFindingsRepository = class {
|
|
|
19887
20225
|
* rule is ever restated in SQL.
|
|
19888
20226
|
*/
|
|
19889
20227
|
listGroupedFindings(query) {
|
|
19890
|
-
const sessionPredicate = query.sessionId ? `
|
|
20228
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20229
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19891
20230
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19892
20231
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19893
|
-
predicate
|
|
20232
|
+
predicate,
|
|
19894
20233
|
params: sessionParams
|
|
19895
20234
|
});
|
|
19896
20235
|
const rows = allRows(
|
|
@@ -19898,24 +20237,26 @@ var SqliteFindingsRepository = class {
|
|
|
19898
20237
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19899
20238
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19900
20239
|
FROM (
|
|
19901
|
-
SELECT f.id AS id,
|
|
19902
|
-
|
|
20240
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20241
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19903
20242
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19904
|
-
e.
|
|
19905
|
-
json_extract(e.
|
|
19906
|
-
json_extract(e.
|
|
19907
|
-
json_extract(e.
|
|
19908
|
-
e.
|
|
20243
|
+
e.started_at AS occurred_at,
|
|
20244
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20245
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20246
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20247
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20248
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19909
20249
|
latest.status AS latest_status,
|
|
19910
20250
|
ROW_NUMBER() OVER (
|
|
19911
|
-
PARTITION BY
|
|
19912
|
-
ORDER BY e.
|
|
20251
|
+
PARTITION BY d.rule_id
|
|
20252
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19913
20253
|
) AS rn
|
|
19914
|
-
FROM
|
|
19915
|
-
JOIN
|
|
20254
|
+
FROM inspection_findings f
|
|
20255
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20256
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19916
20257
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19917
20258
|
ON latest.finding_key = f.finding_key
|
|
19918
|
-
${
|
|
20259
|
+
${predicate}
|
|
19919
20260
|
)
|
|
19920
20261
|
WHERE rn <= :cap
|
|
19921
20262
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19942,17 +20283,29 @@ var SqliteFindingsRepository = class {
|
|
|
19942
20283
|
severity: query.severity,
|
|
19943
20284
|
providers: query.provider,
|
|
19944
20285
|
actions: query.action,
|
|
20286
|
+
statuses: query.status,
|
|
19945
20287
|
subtype: query.subtype,
|
|
19946
20288
|
q: query.q
|
|
19947
20289
|
};
|
|
19948
20290
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19949
20291
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20292
|
+
const statusFilter = query.status ?? [];
|
|
19950
20293
|
const totals = {
|
|
19951
|
-
findings: sorted.reduce((acc, g) =>
|
|
20294
|
+
findings: sorted.reduce((acc, g) => {
|
|
20295
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20296
|
+
const agg = aggregates.get(g.id);
|
|
20297
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20298
|
+
}, 0),
|
|
19952
20299
|
groups: sorted.length
|
|
19953
20300
|
};
|
|
19954
20301
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19955
|
-
const
|
|
20302
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20303
|
+
const items = sorted.slice(0, limit).map(
|
|
20304
|
+
(g) => statusSet ? {
|
|
20305
|
+
...g,
|
|
20306
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20307
|
+
} : g
|
|
20308
|
+
);
|
|
19956
20309
|
return Promise.resolve({
|
|
19957
20310
|
totals,
|
|
19958
20311
|
facets,
|
|
@@ -19966,45 +20319,62 @@ var SqliteFindingsRepository = class {
|
|
|
19966
20319
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19967
20320
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19968
20321
|
*
|
|
19969
|
-
*
|
|
19970
|
-
*
|
|
19971
|
-
*
|
|
19972
|
-
* status
|
|
19973
|
-
*
|
|
19974
|
-
*
|
|
20322
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20323
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20324
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20325
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20326
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20327
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20328
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20329
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20330
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20331
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20332
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19975
20333
|
* a group's row stays small however many findings it holds.
|
|
19976
20334
|
*
|
|
19977
20335
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19978
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19979
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19980
|
-
* that is a string proportional to the store (~8MB over
|
|
19981
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19982
|
-
* match an instance outside the preview, which searching
|
|
19983
|
-
* would silently lose, so it is fetched only when the
|
|
19984
|
-
* carries a `q`.
|
|
20336
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20337
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20338
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20339
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20340
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20341
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20342
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20343
|
+
* path repeating across tuples.)
|
|
19985
20344
|
*/
|
|
19986
20345
|
groupAggregates(withSearchText, scope) {
|
|
19987
|
-
const
|
|
19988
|
-
group_concat(DISTINCT json_extract(e.
|
|
19989
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20346
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20347
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20348
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19990
20349
|
const rows = this.db.prepare(
|
|
19991
|
-
`SELECT
|
|
19992
|
-
|
|
19993
|
-
max(
|
|
19994
|
-
group_concat(
|
|
19995
|
-
group_concat(
|
|
19996
|
-
group_concat(
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20350
|
+
`SELECT rule_id,
|
|
20351
|
+
sum(tuple_count) AS instance_count,
|
|
20352
|
+
max(latest_at) AS latest_at,
|
|
20353
|
+
group_concat(source_tools) AS source_tools,
|
|
20354
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20355
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20356
|
+
group_concat(repos) AS repos,
|
|
20357
|
+
group_concat(files) AS files,
|
|
20358
|
+
group_concat(tool_names) AS tool_names
|
|
20359
|
+
FROM (
|
|
20360
|
+
SELECT d.rule_id AS rule_id,
|
|
20361
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20362
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20363
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20364
|
+
count(*) AS tuple_count,
|
|
20365
|
+
max(e.started_at) AS latest_at,
|
|
20366
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20367
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20368
|
+
${innerSearchColumns}
|
|
20369
|
+
FROM inspection_findings f
|
|
20370
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20371
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20372
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20373
|
+
ON latest.finding_key = f.finding_key
|
|
20374
|
+
${scope.predicate}
|
|
20375
|
+
GROUP BY d.rule_id, status_tuple
|
|
20376
|
+
)
|
|
20377
|
+
GROUP BY rule_id`
|
|
20008
20378
|
).all(scope.params);
|
|
20009
20379
|
return new Map(
|
|
20010
20380
|
rows.map((r) => [
|
|
@@ -20014,13 +20384,14 @@ var SqliteFindingsRepository = class {
|
|
|
20014
20384
|
sourceTools: splitConcat(r.source_tools),
|
|
20015
20385
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20016
20386
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20017
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20387
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20018
20388
|
return {
|
|
20019
20389
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20020
20390
|
// so the marker stands in for the key itself (never rendered).
|
|
20021
20391
|
kind,
|
|
20022
20392
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20023
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20393
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20394
|
+
count: Number(count)
|
|
20024
20395
|
};
|
|
20025
20396
|
}),
|
|
20026
20397
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20037,10 +20408,21 @@ var SqliteFindingsRepository = class {
|
|
|
20037
20408
|
);
|
|
20038
20409
|
}
|
|
20039
20410
|
healthSummary() {
|
|
20040
|
-
const total = countScalar(
|
|
20411
|
+
const total = countScalar(
|
|
20412
|
+
this.db,
|
|
20413
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20414
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20415
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20416
|
+
);
|
|
20041
20417
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20042
20418
|
const grouped = allRows(
|
|
20043
|
-
this.db.prepare(
|
|
20419
|
+
this.db.prepare(
|
|
20420
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20421
|
+
FROM inspection_findings f
|
|
20422
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20423
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20424
|
+
GROUP BY f.action_taken`
|
|
20425
|
+
)
|
|
20044
20426
|
);
|
|
20045
20427
|
for (const row of grouped) {
|
|
20046
20428
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20048,12 +20430,15 @@ var SqliteFindingsRepository = class {
|
|
|
20048
20430
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20049
20431
|
const sevRows = allRows(
|
|
20050
20432
|
this.db.prepare(
|
|
20051
|
-
`SELECT
|
|
20052
|
-
FROM
|
|
20433
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20434
|
+
FROM inspection_findings f
|
|
20435
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20436
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20053
20437
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20054
20438
|
ON latest.finding_key = f.finding_key
|
|
20055
|
-
WHERE
|
|
20056
|
-
|
|
20439
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20440
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20441
|
+
GROUP BY d.severity`
|
|
20057
20442
|
)
|
|
20058
20443
|
);
|
|
20059
20444
|
for (const row of sevRows) {
|
|
@@ -20074,9 +20459,11 @@ var SqliteFindingsRepository = class {
|
|
|
20074
20459
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20075
20460
|
const rows = allRows(
|
|
20076
20461
|
this.db.prepare(
|
|
20077
|
-
`SELECT date(e.
|
|
20078
|
-
FROM
|
|
20079
|
-
|
|
20462
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20463
|
+
FROM inspection_findings f
|
|
20464
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20465
|
+
WHERE e.started_at >= :since
|
|
20466
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20080
20467
|
GROUP BY day, f.action_taken`
|
|
20081
20468
|
),
|
|
20082
20469
|
{ since }
|
|
@@ -20141,15 +20528,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20141
20528
|
this.insertStmt = db.prepare(
|
|
20142
20529
|
`INSERT INTO inspection_findings
|
|
20143
20530
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20144
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20531
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20532
|
+
finding_key, first_detected_at)
|
|
20145
20533
|
VALUES
|
|
20146
20534
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20147
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20148
|
-
|
|
20535
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20536
|
+
:findingKey,
|
|
20537
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20538
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20539
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20540
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20541
|
+
audit_event_id = excluded.audit_event_id,
|
|
20542
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20543
|
+
classified_data_id = excluded.classified_data_id,
|
|
20544
|
+
span_start = excluded.span_start,
|
|
20545
|
+
span_end = excluded.span_end,
|
|
20546
|
+
masked_match = excluded.masked_match,
|
|
20547
|
+
action_taken = excluded.action_taken,
|
|
20548
|
+
confidence = excluded.confidence`
|
|
20549
|
+
);
|
|
20550
|
+
this.sessionDupStmt = db.prepare(
|
|
20551
|
+
`SELECT 1 FROM inspection_findings f
|
|
20552
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20553
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20554
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20555
|
+
AND e.root_session_id = :sessionId
|
|
20556
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20557
|
+
LIMIT 1`
|
|
20558
|
+
);
|
|
20559
|
+
this.eventDupStmt = db.prepare(
|
|
20560
|
+
`SELECT 1 FROM inspection_findings f
|
|
20561
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20562
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20563
|
+
AND f.masked_match = :maskedMatch
|
|
20564
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20565
|
+
LIMIT 1`
|
|
20149
20566
|
);
|
|
20150
20567
|
}
|
|
20151
20568
|
db;
|
|
20152
20569
|
insertStmt;
|
|
20570
|
+
sessionDupStmt;
|
|
20571
|
+
eventDupStmt;
|
|
20572
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20573
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20574
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20575
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20576
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20577
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20578
|
+
}
|
|
20579
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20580
|
+
// recorded against the given audit event.
|
|
20581
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20582
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20583
|
+
}
|
|
20153
20584
|
insertFinding(input) {
|
|
20154
20585
|
const row = toInspectionFindingRow(input);
|
|
20155
20586
|
this.insertStmt.run(
|
|
@@ -20162,7 +20593,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20162
20593
|
spanEnd: row.spanEnd,
|
|
20163
20594
|
maskedMatch: row.maskedMatch,
|
|
20164
20595
|
actionTaken: row.actionTaken,
|
|
20165
|
-
confidence: row.confidence
|
|
20596
|
+
confidence: row.confidence,
|
|
20597
|
+
findingKey: row.findingKey,
|
|
20598
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20166
20599
|
})
|
|
20167
20600
|
);
|
|
20168
20601
|
}
|
|
@@ -20434,7 +20867,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20434
20867
|
installedRuleset() {
|
|
20435
20868
|
const rows = allRows(
|
|
20436
20869
|
this.db.prepare(
|
|
20437
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20870
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20438
20871
|
)
|
|
20439
20872
|
);
|
|
20440
20873
|
const out = {
|
|
@@ -20442,7 +20875,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20442
20875
|
enabledPacks: 0,
|
|
20443
20876
|
rules: [],
|
|
20444
20877
|
invalidRules: 0,
|
|
20445
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20878
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20879
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20446
20880
|
};
|
|
20447
20881
|
for (const row of rows) {
|
|
20448
20882
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20464,6 +20898,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20464
20898
|
if (parsed.success) {
|
|
20465
20899
|
out.rules.push(parsed.data);
|
|
20466
20900
|
out.ruleActions.set(parsed.data.id, action);
|
|
20901
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20467
20902
|
} else out.invalidRules += 1;
|
|
20468
20903
|
}
|
|
20469
20904
|
}
|
|
@@ -21638,19 +22073,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21638
22073
|
);
|
|
21639
22074
|
this.openAtRestStmt = db.prepare(
|
|
21640
22075
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21641
|
-
FROM
|
|
21642
|
-
JOIN
|
|
21643
|
-
WHERE e.
|
|
21644
|
-
AND json_extract(e.
|
|
22076
|
+
FROM inspection_findings f
|
|
22077
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22078
|
+
WHERE e.event_type = 'code_change'
|
|
22079
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21645
22080
|
AND f.finding_key IS NOT NULL
|
|
21646
22081
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21647
22082
|
);
|
|
21648
22083
|
this.resolvedAtRestStmt = db.prepare(
|
|
21649
22084
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21650
|
-
FROM
|
|
21651
|
-
JOIN
|
|
21652
|
-
WHERE e.
|
|
21653
|
-
AND json_extract(e.
|
|
22085
|
+
FROM inspection_findings f
|
|
22086
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22087
|
+
WHERE e.event_type = 'code_change'
|
|
22088
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21654
22089
|
AND f.finding_key IS NOT NULL
|
|
21655
22090
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21656
22091
|
);
|
|
@@ -21873,25 +22308,27 @@ var SqliteSecurityRepository = class {
|
|
|
21873
22308
|
severitySummary() {
|
|
21874
22309
|
const rows = allRows(
|
|
21875
22310
|
this.db.prepare(
|
|
21876
|
-
`SELECT
|
|
22311
|
+
`SELECT d.severity AS severity,
|
|
21877
22312
|
COUNT(*) AS count,
|
|
21878
22313
|
SUM(CASE
|
|
21879
|
-
WHEN e.
|
|
22314
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21880
22315
|
WHEN f.finding_key IS NULL THEN 0
|
|
21881
22316
|
WHEN latest.status = 'resolved' THEN 1
|
|
21882
22317
|
ELSE 0
|
|
21883
22318
|
END) AS caught,
|
|
21884
22319
|
SUM(CASE
|
|
21885
|
-
WHEN e.
|
|
22320
|
+
WHEN e.event_type = 'code_change'
|
|
21886
22321
|
AND f.finding_key IS NOT NULL
|
|
21887
22322
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21888
22323
|
ELSE 0
|
|
21889
22324
|
END) AS open_at_rest
|
|
21890
|
-
FROM
|
|
21891
|
-
JOIN
|
|
22325
|
+
FROM inspection_findings f
|
|
22326
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22327
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21892
22328
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21893
22329
|
ON latest.finding_key = f.finding_key
|
|
21894
|
-
|
|
22330
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22331
|
+
GROUP BY d.severity`
|
|
21895
22332
|
)
|
|
21896
22333
|
);
|
|
21897
22334
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21957,7 +22394,7 @@ var SqliteSecurityRepository = class {
|
|
|
21957
22394
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21958
22395
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21959
22396
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21960
|
-
// (
|
|
22397
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21961
22398
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21962
22399
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21963
22400
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21982,30 +22419,20 @@ var SqliteSecurityRepository = class {
|
|
|
21982
22419
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21983
22420
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21984
22421
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21985
|
-
//
|
|
21986
|
-
// the parent event's
|
|
21987
|
-
// backfill left null.
|
|
21988
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
WHERE fr.finding_key = f.finding_key
|
|
21998
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21999
|
-
LIMIT 1
|
|
22000
|
-
) AS latest_method,
|
|
22001
|
-
(
|
|
22002
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22003
|
-
WHERE fr.finding_key = f.finding_key
|
|
22004
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22005
|
-
LIMIT 1
|
|
22006
|
-
) AS latest_resolved_at
|
|
22007
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22422
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22423
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22424
|
+
// legacy/edge row the backfill left null.
|
|
22425
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22426
|
+
latest.status AS latest_status,
|
|
22427
|
+
latest.method AS latest_method,
|
|
22428
|
+
latest.resolved_at AS latest_resolved_at
|
|
22429
|
+
FROM inspection_findings f
|
|
22430
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22431
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22432
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22433
|
+
ON latest.finding_key = f.finding_key
|
|
22008
22434
|
WHERE f.finding_key IS NOT NULL
|
|
22435
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22009
22436
|
AND EXISTS (
|
|
22010
22437
|
SELECT 1 FROM finding_resolution fr
|
|
22011
22438
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22052,11 +22479,13 @@ var SqliteSecurityRepository = class {
|
|
|
22052
22479
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22053
22480
|
const rows = allRows(
|
|
22054
22481
|
this.db.prepare(
|
|
22055
|
-
`SELECT json_extract(e.
|
|
22056
|
-
FROM
|
|
22057
|
-
|
|
22058
|
-
|
|
22059
|
-
AND
|
|
22482
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22483
|
+
FROM inspection_findings f
|
|
22484
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22485
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22486
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22487
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22488
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22060
22489
|
GROUP BY repo
|
|
22061
22490
|
ORDER BY c DESC, repo
|
|
22062
22491
|
LIMIT :limit`
|
|
@@ -22080,44 +22509,28 @@ var SqliteSecurityRepository = class {
|
|
|
22080
22509
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22081
22510
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22082
22511
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22083
|
-
// finding's parent event (
|
|
22084
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22085
|
-
// capped at `limit`.
|
|
22512
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22513
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22514
|
+
// DESC, capped at `limit`.
|
|
22086
22515
|
recentlyResolved(limit = 20) {
|
|
22087
22516
|
const rows = allRows(
|
|
22088
22517
|
this.db.prepare(
|
|
22089
22518
|
`SELECT f.finding_key AS finding_key,
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
json_extract(e.
|
|
22093
|
-
COALESCE(f.first_detected_at, e.
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
WHERE e.kind = 'code_change'
|
|
22519
|
+
d.rule_id AS rule_id,
|
|
22520
|
+
d.severity AS severity,
|
|
22521
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22522
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22523
|
+
latest.resolved_at AS latest_resolved_at
|
|
22524
|
+
FROM inspection_findings f
|
|
22525
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22526
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22527
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22528
|
+
ON latest.finding_key = f.finding_key
|
|
22529
|
+
WHERE e.event_type = 'code_change'
|
|
22102
22530
|
AND f.finding_key IS NOT NULL
|
|
22103
|
-
AND
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22107
|
-
LIMIT 1
|
|
22108
|
-
) = 'resolved'
|
|
22109
|
-
AND (
|
|
22110
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22111
|
-
WHERE fr.finding_key = f.finding_key
|
|
22112
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22113
|
-
LIMIT 1
|
|
22114
|
-
) = 'fixed-at-source'
|
|
22115
|
-
AND (
|
|
22116
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22117
|
-
WHERE fr.finding_key = f.finding_key
|
|
22118
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22119
|
-
LIMIT 1
|
|
22120
|
-
) IS NOT NULL
|
|
22531
|
+
AND latest.status = 'resolved'
|
|
22532
|
+
AND latest.method = 'fixed-at-source'
|
|
22533
|
+
AND latest.resolved_at IS NOT NULL
|
|
22121
22534
|
ORDER BY latest_resolved_at DESC
|
|
22122
22535
|
LIMIT :limit`
|
|
22123
22536
|
),
|
|
@@ -22136,15 +22549,18 @@ var SqliteSecurityRepository = class {
|
|
|
22136
22549
|
return Promise.resolve({ items });
|
|
22137
22550
|
}
|
|
22138
22551
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22139
|
-
// epoch-millis timestamp.
|
|
22552
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22140
22553
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22141
22554
|
findingsInRange(fromMs, toMs) {
|
|
22142
22555
|
const rows = allRows(
|
|
22143
22556
|
this.db.prepare(
|
|
22144
|
-
`SELECT e.
|
|
22145
|
-
FROM
|
|
22146
|
-
|
|
22147
|
-
|
|
22557
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22558
|
+
FROM inspection_findings f
|
|
22559
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22560
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22561
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22562
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22563
|
+
ORDER BY e.started_at`
|
|
22148
22564
|
),
|
|
22149
22565
|
{ from: fromMs, to: toMs }
|
|
22150
22566
|
);
|
|
@@ -22968,9 +23384,10 @@ function openWithPragmas(file2) {
|
|
|
22968
23384
|
}
|
|
22969
23385
|
function backupLegacyStore(file2) {
|
|
22970
23386
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
23387
|
+
renameSync2(file2, backup);
|
|
23388
|
+
tightenFile(backup);
|
|
23389
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23390
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22974
23391
|
}
|
|
22975
23392
|
return backup;
|
|
22976
23393
|
}
|
|
@@ -22986,7 +23403,7 @@ function openLocalDatabase(dir) {
|
|
|
22986
23403
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22987
23404
|
);
|
|
22988
23405
|
}
|
|
22989
|
-
applyMigrations(db);
|
|
23406
|
+
applyMigrations(db, file2);
|
|
22990
23407
|
tightenPerms(file2);
|
|
22991
23408
|
const events = new SqliteEventsRepository(db);
|
|
22992
23409
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -23013,9 +23430,56 @@ function openLocalDatabase(dir) {
|
|
|
23013
23430
|
policies.seedDefaults();
|
|
23014
23431
|
function recordCapture(event, detected) {
|
|
23015
23432
|
failOpenTransaction(db, () => {
|
|
23016
|
-
events.insertEvent(event);
|
|
23017
23433
|
const sessionId = event.metadata?.sessionId;
|
|
23018
|
-
|
|
23434
|
+
if (sessionId) {
|
|
23435
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23436
|
+
}
|
|
23437
|
+
const auditEventId = captureId(
|
|
23438
|
+
sessionId ?? null,
|
|
23439
|
+
event.contentHash,
|
|
23440
|
+
event.metadata?.filePath ?? null
|
|
23441
|
+
);
|
|
23442
|
+
auditEvents.insertAuditEvent({
|
|
23443
|
+
id: auditEventId,
|
|
23444
|
+
eventType: event.kind,
|
|
23445
|
+
startedAt: event.occurredAt,
|
|
23446
|
+
parentId: sessionId,
|
|
23447
|
+
rootSessionId: sessionId,
|
|
23448
|
+
content: event.content,
|
|
23449
|
+
contentHash: event.contentHash,
|
|
23450
|
+
attributes: toCaptureAttributes(event)
|
|
23451
|
+
});
|
|
23452
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23453
|
+
for (const finding of detected) {
|
|
23454
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23455
|
+
continue;
|
|
23456
|
+
}
|
|
23457
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23458
|
+
auditEventId,
|
|
23459
|
+
finding.ruleId,
|
|
23460
|
+
finding.maskedMatch,
|
|
23461
|
+
finding.span.start,
|
|
23462
|
+
finding.span.end
|
|
23463
|
+
)) {
|
|
23464
|
+
continue;
|
|
23465
|
+
}
|
|
23466
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23467
|
+
let definitionId = definitionIds.get(key);
|
|
23468
|
+
if (!definitionId) {
|
|
23469
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23470
|
+
definitionIds.set(key, definitionId);
|
|
23471
|
+
}
|
|
23472
|
+
inspectionFindings.insertFinding({
|
|
23473
|
+
id: finding.id,
|
|
23474
|
+
auditEventId,
|
|
23475
|
+
inspectionDefinitionId: definitionId,
|
|
23476
|
+
span: finding.span,
|
|
23477
|
+
maskedMatch: finding.maskedMatch,
|
|
23478
|
+
actionTaken: finding.actionTaken,
|
|
23479
|
+
confidence: finding.confidence,
|
|
23480
|
+
findingKey: finding.findingKey ?? void 0
|
|
23481
|
+
});
|
|
23482
|
+
}
|
|
23019
23483
|
});
|
|
23020
23484
|
}
|
|
23021
23485
|
function ensureInventory(ctx) {
|
|
@@ -23163,9 +23627,12 @@ function openLocalDatabase(dir) {
|
|
|
23163
23627
|
};
|
|
23164
23628
|
}
|
|
23165
23629
|
|
|
23630
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23631
|
+
import { createHash as createHash3 } from "crypto";
|
|
23632
|
+
|
|
23166
23633
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23167
23634
|
import { createHmac, randomBytes } from "crypto";
|
|
23168
|
-
import {
|
|
23635
|
+
import { readFileSync } from "fs";
|
|
23169
23636
|
import { join as join2 } from "path";
|
|
23170
23637
|
var KEY_FILENAME = "exception.key";
|
|
23171
23638
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -23202,8 +23669,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
23202
23669
|
}
|
|
23203
23670
|
|
|
23204
23671
|
// ../../packages/persistence/src/local-layout.ts
|
|
23205
|
-
import {
|
|
23206
|
-
import {
|
|
23672
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23673
|
+
import { mkdir } from "fs/promises";
|
|
23207
23674
|
import { homedir } from "os";
|
|
23208
23675
|
import { join as join3 } from "path";
|
|
23209
23676
|
function defaultDataDir() {
|
|
@@ -23218,6 +23685,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23218
23685
|
function dbPath(base = defaultDataDir()) {
|
|
23219
23686
|
return join3(dataDir(base), "aka.db");
|
|
23220
23687
|
}
|
|
23688
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23689
|
+
ensureDataDirSync(dir);
|
|
23690
|
+
}
|
|
23221
23691
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23222
23692
|
const moves = [
|
|
23223
23693
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23225,19 +23695,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23225
23695
|
];
|
|
23226
23696
|
for (const { name, dest } of moves) {
|
|
23227
23697
|
try {
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
|
|
23231
|
-
|
|
23232
|
-
}
|
|
23233
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23698
|
+
ensureDataDirSync(dest);
|
|
23699
|
+
const moved = join3(dest, name);
|
|
23700
|
+
renameSync3(join3(base, name), moved);
|
|
23701
|
+
tightenFile(moved);
|
|
23234
23702
|
} catch {
|
|
23235
23703
|
}
|
|
23236
23704
|
}
|
|
23237
23705
|
}
|
|
23238
23706
|
|
|
23239
23707
|
// ../../packages/persistence/src/settings.ts
|
|
23240
|
-
import { readFileSync as readFileSync2
|
|
23708
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23241
23709
|
import { join as join4 } from "path";
|
|
23242
23710
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23243
23711
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23259,7 +23727,7 @@ function readJson(file2) {
|
|
|
23259
23727
|
}
|
|
23260
23728
|
|
|
23261
23729
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23262
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23730
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
23263
23731
|
import { join as join5 } from "path";
|
|
23264
23732
|
var MARKER = "warn-era-capped";
|
|
23265
23733
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -23267,7 +23735,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
23267
23735
|
const marker = join5(dataDir2, MARKER);
|
|
23268
23736
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
23269
23737
|
const capped = db.policies.capCategoryActions();
|
|
23270
|
-
|
|
23738
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23271
23739
|
`, { mode: DATA_FILE_MODE });
|
|
23272
23740
|
return { capped };
|
|
23273
23741
|
}
|
|
@@ -23332,6 +23800,12 @@ function providerFromModelId(modelId) {
|
|
|
23332
23800
|
|
|
23333
23801
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23334
23802
|
function loadConfig(base = defaultDataDir()) {
|
|
23803
|
+
try {
|
|
23804
|
+
ensureLayoutDirSync(base);
|
|
23805
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23806
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23807
|
+
} catch {
|
|
23808
|
+
}
|
|
23335
23809
|
migrateLegacyLayout(base);
|
|
23336
23810
|
const settings = readWorkspaceSettings(base);
|
|
23337
23811
|
return {
|
|
@@ -23354,7 +23828,7 @@ function resolveProviderSafe() {
|
|
|
23354
23828
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23355
23829
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23356
23830
|
import { homedir as homedir2 } from "os";
|
|
23357
|
-
import { basename as basename2, join as
|
|
23831
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23358
23832
|
|
|
23359
23833
|
// ../../packages/detections/src/egress/registry.ts
|
|
23360
23834
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -26305,7 +26779,7 @@ function ensureBundledPacks() {
|
|
|
26305
26779
|
return false;
|
|
26306
26780
|
}
|
|
26307
26781
|
}
|
|
26308
|
-
function scanText(text) {
|
|
26782
|
+
function scanText(text, ruleVersions) {
|
|
26309
26783
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
26310
26784
|
try {
|
|
26311
26785
|
const rules = getLoadedRules();
|
|
@@ -26317,7 +26791,7 @@ function scanText(text) {
|
|
|
26317
26791
|
return {
|
|
26318
26792
|
ruleId: m.ruleId,
|
|
26319
26793
|
ruleName: rule?.name ?? m.ruleId,
|
|
26320
|
-
ruleVersion: String(rule?.specVersion ?? 1),
|
|
26794
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
26321
26795
|
category: m.category,
|
|
26322
26796
|
severity: m.severity,
|
|
26323
26797
|
span: m.span,
|
|
@@ -26332,8 +26806,8 @@ function scanText(text) {
|
|
|
26332
26806
|
}
|
|
26333
26807
|
|
|
26334
26808
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26335
|
-
import { existsSync as
|
|
26336
|
-
import { basename, dirname, isAbsolute, join as
|
|
26809
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26810
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26337
26811
|
function resolveRepoIdentity(cwd) {
|
|
26338
26812
|
try {
|
|
26339
26813
|
const root = findGitRoot(cwd);
|
|
@@ -26366,32 +26840,32 @@ function resolveRepoNwo(cwd) {
|
|
|
26366
26840
|
function findGitRoot(start) {
|
|
26367
26841
|
let dir = start;
|
|
26368
26842
|
for (; ; ) {
|
|
26369
|
-
if (
|
|
26843
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
26370
26844
|
const parent = dirname(dir);
|
|
26371
26845
|
if (parent === dir) return void 0;
|
|
26372
26846
|
dir = parent;
|
|
26373
26847
|
}
|
|
26374
26848
|
}
|
|
26375
26849
|
function resolveGitContext(root) {
|
|
26376
|
-
const dotGit =
|
|
26850
|
+
const dotGit = join7(root, ".git");
|
|
26377
26851
|
try {
|
|
26378
26852
|
if (statSync(dotGit).isDirectory()) {
|
|
26379
|
-
return { configPath:
|
|
26853
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
26380
26854
|
}
|
|
26381
26855
|
} catch {
|
|
26382
26856
|
return void 0;
|
|
26383
26857
|
}
|
|
26384
26858
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
26385
26859
|
if (!target) return void 0;
|
|
26386
|
-
const gitdir = isAbsolute(target) ? target :
|
|
26387
|
-
if (
|
|
26388
|
-
return { configPath:
|
|
26860
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
26861
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
26862
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
26389
26863
|
}
|
|
26390
|
-
const commonRaw = safeRead(
|
|
26864
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
26391
26865
|
if (!commonRaw) return void 0;
|
|
26392
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
26866
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
26393
26867
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
26394
|
-
return { configPath:
|
|
26868
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
26395
26869
|
}
|
|
26396
26870
|
function safeRead(path) {
|
|
26397
26871
|
try {
|
|
@@ -26446,10 +26920,7 @@ function nwoFromUrl(url2) {
|
|
|
26446
26920
|
}
|
|
26447
26921
|
|
|
26448
26922
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26449
|
-
import { createHash as
|
|
26450
|
-
|
|
26451
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
26452
|
-
import { createHash as createHash4 } from "crypto";
|
|
26923
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26453
26924
|
|
|
26454
26925
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26455
26926
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
@@ -26481,8 +26952,8 @@ function resolveInventoryContext(input) {
|
|
|
26481
26952
|
}
|
|
26482
26953
|
|
|
26483
26954
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26484
|
-
import { mkdirSync as
|
|
26485
|
-
import { join as
|
|
26955
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26956
|
+
import { join as join9 } from "path";
|
|
26486
26957
|
|
|
26487
26958
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
26488
26959
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -26490,8 +26961,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
|
26490
26961
|
|
|
26491
26962
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
26492
26963
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
26493
|
-
import { existsSync as
|
|
26494
|
-
import { basename as basename4, join as
|
|
26964
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26965
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
26495
26966
|
|
|
26496
26967
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
26497
26968
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -26500,8 +26971,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
26500
26971
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26501
26972
|
|
|
26502
26973
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
26503
|
-
import { mkdirSync as
|
|
26504
|
-
import { join as
|
|
26974
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26975
|
+
import { join as join11 } from "path";
|
|
26505
26976
|
|
|
26506
26977
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
26507
26978
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -26532,7 +27003,8 @@ var StandaloneDataGateway = class {
|
|
|
26532
27003
|
}
|
|
26533
27004
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
26534
27005
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
26535
|
-
// hands the natural key across.
|
|
27006
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27007
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
26536
27008
|
recordLlmCall(input) {
|
|
26537
27009
|
this.db.auditEvents.insertLlmCall(input);
|
|
26538
27010
|
return Promise.resolve();
|
|
@@ -26574,7 +27046,9 @@ var StandaloneDataGateway = class {
|
|
|
26574
27046
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
26575
27047
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
26576
27048
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
26577
|
-
// content-addressed
|
|
27049
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27050
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27051
|
+
// than no-opping.
|
|
26578
27052
|
writeToolCall(input) {
|
|
26579
27053
|
this.db.auditEvents.insertToolCall(input);
|
|
26580
27054
|
if (input.inspections.length === 0) return;
|
|
@@ -26594,7 +27068,7 @@ var StandaloneDataGateway = class {
|
|
|
26594
27068
|
});
|
|
26595
27069
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
26596
27070
|
this.db.inspectionFindings.insertFinding({
|
|
26597
|
-
id: inspectionFindingId(auditEventId,
|
|
27071
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
26598
27072
|
auditEventId,
|
|
26599
27073
|
inspectionDefinitionId: definitionId,
|
|
26600
27074
|
classifiedDataId: classifiedDataId2,
|
|
@@ -26643,10 +27117,17 @@ var StandaloneDataGateway = class {
|
|
|
26643
27117
|
try {
|
|
26644
27118
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
26645
27119
|
if (snapshot.installedPacks === 0) return void 0;
|
|
26646
|
-
if (snapshot.enabledPacks === 0)
|
|
27120
|
+
if (snapshot.enabledPacks === 0) {
|
|
27121
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27122
|
+
}
|
|
26647
27123
|
if (snapshot.invalidRules > 0) return void 0;
|
|
26648
27124
|
if (snapshot.rules.length === 0) return void 0;
|
|
26649
|
-
return {
|
|
27125
|
+
return {
|
|
27126
|
+
rules: snapshot.rules,
|
|
27127
|
+
ruleActions: snapshot.ruleActions,
|
|
27128
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27129
|
+
complete: true
|
|
27130
|
+
};
|
|
26650
27131
|
} catch {
|
|
26651
27132
|
return void 0;
|
|
26652
27133
|
}
|
|
@@ -26674,6 +27155,7 @@ var StandaloneDataGateway = class {
|
|
|
26674
27155
|
policies: [...policies, ...rulePolicies],
|
|
26675
27156
|
rules: installed ? installed.rules : [],
|
|
26676
27157
|
...installed ? { rulesComplete: true } : {},
|
|
27158
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
26677
27159
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
26678
27160
|
customKeywords,
|
|
26679
27161
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -26816,15 +27298,15 @@ import { createHash as createHash5 } from "crypto";
|
|
|
26816
27298
|
import {
|
|
26817
27299
|
closeSync,
|
|
26818
27300
|
fstatSync,
|
|
26819
|
-
mkdirSync as
|
|
27301
|
+
mkdirSync as mkdirSync4,
|
|
26820
27302
|
openSync,
|
|
26821
27303
|
readFileSync as readFileSync7,
|
|
26822
27304
|
readSync,
|
|
26823
|
-
writeFileSync as
|
|
27305
|
+
writeFileSync as writeFileSync5
|
|
26824
27306
|
} from "fs";
|
|
26825
|
-
import { join as
|
|
27307
|
+
import { join as join12 } from "path";
|
|
26826
27308
|
function offsetsDir(dataDir2) {
|
|
26827
|
-
return
|
|
27309
|
+
return join12(dataDir2, "usage-offsets");
|
|
26828
27310
|
}
|
|
26829
27311
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
26830
27312
|
function safeSessionId(sessionId) {
|
|
@@ -26834,7 +27316,7 @@ function safeSessionId(sessionId) {
|
|
|
26834
27316
|
return createHash5("sha256").update(sessionId).digest("hex");
|
|
26835
27317
|
}
|
|
26836
27318
|
function offsetPath(dataDir2, sessionId) {
|
|
26837
|
-
return
|
|
27319
|
+
return join12(offsetsDir(dataDir2), safeSessionId(sessionId));
|
|
26838
27320
|
}
|
|
26839
27321
|
function readOffset(dataDir2, sessionId) {
|
|
26840
27322
|
try {
|
|
@@ -26852,9 +27334,9 @@ function readOffset(dataDir2, sessionId) {
|
|
|
26852
27334
|
}
|
|
26853
27335
|
function writeOffset(dataDir2, sessionId, value) {
|
|
26854
27336
|
try {
|
|
26855
|
-
|
|
27337
|
+
mkdirSync4(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
|
|
26856
27338
|
const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
|
|
26857
|
-
|
|
27339
|
+
writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
|
|
26858
27340
|
mode: DATA_FILE_MODE
|
|
26859
27341
|
});
|
|
26860
27342
|
} catch {
|
|
@@ -26898,7 +27380,7 @@ function readTail(transcriptPath, startOffset) {
|
|
|
26898
27380
|
// src/history/transcripts.ts
|
|
26899
27381
|
import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
26900
27382
|
import { homedir as homedir3 } from "os";
|
|
26901
|
-
import { join as
|
|
27383
|
+
import { join as join13 } from "path";
|
|
26902
27384
|
function isRecord(value) {
|
|
26903
27385
|
return typeof value === "object" && value !== null;
|
|
26904
27386
|
}
|
|
@@ -26927,9 +27409,9 @@ function parseTranscriptUsage(jsonl, sinceMs = 0) {
|
|
|
26927
27409
|
if (sinceMs > 0 && Date.parse(optString(rec.timestamp) ?? "") < sinceMs) continue;
|
|
26928
27410
|
if (rec.type === "user") {
|
|
26929
27411
|
const uuid5 = optString(rec.uuid);
|
|
26930
|
-
const
|
|
26931
|
-
if (uuid5 === void 0 ||
|
|
26932
|
-
out.push({ kind: "user", uuid: uuid5, promptId });
|
|
27412
|
+
const promptId2 = optString(rec.promptId);
|
|
27413
|
+
if (uuid5 === void 0 || promptId2 === void 0) continue;
|
|
27414
|
+
out.push({ kind: "user", uuid: uuid5, promptId: promptId2 });
|
|
26933
27415
|
continue;
|
|
26934
27416
|
}
|
|
26935
27417
|
if (rec.type !== "assistant") continue;
|
|
@@ -27149,12 +27631,18 @@ async function reconcileSessionToolCalls(gateway, sessionId, toolCalls, usageRec
|
|
|
27149
27631
|
if (toolCalls.length === 0) return 0;
|
|
27150
27632
|
const promptIdByUuid = /* @__PURE__ */ new Map();
|
|
27151
27633
|
for (const r of usageRecords) if (r.kind === "user") promptIdByUuid.set(r.uuid, r.promptId);
|
|
27634
|
+
let ruleVersions;
|
|
27635
|
+
try {
|
|
27636
|
+
ruleVersions = (await gateway.getPolicyBundle()).ruleVersions;
|
|
27637
|
+
} catch {
|
|
27638
|
+
ruleVersions = void 0;
|
|
27639
|
+
}
|
|
27152
27640
|
const inputs = toolCalls.map((tc) => {
|
|
27153
27641
|
const runKey = (tc.parentUuid !== void 0 ? promptIdByUuid.get(tc.parentUuid) : void 0) ?? opts.seedPromptId;
|
|
27154
27642
|
const attributes = { tool_name: tc.toolName, tool_use_id: tc.toolUseId };
|
|
27155
27643
|
let inspections = [];
|
|
27156
27644
|
if (tc.target !== void 0) {
|
|
27157
|
-
const { masked, findings } = scanText(tc.target);
|
|
27645
|
+
const { masked, findings } = scanText(tc.target, ruleVersions);
|
|
27158
27646
|
if (masked !== "") attributes.target = truncateTarget(masked);
|
|
27159
27647
|
inspections = findings.map((f) => ({
|
|
27160
27648
|
ruleId: f.ruleId,
|