@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/commands/setup.md +101 -19
- package/package.json +3 -3
- package/scripts/apply-suppressions.js +3192 -487
- package/scripts/backfill.js +951 -455
- package/scripts/filescan.js +939 -449
- package/scripts/firstrun.js +895 -413
- package/scripts/intro.js +91 -24
- package/scripts/onboard.js +920 -416
- package/scripts/post-tool-use.js +932 -442
- package/scripts/pre-tool-use.js +937 -447
- package/scripts/query.js +895 -413
- package/scripts/reconcile.js +924 -436
- package/scripts/remediate.js +942 -452
- package/scripts/session-start.js +957 -475
- package/scripts/start-light.js +91 -24
- package/scripts/statusline.js +895 -413
- package/scripts/stop.js +132 -38
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +940 -450
package/scripts/query.js
CHANGED
|
@@ -493,7 +493,7 @@ var require_ignore = __commonJS({
|
|
|
493
493
|
|
|
494
494
|
// ../../packages/persistence/src/database.ts
|
|
495
495
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
496
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
497
497
|
import { join, sep } from "path";
|
|
498
498
|
import { DatabaseSync } from "node:sqlite";
|
|
499
499
|
|
|
@@ -546,6 +546,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
546
546
|
{
|
|
547
547
|
tag: "0011_egress_writer",
|
|
548
548
|
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'
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tag: "0012_handy_the_captain",
|
|
552
|
+
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`);"
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0013_legacy_history_backfill_support",
|
|
556
|
+
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"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0014_drop_legacy_events_findings",
|
|
560
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
549
561
|
}
|
|
550
562
|
];
|
|
551
563
|
|
|
@@ -15436,7 +15448,12 @@ var FindingFacets = external_exports.object({
|
|
|
15436
15448
|
severity: external_exports.array(FindingFacetItem),
|
|
15437
15449
|
subtype: external_exports.array(FindingFacetItem),
|
|
15438
15450
|
provider: external_exports.array(FindingFacetItem),
|
|
15439
|
-
action: external_exports.array(FindingFacetItem)
|
|
15451
|
+
action: external_exports.array(FindingFacetItem),
|
|
15452
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15453
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15454
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15455
|
+
// counted under no value.
|
|
15456
|
+
status: external_exports.array(FindingFacetItem)
|
|
15440
15457
|
}).meta({ id: "FindingFacets" });
|
|
15441
15458
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15442
15459
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15446,6 +15463,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15446
15463
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15447
15464
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15448
15465
|
action: external_exports.array(FindingAction).optional(),
|
|
15466
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15467
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15468
|
+
// one of the requested values.
|
|
15469
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15449
15470
|
q: external_exports.string().optional(),
|
|
15450
15471
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15451
15472
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15633,6 +15654,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15633
15654
|
parent_uuid: external_exports.string().optional(),
|
|
15634
15655
|
run_key: external_exports.string().optional()
|
|
15635
15656
|
}).catchall(external_exports.unknown());
|
|
15657
|
+
var CaptureAttributes = external_exports.object({
|
|
15658
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15659
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15660
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15661
|
+
source_tool: external_exports.string().optional(),
|
|
15662
|
+
file_path: external_exports.string().optional(),
|
|
15663
|
+
repo: external_exports.string().optional(),
|
|
15664
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15665
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15666
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15667
|
+
tool_name: external_exports.string().optional(),
|
|
15668
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15669
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15670
|
+
gitignored: external_exports.boolean().optional(),
|
|
15671
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15672
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15673
|
+
whole_file: external_exports.boolean().optional(),
|
|
15674
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15675
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15676
|
+
// trace id when telemetry is enabled.
|
|
15677
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15678
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15679
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15680
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15681
|
+
// authorized the bypass.
|
|
15682
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15683
|
+
}).catchall(external_exports.unknown());
|
|
15636
15684
|
var ToolCallInspection = external_exports.object({
|
|
15637
15685
|
ruleId: external_exports.string().min(1),
|
|
15638
15686
|
ruleName: external_exports.string(),
|
|
@@ -15719,7 +15767,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15719
15767
|
span: Span,
|
|
15720
15768
|
maskedMatch: external_exports.string(),
|
|
15721
15769
|
actionTaken: ActionTaken,
|
|
15722
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15770
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15771
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15772
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15773
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15774
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15775
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15776
|
+
findingKey: external_exports.string().optional(),
|
|
15777
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15778
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15779
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15780
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15781
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15723
15782
|
});
|
|
15724
15783
|
var InventoryContext = external_exports.object({
|
|
15725
15784
|
host: InventoryInput.optional(),
|
|
@@ -15921,6 +15980,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15921
15980
|
|
|
15922
15981
|
// ../../packages/schema/src/zod/event.ts
|
|
15923
15982
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15983
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15924
15984
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15925
15985
|
var EventMetadata = external_exports.object({
|
|
15926
15986
|
sessionId: external_exports.string().optional(),
|
|
@@ -16410,6 +16470,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16410
16470
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16411
16471
|
// `bundle.exceptions ?? []`.
|
|
16412
16472
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16473
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16474
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16475
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16476
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16477
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16478
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16413
16479
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16414
16480
|
fetchedAt: external_exports.iso.datetime()
|
|
16415
16481
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17209,6 +17275,15 @@ function groupActions(g) {
|
|
|
17209
17275
|
actionsCache.set(g, actions);
|
|
17210
17276
|
return actions;
|
|
17211
17277
|
}
|
|
17278
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17279
|
+
const statusSet = new Set(statuses);
|
|
17280
|
+
let sum = 0;
|
|
17281
|
+
for (const input of statusInputs) {
|
|
17282
|
+
if (input.count === void 0) return null;
|
|
17283
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17284
|
+
}
|
|
17285
|
+
return sum;
|
|
17286
|
+
}
|
|
17212
17287
|
function applyFindingFilters(groups, opts) {
|
|
17213
17288
|
let filtered = groups;
|
|
17214
17289
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17227,6 +17302,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17227
17302
|
const subtypeSet = new Set(opts.subtype);
|
|
17228
17303
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17229
17304
|
}
|
|
17305
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17306
|
+
const statusSet = new Set(opts.statuses);
|
|
17307
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17308
|
+
}
|
|
17230
17309
|
if (opts.q) {
|
|
17231
17310
|
const q = opts.q.toLowerCase();
|
|
17232
17311
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17248,6 +17327,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17248
17327
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17249
17328
|
providers: opts.providers,
|
|
17250
17329
|
actions: opts.actions,
|
|
17330
|
+
statuses: opts.statuses,
|
|
17251
17331
|
q: opts.q,
|
|
17252
17332
|
subtype: opts.subtype
|
|
17253
17333
|
});
|
|
@@ -17257,6 +17337,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17257
17337
|
}
|
|
17258
17338
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17259
17339
|
actions: opts.actions,
|
|
17340
|
+
statuses: opts.statuses,
|
|
17260
17341
|
q: opts.q,
|
|
17261
17342
|
subtype: opts.subtype,
|
|
17262
17343
|
severity: opts.severity
|
|
@@ -17267,6 +17348,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17267
17348
|
}
|
|
17268
17349
|
const forAction = applyFindingFilters(allGroups, {
|
|
17269
17350
|
providers: opts.providers,
|
|
17351
|
+
statuses: opts.statuses,
|
|
17270
17352
|
q: opts.q,
|
|
17271
17353
|
subtype: opts.subtype,
|
|
17272
17354
|
severity: opts.severity
|
|
@@ -17278,17 +17360,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17278
17360
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17279
17361
|
providers: opts.providers,
|
|
17280
17362
|
actions: opts.actions,
|
|
17363
|
+
statuses: opts.statuses,
|
|
17281
17364
|
q: opts.q,
|
|
17282
17365
|
severity: opts.severity
|
|
17283
17366
|
});
|
|
17284
17367
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17285
17368
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17369
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17370
|
+
providers: opts.providers,
|
|
17371
|
+
actions: opts.actions,
|
|
17372
|
+
q: opts.q,
|
|
17373
|
+
subtype: opts.subtype,
|
|
17374
|
+
severity: opts.severity
|
|
17375
|
+
});
|
|
17376
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17377
|
+
for (const g of forStatus) {
|
|
17378
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17379
|
+
}
|
|
17286
17380
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17287
17381
|
return {
|
|
17288
17382
|
severity: toItems(severityMap),
|
|
17289
17383
|
provider: toItems(providerMap),
|
|
17290
17384
|
action: toItems(actionMap),
|
|
17291
|
-
subtype: toItems(subtypeMap)
|
|
17385
|
+
subtype: toItems(subtypeMap),
|
|
17386
|
+
status: toItems(statusMap)
|
|
17292
17387
|
};
|
|
17293
17388
|
}
|
|
17294
17389
|
|
|
@@ -17323,10 +17418,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17323
17418
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17324
17419
|
|
|
17325
17420
|
// ../../packages/schema/src/zod/local.ts
|
|
17326
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17421
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17327
17422
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17328
17423
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17329
17424
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17425
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17426
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17427
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17428
|
+
});
|
|
17330
17429
|
var WorkspaceSettings = external_exports.object({
|
|
17331
17430
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17332
17431
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17342,37 +17441,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17342
17441
|
// Shares writes.
|
|
17343
17442
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17344
17443
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17345
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17444
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17445
|
+
// Records that the user consented to sending findings to the model API for
|
|
17446
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17447
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17448
|
+
// covers the current payload and must be re-granted.
|
|
17449
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17346
17450
|
});
|
|
17347
17451
|
function defaultWorkspaceSettings() {
|
|
17348
17452
|
return WorkspaceSettings.parse({});
|
|
17349
17453
|
}
|
|
17350
|
-
function toEventRow(event) {
|
|
17351
|
-
return {
|
|
17352
|
-
id: event.id,
|
|
17353
|
-
sourceTool: event.sourceTool,
|
|
17354
|
-
kind: event.kind,
|
|
17355
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17356
|
-
contentHash: event.contentHash,
|
|
17357
|
-
content: event.content,
|
|
17358
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17359
|
-
};
|
|
17360
|
-
}
|
|
17361
|
-
function toFindingRow(finding) {
|
|
17362
|
-
return {
|
|
17363
|
-
id: finding.id,
|
|
17364
|
-
eventId: finding.eventId,
|
|
17365
|
-
ruleId: finding.ruleId,
|
|
17366
|
-
category: finding.category,
|
|
17367
|
-
severity: finding.severity,
|
|
17368
|
-
spanStart: finding.span.start,
|
|
17369
|
-
spanEnd: finding.span.end,
|
|
17370
|
-
maskedMatch: finding.maskedMatch,
|
|
17371
|
-
actionTaken: finding.actionTaken,
|
|
17372
|
-
confidence: finding.confidence,
|
|
17373
|
-
findingKey: finding.findingKey ?? null
|
|
17374
|
-
};
|
|
17375
|
-
}
|
|
17376
17454
|
function toInventoryRow(input, id, now) {
|
|
17377
17455
|
return {
|
|
17378
17456
|
id,
|
|
@@ -17442,7 +17520,42 @@ function toInspectionFindingRow(input) {
|
|
|
17442
17520
|
spanEnd: input.span.end,
|
|
17443
17521
|
maskedMatch: input.maskedMatch,
|
|
17444
17522
|
actionTaken: input.actionTaken,
|
|
17445
|
-
confidence: input.confidence
|
|
17523
|
+
confidence: input.confidence,
|
|
17524
|
+
findingKey: input.findingKey ?? null,
|
|
17525
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17526
|
+
};
|
|
17527
|
+
}
|
|
17528
|
+
function toCaptureAttributes(event) {
|
|
17529
|
+
const metadata = event.metadata;
|
|
17530
|
+
return {
|
|
17531
|
+
source_tool: event.sourceTool,
|
|
17532
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17533
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17534
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17535
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17536
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17537
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17538
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17539
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17540
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17541
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17542
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17543
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17544
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17545
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17546
|
+
};
|
|
17547
|
+
}
|
|
17548
|
+
function captureDefinitionVersion(finding) {
|
|
17549
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17550
|
+
}
|
|
17551
|
+
function toCaptureDefinitionInput(finding) {
|
|
17552
|
+
return {
|
|
17553
|
+
ruleId: finding.ruleId,
|
|
17554
|
+
version: captureDefinitionVersion(finding),
|
|
17555
|
+
name: finding.ruleId,
|
|
17556
|
+
category: finding.category,
|
|
17557
|
+
severity: finding.severity,
|
|
17558
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17446
17559
|
};
|
|
17447
17560
|
}
|
|
17448
17561
|
|
|
@@ -17870,6 +17983,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17870
17983
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17871
17984
|
}
|
|
17872
17985
|
|
|
17986
|
+
// ../../packages/persistence/src/ids.ts
|
|
17987
|
+
import { createHash } from "crypto";
|
|
17988
|
+
function sha256Hex(input) {
|
|
17989
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17990
|
+
}
|
|
17991
|
+
function inventoryId(objectType, identityKey) {
|
|
17992
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17993
|
+
}
|
|
17994
|
+
function sourceProjectId(url2) {
|
|
17995
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17996
|
+
}
|
|
17997
|
+
function classifiedDataId(cls) {
|
|
17998
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17999
|
+
}
|
|
18000
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
18001
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
18002
|
+
}
|
|
18003
|
+
function llmCallId(sessionId, messageId) {
|
|
18004
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
18005
|
+
}
|
|
18006
|
+
function toolCallId(sessionId, toolUseId) {
|
|
18007
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
18008
|
+
}
|
|
18009
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
18010
|
+
return sha256Hex(
|
|
18011
|
+
canonicalIdentity([
|
|
18012
|
+
"inspection_finding",
|
|
18013
|
+
auditEventId,
|
|
18014
|
+
ruleId,
|
|
18015
|
+
String(spanStart),
|
|
18016
|
+
String(spanEnd)
|
|
18017
|
+
])
|
|
18018
|
+
);
|
|
18019
|
+
}
|
|
18020
|
+
var NO_SESSION = "no_session";
|
|
18021
|
+
var NO_PATH = "no_path";
|
|
18022
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
18023
|
+
return sha256Hex(
|
|
18024
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
18025
|
+
);
|
|
18026
|
+
}
|
|
18027
|
+
|
|
17873
18028
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17874
18029
|
function escapeLikePattern(s) {
|
|
17875
18030
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17966,39 +18121,81 @@ function evidenceExists(db, object2) {
|
|
|
17966
18121
|
return schemaObjectExists(db, "table", object2.name);
|
|
17967
18122
|
}
|
|
17968
18123
|
|
|
17969
|
-
// ../../packages/persistence/src/
|
|
17970
|
-
|
|
17971
|
-
|
|
17972
|
-
|
|
18124
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18125
|
+
function allRows(stmt, params) {
|
|
18126
|
+
if (params === void 0) return stmt.all();
|
|
18127
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18128
|
+
return stmt.all(params);
|
|
17973
18129
|
}
|
|
17974
|
-
function
|
|
17975
|
-
|
|
18130
|
+
function getRow(stmt, params) {
|
|
18131
|
+
if (params === void 0) return stmt.get();
|
|
18132
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18133
|
+
return stmt.get(params);
|
|
17976
18134
|
}
|
|
17977
|
-
function
|
|
17978
|
-
return
|
|
18135
|
+
function intToBool(raw) {
|
|
18136
|
+
return raw === 1 || raw === true;
|
|
17979
18137
|
}
|
|
17980
|
-
function
|
|
17981
|
-
return
|
|
18138
|
+
function boolToInt(b) {
|
|
18139
|
+
return b ? 1 : 0;
|
|
17982
18140
|
}
|
|
17983
|
-
function
|
|
17984
|
-
|
|
18141
|
+
function bindParams(row) {
|
|
18142
|
+
const out = {};
|
|
18143
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18144
|
+
out[key] = value === void 0 ? null : value;
|
|
18145
|
+
}
|
|
18146
|
+
return out;
|
|
17985
18147
|
}
|
|
17986
|
-
function
|
|
17987
|
-
return
|
|
18148
|
+
function countScalar(db, sql, params) {
|
|
18149
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17988
18150
|
}
|
|
17989
|
-
function
|
|
17990
|
-
|
|
18151
|
+
function countBy(db, sql, params) {
|
|
18152
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18153
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18154
|
+
map2.set(row.k, row.n);
|
|
18155
|
+
}
|
|
18156
|
+
return map2;
|
|
17991
18157
|
}
|
|
17992
|
-
function
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18158
|
+
function mapRowsTolerant(rows, map2) {
|
|
18159
|
+
const out = [];
|
|
18160
|
+
for (const row of rows) {
|
|
18161
|
+
try {
|
|
18162
|
+
out.push(map2(row));
|
|
18163
|
+
} catch {
|
|
18164
|
+
}
|
|
18165
|
+
}
|
|
18166
|
+
return out;
|
|
18167
|
+
}
|
|
18168
|
+
|
|
18169
|
+
// ../../packages/persistence/src/paths.ts
|
|
18170
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18171
|
+
var DATA_DIR_MODE = 448;
|
|
18172
|
+
var DATA_FILE_MODE = 384;
|
|
18173
|
+
var DB_FILENAME = "aka.db";
|
|
18174
|
+
function chmodBestEffort(path, mode) {
|
|
18175
|
+
try {
|
|
18176
|
+
chmodSync(path, mode);
|
|
18177
|
+
} catch {
|
|
18178
|
+
}
|
|
18179
|
+
}
|
|
18180
|
+
function tightenDir(dir) {
|
|
18181
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18182
|
+
}
|
|
18183
|
+
function ensureDataDirSync(dir) {
|
|
18184
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18185
|
+
tightenDir(dir);
|
|
18186
|
+
}
|
|
18187
|
+
function dbSidecars(file2) {
|
|
18188
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18189
|
+
}
|
|
18190
|
+
function tightenFile(file2) {
|
|
18191
|
+
try {
|
|
18192
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18193
|
+
} catch {
|
|
18194
|
+
}
|
|
18195
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18196
|
+
}
|
|
18197
|
+
function tightenPerms(file2) {
|
|
18198
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18002
18199
|
}
|
|
18003
18200
|
|
|
18004
18201
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -18012,7 +18209,8 @@ function createdIndexName(statement) {
|
|
|
18012
18209
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
18013
18210
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
18014
18211
|
}
|
|
18015
|
-
|
|
18212
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18213
|
+
function applyMigrations(db, file2) {
|
|
18016
18214
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
18017
18215
|
db.exec(
|
|
18018
18216
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -18026,6 +18224,7 @@ function applyMigrations(db) {
|
|
|
18026
18224
|
);
|
|
18027
18225
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
18028
18226
|
if (applied.has(migration.tag)) continue;
|
|
18227
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
18029
18228
|
const evidence = evidenceObjects(migration.sql);
|
|
18030
18229
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
18031
18230
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18070,7 +18269,6 @@ function applyMigrations(db) {
|
|
|
18070
18269
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18071
18270
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18072
18271
|
}
|
|
18073
|
-
ensureSyncedAtColumn(db, "events");
|
|
18074
18272
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18075
18273
|
ensureScanLedgerTable(db);
|
|
18076
18274
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18078,6 +18276,47 @@ function applyMigrations(db) {
|
|
|
18078
18276
|
ensureWriteGateTrigger(db);
|
|
18079
18277
|
ensureTokenUsageColumns(db);
|
|
18080
18278
|
reconcileSourceProjectIds(db);
|
|
18279
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18280
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18281
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18282
|
+
}
|
|
18283
|
+
}
|
|
18284
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18285
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18286
|
+
if (!migration) return;
|
|
18287
|
+
if (file2) {
|
|
18288
|
+
try {
|
|
18289
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18290
|
+
} catch (error51) {
|
|
18291
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18292
|
+
return;
|
|
18293
|
+
}
|
|
18294
|
+
}
|
|
18295
|
+
try {
|
|
18296
|
+
withTransaction(
|
|
18297
|
+
db,
|
|
18298
|
+
() => {
|
|
18299
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18300
|
+
if (alreadyDropped) return;
|
|
18301
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18302
|
+
db.exec(statement);
|
|
18303
|
+
}
|
|
18304
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18305
|
+
migration.tag,
|
|
18306
|
+
Date.now()
|
|
18307
|
+
);
|
|
18308
|
+
},
|
|
18309
|
+
"IMMEDIATE"
|
|
18310
|
+
);
|
|
18311
|
+
} catch (error51) {
|
|
18312
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18313
|
+
}
|
|
18314
|
+
}
|
|
18315
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18316
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18317
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18318
|
+
tightenFile(backup);
|
|
18319
|
+
return backup;
|
|
18081
18320
|
}
|
|
18082
18321
|
var TOKEN_USAGE_COLUMNS = [
|
|
18083
18322
|
{
|
|
@@ -18106,6 +18345,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18106
18345
|
}
|
|
18107
18346
|
];
|
|
18108
18347
|
function ensureTokenUsageColumns(db) {
|
|
18348
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18109
18349
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18110
18350
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18111
18351
|
if (!existing.has(column.name)) {
|
|
@@ -18171,11 +18411,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18171
18411
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18172
18412
|
}
|
|
18173
18413
|
}
|
|
18414
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18415
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18416
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18417
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18418
|
+
return row?.lastRowid ?? 0;
|
|
18419
|
+
}
|
|
18420
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18421
|
+
db.prepare(
|
|
18422
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18423
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18424
|
+
).run(source, lastRowid);
|
|
18425
|
+
}
|
|
18426
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18427
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18428
|
+
let processed = 0;
|
|
18429
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18430
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18431
|
+
if (rows.length === 0) return true;
|
|
18432
|
+
withTransaction(
|
|
18433
|
+
db,
|
|
18434
|
+
() => {
|
|
18435
|
+
handleRows(rows);
|
|
18436
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18437
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18438
|
+
},
|
|
18439
|
+
"IMMEDIATE"
|
|
18440
|
+
);
|
|
18441
|
+
processed += rows.length;
|
|
18442
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18443
|
+
}
|
|
18444
|
+
return false;
|
|
18445
|
+
}
|
|
18446
|
+
function parseLegacyEventMetadata(raw) {
|
|
18447
|
+
if (raw === null) return void 0;
|
|
18448
|
+
try {
|
|
18449
|
+
return JSON.parse(raw);
|
|
18450
|
+
} catch {
|
|
18451
|
+
return void 0;
|
|
18452
|
+
}
|
|
18453
|
+
}
|
|
18454
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18455
|
+
return JSON.stringify(
|
|
18456
|
+
toCaptureAttributes({
|
|
18457
|
+
id: row.id,
|
|
18458
|
+
sourceTool: row.sourceTool,
|
|
18459
|
+
kind: row.kind,
|
|
18460
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18461
|
+
contentHash: row.contentHash,
|
|
18462
|
+
content: row.content,
|
|
18463
|
+
metadata: row.metadata
|
|
18464
|
+
})
|
|
18465
|
+
);
|
|
18466
|
+
}
|
|
18467
|
+
function copyLegacyEvents(db) {
|
|
18468
|
+
const selectStmt = db.prepare(
|
|
18469
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18470
|
+
content_hash AS contentHash, content, metadata
|
|
18471
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18472
|
+
);
|
|
18473
|
+
const insertStmt = db.prepare(
|
|
18474
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18475
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18476
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18477
|
+
);
|
|
18478
|
+
const stubRootStmt = db.prepare(
|
|
18479
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18480
|
+
);
|
|
18481
|
+
return drainLegacyTable(
|
|
18482
|
+
db,
|
|
18483
|
+
"events",
|
|
18484
|
+
selectStmt,
|
|
18485
|
+
(rows) => {
|
|
18486
|
+
for (const row of rows) {
|
|
18487
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18488
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18489
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18490
|
+
insertStmt.run(
|
|
18491
|
+
bindParams({
|
|
18492
|
+
id: row.id,
|
|
18493
|
+
parentId: sessionId,
|
|
18494
|
+
rootSessionId: sessionId,
|
|
18495
|
+
eventType: row.kind,
|
|
18496
|
+
startedAt: row.occurredAt,
|
|
18497
|
+
content: row.content,
|
|
18498
|
+
contentHash: row.contentHash,
|
|
18499
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18500
|
+
})
|
|
18501
|
+
);
|
|
18502
|
+
}
|
|
18503
|
+
}
|
|
18504
|
+
);
|
|
18505
|
+
}
|
|
18506
|
+
function copyLegacyFindings(db) {
|
|
18507
|
+
const selectStmt = db.prepare(
|
|
18508
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18509
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18510
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18511
|
+
first_detected_at AS firstDetectedAt
|
|
18512
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18513
|
+
);
|
|
18514
|
+
const definitionStmt = db.prepare(
|
|
18515
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18516
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18517
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18518
|
+
);
|
|
18519
|
+
const findingStmt = db.prepare(
|
|
18520
|
+
`INSERT INTO inspection_findings
|
|
18521
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18522
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18523
|
+
finding_key, first_detected_at)
|
|
18524
|
+
VALUES
|
|
18525
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18526
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18527
|
+
:findingKey, :firstDetectedAt)
|
|
18528
|
+
ON CONFLICT(id) DO NOTHING
|
|
18529
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18530
|
+
first_detected_at = CASE
|
|
18531
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18532
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18533
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18534
|
+
END`
|
|
18535
|
+
);
|
|
18536
|
+
return drainLegacyTable(
|
|
18537
|
+
db,
|
|
18538
|
+
"findings",
|
|
18539
|
+
selectStmt,
|
|
18540
|
+
(rows) => {
|
|
18541
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18542
|
+
for (const row of rows) {
|
|
18543
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18544
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18545
|
+
if (definitionId === void 0) {
|
|
18546
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18547
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18548
|
+
definitionStmt.run(
|
|
18549
|
+
bindParams({
|
|
18550
|
+
id: definitionId,
|
|
18551
|
+
ruleId: row.ruleId,
|
|
18552
|
+
name: row.ruleId,
|
|
18553
|
+
category: row.category,
|
|
18554
|
+
severity: row.severity,
|
|
18555
|
+
definition: "",
|
|
18556
|
+
version: version2
|
|
18557
|
+
})
|
|
18558
|
+
);
|
|
18559
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18560
|
+
}
|
|
18561
|
+
findingStmt.run(
|
|
18562
|
+
bindParams({
|
|
18563
|
+
id: row.id,
|
|
18564
|
+
auditEventId: row.eventId,
|
|
18565
|
+
inspectionDefinitionId: definitionId,
|
|
18566
|
+
spanStart: row.spanStart,
|
|
18567
|
+
spanEnd: row.spanEnd,
|
|
18568
|
+
maskedMatch: row.maskedMatch,
|
|
18569
|
+
actionTaken: row.actionTaken,
|
|
18570
|
+
confidence: row.confidence,
|
|
18571
|
+
findingKey: row.findingKey,
|
|
18572
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18573
|
+
})
|
|
18574
|
+
);
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18577
|
+
);
|
|
18578
|
+
}
|
|
18579
|
+
function runLegacyHistoryBackfill(db) {
|
|
18580
|
+
try {
|
|
18581
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18582
|
+
if (!eventsCaughtUp) return false;
|
|
18583
|
+
return copyLegacyFindings(db);
|
|
18584
|
+
} catch (error51) {
|
|
18585
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18586
|
+
return false;
|
|
18587
|
+
}
|
|
18588
|
+
}
|
|
18174
18589
|
function isForeignSqliteLineage(db) {
|
|
18175
18590
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18176
18591
|
return columnNames(db, "events").includes("tenant_id");
|
|
18177
18592
|
}
|
|
18178
18593
|
function ensureSyncedAtColumn(db, table2) {
|
|
18594
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18179
18595
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18180
18596
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18181
18597
|
}
|
|
@@ -18196,6 +18612,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18196
18612
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18197
18613
|
)`);
|
|
18198
18614
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18615
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18199
18616
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18200
18617
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18201
18618
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18223,30 +18640,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18223
18640
|
)`);
|
|
18224
18641
|
}
|
|
18225
18642
|
|
|
18226
|
-
// ../../packages/persistence/src/paths.ts
|
|
18227
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18228
|
-
var DATA_DIR_MODE = 448;
|
|
18229
|
-
var DATA_FILE_MODE = 384;
|
|
18230
|
-
var DB_FILENAME = "aka.db";
|
|
18231
|
-
function ensureDataDirSync(dir) {
|
|
18232
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18233
|
-
try {
|
|
18234
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18235
|
-
} catch {
|
|
18236
|
-
}
|
|
18237
|
-
}
|
|
18238
|
-
function walSidecars(file2) {
|
|
18239
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18240
|
-
}
|
|
18241
|
-
function tightenPerms(file2) {
|
|
18242
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18243
|
-
try {
|
|
18244
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18245
|
-
} catch {
|
|
18246
|
-
}
|
|
18247
|
-
}
|
|
18248
|
-
}
|
|
18249
|
-
|
|
18250
18643
|
// ../../packages/persistence/src/internal/json.ts
|
|
18251
18644
|
function safeJson(s, fallback) {
|
|
18252
18645
|
if (s == null) return fallback;
|
|
@@ -18266,51 +18659,6 @@ function parseJsonObject(s) {
|
|
|
18266
18659
|
return void 0;
|
|
18267
18660
|
}
|
|
18268
18661
|
|
|
18269
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18270
|
-
function allRows(stmt, params) {
|
|
18271
|
-
if (params === void 0) return stmt.all();
|
|
18272
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18273
|
-
return stmt.all(params);
|
|
18274
|
-
}
|
|
18275
|
-
function getRow(stmt, params) {
|
|
18276
|
-
if (params === void 0) return stmt.get();
|
|
18277
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18278
|
-
return stmt.get(params);
|
|
18279
|
-
}
|
|
18280
|
-
function intToBool(raw) {
|
|
18281
|
-
return raw === 1 || raw === true;
|
|
18282
|
-
}
|
|
18283
|
-
function boolToInt(b) {
|
|
18284
|
-
return b ? 1 : 0;
|
|
18285
|
-
}
|
|
18286
|
-
function bindParams(row) {
|
|
18287
|
-
const out = {};
|
|
18288
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18289
|
-
out[key] = value === void 0 ? null : value;
|
|
18290
|
-
}
|
|
18291
|
-
return out;
|
|
18292
|
-
}
|
|
18293
|
-
function countScalar(db, sql, params) {
|
|
18294
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18295
|
-
}
|
|
18296
|
-
function countBy(db, sql, params) {
|
|
18297
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18298
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18299
|
-
map2.set(row.k, row.n);
|
|
18300
|
-
}
|
|
18301
|
-
return map2;
|
|
18302
|
-
}
|
|
18303
|
-
function mapRowsTolerant(rows, map2) {
|
|
18304
|
-
const out = [];
|
|
18305
|
-
for (const row of rows) {
|
|
18306
|
-
try {
|
|
18307
|
-
out.push(map2(row));
|
|
18308
|
-
} catch {
|
|
18309
|
-
}
|
|
18310
|
-
}
|
|
18311
|
-
return out;
|
|
18312
|
-
}
|
|
18313
|
-
|
|
18314
18662
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18315
18663
|
var DAY_MS = 864e5;
|
|
18316
18664
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18927,6 +19275,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18927
19275
|
})
|
|
18928
19276
|
);
|
|
18929
19277
|
}
|
|
19278
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19279
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19280
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19281
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19282
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19283
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19284
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19285
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19286
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19287
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19288
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19289
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19290
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19291
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19292
|
+
}
|
|
18930
19293
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18931
19294
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18932
19295
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19388,8 +19751,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19388
19751
|
)
|
|
19389
19752
|
);
|
|
19390
19753
|
}
|
|
19391
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19392
|
-
//
|
|
19754
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19755
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19756
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19757
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19758
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19759
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19760
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19761
|
+
// to the old findings⋈events one.
|
|
19393
19762
|
countFindingsLast30d(ruleIds) {
|
|
19394
19763
|
if (ruleIds.length === 0) return 0;
|
|
19395
19764
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19397,8 +19766,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19397
19766
|
return countScalar(
|
|
19398
19767
|
this.db,
|
|
19399
19768
|
`SELECT count(*) AS n
|
|
19400
|
-
FROM
|
|
19401
|
-
|
|
19769
|
+
FROM inspection_findings f
|
|
19770
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19771
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19772
|
+
WHERE e.started_at >= ?
|
|
19773
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19774
|
+
AND d.rule_id IN (${inClause})`,
|
|
19402
19775
|
[since, ...ruleIds]
|
|
19403
19776
|
);
|
|
19404
19777
|
}
|
|
@@ -19408,35 +19781,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19408
19781
|
var SqliteEventsRepository = class {
|
|
19409
19782
|
constructor(db) {
|
|
19410
19783
|
this.db = db;
|
|
19411
|
-
this.insertStmt = db.prepare(
|
|
19412
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19413
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19414
|
-
);
|
|
19415
19784
|
}
|
|
19416
19785
|
db;
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
this.insertStmt.run(
|
|
19421
|
-
bindParams({
|
|
19422
|
-
id: row.id,
|
|
19423
|
-
sourceTool: row.sourceTool,
|
|
19424
|
-
kind: row.kind,
|
|
19425
|
-
occurredAt: row.occurredAt,
|
|
19426
|
-
contentHash: row.contentHash,
|
|
19427
|
-
content: row.content,
|
|
19428
|
-
metadata: row.metadata
|
|
19429
|
-
})
|
|
19430
|
-
);
|
|
19431
|
-
}
|
|
19432
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19433
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19434
|
-
// never duplicates findings.
|
|
19786
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19787
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19788
|
+
// scan never duplicates findings.
|
|
19435
19789
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19436
19790
|
// async EventsReadPort contract.
|
|
19791
|
+
//
|
|
19792
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19793
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19794
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19795
|
+
// index-friendly rather than walking rows that can never match.
|
|
19437
19796
|
contentHashes() {
|
|
19438
19797
|
const rows = allRows(
|
|
19439
|
-
this.db.prepare(
|
|
19798
|
+
this.db.prepare(
|
|
19799
|
+
`SELECT content_hash FROM audit_events
|
|
19800
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19801
|
+
)
|
|
19440
19802
|
);
|
|
19441
19803
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19442
19804
|
}
|
|
@@ -19772,17 +20134,20 @@ function parseExceptionRow(row) {
|
|
|
19772
20134
|
}
|
|
19773
20135
|
|
|
19774
20136
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19775
|
-
function
|
|
20137
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19776
20138
|
return `(
|
|
19777
|
-
SELECT fr
|
|
20139
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19778
20140
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19779
20141
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19780
20142
|
LIMIT 1
|
|
19781
20143
|
)`;
|
|
19782
20144
|
}
|
|
20145
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20146
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20147
|
+
}
|
|
19783
20148
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19784
|
-
SELECT finding_key, status FROM (
|
|
19785
|
-
SELECT fr.finding_key, fr.status,
|
|
20149
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20150
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19786
20151
|
ROW_NUMBER() OVER (
|
|
19787
20152
|
PARTITION BY fr.finding_key
|
|
19788
20153
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19809,68 +20174,21 @@ var DAY_MS3 = 864e5;
|
|
|
19809
20174
|
var SqliteFindingsRepository = class {
|
|
19810
20175
|
constructor(db) {
|
|
19811
20176
|
this.db = db;
|
|
19812
|
-
this.insertStmt = db.prepare(
|
|
19813
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19814
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19815
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19816
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19817
|
-
event_id = excluded.event_id,
|
|
19818
|
-
category = excluded.category,
|
|
19819
|
-
severity = excluded.severity,
|
|
19820
|
-
span_start = excluded.span_start,
|
|
19821
|
-
span_end = excluded.span_end,
|
|
19822
|
-
masked_match = excluded.masked_match,
|
|
19823
|
-
action_taken = excluded.action_taken,
|
|
19824
|
-
confidence = excluded.confidence`
|
|
19825
|
-
);
|
|
19826
|
-
this.sessionDupStmt = db.prepare(
|
|
19827
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19828
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19829
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19830
|
-
LIMIT 1`
|
|
19831
|
-
);
|
|
19832
20177
|
}
|
|
19833
20178
|
db;
|
|
19834
|
-
insertStmt;
|
|
19835
|
-
sessionDupStmt;
|
|
19836
|
-
insertFindings(findings, scope = {}) {
|
|
19837
|
-
for (const finding of findings) {
|
|
19838
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19839
|
-
const row = toFindingRow(finding);
|
|
19840
|
-
this.insertStmt.run({
|
|
19841
|
-
id: row.id,
|
|
19842
|
-
eventId: row.eventId,
|
|
19843
|
-
ruleId: row.ruleId,
|
|
19844
|
-
category: row.category,
|
|
19845
|
-
severity: row.severity,
|
|
19846
|
-
spanStart: row.spanStart,
|
|
19847
|
-
spanEnd: row.spanEnd,
|
|
19848
|
-
maskedMatch: row.maskedMatch,
|
|
19849
|
-
actionTaken: row.actionTaken,
|
|
19850
|
-
confidence: row.confidence,
|
|
19851
|
-
findingKey: row.findingKey ?? null
|
|
19852
|
-
});
|
|
19853
|
-
}
|
|
19854
|
-
}
|
|
19855
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19856
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19857
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19858
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19859
|
-
const hit = this.sessionDupStmt.get({
|
|
19860
|
-
ruleId: finding.ruleId,
|
|
19861
|
-
maskedMatch: finding.maskedMatch,
|
|
19862
|
-
sessionId
|
|
19863
|
-
});
|
|
19864
|
-
return hit !== void 0;
|
|
19865
|
-
}
|
|
19866
20179
|
recentFindings(opts) {
|
|
19867
20180
|
const limit = opts?.limit ?? 50;
|
|
19868
20181
|
const rows = allRows(
|
|
19869
20182
|
this.db.prepare(
|
|
19870
|
-
`SELECT f.id, f.event_id,
|
|
19871
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19872
|
-
|
|
19873
|
-
|
|
20183
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20184
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20185
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20186
|
+
e.event_type AS kind
|
|
20187
|
+
FROM inspection_findings f
|
|
20188
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20189
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20190
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20191
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19874
20192
|
LIMIT :limit`
|
|
19875
20193
|
),
|
|
19876
20194
|
{ limit }
|
|
@@ -19892,25 +20210,34 @@ var SqliteFindingsRepository = class {
|
|
|
19892
20210
|
);
|
|
19893
20211
|
}
|
|
19894
20212
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19895
|
-
* session-stamped
|
|
20213
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19896
20214
|
* page can label its findings link without the grouped pipeline. */
|
|
19897
20215
|
sessionFindingsCount(sessionId) {
|
|
19898
20216
|
if (!sessionId) return Promise.resolve(0);
|
|
19899
20217
|
return Promise.resolve(
|
|
19900
20218
|
countScalar(
|
|
19901
20219
|
this.db,
|
|
19902
|
-
`SELECT count(*) AS n FROM
|
|
19903
|
-
JOIN
|
|
19904
|
-
WHERE
|
|
20220
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20221
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20222
|
+
WHERE e.root_session_id = :sessionId
|
|
20223
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19905
20224
|
{ sessionId }
|
|
19906
20225
|
)
|
|
19907
20226
|
);
|
|
19908
20227
|
}
|
|
19909
|
-
/** Per-rule transcript firing tally for one session —
|
|
19910
|
-
*
|
|
19911
|
-
*
|
|
19912
|
-
*
|
|
19913
|
-
*
|
|
20228
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20229
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20230
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20231
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20232
|
+
* tally with the deduped groups it lists.
|
|
20233
|
+
*
|
|
20234
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20235
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20236
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20237
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20238
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20239
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20240
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19914
20241
|
sessionFirings(sessionId) {
|
|
19915
20242
|
return Object.fromEntries(
|
|
19916
20243
|
countBy(
|
|
@@ -19920,18 +20247,25 @@ var SqliteFindingsRepository = class {
|
|
|
19920
20247
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19921
20248
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19922
20249
|
WHERE e.root_session_id = :sessionId
|
|
20250
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19923
20251
|
GROUP BY d.rule_id`,
|
|
19924
20252
|
{ sessionId }
|
|
19925
20253
|
)
|
|
19926
20254
|
);
|
|
19927
20255
|
}
|
|
19928
20256
|
/**
|
|
19929
|
-
* Grouped findings for the dashboard — joins
|
|
19930
|
-
* toolName from
|
|
19931
|
-
*
|
|
20257
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20258
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20259
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20260
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20261
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20262
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20263
|
+
* severity then recency. Filtering
|
|
19932
20264
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19933
20265
|
* reflect the full filtered set; `items` is the requested
|
|
19934
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20266
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20267
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20268
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19935
20269
|
*
|
|
19936
20270
|
* Two reads, neither of which materializes a row per finding:
|
|
19937
20271
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19944,10 +20278,11 @@ var SqliteFindingsRepository = class {
|
|
|
19944
20278
|
* rule is ever restated in SQL.
|
|
19945
20279
|
*/
|
|
19946
20280
|
listGroupedFindings(query) {
|
|
19947
|
-
const sessionPredicate = query.sessionId ? `
|
|
20281
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20282
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19948
20283
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19949
20284
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19950
|
-
predicate
|
|
20285
|
+
predicate,
|
|
19951
20286
|
params: sessionParams
|
|
19952
20287
|
});
|
|
19953
20288
|
const rows = allRows(
|
|
@@ -19955,24 +20290,26 @@ var SqliteFindingsRepository = class {
|
|
|
19955
20290
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19956
20291
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19957
20292
|
FROM (
|
|
19958
|
-
SELECT f.id AS id,
|
|
19959
|
-
|
|
20293
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20294
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19960
20295
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19961
|
-
e.
|
|
19962
|
-
json_extract(e.
|
|
19963
|
-
json_extract(e.
|
|
19964
|
-
json_extract(e.
|
|
19965
|
-
e.
|
|
20296
|
+
e.started_at AS occurred_at,
|
|
20297
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20298
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20299
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20300
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20301
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19966
20302
|
latest.status AS latest_status,
|
|
19967
20303
|
ROW_NUMBER() OVER (
|
|
19968
|
-
PARTITION BY
|
|
19969
|
-
ORDER BY e.
|
|
20304
|
+
PARTITION BY d.rule_id
|
|
20305
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19970
20306
|
) AS rn
|
|
19971
|
-
FROM
|
|
19972
|
-
JOIN
|
|
20307
|
+
FROM inspection_findings f
|
|
20308
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20309
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19973
20310
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19974
20311
|
ON latest.finding_key = f.finding_key
|
|
19975
|
-
${
|
|
20312
|
+
${predicate}
|
|
19976
20313
|
)
|
|
19977
20314
|
WHERE rn <= :cap
|
|
19978
20315
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19999,17 +20336,29 @@ var SqliteFindingsRepository = class {
|
|
|
19999
20336
|
severity: query.severity,
|
|
20000
20337
|
providers: query.provider,
|
|
20001
20338
|
actions: query.action,
|
|
20339
|
+
statuses: query.status,
|
|
20002
20340
|
subtype: query.subtype,
|
|
20003
20341
|
q: query.q
|
|
20004
20342
|
};
|
|
20005
20343
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
20006
20344
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20345
|
+
const statusFilter = query.status ?? [];
|
|
20007
20346
|
const totals = {
|
|
20008
|
-
findings: sorted.reduce((acc, g) =>
|
|
20347
|
+
findings: sorted.reduce((acc, g) => {
|
|
20348
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20349
|
+
const agg = aggregates.get(g.id);
|
|
20350
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20351
|
+
}, 0),
|
|
20009
20352
|
groups: sorted.length
|
|
20010
20353
|
};
|
|
20011
20354
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
20012
|
-
const
|
|
20355
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20356
|
+
const items = sorted.slice(0, limit).map(
|
|
20357
|
+
(g) => statusSet ? {
|
|
20358
|
+
...g,
|
|
20359
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20360
|
+
} : g
|
|
20361
|
+
);
|
|
20013
20362
|
return Promise.resolve({
|
|
20014
20363
|
totals,
|
|
20015
20364
|
facets,
|
|
@@ -20023,45 +20372,62 @@ var SqliteFindingsRepository = class {
|
|
|
20023
20372
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
20024
20373
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
20025
20374
|
*
|
|
20026
|
-
*
|
|
20027
|
-
*
|
|
20028
|
-
*
|
|
20029
|
-
* status
|
|
20030
|
-
*
|
|
20031
|
-
*
|
|
20375
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20376
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20377
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20378
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20379
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20380
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20381
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20382
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20383
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20384
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20385
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
20032
20386
|
* a group's row stays small however many findings it holds.
|
|
20033
20387
|
*
|
|
20034
20388
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
20035
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
20036
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
20037
|
-
* that is a string proportional to the store (~8MB over
|
|
20038
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
20039
|
-
* match an instance outside the preview, which searching
|
|
20040
|
-
* would silently lose, so it is fetched only when the
|
|
20041
|
-
* carries a `q`.
|
|
20389
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20390
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20391
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20392
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20393
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20394
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20395
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20396
|
+
* path repeating across tuples.)
|
|
20042
20397
|
*/
|
|
20043
20398
|
groupAggregates(withSearchText, scope) {
|
|
20044
|
-
const
|
|
20045
|
-
group_concat(DISTINCT json_extract(e.
|
|
20046
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20399
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20400
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20401
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
20047
20402
|
const rows = this.db.prepare(
|
|
20048
|
-
`SELECT
|
|
20049
|
-
|
|
20050
|
-
max(
|
|
20051
|
-
group_concat(
|
|
20052
|
-
group_concat(
|
|
20053
|
-
group_concat(
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20403
|
+
`SELECT rule_id,
|
|
20404
|
+
sum(tuple_count) AS instance_count,
|
|
20405
|
+
max(latest_at) AS latest_at,
|
|
20406
|
+
group_concat(source_tools) AS source_tools,
|
|
20407
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20408
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20409
|
+
group_concat(repos) AS repos,
|
|
20410
|
+
group_concat(files) AS files,
|
|
20411
|
+
group_concat(tool_names) AS tool_names
|
|
20412
|
+
FROM (
|
|
20413
|
+
SELECT d.rule_id AS rule_id,
|
|
20414
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20415
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20416
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20417
|
+
count(*) AS tuple_count,
|
|
20418
|
+
max(e.started_at) AS latest_at,
|
|
20419
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20420
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20421
|
+
${innerSearchColumns}
|
|
20422
|
+
FROM inspection_findings f
|
|
20423
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20424
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20425
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20426
|
+
ON latest.finding_key = f.finding_key
|
|
20427
|
+
${scope.predicate}
|
|
20428
|
+
GROUP BY d.rule_id, status_tuple
|
|
20429
|
+
)
|
|
20430
|
+
GROUP BY rule_id`
|
|
20065
20431
|
).all(scope.params);
|
|
20066
20432
|
return new Map(
|
|
20067
20433
|
rows.map((r) => [
|
|
@@ -20071,13 +20437,14 @@ var SqliteFindingsRepository = class {
|
|
|
20071
20437
|
sourceTools: splitConcat(r.source_tools),
|
|
20072
20438
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20073
20439
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20074
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20440
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20075
20441
|
return {
|
|
20076
20442
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20077
20443
|
// so the marker stands in for the key itself (never rendered).
|
|
20078
20444
|
kind,
|
|
20079
20445
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20080
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20446
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20447
|
+
count: Number(count)
|
|
20081
20448
|
};
|
|
20082
20449
|
}),
|
|
20083
20450
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20094,10 +20461,21 @@ var SqliteFindingsRepository = class {
|
|
|
20094
20461
|
);
|
|
20095
20462
|
}
|
|
20096
20463
|
healthSummary() {
|
|
20097
|
-
const total = countScalar(
|
|
20464
|
+
const total = countScalar(
|
|
20465
|
+
this.db,
|
|
20466
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20467
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20468
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20469
|
+
);
|
|
20098
20470
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20099
20471
|
const grouped = allRows(
|
|
20100
|
-
this.db.prepare(
|
|
20472
|
+
this.db.prepare(
|
|
20473
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20474
|
+
FROM inspection_findings f
|
|
20475
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20476
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20477
|
+
GROUP BY f.action_taken`
|
|
20478
|
+
)
|
|
20101
20479
|
);
|
|
20102
20480
|
for (const row of grouped) {
|
|
20103
20481
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20105,12 +20483,15 @@ var SqliteFindingsRepository = class {
|
|
|
20105
20483
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20106
20484
|
const sevRows = allRows(
|
|
20107
20485
|
this.db.prepare(
|
|
20108
|
-
`SELECT
|
|
20109
|
-
FROM
|
|
20486
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20487
|
+
FROM inspection_findings f
|
|
20488
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20489
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20110
20490
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20111
20491
|
ON latest.finding_key = f.finding_key
|
|
20112
|
-
WHERE
|
|
20113
|
-
|
|
20492
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20493
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20494
|
+
GROUP BY d.severity`
|
|
20114
20495
|
)
|
|
20115
20496
|
);
|
|
20116
20497
|
for (const row of sevRows) {
|
|
@@ -20131,9 +20512,11 @@ var SqliteFindingsRepository = class {
|
|
|
20131
20512
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20132
20513
|
const rows = allRows(
|
|
20133
20514
|
this.db.prepare(
|
|
20134
|
-
`SELECT date(e.
|
|
20135
|
-
FROM
|
|
20136
|
-
|
|
20515
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20516
|
+
FROM inspection_findings f
|
|
20517
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20518
|
+
WHERE e.started_at >= :since
|
|
20519
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20137
20520
|
GROUP BY day, f.action_taken`
|
|
20138
20521
|
),
|
|
20139
20522
|
{ since }
|
|
@@ -20198,15 +20581,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20198
20581
|
this.insertStmt = db.prepare(
|
|
20199
20582
|
`INSERT INTO inspection_findings
|
|
20200
20583
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20201
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20584
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20585
|
+
finding_key, first_detected_at)
|
|
20202
20586
|
VALUES
|
|
20203
20587
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20204
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20205
|
-
|
|
20588
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20589
|
+
:findingKey,
|
|
20590
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20591
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20592
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20593
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20594
|
+
audit_event_id = excluded.audit_event_id,
|
|
20595
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20596
|
+
classified_data_id = excluded.classified_data_id,
|
|
20597
|
+
span_start = excluded.span_start,
|
|
20598
|
+
span_end = excluded.span_end,
|
|
20599
|
+
masked_match = excluded.masked_match,
|
|
20600
|
+
action_taken = excluded.action_taken,
|
|
20601
|
+
confidence = excluded.confidence`
|
|
20602
|
+
);
|
|
20603
|
+
this.sessionDupStmt = db.prepare(
|
|
20604
|
+
`SELECT 1 FROM inspection_findings f
|
|
20605
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20606
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20607
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20608
|
+
AND e.root_session_id = :sessionId
|
|
20609
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20610
|
+
LIMIT 1`
|
|
20611
|
+
);
|
|
20612
|
+
this.eventDupStmt = db.prepare(
|
|
20613
|
+
`SELECT 1 FROM inspection_findings f
|
|
20614
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20615
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20616
|
+
AND f.masked_match = :maskedMatch
|
|
20617
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20618
|
+
LIMIT 1`
|
|
20206
20619
|
);
|
|
20207
20620
|
}
|
|
20208
20621
|
db;
|
|
20209
20622
|
insertStmt;
|
|
20623
|
+
sessionDupStmt;
|
|
20624
|
+
eventDupStmt;
|
|
20625
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20626
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20627
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20628
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20629
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20630
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20631
|
+
}
|
|
20632
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20633
|
+
// recorded against the given audit event.
|
|
20634
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20635
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20636
|
+
}
|
|
20210
20637
|
insertFinding(input) {
|
|
20211
20638
|
const row = toInspectionFindingRow(input);
|
|
20212
20639
|
this.insertStmt.run(
|
|
@@ -20219,7 +20646,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20219
20646
|
spanEnd: row.spanEnd,
|
|
20220
20647
|
maskedMatch: row.maskedMatch,
|
|
20221
20648
|
actionTaken: row.actionTaken,
|
|
20222
|
-
confidence: row.confidence
|
|
20649
|
+
confidence: row.confidence,
|
|
20650
|
+
findingKey: row.findingKey,
|
|
20651
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20223
20652
|
})
|
|
20224
20653
|
);
|
|
20225
20654
|
}
|
|
@@ -20491,7 +20920,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20491
20920
|
installedRuleset() {
|
|
20492
20921
|
const rows = allRows(
|
|
20493
20922
|
this.db.prepare(
|
|
20494
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20923
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20495
20924
|
)
|
|
20496
20925
|
);
|
|
20497
20926
|
const out = {
|
|
@@ -20499,7 +20928,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20499
20928
|
enabledPacks: 0,
|
|
20500
20929
|
rules: [],
|
|
20501
20930
|
invalidRules: 0,
|
|
20502
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20931
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20932
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20503
20933
|
};
|
|
20504
20934
|
for (const row of rows) {
|
|
20505
20935
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20521,6 +20951,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20521
20951
|
if (parsed.success) {
|
|
20522
20952
|
out.rules.push(parsed.data);
|
|
20523
20953
|
out.ruleActions.set(parsed.data.id, action);
|
|
20954
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20524
20955
|
} else out.invalidRules += 1;
|
|
20525
20956
|
}
|
|
20526
20957
|
}
|
|
@@ -21695,19 +22126,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21695
22126
|
);
|
|
21696
22127
|
this.openAtRestStmt = db.prepare(
|
|
21697
22128
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21698
|
-
FROM
|
|
21699
|
-
JOIN
|
|
21700
|
-
WHERE e.
|
|
21701
|
-
AND json_extract(e.
|
|
22129
|
+
FROM inspection_findings f
|
|
22130
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22131
|
+
WHERE e.event_type = 'code_change'
|
|
22132
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21702
22133
|
AND f.finding_key IS NOT NULL
|
|
21703
22134
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21704
22135
|
);
|
|
21705
22136
|
this.resolvedAtRestStmt = db.prepare(
|
|
21706
22137
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21707
|
-
FROM
|
|
21708
|
-
JOIN
|
|
21709
|
-
WHERE e.
|
|
21710
|
-
AND json_extract(e.
|
|
22138
|
+
FROM inspection_findings f
|
|
22139
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22140
|
+
WHERE e.event_type = 'code_change'
|
|
22141
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21711
22142
|
AND f.finding_key IS NOT NULL
|
|
21712
22143
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21713
22144
|
);
|
|
@@ -21930,25 +22361,27 @@ var SqliteSecurityRepository = class {
|
|
|
21930
22361
|
severitySummary() {
|
|
21931
22362
|
const rows = allRows(
|
|
21932
22363
|
this.db.prepare(
|
|
21933
|
-
`SELECT
|
|
22364
|
+
`SELECT d.severity AS severity,
|
|
21934
22365
|
COUNT(*) AS count,
|
|
21935
22366
|
SUM(CASE
|
|
21936
|
-
WHEN e.
|
|
22367
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21937
22368
|
WHEN f.finding_key IS NULL THEN 0
|
|
21938
22369
|
WHEN latest.status = 'resolved' THEN 1
|
|
21939
22370
|
ELSE 0
|
|
21940
22371
|
END) AS caught,
|
|
21941
22372
|
SUM(CASE
|
|
21942
|
-
WHEN e.
|
|
22373
|
+
WHEN e.event_type = 'code_change'
|
|
21943
22374
|
AND f.finding_key IS NOT NULL
|
|
21944
22375
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21945
22376
|
ELSE 0
|
|
21946
22377
|
END) AS open_at_rest
|
|
21947
|
-
FROM
|
|
21948
|
-
JOIN
|
|
22378
|
+
FROM inspection_findings f
|
|
22379
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22380
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21949
22381
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21950
22382
|
ON latest.finding_key = f.finding_key
|
|
21951
|
-
|
|
22383
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22384
|
+
GROUP BY d.severity`
|
|
21952
22385
|
)
|
|
21953
22386
|
);
|
|
21954
22387
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -22014,7 +22447,7 @@ var SqliteSecurityRepository = class {
|
|
|
22014
22447
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
22015
22448
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
22016
22449
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
22017
|
-
// (
|
|
22450
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
22018
22451
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
22019
22452
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
22020
22453
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -22039,30 +22472,20 @@ var SqliteSecurityRepository = class {
|
|
|
22039
22472
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
22040
22473
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
22041
22474
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
22042
|
-
//
|
|
22043
|
-
// the parent event's
|
|
22044
|
-
// backfill left null.
|
|
22045
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
22046
|
-
|
|
22047
|
-
|
|
22048
|
-
|
|
22049
|
-
|
|
22050
|
-
|
|
22051
|
-
|
|
22052
|
-
|
|
22053
|
-
|
|
22054
|
-
WHERE fr.finding_key = f.finding_key
|
|
22055
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22056
|
-
LIMIT 1
|
|
22057
|
-
) AS latest_method,
|
|
22058
|
-
(
|
|
22059
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22060
|
-
WHERE fr.finding_key = f.finding_key
|
|
22061
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22062
|
-
LIMIT 1
|
|
22063
|
-
) AS latest_resolved_at
|
|
22064
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22475
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22476
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22477
|
+
// legacy/edge row the backfill left null.
|
|
22478
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22479
|
+
latest.status AS latest_status,
|
|
22480
|
+
latest.method AS latest_method,
|
|
22481
|
+
latest.resolved_at AS latest_resolved_at
|
|
22482
|
+
FROM inspection_findings f
|
|
22483
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22484
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22485
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22486
|
+
ON latest.finding_key = f.finding_key
|
|
22065
22487
|
WHERE f.finding_key IS NOT NULL
|
|
22488
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22066
22489
|
AND EXISTS (
|
|
22067
22490
|
SELECT 1 FROM finding_resolution fr
|
|
22068
22491
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22109,11 +22532,13 @@ var SqliteSecurityRepository = class {
|
|
|
22109
22532
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22110
22533
|
const rows = allRows(
|
|
22111
22534
|
this.db.prepare(
|
|
22112
|
-
`SELECT json_extract(e.
|
|
22113
|
-
FROM
|
|
22114
|
-
|
|
22115
|
-
|
|
22116
|
-
AND
|
|
22535
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22536
|
+
FROM inspection_findings f
|
|
22537
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22538
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22539
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22540
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22541
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22117
22542
|
GROUP BY repo
|
|
22118
22543
|
ORDER BY c DESC, repo
|
|
22119
22544
|
LIMIT :limit`
|
|
@@ -22137,44 +22562,28 @@ var SqliteSecurityRepository = class {
|
|
|
22137
22562
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22138
22563
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22139
22564
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22140
|
-
// finding's parent event (
|
|
22141
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22142
|
-
// capped at `limit`.
|
|
22565
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22566
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22567
|
+
// DESC, capped at `limit`.
|
|
22143
22568
|
recentlyResolved(limit = 20) {
|
|
22144
22569
|
const rows = allRows(
|
|
22145
22570
|
this.db.prepare(
|
|
22146
22571
|
`SELECT f.finding_key AS finding_key,
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
json_extract(e.
|
|
22150
|
-
COALESCE(f.first_detected_at, e.
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
22157
|
-
|
|
22158
|
-
WHERE e.kind = 'code_change'
|
|
22572
|
+
d.rule_id AS rule_id,
|
|
22573
|
+
d.severity AS severity,
|
|
22574
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22575
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22576
|
+
latest.resolved_at AS latest_resolved_at
|
|
22577
|
+
FROM inspection_findings f
|
|
22578
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22579
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22580
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22581
|
+
ON latest.finding_key = f.finding_key
|
|
22582
|
+
WHERE e.event_type = 'code_change'
|
|
22159
22583
|
AND f.finding_key IS NOT NULL
|
|
22160
|
-
AND
|
|
22161
|
-
|
|
22162
|
-
|
|
22163
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22164
|
-
LIMIT 1
|
|
22165
|
-
) = 'resolved'
|
|
22166
|
-
AND (
|
|
22167
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22168
|
-
WHERE fr.finding_key = f.finding_key
|
|
22169
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22170
|
-
LIMIT 1
|
|
22171
|
-
) = 'fixed-at-source'
|
|
22172
|
-
AND (
|
|
22173
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22174
|
-
WHERE fr.finding_key = f.finding_key
|
|
22175
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22176
|
-
LIMIT 1
|
|
22177
|
-
) IS NOT NULL
|
|
22584
|
+
AND latest.status = 'resolved'
|
|
22585
|
+
AND latest.method = 'fixed-at-source'
|
|
22586
|
+
AND latest.resolved_at IS NOT NULL
|
|
22178
22587
|
ORDER BY latest_resolved_at DESC
|
|
22179
22588
|
LIMIT :limit`
|
|
22180
22589
|
),
|
|
@@ -22193,15 +22602,18 @@ var SqliteSecurityRepository = class {
|
|
|
22193
22602
|
return Promise.resolve({ items });
|
|
22194
22603
|
}
|
|
22195
22604
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22196
|
-
// epoch-millis timestamp.
|
|
22605
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22197
22606
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22198
22607
|
findingsInRange(fromMs, toMs) {
|
|
22199
22608
|
const rows = allRows(
|
|
22200
22609
|
this.db.prepare(
|
|
22201
|
-
`SELECT e.
|
|
22202
|
-
FROM
|
|
22203
|
-
|
|
22204
|
-
|
|
22610
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22611
|
+
FROM inspection_findings f
|
|
22612
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22613
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22614
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22615
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22616
|
+
ORDER BY e.started_at`
|
|
22205
22617
|
),
|
|
22206
22618
|
{ from: fromMs, to: toMs }
|
|
22207
22619
|
);
|
|
@@ -23025,9 +23437,10 @@ function openWithPragmas(file2) {
|
|
|
23025
23437
|
}
|
|
23026
23438
|
function backupLegacyStore(file2) {
|
|
23027
23439
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
23028
|
-
|
|
23029
|
-
|
|
23030
|
-
|
|
23440
|
+
renameSync2(file2, backup);
|
|
23441
|
+
tightenFile(backup);
|
|
23442
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23443
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
23031
23444
|
}
|
|
23032
23445
|
return backup;
|
|
23033
23446
|
}
|
|
@@ -23043,7 +23456,7 @@ function openLocalDatabase(dir) {
|
|
|
23043
23456
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
23044
23457
|
);
|
|
23045
23458
|
}
|
|
23046
|
-
applyMigrations(db);
|
|
23459
|
+
applyMigrations(db, file2);
|
|
23047
23460
|
tightenPerms(file2);
|
|
23048
23461
|
const events = new SqliteEventsRepository(db);
|
|
23049
23462
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -23070,9 +23483,56 @@ function openLocalDatabase(dir) {
|
|
|
23070
23483
|
policies.seedDefaults();
|
|
23071
23484
|
function recordCapture(event, detected) {
|
|
23072
23485
|
failOpenTransaction(db, () => {
|
|
23073
|
-
events.insertEvent(event);
|
|
23074
23486
|
const sessionId = event.metadata?.sessionId;
|
|
23075
|
-
|
|
23487
|
+
if (sessionId) {
|
|
23488
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23489
|
+
}
|
|
23490
|
+
const auditEventId = captureId(
|
|
23491
|
+
sessionId ?? null,
|
|
23492
|
+
event.contentHash,
|
|
23493
|
+
event.metadata?.filePath ?? null
|
|
23494
|
+
);
|
|
23495
|
+
auditEvents.insertAuditEvent({
|
|
23496
|
+
id: auditEventId,
|
|
23497
|
+
eventType: event.kind,
|
|
23498
|
+
startedAt: event.occurredAt,
|
|
23499
|
+
parentId: sessionId,
|
|
23500
|
+
rootSessionId: sessionId,
|
|
23501
|
+
content: event.content,
|
|
23502
|
+
contentHash: event.contentHash,
|
|
23503
|
+
attributes: toCaptureAttributes(event)
|
|
23504
|
+
});
|
|
23505
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23506
|
+
for (const finding of detected) {
|
|
23507
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23508
|
+
continue;
|
|
23509
|
+
}
|
|
23510
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23511
|
+
auditEventId,
|
|
23512
|
+
finding.ruleId,
|
|
23513
|
+
finding.maskedMatch,
|
|
23514
|
+
finding.span.start,
|
|
23515
|
+
finding.span.end
|
|
23516
|
+
)) {
|
|
23517
|
+
continue;
|
|
23518
|
+
}
|
|
23519
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23520
|
+
let definitionId = definitionIds.get(key);
|
|
23521
|
+
if (!definitionId) {
|
|
23522
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23523
|
+
definitionIds.set(key, definitionId);
|
|
23524
|
+
}
|
|
23525
|
+
inspectionFindings.insertFinding({
|
|
23526
|
+
id: finding.id,
|
|
23527
|
+
auditEventId,
|
|
23528
|
+
inspectionDefinitionId: definitionId,
|
|
23529
|
+
span: finding.span,
|
|
23530
|
+
maskedMatch: finding.maskedMatch,
|
|
23531
|
+
actionTaken: finding.actionTaken,
|
|
23532
|
+
confidence: finding.confidence,
|
|
23533
|
+
findingKey: finding.findingKey ?? void 0
|
|
23534
|
+
});
|
|
23535
|
+
}
|
|
23076
23536
|
});
|
|
23077
23537
|
}
|
|
23078
23538
|
function ensureInventory(ctx) {
|
|
@@ -23220,9 +23680,12 @@ function openLocalDatabase(dir) {
|
|
|
23220
23680
|
};
|
|
23221
23681
|
}
|
|
23222
23682
|
|
|
23683
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23684
|
+
import { createHash as createHash3 } from "crypto";
|
|
23685
|
+
|
|
23223
23686
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23224
23687
|
import { createHmac, randomBytes } from "crypto";
|
|
23225
|
-
import {
|
|
23688
|
+
import { readFileSync } from "fs";
|
|
23226
23689
|
import { join as join2 } from "path";
|
|
23227
23690
|
var KEY_FILENAME = "exception.key";
|
|
23228
23691
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -23259,8 +23722,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
23259
23722
|
}
|
|
23260
23723
|
|
|
23261
23724
|
// ../../packages/persistence/src/local-layout.ts
|
|
23262
|
-
import {
|
|
23263
|
-
import {
|
|
23725
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23726
|
+
import { mkdir } from "fs/promises";
|
|
23264
23727
|
import { homedir } from "os";
|
|
23265
23728
|
import { join as join3 } from "path";
|
|
23266
23729
|
function defaultDataDir() {
|
|
@@ -23275,6 +23738,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23275
23738
|
function dbPath(base = defaultDataDir()) {
|
|
23276
23739
|
return join3(dataDir(base), "aka.db");
|
|
23277
23740
|
}
|
|
23741
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23742
|
+
ensureDataDirSync(dir);
|
|
23743
|
+
}
|
|
23278
23744
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23279
23745
|
const moves = [
|
|
23280
23746
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23282,19 +23748,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23282
23748
|
];
|
|
23283
23749
|
for (const { name, dest } of moves) {
|
|
23284
23750
|
try {
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
}
|
|
23290
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23751
|
+
ensureDataDirSync(dest);
|
|
23752
|
+
const moved = join3(dest, name);
|
|
23753
|
+
renameSync3(join3(base, name), moved);
|
|
23754
|
+
tightenFile(moved);
|
|
23291
23755
|
} catch {
|
|
23292
23756
|
}
|
|
23293
23757
|
}
|
|
23294
23758
|
}
|
|
23295
23759
|
|
|
23296
23760
|
// ../../packages/persistence/src/settings.ts
|
|
23297
|
-
import { readFileSync as readFileSync2
|
|
23761
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23298
23762
|
import { join as join4 } from "path";
|
|
23299
23763
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23300
23764
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23316,7 +23780,7 @@ function readJson(file2) {
|
|
|
23316
23780
|
}
|
|
23317
23781
|
|
|
23318
23782
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23319
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23783
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
23320
23784
|
import { join as join5 } from "path";
|
|
23321
23785
|
var MARKER = "warn-era-capped";
|
|
23322
23786
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -23324,11 +23788,15 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
23324
23788
|
const marker = join5(dataDir2, MARKER);
|
|
23325
23789
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
23326
23790
|
const capped = db.policies.capCategoryActions();
|
|
23327
|
-
|
|
23791
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23328
23792
|
`, { mode: DATA_FILE_MODE });
|
|
23329
23793
|
return { capped };
|
|
23330
23794
|
}
|
|
23331
23795
|
|
|
23796
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23797
|
+
import { existsSync as existsSync3 } from "fs";
|
|
23798
|
+
import { join as join6 } from "path";
|
|
23799
|
+
|
|
23332
23800
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
23333
23801
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
23334
23802
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -23379,6 +23847,12 @@ function resolveProvider() {
|
|
|
23379
23847
|
|
|
23380
23848
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23381
23849
|
function loadConfig(base = defaultDataDir()) {
|
|
23850
|
+
try {
|
|
23851
|
+
ensureLayoutDirSync(base);
|
|
23852
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23853
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23854
|
+
} catch {
|
|
23855
|
+
}
|
|
23382
23856
|
migrateLegacyLayout(base);
|
|
23383
23857
|
const settings = readWorkspaceSettings(base);
|
|
23384
23858
|
return {
|
|
@@ -23401,7 +23875,7 @@ function resolveProviderSafe() {
|
|
|
23401
23875
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23402
23876
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23403
23877
|
import { homedir as homedir2 } from "os";
|
|
23404
|
-
import { basename as basename2, join as
|
|
23878
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23405
23879
|
|
|
23406
23880
|
// ../../packages/detections/src/egress/registry.ts
|
|
23407
23881
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -26137,21 +26611,18 @@ function bundledDetections() {
|
|
|
26137
26611
|
}
|
|
26138
26612
|
|
|
26139
26613
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26140
|
-
import { existsSync as
|
|
26141
|
-
import { basename, dirname, isAbsolute, join as
|
|
26614
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26615
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26142
26616
|
|
|
26143
26617
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26144
|
-
import { createHash as
|
|
26145
|
-
|
|
26146
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
26147
|
-
import { createHash as createHash4 } from "crypto";
|
|
26618
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26148
26619
|
|
|
26149
26620
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26150
26621
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
26151
26622
|
|
|
26152
26623
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26153
|
-
import { mkdirSync as
|
|
26154
|
-
import { join as
|
|
26624
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26625
|
+
import { join as join9 } from "path";
|
|
26155
26626
|
|
|
26156
26627
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
26157
26628
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -26159,8 +26630,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
|
26159
26630
|
|
|
26160
26631
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
26161
26632
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
26162
|
-
import { existsSync as
|
|
26163
|
-
import { basename as basename4, join as
|
|
26633
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26634
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
26164
26635
|
|
|
26165
26636
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
26166
26637
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -26169,8 +26640,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
26169
26640
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26170
26641
|
|
|
26171
26642
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
26172
|
-
import { mkdirSync as
|
|
26173
|
-
import { join as
|
|
26643
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26644
|
+
import { join as join11 } from "path";
|
|
26174
26645
|
|
|
26175
26646
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
26176
26647
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -26201,7 +26672,8 @@ var StandaloneDataGateway = class {
|
|
|
26201
26672
|
}
|
|
26202
26673
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
26203
26674
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
26204
|
-
// hands the natural key across.
|
|
26675
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
26676
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
26205
26677
|
recordLlmCall(input) {
|
|
26206
26678
|
this.db.auditEvents.insertLlmCall(input);
|
|
26207
26679
|
return Promise.resolve();
|
|
@@ -26243,7 +26715,9 @@ var StandaloneDataGateway = class {
|
|
|
26243
26715
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
26244
26716
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
26245
26717
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
26246
|
-
// content-addressed
|
|
26718
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
26719
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
26720
|
+
// than no-opping.
|
|
26247
26721
|
writeToolCall(input) {
|
|
26248
26722
|
this.db.auditEvents.insertToolCall(input);
|
|
26249
26723
|
if (input.inspections.length === 0) return;
|
|
@@ -26263,7 +26737,7 @@ var StandaloneDataGateway = class {
|
|
|
26263
26737
|
});
|
|
26264
26738
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
26265
26739
|
this.db.inspectionFindings.insertFinding({
|
|
26266
|
-
id: inspectionFindingId(auditEventId,
|
|
26740
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
26267
26741
|
auditEventId,
|
|
26268
26742
|
inspectionDefinitionId: definitionId,
|
|
26269
26743
|
classifiedDataId: classifiedDataId2,
|
|
@@ -26312,10 +26786,17 @@ var StandaloneDataGateway = class {
|
|
|
26312
26786
|
try {
|
|
26313
26787
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
26314
26788
|
if (snapshot.installedPacks === 0) return void 0;
|
|
26315
|
-
if (snapshot.enabledPacks === 0)
|
|
26789
|
+
if (snapshot.enabledPacks === 0) {
|
|
26790
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
26791
|
+
}
|
|
26316
26792
|
if (snapshot.invalidRules > 0) return void 0;
|
|
26317
26793
|
if (snapshot.rules.length === 0) return void 0;
|
|
26318
|
-
return {
|
|
26794
|
+
return {
|
|
26795
|
+
rules: snapshot.rules,
|
|
26796
|
+
ruleActions: snapshot.ruleActions,
|
|
26797
|
+
ruleVersions: snapshot.ruleVersions,
|
|
26798
|
+
complete: true
|
|
26799
|
+
};
|
|
26319
26800
|
} catch {
|
|
26320
26801
|
return void 0;
|
|
26321
26802
|
}
|
|
@@ -26343,6 +26824,7 @@ var StandaloneDataGateway = class {
|
|
|
26343
26824
|
policies: [...policies, ...rulePolicies],
|
|
26344
26825
|
rules: installed ? installed.rules : [],
|
|
26345
26826
|
...installed ? { rulesComplete: true } : {},
|
|
26827
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
26346
26828
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
26347
26829
|
customKeywords,
|
|
26348
26830
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|