@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +3251 -497
- package/scripts/backfill.js +1081 -493
- package/scripts/filescan.js +1069 -487
- package/scripts/firstrun.js +981 -450
- package/scripts/intro.js +92 -24
- package/scripts/onboard.js +1006 -453
- package/scripts/post-tool-use.js +1062 -480
- package/scripts/pre-tool-use.js +1067 -485
- package/scripts/query.js +981 -450
- package/scripts/reconcile.js +1010 -473
- package/scripts/remediate.js +1072 -490
- package/scripts/session-start.js +1043 -512
- package/scripts/start-light.js +92 -24
- package/scripts/statusline.js +981 -450
- package/scripts/stop.js +133 -38
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +1070 -488
package/scripts/backfill.js
CHANGED
|
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
|
|
|
494
494
|
// src/backfill.ts
|
|
495
495
|
import { fileURLToPath } from "url";
|
|
496
496
|
|
|
497
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
498
|
+
import { existsSync as existsSync4 } from "fs";
|
|
499
|
+
import { join as join6 } from "path";
|
|
500
|
+
|
|
497
501
|
// ../../packages/persistence/src/database.ts
|
|
498
502
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
499
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
503
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
500
504
|
import { join, sep } from "path";
|
|
501
505
|
import { DatabaseSync } from "node:sqlite";
|
|
502
506
|
|
|
@@ -549,6 +553,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
549
553
|
{
|
|
550
554
|
tag: "0011_egress_writer",
|
|
551
555
|
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
tag: "0012_handy_the_captain",
|
|
559
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
tag: "0013_legacy_history_backfill_support",
|
|
563
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
tag: "0014_drop_legacy_events_findings",
|
|
567
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
552
568
|
}
|
|
553
569
|
];
|
|
554
570
|
|
|
@@ -15379,7 +15395,12 @@ var FindingFacets = external_exports.object({
|
|
|
15379
15395
|
severity: external_exports.array(FindingFacetItem),
|
|
15380
15396
|
subtype: external_exports.array(FindingFacetItem),
|
|
15381
15397
|
provider: external_exports.array(FindingFacetItem),
|
|
15382
|
-
action: external_exports.array(FindingFacetItem)
|
|
15398
|
+
action: external_exports.array(FindingFacetItem),
|
|
15399
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15400
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15401
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15402
|
+
// counted under no value.
|
|
15403
|
+
status: external_exports.array(FindingFacetItem)
|
|
15383
15404
|
}).meta({ id: "FindingFacets" });
|
|
15384
15405
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15385
15406
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15389,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15389
15410
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15390
15411
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15391
15412
|
action: external_exports.array(FindingAction).optional(),
|
|
15413
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15414
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15415
|
+
// one of the requested values.
|
|
15416
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15392
15417
|
q: external_exports.string().optional(),
|
|
15393
15418
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15394
15419
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15579,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15579
15604
|
parent_uuid: external_exports.string().optional(),
|
|
15580
15605
|
run_key: external_exports.string().optional()
|
|
15581
15606
|
}).catchall(external_exports.unknown());
|
|
15607
|
+
var CaptureAttributes = external_exports.object({
|
|
15608
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15609
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15610
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15611
|
+
source_tool: external_exports.string().optional(),
|
|
15612
|
+
file_path: external_exports.string().optional(),
|
|
15613
|
+
repo: external_exports.string().optional(),
|
|
15614
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15615
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15616
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15617
|
+
tool_name: external_exports.string().optional(),
|
|
15618
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15619
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15620
|
+
gitignored: external_exports.boolean().optional(),
|
|
15621
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15622
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15623
|
+
whole_file: external_exports.boolean().optional(),
|
|
15624
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15625
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15626
|
+
// trace id when telemetry is enabled.
|
|
15627
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15628
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15629
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15630
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15631
|
+
// authorized the bypass.
|
|
15632
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15633
|
+
}).catchall(external_exports.unknown());
|
|
15582
15634
|
var ToolCallInspection = external_exports.object({
|
|
15583
15635
|
ruleId: external_exports.string().min(1),
|
|
15584
15636
|
ruleName: external_exports.string(),
|
|
@@ -15665,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15665
15717
|
span: Span,
|
|
15666
15718
|
maskedMatch: external_exports.string(),
|
|
15667
15719
|
actionTaken: ActionTaken,
|
|
15668
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15720
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15721
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15722
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15723
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15724
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15725
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15726
|
+
findingKey: external_exports.string().optional(),
|
|
15727
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15728
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15729
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15730
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15731
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15669
15732
|
});
|
|
15670
15733
|
var InventoryContext = external_exports.object({
|
|
15671
15734
|
host: InventoryInput.optional(),
|
|
@@ -15867,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15867
15930
|
|
|
15868
15931
|
// ../../packages/schema/src/zod/event.ts
|
|
15869
15932
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15933
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15870
15934
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15871
15935
|
var EventMetadata = external_exports.object({
|
|
15872
15936
|
sessionId: external_exports.string().optional(),
|
|
@@ -16356,6 +16420,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16356
16420
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16357
16421
|
// `bundle.exceptions ?? []`.
|
|
16358
16422
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16423
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16424
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16425
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16426
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16427
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16428
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16359
16429
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16360
16430
|
fetchedAt: external_exports.iso.datetime()
|
|
16361
16431
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17155,6 +17225,15 @@ function groupActions(g) {
|
|
|
17155
17225
|
actionsCache.set(g, actions);
|
|
17156
17226
|
return actions;
|
|
17157
17227
|
}
|
|
17228
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17229
|
+
const statusSet = new Set(statuses);
|
|
17230
|
+
let sum = 0;
|
|
17231
|
+
for (const input of statusInputs) {
|
|
17232
|
+
if (input.count === void 0) return null;
|
|
17233
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17234
|
+
}
|
|
17235
|
+
return sum;
|
|
17236
|
+
}
|
|
17158
17237
|
function applyFindingFilters(groups, opts) {
|
|
17159
17238
|
let filtered = groups;
|
|
17160
17239
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17173,6 +17252,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17173
17252
|
const subtypeSet = new Set(opts.subtype);
|
|
17174
17253
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17175
17254
|
}
|
|
17255
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17256
|
+
const statusSet = new Set(opts.statuses);
|
|
17257
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17258
|
+
}
|
|
17176
17259
|
if (opts.q) {
|
|
17177
17260
|
const q = opts.q.toLowerCase();
|
|
17178
17261
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17194,6 +17277,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17194
17277
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17195
17278
|
providers: opts.providers,
|
|
17196
17279
|
actions: opts.actions,
|
|
17280
|
+
statuses: opts.statuses,
|
|
17197
17281
|
q: opts.q,
|
|
17198
17282
|
subtype: opts.subtype
|
|
17199
17283
|
});
|
|
@@ -17203,6 +17287,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17203
17287
|
}
|
|
17204
17288
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17205
17289
|
actions: opts.actions,
|
|
17290
|
+
statuses: opts.statuses,
|
|
17206
17291
|
q: opts.q,
|
|
17207
17292
|
subtype: opts.subtype,
|
|
17208
17293
|
severity: opts.severity
|
|
@@ -17213,6 +17298,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17213
17298
|
}
|
|
17214
17299
|
const forAction = applyFindingFilters(allGroups, {
|
|
17215
17300
|
providers: opts.providers,
|
|
17301
|
+
statuses: opts.statuses,
|
|
17216
17302
|
q: opts.q,
|
|
17217
17303
|
subtype: opts.subtype,
|
|
17218
17304
|
severity: opts.severity
|
|
@@ -17224,17 +17310,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17224
17310
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17225
17311
|
providers: opts.providers,
|
|
17226
17312
|
actions: opts.actions,
|
|
17313
|
+
statuses: opts.statuses,
|
|
17227
17314
|
q: opts.q,
|
|
17228
17315
|
severity: opts.severity
|
|
17229
17316
|
});
|
|
17230
17317
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17231
17318
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17319
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17320
|
+
providers: opts.providers,
|
|
17321
|
+
actions: opts.actions,
|
|
17322
|
+
q: opts.q,
|
|
17323
|
+
subtype: opts.subtype,
|
|
17324
|
+
severity: opts.severity
|
|
17325
|
+
});
|
|
17326
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17327
|
+
for (const g of forStatus) {
|
|
17328
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17329
|
+
}
|
|
17232
17330
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17233
17331
|
return {
|
|
17234
17332
|
severity: toItems(severityMap),
|
|
17235
17333
|
provider: toItems(providerMap),
|
|
17236
17334
|
action: toItems(actionMap),
|
|
17237
|
-
subtype: toItems(subtypeMap)
|
|
17335
|
+
subtype: toItems(subtypeMap),
|
|
17336
|
+
status: toItems(statusMap)
|
|
17238
17337
|
};
|
|
17239
17338
|
}
|
|
17240
17339
|
|
|
@@ -17269,10 +17368,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17269
17368
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17270
17369
|
|
|
17271
17370
|
// ../../packages/schema/src/zod/local.ts
|
|
17272
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17371
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17273
17372
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17274
17373
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17275
17374
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17375
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17376
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17377
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17378
|
+
});
|
|
17276
17379
|
var WorkspaceSettings = external_exports.object({
|
|
17277
17380
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17278
17381
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17288,37 +17391,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17288
17391
|
// Shares writes.
|
|
17289
17392
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17290
17393
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17291
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17394
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17395
|
+
// Records that the user consented to sending findings to the model API for
|
|
17396
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17397
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17398
|
+
// covers the current payload and must be re-granted.
|
|
17399
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17292
17400
|
});
|
|
17293
17401
|
function defaultWorkspaceSettings() {
|
|
17294
17402
|
return WorkspaceSettings.parse({});
|
|
17295
17403
|
}
|
|
17296
|
-
function toEventRow(event) {
|
|
17297
|
-
return {
|
|
17298
|
-
id: event.id,
|
|
17299
|
-
sourceTool: event.sourceTool,
|
|
17300
|
-
kind: event.kind,
|
|
17301
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17302
|
-
contentHash: event.contentHash,
|
|
17303
|
-
content: event.content,
|
|
17304
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17305
|
-
};
|
|
17306
|
-
}
|
|
17307
|
-
function toFindingRow(finding) {
|
|
17308
|
-
return {
|
|
17309
|
-
id: finding.id,
|
|
17310
|
-
eventId: finding.eventId,
|
|
17311
|
-
ruleId: finding.ruleId,
|
|
17312
|
-
category: finding.category,
|
|
17313
|
-
severity: finding.severity,
|
|
17314
|
-
spanStart: finding.span.start,
|
|
17315
|
-
spanEnd: finding.span.end,
|
|
17316
|
-
maskedMatch: finding.maskedMatch,
|
|
17317
|
-
actionTaken: finding.actionTaken,
|
|
17318
|
-
confidence: finding.confidence,
|
|
17319
|
-
findingKey: finding.findingKey ?? null
|
|
17320
|
-
};
|
|
17321
|
-
}
|
|
17322
17404
|
function toInventoryRow(input, id, now) {
|
|
17323
17405
|
return {
|
|
17324
17406
|
id,
|
|
@@ -17388,7 +17470,42 @@ function toInspectionFindingRow(input) {
|
|
|
17388
17470
|
spanEnd: input.span.end,
|
|
17389
17471
|
maskedMatch: input.maskedMatch,
|
|
17390
17472
|
actionTaken: input.actionTaken,
|
|
17391
|
-
confidence: input.confidence
|
|
17473
|
+
confidence: input.confidence,
|
|
17474
|
+
findingKey: input.findingKey ?? null,
|
|
17475
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17476
|
+
};
|
|
17477
|
+
}
|
|
17478
|
+
function toCaptureAttributes(event) {
|
|
17479
|
+
const metadata = event.metadata;
|
|
17480
|
+
return {
|
|
17481
|
+
source_tool: event.sourceTool,
|
|
17482
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17483
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17484
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17485
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17486
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17487
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17488
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17489
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17490
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17491
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17492
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17493
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17494
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17495
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17496
|
+
};
|
|
17497
|
+
}
|
|
17498
|
+
function captureDefinitionVersion(finding) {
|
|
17499
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17500
|
+
}
|
|
17501
|
+
function toCaptureDefinitionInput(finding) {
|
|
17502
|
+
return {
|
|
17503
|
+
ruleId: finding.ruleId,
|
|
17504
|
+
version: captureDefinitionVersion(finding),
|
|
17505
|
+
name: finding.ruleId,
|
|
17506
|
+
category: finding.category,
|
|
17507
|
+
severity: finding.severity,
|
|
17508
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17392
17509
|
};
|
|
17393
17510
|
}
|
|
17394
17511
|
|
|
@@ -17816,6 +17933,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17816
17933
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17817
17934
|
}
|
|
17818
17935
|
|
|
17936
|
+
// ../../packages/persistence/src/ids.ts
|
|
17937
|
+
import { createHash } from "crypto";
|
|
17938
|
+
function sha256Hex(input) {
|
|
17939
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17940
|
+
}
|
|
17941
|
+
function inventoryId(objectType, identityKey) {
|
|
17942
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17943
|
+
}
|
|
17944
|
+
function sourceProjectId(url2) {
|
|
17945
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17946
|
+
}
|
|
17947
|
+
function classifiedDataId(cls) {
|
|
17948
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17949
|
+
}
|
|
17950
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17951
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17952
|
+
}
|
|
17953
|
+
function llmCallId(sessionId, messageId) {
|
|
17954
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17955
|
+
}
|
|
17956
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17957
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17958
|
+
}
|
|
17959
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17960
|
+
return sha256Hex(
|
|
17961
|
+
canonicalIdentity([
|
|
17962
|
+
"inspection_finding",
|
|
17963
|
+
auditEventId,
|
|
17964
|
+
ruleId,
|
|
17965
|
+
String(spanStart),
|
|
17966
|
+
String(spanEnd)
|
|
17967
|
+
])
|
|
17968
|
+
);
|
|
17969
|
+
}
|
|
17970
|
+
var NO_SESSION = "no_session";
|
|
17971
|
+
var NO_PATH = "no_path";
|
|
17972
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17973
|
+
return sha256Hex(
|
|
17974
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17975
|
+
);
|
|
17976
|
+
}
|
|
17977
|
+
|
|
17819
17978
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17820
17979
|
function escapeLikePattern(s) {
|
|
17821
17980
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17912,39 +18071,98 @@ function evidenceExists(db, object2) {
|
|
|
17912
18071
|
return schemaObjectExists(db, "table", object2.name);
|
|
17913
18072
|
}
|
|
17914
18073
|
|
|
17915
|
-
// ../../packages/persistence/src/
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
18074
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18075
|
+
function allRows(stmt, params) {
|
|
18076
|
+
if (params === void 0) return stmt.all();
|
|
18077
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18078
|
+
return stmt.all(params);
|
|
17919
18079
|
}
|
|
17920
|
-
function
|
|
17921
|
-
|
|
18080
|
+
function getRow(stmt, params) {
|
|
18081
|
+
if (params === void 0) return stmt.get();
|
|
18082
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18083
|
+
return stmt.get(params);
|
|
17922
18084
|
}
|
|
17923
|
-
function
|
|
17924
|
-
return
|
|
18085
|
+
function intToBool(raw) {
|
|
18086
|
+
return raw === 1 || raw === true;
|
|
17925
18087
|
}
|
|
17926
|
-
function
|
|
17927
|
-
return
|
|
18088
|
+
function boolToInt(b) {
|
|
18089
|
+
return b ? 1 : 0;
|
|
17928
18090
|
}
|
|
17929
|
-
function
|
|
17930
|
-
|
|
18091
|
+
function bindParams(row) {
|
|
18092
|
+
const out = {};
|
|
18093
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18094
|
+
out[key] = value === void 0 ? null : value;
|
|
18095
|
+
}
|
|
18096
|
+
return out;
|
|
17931
18097
|
}
|
|
17932
|
-
function
|
|
17933
|
-
return
|
|
18098
|
+
function countScalar(db, sql, params) {
|
|
18099
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17934
18100
|
}
|
|
17935
|
-
function
|
|
17936
|
-
|
|
18101
|
+
function countBy(db, sql, params) {
|
|
18102
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18103
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18104
|
+
map2.set(row.k, row.n);
|
|
18105
|
+
}
|
|
18106
|
+
return map2;
|
|
17937
18107
|
}
|
|
17938
|
-
function
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
18108
|
+
function mapRowsTolerant(rows, map2) {
|
|
18109
|
+
const out = [];
|
|
18110
|
+
for (const row of rows) {
|
|
18111
|
+
try {
|
|
18112
|
+
out.push(map2(row));
|
|
18113
|
+
} catch {
|
|
18114
|
+
}
|
|
18115
|
+
}
|
|
18116
|
+
return out;
|
|
18117
|
+
}
|
|
18118
|
+
|
|
18119
|
+
// ../../packages/persistence/src/paths.ts
|
|
18120
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18121
|
+
var DATA_DIR_MODE = 448;
|
|
18122
|
+
var DATA_FILE_MODE = 384;
|
|
18123
|
+
var DB_FILENAME = "aka.db";
|
|
18124
|
+
function chmodBestEffort(path, mode) {
|
|
18125
|
+
try {
|
|
18126
|
+
chmodSync(path, mode);
|
|
18127
|
+
} catch {
|
|
18128
|
+
}
|
|
18129
|
+
}
|
|
18130
|
+
function tightenDir(dir) {
|
|
18131
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18132
|
+
}
|
|
18133
|
+
function ensureDataDirSync(dir) {
|
|
18134
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18135
|
+
tightenDir(dir);
|
|
18136
|
+
}
|
|
18137
|
+
function dbSidecars(file2) {
|
|
18138
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18139
|
+
}
|
|
18140
|
+
function tightenFile(file2) {
|
|
18141
|
+
try {
|
|
18142
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18143
|
+
} catch {
|
|
18144
|
+
}
|
|
18145
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18146
|
+
}
|
|
18147
|
+
function tightenPerms(file2) {
|
|
18148
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18149
|
+
}
|
|
18150
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
18151
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
18152
|
+
try {
|
|
18153
|
+
rmSync(tmp, { force: true });
|
|
18154
|
+
} catch {
|
|
18155
|
+
}
|
|
18156
|
+
try {
|
|
18157
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18158
|
+
renameSync(tmp, file2);
|
|
18159
|
+
} finally {
|
|
18160
|
+
try {
|
|
18161
|
+
rmSync(tmp, { force: true });
|
|
18162
|
+
} catch {
|
|
18163
|
+
}
|
|
18164
|
+
}
|
|
18165
|
+
tightenFile(file2);
|
|
17948
18166
|
}
|
|
17949
18167
|
|
|
17950
18168
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17958,7 +18176,8 @@ function createdIndexName(statement) {
|
|
|
17958
18176
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17959
18177
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17960
18178
|
}
|
|
17961
|
-
|
|
18179
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18180
|
+
function applyMigrations(db, file2) {
|
|
17962
18181
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17963
18182
|
db.exec(
|
|
17964
18183
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17972,6 +18191,7 @@ function applyMigrations(db) {
|
|
|
17972
18191
|
);
|
|
17973
18192
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17974
18193
|
if (applied.has(migration.tag)) continue;
|
|
18194
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17975
18195
|
const evidence = evidenceObjects(migration.sql);
|
|
17976
18196
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17977
18197
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18016,7 +18236,6 @@ function applyMigrations(db) {
|
|
|
18016
18236
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18017
18237
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18018
18238
|
}
|
|
18019
|
-
ensureSyncedAtColumn(db, "events");
|
|
18020
18239
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18021
18240
|
ensureScanLedgerTable(db);
|
|
18022
18241
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18024,6 +18243,47 @@ function applyMigrations(db) {
|
|
|
18024
18243
|
ensureWriteGateTrigger(db);
|
|
18025
18244
|
ensureTokenUsageColumns(db);
|
|
18026
18245
|
reconcileSourceProjectIds(db);
|
|
18246
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18247
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18248
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18249
|
+
}
|
|
18250
|
+
}
|
|
18251
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18252
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18253
|
+
if (!migration) return;
|
|
18254
|
+
if (file2) {
|
|
18255
|
+
try {
|
|
18256
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18257
|
+
} catch (error51) {
|
|
18258
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18259
|
+
return;
|
|
18260
|
+
}
|
|
18261
|
+
}
|
|
18262
|
+
try {
|
|
18263
|
+
withTransaction(
|
|
18264
|
+
db,
|
|
18265
|
+
() => {
|
|
18266
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18267
|
+
if (alreadyDropped) return;
|
|
18268
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18269
|
+
db.exec(statement);
|
|
18270
|
+
}
|
|
18271
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18272
|
+
migration.tag,
|
|
18273
|
+
Date.now()
|
|
18274
|
+
);
|
|
18275
|
+
},
|
|
18276
|
+
"IMMEDIATE"
|
|
18277
|
+
);
|
|
18278
|
+
} catch (error51) {
|
|
18279
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18280
|
+
}
|
|
18281
|
+
}
|
|
18282
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18283
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18284
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18285
|
+
tightenFile(backup);
|
|
18286
|
+
return backup;
|
|
18027
18287
|
}
|
|
18028
18288
|
var TOKEN_USAGE_COLUMNS = [
|
|
18029
18289
|
{
|
|
@@ -18052,6 +18312,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18052
18312
|
}
|
|
18053
18313
|
];
|
|
18054
18314
|
function ensureTokenUsageColumns(db) {
|
|
18315
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18055
18316
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18056
18317
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18057
18318
|
if (!existing.has(column.name)) {
|
|
@@ -18117,11 +18378,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18117
18378
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18118
18379
|
}
|
|
18119
18380
|
}
|
|
18381
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18382
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18383
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18384
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18385
|
+
return row?.lastRowid ?? 0;
|
|
18386
|
+
}
|
|
18387
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18388
|
+
db.prepare(
|
|
18389
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18390
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18391
|
+
).run(source, lastRowid);
|
|
18392
|
+
}
|
|
18393
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18394
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18395
|
+
let processed = 0;
|
|
18396
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18397
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18398
|
+
if (rows.length === 0) return true;
|
|
18399
|
+
withTransaction(
|
|
18400
|
+
db,
|
|
18401
|
+
() => {
|
|
18402
|
+
handleRows(rows);
|
|
18403
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18404
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18405
|
+
},
|
|
18406
|
+
"IMMEDIATE"
|
|
18407
|
+
);
|
|
18408
|
+
processed += rows.length;
|
|
18409
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18410
|
+
}
|
|
18411
|
+
return false;
|
|
18412
|
+
}
|
|
18413
|
+
function parseLegacyEventMetadata(raw) {
|
|
18414
|
+
if (raw === null) return void 0;
|
|
18415
|
+
try {
|
|
18416
|
+
return JSON.parse(raw);
|
|
18417
|
+
} catch {
|
|
18418
|
+
return void 0;
|
|
18419
|
+
}
|
|
18420
|
+
}
|
|
18421
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18422
|
+
return JSON.stringify(
|
|
18423
|
+
toCaptureAttributes({
|
|
18424
|
+
id: row.id,
|
|
18425
|
+
sourceTool: row.sourceTool,
|
|
18426
|
+
kind: row.kind,
|
|
18427
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18428
|
+
contentHash: row.contentHash,
|
|
18429
|
+
content: row.content,
|
|
18430
|
+
metadata: row.metadata
|
|
18431
|
+
})
|
|
18432
|
+
);
|
|
18433
|
+
}
|
|
18434
|
+
function copyLegacyEvents(db) {
|
|
18435
|
+
const selectStmt = db.prepare(
|
|
18436
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18437
|
+
content_hash AS contentHash, content, metadata
|
|
18438
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18439
|
+
);
|
|
18440
|
+
const insertStmt = db.prepare(
|
|
18441
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18442
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18443
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18444
|
+
);
|
|
18445
|
+
const stubRootStmt = db.prepare(
|
|
18446
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18447
|
+
);
|
|
18448
|
+
return drainLegacyTable(
|
|
18449
|
+
db,
|
|
18450
|
+
"events",
|
|
18451
|
+
selectStmt,
|
|
18452
|
+
(rows) => {
|
|
18453
|
+
for (const row of rows) {
|
|
18454
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18455
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18456
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18457
|
+
insertStmt.run(
|
|
18458
|
+
bindParams({
|
|
18459
|
+
id: row.id,
|
|
18460
|
+
parentId: sessionId,
|
|
18461
|
+
rootSessionId: sessionId,
|
|
18462
|
+
eventType: row.kind,
|
|
18463
|
+
startedAt: row.occurredAt,
|
|
18464
|
+
content: row.content,
|
|
18465
|
+
contentHash: row.contentHash,
|
|
18466
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18467
|
+
})
|
|
18468
|
+
);
|
|
18469
|
+
}
|
|
18470
|
+
}
|
|
18471
|
+
);
|
|
18472
|
+
}
|
|
18473
|
+
function copyLegacyFindings(db) {
|
|
18474
|
+
const selectStmt = db.prepare(
|
|
18475
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18476
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18477
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18478
|
+
first_detected_at AS firstDetectedAt
|
|
18479
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18480
|
+
);
|
|
18481
|
+
const definitionStmt = db.prepare(
|
|
18482
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18483
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18484
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18485
|
+
);
|
|
18486
|
+
const findingStmt = db.prepare(
|
|
18487
|
+
`INSERT INTO inspection_findings
|
|
18488
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18489
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18490
|
+
finding_key, first_detected_at)
|
|
18491
|
+
VALUES
|
|
18492
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18493
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18494
|
+
:findingKey, :firstDetectedAt)
|
|
18495
|
+
ON CONFLICT(id) DO NOTHING
|
|
18496
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18497
|
+
first_detected_at = CASE
|
|
18498
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18499
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18500
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18501
|
+
END`
|
|
18502
|
+
);
|
|
18503
|
+
return drainLegacyTable(
|
|
18504
|
+
db,
|
|
18505
|
+
"findings",
|
|
18506
|
+
selectStmt,
|
|
18507
|
+
(rows) => {
|
|
18508
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18509
|
+
for (const row of rows) {
|
|
18510
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18511
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18512
|
+
if (definitionId === void 0) {
|
|
18513
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18514
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18515
|
+
definitionStmt.run(
|
|
18516
|
+
bindParams({
|
|
18517
|
+
id: definitionId,
|
|
18518
|
+
ruleId: row.ruleId,
|
|
18519
|
+
name: row.ruleId,
|
|
18520
|
+
category: row.category,
|
|
18521
|
+
severity: row.severity,
|
|
18522
|
+
definition: "",
|
|
18523
|
+
version: version2
|
|
18524
|
+
})
|
|
18525
|
+
);
|
|
18526
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18527
|
+
}
|
|
18528
|
+
findingStmt.run(
|
|
18529
|
+
bindParams({
|
|
18530
|
+
id: row.id,
|
|
18531
|
+
auditEventId: row.eventId,
|
|
18532
|
+
inspectionDefinitionId: definitionId,
|
|
18533
|
+
spanStart: row.spanStart,
|
|
18534
|
+
spanEnd: row.spanEnd,
|
|
18535
|
+
maskedMatch: row.maskedMatch,
|
|
18536
|
+
actionTaken: row.actionTaken,
|
|
18537
|
+
confidence: row.confidence,
|
|
18538
|
+
findingKey: row.findingKey,
|
|
18539
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18540
|
+
})
|
|
18541
|
+
);
|
|
18542
|
+
}
|
|
18543
|
+
}
|
|
18544
|
+
);
|
|
18545
|
+
}
|
|
18546
|
+
function runLegacyHistoryBackfill(db) {
|
|
18547
|
+
try {
|
|
18548
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18549
|
+
if (!eventsCaughtUp) return false;
|
|
18550
|
+
return copyLegacyFindings(db);
|
|
18551
|
+
} catch (error51) {
|
|
18552
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18553
|
+
return false;
|
|
18554
|
+
}
|
|
18555
|
+
}
|
|
18120
18556
|
function isForeignSqliteLineage(db) {
|
|
18121
18557
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18122
18558
|
return columnNames(db, "events").includes("tenant_id");
|
|
18123
18559
|
}
|
|
18124
18560
|
function ensureSyncedAtColumn(db, table) {
|
|
18561
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
18125
18562
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
18126
18563
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18127
18564
|
}
|
|
@@ -18142,6 +18579,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18142
18579
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18143
18580
|
)`);
|
|
18144
18581
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18582
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18145
18583
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18146
18584
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18147
18585
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18169,30 +18607,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18169
18607
|
)`);
|
|
18170
18608
|
}
|
|
18171
18609
|
|
|
18172
|
-
// ../../packages/persistence/src/paths.ts
|
|
18173
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18174
|
-
var DATA_DIR_MODE = 448;
|
|
18175
|
-
var DATA_FILE_MODE = 384;
|
|
18176
|
-
var DB_FILENAME = "aka.db";
|
|
18177
|
-
function ensureDataDirSync(dir) {
|
|
18178
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18179
|
-
try {
|
|
18180
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18181
|
-
} catch {
|
|
18182
|
-
}
|
|
18183
|
-
}
|
|
18184
|
-
function walSidecars(file2) {
|
|
18185
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18186
|
-
}
|
|
18187
|
-
function tightenPerms(file2) {
|
|
18188
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18189
|
-
try {
|
|
18190
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18191
|
-
} catch {
|
|
18192
|
-
}
|
|
18193
|
-
}
|
|
18194
|
-
}
|
|
18195
|
-
|
|
18196
18610
|
// ../../packages/persistence/src/internal/json.ts
|
|
18197
18611
|
function safeJson(s, fallback) {
|
|
18198
18612
|
if (s == null) return fallback;
|
|
@@ -18212,51 +18626,6 @@ function parseJsonObject(s) {
|
|
|
18212
18626
|
return void 0;
|
|
18213
18627
|
}
|
|
18214
18628
|
|
|
18215
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18216
|
-
function allRows(stmt, params) {
|
|
18217
|
-
if (params === void 0) return stmt.all();
|
|
18218
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18219
|
-
return stmt.all(params);
|
|
18220
|
-
}
|
|
18221
|
-
function getRow(stmt, params) {
|
|
18222
|
-
if (params === void 0) return stmt.get();
|
|
18223
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18224
|
-
return stmt.get(params);
|
|
18225
|
-
}
|
|
18226
|
-
function intToBool(raw) {
|
|
18227
|
-
return raw === 1 || raw === true;
|
|
18228
|
-
}
|
|
18229
|
-
function boolToInt(b) {
|
|
18230
|
-
return b ? 1 : 0;
|
|
18231
|
-
}
|
|
18232
|
-
function bindParams(row) {
|
|
18233
|
-
const out = {};
|
|
18234
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18235
|
-
out[key] = value === void 0 ? null : value;
|
|
18236
|
-
}
|
|
18237
|
-
return out;
|
|
18238
|
-
}
|
|
18239
|
-
function countScalar(db, sql, params) {
|
|
18240
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18241
|
-
}
|
|
18242
|
-
function countBy(db, sql, params) {
|
|
18243
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18244
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18245
|
-
map2.set(row.k, row.n);
|
|
18246
|
-
}
|
|
18247
|
-
return map2;
|
|
18248
|
-
}
|
|
18249
|
-
function mapRowsTolerant(rows, map2) {
|
|
18250
|
-
const out = [];
|
|
18251
|
-
for (const row of rows) {
|
|
18252
|
-
try {
|
|
18253
|
-
out.push(map2(row));
|
|
18254
|
-
} catch {
|
|
18255
|
-
}
|
|
18256
|
-
}
|
|
18257
|
-
return out;
|
|
18258
|
-
}
|
|
18259
|
-
|
|
18260
18629
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18261
18630
|
var DAY_MS = 864e5;
|
|
18262
18631
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18873,6 +19242,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18873
19242
|
})
|
|
18874
19243
|
);
|
|
18875
19244
|
}
|
|
19245
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19246
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19247
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19248
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19249
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19250
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19251
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19252
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19253
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19254
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19255
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19256
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19257
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19258
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19259
|
+
}
|
|
18876
19260
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18877
19261
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18878
19262
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19334,8 +19718,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19334
19718
|
)
|
|
19335
19719
|
);
|
|
19336
19720
|
}
|
|
19337
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19338
|
-
//
|
|
19721
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19722
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19723
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19724
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19725
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19726
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19727
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19728
|
+
// to the old findings⋈events one.
|
|
19339
19729
|
countFindingsLast30d(ruleIds) {
|
|
19340
19730
|
if (ruleIds.length === 0) return 0;
|
|
19341
19731
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19343,8 +19733,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19343
19733
|
return countScalar(
|
|
19344
19734
|
this.db,
|
|
19345
19735
|
`SELECT count(*) AS n
|
|
19346
|
-
FROM
|
|
19347
|
-
|
|
19736
|
+
FROM inspection_findings f
|
|
19737
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19738
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19739
|
+
WHERE e.started_at >= ?
|
|
19740
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19741
|
+
AND d.rule_id IN (${inClause})`,
|
|
19348
19742
|
[since, ...ruleIds]
|
|
19349
19743
|
);
|
|
19350
19744
|
}
|
|
@@ -19354,35 +19748,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19354
19748
|
var SqliteEventsRepository = class {
|
|
19355
19749
|
constructor(db) {
|
|
19356
19750
|
this.db = db;
|
|
19357
|
-
this.insertStmt = db.prepare(
|
|
19358
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19359
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19360
|
-
);
|
|
19361
19751
|
}
|
|
19362
19752
|
db;
|
|
19363
|
-
|
|
19364
|
-
|
|
19365
|
-
|
|
19366
|
-
this.insertStmt.run(
|
|
19367
|
-
bindParams({
|
|
19368
|
-
id: row.id,
|
|
19369
|
-
sourceTool: row.sourceTool,
|
|
19370
|
-
kind: row.kind,
|
|
19371
|
-
occurredAt: row.occurredAt,
|
|
19372
|
-
contentHash: row.contentHash,
|
|
19373
|
-
content: row.content,
|
|
19374
|
-
metadata: row.metadata
|
|
19375
|
-
})
|
|
19376
|
-
);
|
|
19377
|
-
}
|
|
19378
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19379
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19380
|
-
// never duplicates findings.
|
|
19753
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19754
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19755
|
+
// scan never duplicates findings.
|
|
19381
19756
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19382
19757
|
// async EventsReadPort contract.
|
|
19758
|
+
//
|
|
19759
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19760
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19761
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19762
|
+
// index-friendly rather than walking rows that can never match.
|
|
19383
19763
|
contentHashes() {
|
|
19384
19764
|
const rows = allRows(
|
|
19385
|
-
this.db.prepare(
|
|
19765
|
+
this.db.prepare(
|
|
19766
|
+
`SELECT content_hash FROM audit_events
|
|
19767
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19768
|
+
)
|
|
19386
19769
|
);
|
|
19387
19770
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19388
19771
|
}
|
|
@@ -19718,17 +20101,20 @@ function parseExceptionRow(row) {
|
|
|
19718
20101
|
}
|
|
19719
20102
|
|
|
19720
20103
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19721
|
-
function
|
|
20104
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19722
20105
|
return `(
|
|
19723
|
-
SELECT fr
|
|
20106
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19724
20107
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19725
20108
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19726
20109
|
LIMIT 1
|
|
19727
20110
|
)`;
|
|
19728
20111
|
}
|
|
20112
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20113
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20114
|
+
}
|
|
19729
20115
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19730
|
-
SELECT finding_key, status FROM (
|
|
19731
|
-
SELECT fr.finding_key, fr.status,
|
|
20116
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20117
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19732
20118
|
ROW_NUMBER() OVER (
|
|
19733
20119
|
PARTITION BY fr.finding_key
|
|
19734
20120
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19755,68 +20141,21 @@ var DAY_MS3 = 864e5;
|
|
|
19755
20141
|
var SqliteFindingsRepository = class {
|
|
19756
20142
|
constructor(db) {
|
|
19757
20143
|
this.db = db;
|
|
19758
|
-
this.insertStmt = db.prepare(
|
|
19759
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19760
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19761
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19762
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19763
|
-
event_id = excluded.event_id,
|
|
19764
|
-
category = excluded.category,
|
|
19765
|
-
severity = excluded.severity,
|
|
19766
|
-
span_start = excluded.span_start,
|
|
19767
|
-
span_end = excluded.span_end,
|
|
19768
|
-
masked_match = excluded.masked_match,
|
|
19769
|
-
action_taken = excluded.action_taken,
|
|
19770
|
-
confidence = excluded.confidence`
|
|
19771
|
-
);
|
|
19772
|
-
this.sessionDupStmt = db.prepare(
|
|
19773
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19774
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19775
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19776
|
-
LIMIT 1`
|
|
19777
|
-
);
|
|
19778
20144
|
}
|
|
19779
20145
|
db;
|
|
19780
|
-
insertStmt;
|
|
19781
|
-
sessionDupStmt;
|
|
19782
|
-
insertFindings(findings, scope = {}) {
|
|
19783
|
-
for (const finding of findings) {
|
|
19784
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19785
|
-
const row = toFindingRow(finding);
|
|
19786
|
-
this.insertStmt.run({
|
|
19787
|
-
id: row.id,
|
|
19788
|
-
eventId: row.eventId,
|
|
19789
|
-
ruleId: row.ruleId,
|
|
19790
|
-
category: row.category,
|
|
19791
|
-
severity: row.severity,
|
|
19792
|
-
spanStart: row.spanStart,
|
|
19793
|
-
spanEnd: row.spanEnd,
|
|
19794
|
-
maskedMatch: row.maskedMatch,
|
|
19795
|
-
actionTaken: row.actionTaken,
|
|
19796
|
-
confidence: row.confidence,
|
|
19797
|
-
findingKey: row.findingKey ?? null
|
|
19798
|
-
});
|
|
19799
|
-
}
|
|
19800
|
-
}
|
|
19801
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19802
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19803
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19804
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19805
|
-
const hit = this.sessionDupStmt.get({
|
|
19806
|
-
ruleId: finding.ruleId,
|
|
19807
|
-
maskedMatch: finding.maskedMatch,
|
|
19808
|
-
sessionId
|
|
19809
|
-
});
|
|
19810
|
-
return hit !== void 0;
|
|
19811
|
-
}
|
|
19812
20146
|
recentFindings(opts) {
|
|
19813
20147
|
const limit = opts?.limit ?? 50;
|
|
19814
20148
|
const rows = allRows(
|
|
19815
20149
|
this.db.prepare(
|
|
19816
|
-
`SELECT f.id, f.event_id,
|
|
19817
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19818
|
-
|
|
19819
|
-
|
|
20150
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20151
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20152
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20153
|
+
e.event_type AS kind
|
|
20154
|
+
FROM inspection_findings f
|
|
20155
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20156
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20157
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20158
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19820
20159
|
LIMIT :limit`
|
|
19821
20160
|
),
|
|
19822
20161
|
{ limit }
|
|
@@ -19838,25 +20177,34 @@ var SqliteFindingsRepository = class {
|
|
|
19838
20177
|
);
|
|
19839
20178
|
}
|
|
19840
20179
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19841
|
-
* session-stamped
|
|
20180
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19842
20181
|
* page can label its findings link without the grouped pipeline. */
|
|
19843
20182
|
sessionFindingsCount(sessionId) {
|
|
19844
20183
|
if (!sessionId) return Promise.resolve(0);
|
|
19845
20184
|
return Promise.resolve(
|
|
19846
20185
|
countScalar(
|
|
19847
20186
|
this.db,
|
|
19848
|
-
`SELECT count(*) AS n FROM
|
|
19849
|
-
JOIN
|
|
19850
|
-
WHERE
|
|
20187
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20188
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20189
|
+
WHERE e.root_session_id = :sessionId
|
|
20190
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19851
20191
|
{ sessionId }
|
|
19852
20192
|
)
|
|
19853
20193
|
);
|
|
19854
20194
|
}
|
|
19855
|
-
/** Per-rule transcript firing tally for one session —
|
|
19856
|
-
*
|
|
19857
|
-
*
|
|
19858
|
-
*
|
|
19859
|
-
*
|
|
20195
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20196
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20197
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20198
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20199
|
+
* tally with the deduped groups it lists.
|
|
20200
|
+
*
|
|
20201
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20202
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20203
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20204
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20205
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20206
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20207
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19860
20208
|
sessionFirings(sessionId) {
|
|
19861
20209
|
return Object.fromEntries(
|
|
19862
20210
|
countBy(
|
|
@@ -19866,18 +20214,25 @@ var SqliteFindingsRepository = class {
|
|
|
19866
20214
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19867
20215
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19868
20216
|
WHERE e.root_session_id = :sessionId
|
|
20217
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19869
20218
|
GROUP BY d.rule_id`,
|
|
19870
20219
|
{ sessionId }
|
|
19871
20220
|
)
|
|
19872
20221
|
);
|
|
19873
20222
|
}
|
|
19874
20223
|
/**
|
|
19875
|
-
* Grouped findings for the dashboard — joins
|
|
19876
|
-
* toolName from
|
|
19877
|
-
*
|
|
20224
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20225
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20226
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20227
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20228
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20229
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20230
|
+
* severity then recency. Filtering
|
|
19878
20231
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19879
20232
|
* reflect the full filtered set; `items` is the requested
|
|
19880
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20233
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20234
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20235
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19881
20236
|
*
|
|
19882
20237
|
* Two reads, neither of which materializes a row per finding:
|
|
19883
20238
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19890,10 +20245,11 @@ var SqliteFindingsRepository = class {
|
|
|
19890
20245
|
* rule is ever restated in SQL.
|
|
19891
20246
|
*/
|
|
19892
20247
|
listGroupedFindings(query) {
|
|
19893
|
-
const sessionPredicate = query.sessionId ? `
|
|
20248
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20249
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19894
20250
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19895
20251
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19896
|
-
predicate
|
|
20252
|
+
predicate,
|
|
19897
20253
|
params: sessionParams
|
|
19898
20254
|
});
|
|
19899
20255
|
const rows = allRows(
|
|
@@ -19901,24 +20257,26 @@ var SqliteFindingsRepository = class {
|
|
|
19901
20257
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19902
20258
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19903
20259
|
FROM (
|
|
19904
|
-
SELECT f.id AS id,
|
|
19905
|
-
|
|
20260
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20261
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19906
20262
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19907
|
-
e.
|
|
19908
|
-
json_extract(e.
|
|
19909
|
-
json_extract(e.
|
|
19910
|
-
json_extract(e.
|
|
19911
|
-
e.
|
|
20263
|
+
e.started_at AS occurred_at,
|
|
20264
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20265
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20266
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20267
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20268
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19912
20269
|
latest.status AS latest_status,
|
|
19913
20270
|
ROW_NUMBER() OVER (
|
|
19914
|
-
PARTITION BY
|
|
19915
|
-
ORDER BY e.
|
|
20271
|
+
PARTITION BY d.rule_id
|
|
20272
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19916
20273
|
) AS rn
|
|
19917
|
-
FROM
|
|
19918
|
-
JOIN
|
|
20274
|
+
FROM inspection_findings f
|
|
20275
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20276
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19919
20277
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19920
20278
|
ON latest.finding_key = f.finding_key
|
|
19921
|
-
${
|
|
20279
|
+
${predicate}
|
|
19922
20280
|
)
|
|
19923
20281
|
WHERE rn <= :cap
|
|
19924
20282
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19945,17 +20303,29 @@ var SqliteFindingsRepository = class {
|
|
|
19945
20303
|
severity: query.severity,
|
|
19946
20304
|
providers: query.provider,
|
|
19947
20305
|
actions: query.action,
|
|
20306
|
+
statuses: query.status,
|
|
19948
20307
|
subtype: query.subtype,
|
|
19949
20308
|
q: query.q
|
|
19950
20309
|
};
|
|
19951
20310
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19952
20311
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20312
|
+
const statusFilter = query.status ?? [];
|
|
19953
20313
|
const totals = {
|
|
19954
|
-
findings: sorted.reduce((acc, g) =>
|
|
20314
|
+
findings: sorted.reduce((acc, g) => {
|
|
20315
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20316
|
+
const agg = aggregates.get(g.id);
|
|
20317
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20318
|
+
}, 0),
|
|
19955
20319
|
groups: sorted.length
|
|
19956
20320
|
};
|
|
19957
20321
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19958
|
-
const
|
|
20322
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20323
|
+
const items = sorted.slice(0, limit).map(
|
|
20324
|
+
(g) => statusSet ? {
|
|
20325
|
+
...g,
|
|
20326
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20327
|
+
} : g
|
|
20328
|
+
);
|
|
19959
20329
|
return Promise.resolve({
|
|
19960
20330
|
totals,
|
|
19961
20331
|
facets,
|
|
@@ -19969,45 +20339,62 @@ var SqliteFindingsRepository = class {
|
|
|
19969
20339
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19970
20340
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19971
20341
|
*
|
|
19972
|
-
*
|
|
19973
|
-
*
|
|
19974
|
-
*
|
|
19975
|
-
* status
|
|
19976
|
-
*
|
|
19977
|
-
*
|
|
20342
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20343
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20344
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20345
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20346
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20347
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20348
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20349
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20350
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20351
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20352
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19978
20353
|
* a group's row stays small however many findings it holds.
|
|
19979
20354
|
*
|
|
19980
20355
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19981
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19982
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19983
|
-
* that is a string proportional to the store (~8MB over
|
|
19984
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19985
|
-
* match an instance outside the preview, which searching
|
|
19986
|
-
* would silently lose, so it is fetched only when the
|
|
19987
|
-
* carries a `q`.
|
|
20356
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20357
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20358
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20359
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20360
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20361
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20362
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20363
|
+
* path repeating across tuples.)
|
|
19988
20364
|
*/
|
|
19989
20365
|
groupAggregates(withSearchText, scope) {
|
|
19990
|
-
const
|
|
19991
|
-
group_concat(DISTINCT json_extract(e.
|
|
19992
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20366
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20367
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20368
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19993
20369
|
const rows = this.db.prepare(
|
|
19994
|
-
`SELECT
|
|
19995
|
-
|
|
19996
|
-
max(
|
|
19997
|
-
group_concat(
|
|
19998
|
-
group_concat(
|
|
19999
|
-
group_concat(
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20370
|
+
`SELECT rule_id,
|
|
20371
|
+
sum(tuple_count) AS instance_count,
|
|
20372
|
+
max(latest_at) AS latest_at,
|
|
20373
|
+
group_concat(source_tools) AS source_tools,
|
|
20374
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20375
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20376
|
+
group_concat(repos) AS repos,
|
|
20377
|
+
group_concat(files) AS files,
|
|
20378
|
+
group_concat(tool_names) AS tool_names
|
|
20379
|
+
FROM (
|
|
20380
|
+
SELECT d.rule_id AS rule_id,
|
|
20381
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20382
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20383
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20384
|
+
count(*) AS tuple_count,
|
|
20385
|
+
max(e.started_at) AS latest_at,
|
|
20386
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20387
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20388
|
+
${innerSearchColumns}
|
|
20389
|
+
FROM inspection_findings f
|
|
20390
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20391
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20392
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20393
|
+
ON latest.finding_key = f.finding_key
|
|
20394
|
+
${scope.predicate}
|
|
20395
|
+
GROUP BY d.rule_id, status_tuple
|
|
20396
|
+
)
|
|
20397
|
+
GROUP BY rule_id`
|
|
20011
20398
|
).all(scope.params);
|
|
20012
20399
|
return new Map(
|
|
20013
20400
|
rows.map((r) => [
|
|
@@ -20017,13 +20404,14 @@ var SqliteFindingsRepository = class {
|
|
|
20017
20404
|
sourceTools: splitConcat(r.source_tools),
|
|
20018
20405
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20019
20406
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20020
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20407
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20021
20408
|
return {
|
|
20022
20409
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20023
20410
|
// so the marker stands in for the key itself (never rendered).
|
|
20024
20411
|
kind,
|
|
20025
20412
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20026
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20413
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20414
|
+
count: Number(count)
|
|
20027
20415
|
};
|
|
20028
20416
|
}),
|
|
20029
20417
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20040,10 +20428,21 @@ var SqliteFindingsRepository = class {
|
|
|
20040
20428
|
);
|
|
20041
20429
|
}
|
|
20042
20430
|
healthSummary() {
|
|
20043
|
-
const total = countScalar(
|
|
20431
|
+
const total = countScalar(
|
|
20432
|
+
this.db,
|
|
20433
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20434
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20435
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20436
|
+
);
|
|
20044
20437
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20045
20438
|
const grouped = allRows(
|
|
20046
|
-
this.db.prepare(
|
|
20439
|
+
this.db.prepare(
|
|
20440
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20441
|
+
FROM inspection_findings f
|
|
20442
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20443
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20444
|
+
GROUP BY f.action_taken`
|
|
20445
|
+
)
|
|
20047
20446
|
);
|
|
20048
20447
|
for (const row of grouped) {
|
|
20049
20448
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20051,12 +20450,15 @@ var SqliteFindingsRepository = class {
|
|
|
20051
20450
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20052
20451
|
const sevRows = allRows(
|
|
20053
20452
|
this.db.prepare(
|
|
20054
|
-
`SELECT
|
|
20055
|
-
FROM
|
|
20453
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20454
|
+
FROM inspection_findings f
|
|
20455
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20456
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20056
20457
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20057
20458
|
ON latest.finding_key = f.finding_key
|
|
20058
|
-
WHERE
|
|
20059
|
-
|
|
20459
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20460
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20461
|
+
GROUP BY d.severity`
|
|
20060
20462
|
)
|
|
20061
20463
|
);
|
|
20062
20464
|
for (const row of sevRows) {
|
|
@@ -20077,9 +20479,11 @@ var SqliteFindingsRepository = class {
|
|
|
20077
20479
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20078
20480
|
const rows = allRows(
|
|
20079
20481
|
this.db.prepare(
|
|
20080
|
-
`SELECT date(e.
|
|
20081
|
-
FROM
|
|
20082
|
-
|
|
20482
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20483
|
+
FROM inspection_findings f
|
|
20484
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20485
|
+
WHERE e.started_at >= :since
|
|
20486
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20083
20487
|
GROUP BY day, f.action_taken`
|
|
20084
20488
|
),
|
|
20085
20489
|
{ since }
|
|
@@ -20144,15 +20548,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20144
20548
|
this.insertStmt = db.prepare(
|
|
20145
20549
|
`INSERT INTO inspection_findings
|
|
20146
20550
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20147
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20551
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20552
|
+
finding_key, first_detected_at)
|
|
20148
20553
|
VALUES
|
|
20149
20554
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20150
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20151
|
-
|
|
20555
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20556
|
+
:findingKey,
|
|
20557
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20558
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20559
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20560
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20561
|
+
audit_event_id = excluded.audit_event_id,
|
|
20562
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20563
|
+
classified_data_id = excluded.classified_data_id,
|
|
20564
|
+
span_start = excluded.span_start,
|
|
20565
|
+
span_end = excluded.span_end,
|
|
20566
|
+
masked_match = excluded.masked_match,
|
|
20567
|
+
action_taken = excluded.action_taken,
|
|
20568
|
+
confidence = excluded.confidence`
|
|
20569
|
+
);
|
|
20570
|
+
this.sessionDupStmt = db.prepare(
|
|
20571
|
+
`SELECT 1 FROM inspection_findings f
|
|
20572
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20573
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20574
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20575
|
+
AND e.root_session_id = :sessionId
|
|
20576
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20577
|
+
LIMIT 1`
|
|
20578
|
+
);
|
|
20579
|
+
this.eventDupStmt = db.prepare(
|
|
20580
|
+
`SELECT 1 FROM inspection_findings f
|
|
20581
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20582
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20583
|
+
AND f.masked_match = :maskedMatch
|
|
20584
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20585
|
+
LIMIT 1`
|
|
20152
20586
|
);
|
|
20153
20587
|
}
|
|
20154
20588
|
db;
|
|
20155
20589
|
insertStmt;
|
|
20590
|
+
sessionDupStmt;
|
|
20591
|
+
eventDupStmt;
|
|
20592
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20593
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20594
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20595
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20596
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20597
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20598
|
+
}
|
|
20599
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20600
|
+
// recorded against the given audit event.
|
|
20601
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20602
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20603
|
+
}
|
|
20156
20604
|
insertFinding(input) {
|
|
20157
20605
|
const row = toInspectionFindingRow(input);
|
|
20158
20606
|
this.insertStmt.run(
|
|
@@ -20165,7 +20613,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20165
20613
|
spanEnd: row.spanEnd,
|
|
20166
20614
|
maskedMatch: row.maskedMatch,
|
|
20167
20615
|
actionTaken: row.actionTaken,
|
|
20168
|
-
confidence: row.confidence
|
|
20616
|
+
confidence: row.confidence,
|
|
20617
|
+
findingKey: row.findingKey,
|
|
20618
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20169
20619
|
})
|
|
20170
20620
|
);
|
|
20171
20621
|
}
|
|
@@ -20437,7 +20887,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20437
20887
|
installedRuleset() {
|
|
20438
20888
|
const rows = allRows(
|
|
20439
20889
|
this.db.prepare(
|
|
20440
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20890
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20441
20891
|
)
|
|
20442
20892
|
);
|
|
20443
20893
|
const out = {
|
|
@@ -20445,7 +20895,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20445
20895
|
enabledPacks: 0,
|
|
20446
20896
|
rules: [],
|
|
20447
20897
|
invalidRules: 0,
|
|
20448
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20898
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20899
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20449
20900
|
};
|
|
20450
20901
|
for (const row of rows) {
|
|
20451
20902
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20467,6 +20918,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20467
20918
|
if (parsed.success) {
|
|
20468
20919
|
out.rules.push(parsed.data);
|
|
20469
20920
|
out.ruleActions.set(parsed.data.id, action);
|
|
20921
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20470
20922
|
} else out.invalidRules += 1;
|
|
20471
20923
|
}
|
|
20472
20924
|
}
|
|
@@ -21641,19 +22093,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21641
22093
|
);
|
|
21642
22094
|
this.openAtRestStmt = db.prepare(
|
|
21643
22095
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21644
|
-
FROM
|
|
21645
|
-
JOIN
|
|
21646
|
-
WHERE e.
|
|
21647
|
-
AND json_extract(e.
|
|
22096
|
+
FROM inspection_findings f
|
|
22097
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22098
|
+
WHERE e.event_type = 'code_change'
|
|
22099
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21648
22100
|
AND f.finding_key IS NOT NULL
|
|
21649
22101
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21650
22102
|
);
|
|
21651
22103
|
this.resolvedAtRestStmt = db.prepare(
|
|
21652
22104
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21653
|
-
FROM
|
|
21654
|
-
JOIN
|
|
21655
|
-
WHERE e.
|
|
21656
|
-
AND json_extract(e.
|
|
22105
|
+
FROM inspection_findings f
|
|
22106
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22107
|
+
WHERE e.event_type = 'code_change'
|
|
22108
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21657
22109
|
AND f.finding_key IS NOT NULL
|
|
21658
22110
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21659
22111
|
);
|
|
@@ -21876,25 +22328,27 @@ var SqliteSecurityRepository = class {
|
|
|
21876
22328
|
severitySummary() {
|
|
21877
22329
|
const rows = allRows(
|
|
21878
22330
|
this.db.prepare(
|
|
21879
|
-
`SELECT
|
|
22331
|
+
`SELECT d.severity AS severity,
|
|
21880
22332
|
COUNT(*) AS count,
|
|
21881
22333
|
SUM(CASE
|
|
21882
|
-
WHEN e.
|
|
22334
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21883
22335
|
WHEN f.finding_key IS NULL THEN 0
|
|
21884
22336
|
WHEN latest.status = 'resolved' THEN 1
|
|
21885
22337
|
ELSE 0
|
|
21886
22338
|
END) AS caught,
|
|
21887
22339
|
SUM(CASE
|
|
21888
|
-
WHEN e.
|
|
22340
|
+
WHEN e.event_type = 'code_change'
|
|
21889
22341
|
AND f.finding_key IS NOT NULL
|
|
21890
22342
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21891
22343
|
ELSE 0
|
|
21892
22344
|
END) AS open_at_rest
|
|
21893
|
-
FROM
|
|
21894
|
-
JOIN
|
|
22345
|
+
FROM inspection_findings f
|
|
22346
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22347
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21895
22348
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21896
22349
|
ON latest.finding_key = f.finding_key
|
|
21897
|
-
|
|
22350
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22351
|
+
GROUP BY d.severity`
|
|
21898
22352
|
)
|
|
21899
22353
|
);
|
|
21900
22354
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21960,7 +22414,7 @@ var SqliteSecurityRepository = class {
|
|
|
21960
22414
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21961
22415
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21962
22416
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21963
|
-
// (
|
|
22417
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21964
22418
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21965
22419
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21966
22420
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21985,30 +22439,20 @@ var SqliteSecurityRepository = class {
|
|
|
21985
22439
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21986
22440
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21987
22441
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21988
|
-
//
|
|
21989
|
-
// the parent event's
|
|
21990
|
-
// backfill left null.
|
|
21991
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
|
|
21998
|
-
|
|
21999
|
-
|
|
22000
|
-
WHERE fr.finding_key = f.finding_key
|
|
22001
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22002
|
-
LIMIT 1
|
|
22003
|
-
) AS latest_method,
|
|
22004
|
-
(
|
|
22005
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22006
|
-
WHERE fr.finding_key = f.finding_key
|
|
22007
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22008
|
-
LIMIT 1
|
|
22009
|
-
) AS latest_resolved_at
|
|
22010
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22442
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22443
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22444
|
+
// legacy/edge row the backfill left null.
|
|
22445
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22446
|
+
latest.status AS latest_status,
|
|
22447
|
+
latest.method AS latest_method,
|
|
22448
|
+
latest.resolved_at AS latest_resolved_at
|
|
22449
|
+
FROM inspection_findings f
|
|
22450
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22451
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22452
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22453
|
+
ON latest.finding_key = f.finding_key
|
|
22011
22454
|
WHERE f.finding_key IS NOT NULL
|
|
22455
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22012
22456
|
AND EXISTS (
|
|
22013
22457
|
SELECT 1 FROM finding_resolution fr
|
|
22014
22458
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22055,11 +22499,13 @@ var SqliteSecurityRepository = class {
|
|
|
22055
22499
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22056
22500
|
const rows = allRows(
|
|
22057
22501
|
this.db.prepare(
|
|
22058
|
-
`SELECT json_extract(e.
|
|
22059
|
-
FROM
|
|
22060
|
-
|
|
22061
|
-
|
|
22062
|
-
AND
|
|
22502
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22503
|
+
FROM inspection_findings f
|
|
22504
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22505
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22506
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22507
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22508
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22063
22509
|
GROUP BY repo
|
|
22064
22510
|
ORDER BY c DESC, repo
|
|
22065
22511
|
LIMIT :limit`
|
|
@@ -22083,44 +22529,28 @@ var SqliteSecurityRepository = class {
|
|
|
22083
22529
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22084
22530
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22085
22531
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22086
|
-
// finding's parent event (
|
|
22087
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22088
|
-
// capped at `limit`.
|
|
22532
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22533
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22534
|
+
// DESC, capped at `limit`.
|
|
22089
22535
|
recentlyResolved(limit = 20) {
|
|
22090
22536
|
const rows = allRows(
|
|
22091
22537
|
this.db.prepare(
|
|
22092
22538
|
`SELECT f.finding_key AS finding_key,
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
json_extract(e.
|
|
22096
|
-
COALESCE(f.first_detected_at, e.
|
|
22097
|
-
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
WHERE e.kind = 'code_change'
|
|
22539
|
+
d.rule_id AS rule_id,
|
|
22540
|
+
d.severity AS severity,
|
|
22541
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22542
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22543
|
+
latest.resolved_at AS latest_resolved_at
|
|
22544
|
+
FROM inspection_findings f
|
|
22545
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22546
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22547
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22548
|
+
ON latest.finding_key = f.finding_key
|
|
22549
|
+
WHERE e.event_type = 'code_change'
|
|
22105
22550
|
AND f.finding_key IS NOT NULL
|
|
22106
|
-
AND
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22110
|
-
LIMIT 1
|
|
22111
|
-
) = 'resolved'
|
|
22112
|
-
AND (
|
|
22113
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22114
|
-
WHERE fr.finding_key = f.finding_key
|
|
22115
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22116
|
-
LIMIT 1
|
|
22117
|
-
) = 'fixed-at-source'
|
|
22118
|
-
AND (
|
|
22119
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22120
|
-
WHERE fr.finding_key = f.finding_key
|
|
22121
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22122
|
-
LIMIT 1
|
|
22123
|
-
) IS NOT NULL
|
|
22551
|
+
AND latest.status = 'resolved'
|
|
22552
|
+
AND latest.method = 'fixed-at-source'
|
|
22553
|
+
AND latest.resolved_at IS NOT NULL
|
|
22124
22554
|
ORDER BY latest_resolved_at DESC
|
|
22125
22555
|
LIMIT :limit`
|
|
22126
22556
|
),
|
|
@@ -22139,15 +22569,18 @@ var SqliteSecurityRepository = class {
|
|
|
22139
22569
|
return Promise.resolve({ items });
|
|
22140
22570
|
}
|
|
22141
22571
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22142
|
-
// epoch-millis timestamp.
|
|
22572
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22143
22573
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22144
22574
|
findingsInRange(fromMs, toMs) {
|
|
22145
22575
|
const rows = allRows(
|
|
22146
22576
|
this.db.prepare(
|
|
22147
|
-
`SELECT e.
|
|
22148
|
-
FROM
|
|
22149
|
-
|
|
22150
|
-
|
|
22577
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22578
|
+
FROM inspection_findings f
|
|
22579
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22580
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22581
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22582
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22583
|
+
ORDER BY e.started_at`
|
|
22151
22584
|
),
|
|
22152
22585
|
{ from: fromMs, to: toMs }
|
|
22153
22586
|
);
|
|
@@ -22962,63 +23395,159 @@ function purgeSampleData(db) {
|
|
|
22962
23395
|
function linkHost(input, hostId) {
|
|
22963
23396
|
return hostId ? { ...input, hostId } : input;
|
|
22964
23397
|
}
|
|
23398
|
+
function closeQuietly(db) {
|
|
23399
|
+
try {
|
|
23400
|
+
db.close();
|
|
23401
|
+
} catch {
|
|
23402
|
+
}
|
|
23403
|
+
}
|
|
22965
23404
|
function openWithPragmas(file2) {
|
|
22966
23405
|
const db = new DatabaseSync(file2);
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
23406
|
+
try {
|
|
23407
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
23408
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
23409
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
23410
|
+
} catch (err) {
|
|
23411
|
+
closeQuietly(db);
|
|
23412
|
+
throw err;
|
|
23413
|
+
}
|
|
22970
23414
|
return db;
|
|
22971
23415
|
}
|
|
22972
23416
|
function backupLegacyStore(file2) {
|
|
22973
23417
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
23418
|
+
renameSync2(file2, backup);
|
|
23419
|
+
tightenFile(backup);
|
|
23420
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23421
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22977
23422
|
}
|
|
22978
23423
|
return backup;
|
|
22979
23424
|
}
|
|
23425
|
+
function openAndInitialize(file2) {
|
|
23426
|
+
let db = openWithPragmas(file2);
|
|
23427
|
+
try {
|
|
23428
|
+
if (isForeignSqliteLineage(db)) {
|
|
23429
|
+
db.close();
|
|
23430
|
+
const backup = backupLegacyStore(file2);
|
|
23431
|
+
db = openWithPragmas(file2);
|
|
23432
|
+
akaWarn(
|
|
23433
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
23434
|
+
);
|
|
23435
|
+
}
|
|
23436
|
+
applyMigrations(db, file2);
|
|
23437
|
+
tightenPerms(file2);
|
|
23438
|
+
const policies = new SqlitePoliciesRepository(db);
|
|
23439
|
+
const installedPacks = new SqliteInstalledPacksRepository(db);
|
|
23440
|
+
const repositories = {
|
|
23441
|
+
events: new SqliteEventsRepository(db),
|
|
23442
|
+
findings: new SqliteFindingsRepository(db),
|
|
23443
|
+
policies,
|
|
23444
|
+
installedPacks,
|
|
23445
|
+
scanLedger: new SqliteScanLedgerRepository(db),
|
|
23446
|
+
exceptions: new SqliteExceptionsRepository(db),
|
|
23447
|
+
resolutions: new SqliteResolutionsRepository(db),
|
|
23448
|
+
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
23449
|
+
security: new SqliteSecurityRepository(db),
|
|
23450
|
+
detections: new SqliteDetectionsRepository(db),
|
|
23451
|
+
shares: new SqliteSharesRepository(db),
|
|
23452
|
+
policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
|
|
23453
|
+
inventory: new SqliteInventoryRepository(db),
|
|
23454
|
+
inventoryAssets: new SqliteInventoryAssetsRepository(db),
|
|
23455
|
+
projectFiles: new SqliteProjectFilesRepository(db),
|
|
23456
|
+
activity: new SqliteActivityRepository(db),
|
|
23457
|
+
sourceProject: new SqliteSourceProjectRepository(db),
|
|
23458
|
+
auditEvents: new SqliteAuditEventsRepository(db),
|
|
23459
|
+
classifiedData: new SqliteClassifiedDataRepository(db),
|
|
23460
|
+
inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
|
|
23461
|
+
inspectionFindings: new SqliteInspectionFindingsRepository(db),
|
|
23462
|
+
configInventory: new SqliteConfigInventoryRepository(db)
|
|
23463
|
+
};
|
|
23464
|
+
policies.seedDefaults();
|
|
23465
|
+
return { db, ...repositories };
|
|
23466
|
+
} catch (err) {
|
|
23467
|
+
closeQuietly(db);
|
|
23468
|
+
throw err;
|
|
23469
|
+
}
|
|
23470
|
+
}
|
|
22980
23471
|
function openLocalDatabase(dir) {
|
|
22981
23472
|
ensureDataDirSync(dir);
|
|
22982
23473
|
const file2 = join(dir, DB_FILENAME);
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
const projectFiles = new SqliteProjectFilesRepository(db);
|
|
23009
|
-
const activity = new SqliteActivityRepository(db);
|
|
23010
|
-
const sourceProject = new SqliteSourceProjectRepository(db);
|
|
23011
|
-
const auditEvents = new SqliteAuditEventsRepository(db);
|
|
23012
|
-
const classifiedData = new SqliteClassifiedDataRepository(db);
|
|
23013
|
-
const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
|
|
23014
|
-
const inspectionFindings = new SqliteInspectionFindingsRepository(db);
|
|
23015
|
-
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
23016
|
-
policies.seedDefaults();
|
|
23474
|
+
const {
|
|
23475
|
+
db,
|
|
23476
|
+
events,
|
|
23477
|
+
findings,
|
|
23478
|
+
policies,
|
|
23479
|
+
installedPacks,
|
|
23480
|
+
scanLedger,
|
|
23481
|
+
exceptions,
|
|
23482
|
+
resolutions,
|
|
23483
|
+
ruleProbeCache,
|
|
23484
|
+
security,
|
|
23485
|
+
detections,
|
|
23486
|
+
shares,
|
|
23487
|
+
policyCatalog,
|
|
23488
|
+
inventory,
|
|
23489
|
+
inventoryAssets,
|
|
23490
|
+
projectFiles,
|
|
23491
|
+
activity,
|
|
23492
|
+
sourceProject,
|
|
23493
|
+
auditEvents,
|
|
23494
|
+
classifiedData,
|
|
23495
|
+
inspectionDefinitions,
|
|
23496
|
+
inspectionFindings,
|
|
23497
|
+
configInventory
|
|
23498
|
+
} = openAndInitialize(file2);
|
|
23017
23499
|
function recordCapture(event, detected) {
|
|
23018
23500
|
failOpenTransaction(db, () => {
|
|
23019
|
-
events.insertEvent(event);
|
|
23020
23501
|
const sessionId = event.metadata?.sessionId;
|
|
23021
|
-
|
|
23502
|
+
if (sessionId) {
|
|
23503
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23504
|
+
}
|
|
23505
|
+
const auditEventId = captureId(
|
|
23506
|
+
sessionId ?? null,
|
|
23507
|
+
event.contentHash,
|
|
23508
|
+
event.metadata?.filePath ?? null
|
|
23509
|
+
);
|
|
23510
|
+
auditEvents.insertAuditEvent({
|
|
23511
|
+
id: auditEventId,
|
|
23512
|
+
eventType: event.kind,
|
|
23513
|
+
startedAt: event.occurredAt,
|
|
23514
|
+
parentId: sessionId,
|
|
23515
|
+
rootSessionId: sessionId,
|
|
23516
|
+
content: event.content,
|
|
23517
|
+
contentHash: event.contentHash,
|
|
23518
|
+
attributes: toCaptureAttributes(event)
|
|
23519
|
+
});
|
|
23520
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23521
|
+
for (const finding of detected) {
|
|
23522
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23523
|
+
continue;
|
|
23524
|
+
}
|
|
23525
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23526
|
+
auditEventId,
|
|
23527
|
+
finding.ruleId,
|
|
23528
|
+
finding.maskedMatch,
|
|
23529
|
+
finding.span.start,
|
|
23530
|
+
finding.span.end
|
|
23531
|
+
)) {
|
|
23532
|
+
continue;
|
|
23533
|
+
}
|
|
23534
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23535
|
+
let definitionId = definitionIds.get(key);
|
|
23536
|
+
if (!definitionId) {
|
|
23537
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23538
|
+
definitionIds.set(key, definitionId);
|
|
23539
|
+
}
|
|
23540
|
+
inspectionFindings.insertFinding({
|
|
23541
|
+
id: finding.id,
|
|
23542
|
+
auditEventId,
|
|
23543
|
+
inspectionDefinitionId: definitionId,
|
|
23544
|
+
span: finding.span,
|
|
23545
|
+
maskedMatch: finding.maskedMatch,
|
|
23546
|
+
actionTaken: finding.actionTaken,
|
|
23547
|
+
confidence: finding.confidence,
|
|
23548
|
+
findingKey: finding.findingKey ?? void 0
|
|
23549
|
+
});
|
|
23550
|
+
}
|
|
23022
23551
|
});
|
|
23023
23552
|
}
|
|
23024
23553
|
function ensureInventory(ctx) {
|
|
@@ -23166,10 +23695,21 @@ function openLocalDatabase(dir) {
|
|
|
23166
23695
|
};
|
|
23167
23696
|
}
|
|
23168
23697
|
|
|
23698
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23699
|
+
import { createHash as createHash3 } from "crypto";
|
|
23700
|
+
function normalizeFilePath(filePath) {
|
|
23701
|
+
return filePath.replaceAll("\\", "/");
|
|
23702
|
+
}
|
|
23703
|
+
function computeFindingKey(input) {
|
|
23704
|
+
const normalizedPath = normalizeFilePath(input.filePath);
|
|
23705
|
+
return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
23706
|
+
}
|
|
23707
|
+
|
|
23169
23708
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23170
23709
|
import { createHmac, randomBytes } from "crypto";
|
|
23171
|
-
import {
|
|
23710
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
23172
23711
|
import { join as join2 } from "path";
|
|
23712
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
23173
23713
|
var KEY_FILENAME = "exception.key";
|
|
23174
23714
|
var KEY_MATERIAL_BYTES = 32;
|
|
23175
23715
|
function keyFilePath(dataDir2) {
|
|
@@ -23193,18 +23733,52 @@ function parseKeyFile(raw) {
|
|
|
23193
23733
|
}
|
|
23194
23734
|
return { version: version2, material: bytes };
|
|
23195
23735
|
}
|
|
23736
|
+
var KEY_VERSION_TABLES = ["exceptions", "blocked_detections"];
|
|
23737
|
+
var SQLITE_ERROR = 1;
|
|
23738
|
+
var FLOOR_BUSY_TIMEOUT_MS = 250;
|
|
23739
|
+
var FloorUnreadableError = class extends Error {
|
|
23740
|
+
code = "floor-unreadable";
|
|
23741
|
+
constructor(cause) {
|
|
23742
|
+
super(
|
|
23743
|
+
`cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
23744
|
+
{ cause }
|
|
23745
|
+
);
|
|
23746
|
+
this.name = "FloorUnreadableError";
|
|
23747
|
+
}
|
|
23748
|
+
};
|
|
23749
|
+
function storedKeyVersionFloor(dataDir2) {
|
|
23750
|
+
const file2 = join2(dataDir2, DB_FILENAME);
|
|
23751
|
+
if (!existsSync2(file2)) return 0;
|
|
23752
|
+
let db;
|
|
23753
|
+
try {
|
|
23754
|
+
db = new DatabaseSync2(file2, { readOnly: true });
|
|
23755
|
+
db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
|
|
23756
|
+
let floor = 0;
|
|
23757
|
+
for (const table of KEY_VERSION_TABLES) {
|
|
23758
|
+
try {
|
|
23759
|
+
const row = getRow(
|
|
23760
|
+
db.prepare(`SELECT MAX(key_version) AS v FROM ${table}`)
|
|
23761
|
+
);
|
|
23762
|
+
floor = Math.max(floor, row?.v ?? 0);
|
|
23763
|
+
} catch (err) {
|
|
23764
|
+
if (err.errcode !== SQLITE_ERROR) {
|
|
23765
|
+
throw new FloorUnreadableError(err);
|
|
23766
|
+
}
|
|
23767
|
+
}
|
|
23768
|
+
}
|
|
23769
|
+
return floor;
|
|
23770
|
+
} catch (err) {
|
|
23771
|
+
throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
|
|
23772
|
+
} finally {
|
|
23773
|
+
db?.close();
|
|
23774
|
+
}
|
|
23775
|
+
}
|
|
23196
23776
|
function writeKeyFile(dataDir2, key) {
|
|
23197
23777
|
ensureDataDirSync(dataDir2);
|
|
23198
23778
|
const file2 = keyFilePath(dataDir2);
|
|
23199
|
-
const tmp = `${file2}.tmp`;
|
|
23200
23779
|
const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
renameSync2(tmp, file2);
|
|
23204
|
-
try {
|
|
23205
|
-
chmodSync2(file2, DATA_FILE_MODE);
|
|
23206
|
-
} catch {
|
|
23207
|
-
}
|
|
23780
|
+
writeOwnerOnlyFileSync(file2, `${body}
|
|
23781
|
+
`);
|
|
23208
23782
|
return key;
|
|
23209
23783
|
}
|
|
23210
23784
|
function readFingerprintKey(dataDir2) {
|
|
@@ -23220,21 +23794,21 @@ function readFingerprintKey(dataDir2) {
|
|
|
23220
23794
|
function loadOrCreateFingerprintKey(dataDir2) {
|
|
23221
23795
|
const existing = readFingerprintKey(dataDir2);
|
|
23222
23796
|
if (existing) {
|
|
23223
|
-
|
|
23224
|
-
chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
|
|
23225
|
-
} catch {
|
|
23226
|
-
}
|
|
23797
|
+
tightenFile(keyFilePath(dataDir2));
|
|
23227
23798
|
return existing;
|
|
23228
23799
|
}
|
|
23229
|
-
return writeKeyFile(dataDir2, {
|
|
23800
|
+
return writeKeyFile(dataDir2, {
|
|
23801
|
+
version: storedKeyVersionFloor(dataDir2) + 1,
|
|
23802
|
+
material: randomBytes(KEY_MATERIAL_BYTES)
|
|
23803
|
+
});
|
|
23230
23804
|
}
|
|
23231
23805
|
function fingerprintValue(key, raw) {
|
|
23232
23806
|
return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
|
|
23233
23807
|
}
|
|
23234
23808
|
|
|
23235
23809
|
// ../../packages/persistence/src/local-layout.ts
|
|
23236
|
-
import {
|
|
23237
|
-
import {
|
|
23810
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23811
|
+
import { mkdir } from "fs/promises";
|
|
23238
23812
|
import { homedir } from "os";
|
|
23239
23813
|
import { join as join3 } from "path";
|
|
23240
23814
|
function defaultDataDir() {
|
|
@@ -23249,6 +23823,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23249
23823
|
function dbPath(base = defaultDataDir()) {
|
|
23250
23824
|
return join3(dataDir(base), "aka.db");
|
|
23251
23825
|
}
|
|
23826
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23827
|
+
ensureDataDirSync(dir);
|
|
23828
|
+
}
|
|
23252
23829
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23253
23830
|
const moves = [
|
|
23254
23831
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23256,19 +23833,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23256
23833
|
];
|
|
23257
23834
|
for (const { name, dest } of moves) {
|
|
23258
23835
|
try {
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
}
|
|
23264
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23836
|
+
ensureDataDirSync(dest);
|
|
23837
|
+
const moved = join3(dest, name);
|
|
23838
|
+
renameSync3(join3(base, name), moved);
|
|
23839
|
+
tightenFile(moved);
|
|
23265
23840
|
} catch {
|
|
23266
23841
|
}
|
|
23267
23842
|
}
|
|
23268
23843
|
}
|
|
23269
23844
|
|
|
23270
23845
|
// ../../packages/persistence/src/settings.ts
|
|
23271
|
-
import { readFileSync as readFileSync2
|
|
23846
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23272
23847
|
import { join as join4 } from "path";
|
|
23273
23848
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23274
23849
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23290,15 +23865,15 @@ function readJson(file2) {
|
|
|
23290
23865
|
}
|
|
23291
23866
|
|
|
23292
23867
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23293
|
-
import { existsSync as
|
|
23868
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
23294
23869
|
import { join as join5 } from "path";
|
|
23295
23870
|
var MARKER = "warn-era-capped";
|
|
23296
23871
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
23297
23872
|
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
23298
23873
|
const marker = join5(dataDir2, MARKER);
|
|
23299
|
-
if (
|
|
23874
|
+
if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
|
|
23300
23875
|
const capped = db.policies.capCategoryActions();
|
|
23301
|
-
|
|
23876
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23302
23877
|
`, { mode: DATA_FILE_MODE });
|
|
23303
23878
|
return { capped };
|
|
23304
23879
|
}
|
|
@@ -23363,6 +23938,12 @@ function providerFromModelId(modelId) {
|
|
|
23363
23938
|
|
|
23364
23939
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23365
23940
|
function loadConfig(base = defaultDataDir()) {
|
|
23941
|
+
try {
|
|
23942
|
+
ensureLayoutDirSync(base);
|
|
23943
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23944
|
+
if (existsSync4(settingsFile)) tightenFile(settingsFile);
|
|
23945
|
+
} catch {
|
|
23946
|
+
}
|
|
23366
23947
|
migrateLegacyLayout(base);
|
|
23367
23948
|
const settings = readWorkspaceSettings(base);
|
|
23368
23949
|
return {
|
|
@@ -23385,7 +23966,7 @@ function resolveProviderSafe() {
|
|
|
23385
23966
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23386
23967
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23387
23968
|
import { homedir as homedir2 } from "os";
|
|
23388
|
-
import { basename as basename2, join as
|
|
23969
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23389
23970
|
|
|
23390
23971
|
// ../../packages/detections/src/egress/registry.ts
|
|
23391
23972
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -26417,7 +26998,7 @@ function ensureBundledPacks() {
|
|
|
26417
26998
|
return false;
|
|
26418
26999
|
}
|
|
26419
27000
|
}
|
|
26420
|
-
function scanText(text) {
|
|
27001
|
+
function scanText(text, ruleVersions) {
|
|
26421
27002
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
26422
27003
|
try {
|
|
26423
27004
|
const rules = getLoadedRules();
|
|
@@ -26429,7 +27010,7 @@ function scanText(text) {
|
|
|
26429
27010
|
return {
|
|
26430
27011
|
ruleId: m.ruleId,
|
|
26431
27012
|
ruleName: rule?.name ?? m.ruleId,
|
|
26432
|
-
ruleVersion: String(rule?.specVersion ?? 1),
|
|
27013
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
26433
27014
|
category: m.category,
|
|
26434
27015
|
severity: m.severity,
|
|
26435
27016
|
span: m.span,
|
|
@@ -26444,8 +27025,8 @@ function scanText(text) {
|
|
|
26444
27025
|
}
|
|
26445
27026
|
|
|
26446
27027
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26447
|
-
import { existsSync as
|
|
26448
|
-
import { basename, dirname, isAbsolute, join as
|
|
27028
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
|
|
27029
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26449
27030
|
function resolveRepoIdentity(cwd) {
|
|
26450
27031
|
try {
|
|
26451
27032
|
const root = findGitRoot(cwd);
|
|
@@ -26478,32 +27059,32 @@ function resolveRepoNwo(cwd) {
|
|
|
26478
27059
|
function findGitRoot(start) {
|
|
26479
27060
|
let dir = start;
|
|
26480
27061
|
for (; ; ) {
|
|
26481
|
-
if (
|
|
27062
|
+
if (existsSync5(join7(dir, ".git"))) return dir;
|
|
26482
27063
|
const parent = dirname(dir);
|
|
26483
27064
|
if (parent === dir) return void 0;
|
|
26484
27065
|
dir = parent;
|
|
26485
27066
|
}
|
|
26486
27067
|
}
|
|
26487
27068
|
function resolveGitContext(root) {
|
|
26488
|
-
const dotGit =
|
|
27069
|
+
const dotGit = join7(root, ".git");
|
|
26489
27070
|
try {
|
|
26490
27071
|
if (statSync(dotGit).isDirectory()) {
|
|
26491
|
-
return { configPath:
|
|
27072
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
26492
27073
|
}
|
|
26493
27074
|
} catch {
|
|
26494
27075
|
return void 0;
|
|
26495
27076
|
}
|
|
26496
27077
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
26497
27078
|
if (!target) return void 0;
|
|
26498
|
-
const gitdir = isAbsolute(target) ? target :
|
|
26499
|
-
if (
|
|
26500
|
-
return { configPath:
|
|
27079
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
27080
|
+
if (existsSync5(join7(gitdir, "config"))) {
|
|
27081
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
26501
27082
|
}
|
|
26502
|
-
const commonRaw = safeRead(
|
|
27083
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
26503
27084
|
if (!commonRaw) return void 0;
|
|
26504
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
27085
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
26505
27086
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
26506
|
-
return { configPath:
|
|
27087
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
26507
27088
|
}
|
|
26508
27089
|
function safeRead(path) {
|
|
26509
27090
|
try {
|
|
@@ -26558,9 +27139,9 @@ function nwoFromUrl(url2) {
|
|
|
26558
27139
|
}
|
|
26559
27140
|
|
|
26560
27141
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26561
|
-
import { createHash as
|
|
27142
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26562
27143
|
function contentHashOf(text) {
|
|
26563
|
-
return
|
|
27144
|
+
return createHash4("sha256").update(text).digest("hex");
|
|
26564
27145
|
}
|
|
26565
27146
|
function buildIngestEvent(input) {
|
|
26566
27147
|
return {
|
|
@@ -26580,16 +27161,6 @@ function buildIngestEvent(input) {
|
|
|
26580
27161
|
};
|
|
26581
27162
|
}
|
|
26582
27163
|
|
|
26583
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
26584
|
-
import { createHash as createHash4 } from "crypto";
|
|
26585
|
-
function normalizeFilePath(filePath) {
|
|
26586
|
-
return filePath.replaceAll("\\", "/");
|
|
26587
|
-
}
|
|
26588
|
-
function computeFindingKey(input) {
|
|
26589
|
-
const normalizedPath = normalizeFilePath(input.filePath);
|
|
26590
|
-
return createHash4("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
26591
|
-
}
|
|
26592
|
-
|
|
26593
27164
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26594
27165
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
26595
27166
|
function resolveInventoryContext(input) {
|
|
@@ -26620,8 +27191,8 @@ function resolveInventoryContext(input) {
|
|
|
26620
27191
|
}
|
|
26621
27192
|
|
|
26622
27193
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26623
|
-
import { mkdirSync as
|
|
26624
|
-
import { join as
|
|
27194
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
27195
|
+
import { join as join9 } from "path";
|
|
26625
27196
|
|
|
26626
27197
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
26627
27198
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -26629,8 +27200,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
|
26629
27200
|
|
|
26630
27201
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
26631
27202
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
26632
|
-
import { existsSync as
|
|
26633
|
-
import { basename as basename4, join as
|
|
27203
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27204
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
26634
27205
|
|
|
26635
27206
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
26636
27207
|
var RawEgressError = class extends Error {
|
|
@@ -27020,8 +27591,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27020
27591
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27021
27592
|
|
|
27022
27593
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
27023
|
-
import { mkdirSync as
|
|
27024
|
-
import { join as
|
|
27594
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
27595
|
+
import { join as join11 } from "path";
|
|
27025
27596
|
|
|
27026
27597
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
27027
27598
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -27052,7 +27623,8 @@ var StandaloneDataGateway = class {
|
|
|
27052
27623
|
}
|
|
27053
27624
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
27054
27625
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
27055
|
-
// hands the natural key across.
|
|
27626
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27627
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
27056
27628
|
recordLlmCall(input) {
|
|
27057
27629
|
this.db.auditEvents.insertLlmCall(input);
|
|
27058
27630
|
return Promise.resolve();
|
|
@@ -27094,7 +27666,9 @@ var StandaloneDataGateway = class {
|
|
|
27094
27666
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
27095
27667
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
27096
27668
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
27097
|
-
// content-addressed
|
|
27669
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27670
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27671
|
+
// than no-opping.
|
|
27098
27672
|
writeToolCall(input) {
|
|
27099
27673
|
this.db.auditEvents.insertToolCall(input);
|
|
27100
27674
|
if (input.inspections.length === 0) return;
|
|
@@ -27114,7 +27688,7 @@ var StandaloneDataGateway = class {
|
|
|
27114
27688
|
});
|
|
27115
27689
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
27116
27690
|
this.db.inspectionFindings.insertFinding({
|
|
27117
|
-
id: inspectionFindingId(auditEventId,
|
|
27691
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
27118
27692
|
auditEventId,
|
|
27119
27693
|
inspectionDefinitionId: definitionId,
|
|
27120
27694
|
classifiedDataId: classifiedDataId2,
|
|
@@ -27163,10 +27737,17 @@ var StandaloneDataGateway = class {
|
|
|
27163
27737
|
try {
|
|
27164
27738
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
27165
27739
|
if (snapshot.installedPacks === 0) return void 0;
|
|
27166
|
-
if (snapshot.enabledPacks === 0)
|
|
27740
|
+
if (snapshot.enabledPacks === 0) {
|
|
27741
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27742
|
+
}
|
|
27167
27743
|
if (snapshot.invalidRules > 0) return void 0;
|
|
27168
27744
|
if (snapshot.rules.length === 0) return void 0;
|
|
27169
|
-
return {
|
|
27745
|
+
return {
|
|
27746
|
+
rules: snapshot.rules,
|
|
27747
|
+
ruleActions: snapshot.ruleActions,
|
|
27748
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27749
|
+
complete: true
|
|
27750
|
+
};
|
|
27170
27751
|
} catch {
|
|
27171
27752
|
return void 0;
|
|
27172
27753
|
}
|
|
@@ -27194,6 +27775,7 @@ var StandaloneDataGateway = class {
|
|
|
27194
27775
|
policies: [...policies, ...rulePolicies],
|
|
27195
27776
|
rules: installed ? installed.rules : [],
|
|
27196
27777
|
...installed ? { rulesComplete: true } : {},
|
|
27778
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
27197
27779
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
27198
27780
|
customKeywords,
|
|
27199
27781
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -27334,9 +27916,9 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
|
27334
27916
|
// src/history/transcripts.ts
|
|
27335
27917
|
import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
|
|
27336
27918
|
import { homedir as homedir3 } from "os";
|
|
27337
|
-
import { join as
|
|
27919
|
+
import { join as join12 } from "path";
|
|
27338
27920
|
function transcriptsDir(home) {
|
|
27339
|
-
return
|
|
27921
|
+
return join12(home ?? homedir3(), ".claude", "projects");
|
|
27340
27922
|
}
|
|
27341
27923
|
function isRecord(value) {
|
|
27342
27924
|
return typeof value === "object" && value !== null;
|
|
@@ -27404,9 +27986,9 @@ function parseTranscriptUsage(jsonl, sinceMs = 0) {
|
|
|
27404
27986
|
if (sinceMs > 0 && Date.parse(optString(rec.timestamp) ?? "") < sinceMs) continue;
|
|
27405
27987
|
if (rec.type === "user") {
|
|
27406
27988
|
const uuid5 = optString(rec.uuid);
|
|
27407
|
-
const
|
|
27408
|
-
if (uuid5 === void 0 ||
|
|
27409
|
-
out.push({ kind: "user", uuid: uuid5, promptId });
|
|
27989
|
+
const promptId2 = optString(rec.promptId);
|
|
27990
|
+
if (uuid5 === void 0 || promptId2 === void 0) continue;
|
|
27991
|
+
out.push({ kind: "user", uuid: uuid5, promptId: promptId2 });
|
|
27410
27992
|
continue;
|
|
27411
27993
|
}
|
|
27412
27994
|
if (rec.type !== "assistant") continue;
|
|
@@ -27584,7 +28166,7 @@ function* iterateFileContents(dir, excludeSessionId) {
|
|
|
27584
28166
|
return;
|
|
27585
28167
|
}
|
|
27586
28168
|
for (const project of projects) {
|
|
27587
|
-
const projectDir =
|
|
28169
|
+
const projectDir = join12(dir, project);
|
|
27588
28170
|
let files;
|
|
27589
28171
|
try {
|
|
27590
28172
|
files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
|
|
@@ -27594,7 +28176,7 @@ function* iterateFileContents(dir, excludeSessionId) {
|
|
|
27594
28176
|
for (const file2 of files) {
|
|
27595
28177
|
if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
|
|
27596
28178
|
continue;
|
|
27597
|
-
const filePath =
|
|
28179
|
+
const filePath = join12(projectDir, file2);
|
|
27598
28180
|
let content;
|
|
27599
28181
|
try {
|
|
27600
28182
|
content = readFileSync7(filePath, "utf8");
|
|
@@ -27715,13 +28297,13 @@ import { createHash as createHash5 } from "crypto";
|
|
|
27715
28297
|
import {
|
|
27716
28298
|
closeSync,
|
|
27717
28299
|
fstatSync,
|
|
27718
|
-
mkdirSync as
|
|
28300
|
+
mkdirSync as mkdirSync4,
|
|
27719
28301
|
openSync,
|
|
27720
28302
|
readFileSync as readFileSync8,
|
|
27721
28303
|
readSync,
|
|
27722
|
-
writeFileSync as
|
|
28304
|
+
writeFileSync as writeFileSync5
|
|
27723
28305
|
} from "fs";
|
|
27724
|
-
import { join as
|
|
28306
|
+
import { join as join13 } from "path";
|
|
27725
28307
|
|
|
27726
28308
|
// src/history/usage.ts
|
|
27727
28309
|
var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
|
|
@@ -27772,12 +28354,18 @@ async function reconcileSessionToolCalls(gateway, sessionId, toolCalls, usageRec
|
|
|
27772
28354
|
if (toolCalls.length === 0) return 0;
|
|
27773
28355
|
const promptIdByUuid = /* @__PURE__ */ new Map();
|
|
27774
28356
|
for (const r of usageRecords) if (r.kind === "user") promptIdByUuid.set(r.uuid, r.promptId);
|
|
28357
|
+
let ruleVersions;
|
|
28358
|
+
try {
|
|
28359
|
+
ruleVersions = (await gateway.getPolicyBundle()).ruleVersions;
|
|
28360
|
+
} catch {
|
|
28361
|
+
ruleVersions = void 0;
|
|
28362
|
+
}
|
|
27775
28363
|
const inputs = toolCalls.map((tc) => {
|
|
27776
28364
|
const runKey = (tc.parentUuid !== void 0 ? promptIdByUuid.get(tc.parentUuid) : void 0) ?? opts.seedPromptId;
|
|
27777
28365
|
const attributes = { tool_name: tc.toolName, tool_use_id: tc.toolUseId };
|
|
27778
28366
|
let inspections = [];
|
|
27779
28367
|
if (tc.target !== void 0) {
|
|
27780
|
-
const { masked, findings } = scanText(tc.target);
|
|
28368
|
+
const { masked, findings } = scanText(tc.target, ruleVersions);
|
|
27781
28369
|
if (masked !== "") attributes.target = truncateTarget(masked);
|
|
27782
28370
|
inspections = findings.map((f) => ({
|
|
27783
28371
|
ruleId: f.ruleId,
|