@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/firstrun.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
|
|
|
@@ -15376,7 +15388,12 @@ var FindingFacets = external_exports.object({
|
|
|
15376
15388
|
severity: external_exports.array(FindingFacetItem),
|
|
15377
15389
|
subtype: external_exports.array(FindingFacetItem),
|
|
15378
15390
|
provider: external_exports.array(FindingFacetItem),
|
|
15379
|
-
action: external_exports.array(FindingFacetItem)
|
|
15391
|
+
action: external_exports.array(FindingFacetItem),
|
|
15392
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15393
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15394
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15395
|
+
// counted under no value.
|
|
15396
|
+
status: external_exports.array(FindingFacetItem)
|
|
15380
15397
|
}).meta({ id: "FindingFacets" });
|
|
15381
15398
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15382
15399
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15386,6 +15403,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15386
15403
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15387
15404
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15388
15405
|
action: external_exports.array(FindingAction).optional(),
|
|
15406
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15407
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15408
|
+
// one of the requested values.
|
|
15409
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15389
15410
|
q: external_exports.string().optional(),
|
|
15390
15411
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15391
15412
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15573,6 +15594,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15573
15594
|
parent_uuid: external_exports.string().optional(),
|
|
15574
15595
|
run_key: external_exports.string().optional()
|
|
15575
15596
|
}).catchall(external_exports.unknown());
|
|
15597
|
+
var CaptureAttributes = external_exports.object({
|
|
15598
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15599
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15600
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15601
|
+
source_tool: external_exports.string().optional(),
|
|
15602
|
+
file_path: external_exports.string().optional(),
|
|
15603
|
+
repo: external_exports.string().optional(),
|
|
15604
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15605
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15606
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15607
|
+
tool_name: external_exports.string().optional(),
|
|
15608
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15609
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15610
|
+
gitignored: external_exports.boolean().optional(),
|
|
15611
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15612
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15613
|
+
whole_file: external_exports.boolean().optional(),
|
|
15614
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15615
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15616
|
+
// trace id when telemetry is enabled.
|
|
15617
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15618
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15619
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15620
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15621
|
+
// authorized the bypass.
|
|
15622
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15623
|
+
}).catchall(external_exports.unknown());
|
|
15576
15624
|
var ToolCallInspection = external_exports.object({
|
|
15577
15625
|
ruleId: external_exports.string().min(1),
|
|
15578
15626
|
ruleName: external_exports.string(),
|
|
@@ -15659,7 +15707,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15659
15707
|
span: Span,
|
|
15660
15708
|
maskedMatch: external_exports.string(),
|
|
15661
15709
|
actionTaken: ActionTaken,
|
|
15662
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15710
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15711
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15712
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15713
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15714
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15715
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15716
|
+
findingKey: external_exports.string().optional(),
|
|
15717
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15718
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15719
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15720
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15721
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15663
15722
|
});
|
|
15664
15723
|
var InventoryContext = external_exports.object({
|
|
15665
15724
|
host: InventoryInput.optional(),
|
|
@@ -15861,6 +15920,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15861
15920
|
|
|
15862
15921
|
// ../../packages/schema/src/zod/event.ts
|
|
15863
15922
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15923
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15864
15924
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15865
15925
|
var EventMetadata = external_exports.object({
|
|
15866
15926
|
sessionId: external_exports.string().optional(),
|
|
@@ -16350,6 +16410,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16350
16410
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16351
16411
|
// `bundle.exceptions ?? []`.
|
|
16352
16412
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16413
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16414
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16415
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16416
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16417
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16418
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16353
16419
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16354
16420
|
fetchedAt: external_exports.iso.datetime()
|
|
16355
16421
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17149,6 +17215,15 @@ function groupActions(g) {
|
|
|
17149
17215
|
actionsCache.set(g, actions);
|
|
17150
17216
|
return actions;
|
|
17151
17217
|
}
|
|
17218
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17219
|
+
const statusSet = new Set(statuses);
|
|
17220
|
+
let sum = 0;
|
|
17221
|
+
for (const input of statusInputs) {
|
|
17222
|
+
if (input.count === void 0) return null;
|
|
17223
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17224
|
+
}
|
|
17225
|
+
return sum;
|
|
17226
|
+
}
|
|
17152
17227
|
function applyFindingFilters(groups, opts) {
|
|
17153
17228
|
let filtered = groups;
|
|
17154
17229
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17167,6 +17242,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17167
17242
|
const subtypeSet = new Set(opts.subtype);
|
|
17168
17243
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17169
17244
|
}
|
|
17245
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17246
|
+
const statusSet = new Set(opts.statuses);
|
|
17247
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17248
|
+
}
|
|
17170
17249
|
if (opts.q) {
|
|
17171
17250
|
const q = opts.q.toLowerCase();
|
|
17172
17251
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17188,6 +17267,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17188
17267
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17189
17268
|
providers: opts.providers,
|
|
17190
17269
|
actions: opts.actions,
|
|
17270
|
+
statuses: opts.statuses,
|
|
17191
17271
|
q: opts.q,
|
|
17192
17272
|
subtype: opts.subtype
|
|
17193
17273
|
});
|
|
@@ -17197,6 +17277,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17197
17277
|
}
|
|
17198
17278
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17199
17279
|
actions: opts.actions,
|
|
17280
|
+
statuses: opts.statuses,
|
|
17200
17281
|
q: opts.q,
|
|
17201
17282
|
subtype: opts.subtype,
|
|
17202
17283
|
severity: opts.severity
|
|
@@ -17207,6 +17288,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17207
17288
|
}
|
|
17208
17289
|
const forAction = applyFindingFilters(allGroups, {
|
|
17209
17290
|
providers: opts.providers,
|
|
17291
|
+
statuses: opts.statuses,
|
|
17210
17292
|
q: opts.q,
|
|
17211
17293
|
subtype: opts.subtype,
|
|
17212
17294
|
severity: opts.severity
|
|
@@ -17218,17 +17300,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17218
17300
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17219
17301
|
providers: opts.providers,
|
|
17220
17302
|
actions: opts.actions,
|
|
17303
|
+
statuses: opts.statuses,
|
|
17221
17304
|
q: opts.q,
|
|
17222
17305
|
severity: opts.severity
|
|
17223
17306
|
});
|
|
17224
17307
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17225
17308
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17309
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17310
|
+
providers: opts.providers,
|
|
17311
|
+
actions: opts.actions,
|
|
17312
|
+
q: opts.q,
|
|
17313
|
+
subtype: opts.subtype,
|
|
17314
|
+
severity: opts.severity
|
|
17315
|
+
});
|
|
17316
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17317
|
+
for (const g of forStatus) {
|
|
17318
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17319
|
+
}
|
|
17226
17320
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17227
17321
|
return {
|
|
17228
17322
|
severity: toItems(severityMap),
|
|
17229
17323
|
provider: toItems(providerMap),
|
|
17230
17324
|
action: toItems(actionMap),
|
|
17231
|
-
subtype: toItems(subtypeMap)
|
|
17325
|
+
subtype: toItems(subtypeMap),
|
|
17326
|
+
status: toItems(statusMap)
|
|
17232
17327
|
};
|
|
17233
17328
|
}
|
|
17234
17329
|
|
|
@@ -17263,10 +17358,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17263
17358
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17264
17359
|
|
|
17265
17360
|
// ../../packages/schema/src/zod/local.ts
|
|
17266
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17361
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17267
17362
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17268
17363
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17269
17364
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17365
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17366
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17367
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17368
|
+
});
|
|
17270
17369
|
var WorkspaceSettings = external_exports.object({
|
|
17271
17370
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17272
17371
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17282,37 +17381,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17282
17381
|
// Shares writes.
|
|
17283
17382
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17284
17383
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17285
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17384
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17385
|
+
// Records that the user consented to sending findings to the model API for
|
|
17386
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17387
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17388
|
+
// covers the current payload and must be re-granted.
|
|
17389
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17286
17390
|
});
|
|
17287
17391
|
function defaultWorkspaceSettings() {
|
|
17288
17392
|
return WorkspaceSettings.parse({});
|
|
17289
17393
|
}
|
|
17290
|
-
function toEventRow(event) {
|
|
17291
|
-
return {
|
|
17292
|
-
id: event.id,
|
|
17293
|
-
sourceTool: event.sourceTool,
|
|
17294
|
-
kind: event.kind,
|
|
17295
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17296
|
-
contentHash: event.contentHash,
|
|
17297
|
-
content: event.content,
|
|
17298
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17299
|
-
};
|
|
17300
|
-
}
|
|
17301
|
-
function toFindingRow(finding) {
|
|
17302
|
-
return {
|
|
17303
|
-
id: finding.id,
|
|
17304
|
-
eventId: finding.eventId,
|
|
17305
|
-
ruleId: finding.ruleId,
|
|
17306
|
-
category: finding.category,
|
|
17307
|
-
severity: finding.severity,
|
|
17308
|
-
spanStart: finding.span.start,
|
|
17309
|
-
spanEnd: finding.span.end,
|
|
17310
|
-
maskedMatch: finding.maskedMatch,
|
|
17311
|
-
actionTaken: finding.actionTaken,
|
|
17312
|
-
confidence: finding.confidence,
|
|
17313
|
-
findingKey: finding.findingKey ?? null
|
|
17314
|
-
};
|
|
17315
|
-
}
|
|
17316
17394
|
function toInventoryRow(input, id, now) {
|
|
17317
17395
|
return {
|
|
17318
17396
|
id,
|
|
@@ -17382,7 +17460,42 @@ function toInspectionFindingRow(input) {
|
|
|
17382
17460
|
spanEnd: input.span.end,
|
|
17383
17461
|
maskedMatch: input.maskedMatch,
|
|
17384
17462
|
actionTaken: input.actionTaken,
|
|
17385
|
-
confidence: input.confidence
|
|
17463
|
+
confidence: input.confidence,
|
|
17464
|
+
findingKey: input.findingKey ?? null,
|
|
17465
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17466
|
+
};
|
|
17467
|
+
}
|
|
17468
|
+
function toCaptureAttributes(event) {
|
|
17469
|
+
const metadata = event.metadata;
|
|
17470
|
+
return {
|
|
17471
|
+
source_tool: event.sourceTool,
|
|
17472
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17473
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17474
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17475
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17476
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17477
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17478
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17479
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17480
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17481
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17482
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17483
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17484
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17485
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17486
|
+
};
|
|
17487
|
+
}
|
|
17488
|
+
function captureDefinitionVersion(finding) {
|
|
17489
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17490
|
+
}
|
|
17491
|
+
function toCaptureDefinitionInput(finding) {
|
|
17492
|
+
return {
|
|
17493
|
+
ruleId: finding.ruleId,
|
|
17494
|
+
version: captureDefinitionVersion(finding),
|
|
17495
|
+
name: finding.ruleId,
|
|
17496
|
+
category: finding.category,
|
|
17497
|
+
severity: finding.severity,
|
|
17498
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17386
17499
|
};
|
|
17387
17500
|
}
|
|
17388
17501
|
|
|
@@ -17810,6 +17923,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17810
17923
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17811
17924
|
}
|
|
17812
17925
|
|
|
17926
|
+
// ../../packages/persistence/src/ids.ts
|
|
17927
|
+
import { createHash } from "crypto";
|
|
17928
|
+
function sha256Hex(input) {
|
|
17929
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17930
|
+
}
|
|
17931
|
+
function inventoryId(objectType, identityKey) {
|
|
17932
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17933
|
+
}
|
|
17934
|
+
function sourceProjectId(url2) {
|
|
17935
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17936
|
+
}
|
|
17937
|
+
function classifiedDataId(cls) {
|
|
17938
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17939
|
+
}
|
|
17940
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17941
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17942
|
+
}
|
|
17943
|
+
function llmCallId(sessionId, messageId) {
|
|
17944
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17945
|
+
}
|
|
17946
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17947
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17948
|
+
}
|
|
17949
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17950
|
+
return sha256Hex(
|
|
17951
|
+
canonicalIdentity([
|
|
17952
|
+
"inspection_finding",
|
|
17953
|
+
auditEventId,
|
|
17954
|
+
ruleId,
|
|
17955
|
+
String(spanStart),
|
|
17956
|
+
String(spanEnd)
|
|
17957
|
+
])
|
|
17958
|
+
);
|
|
17959
|
+
}
|
|
17960
|
+
var NO_SESSION = "no_session";
|
|
17961
|
+
var NO_PATH = "no_path";
|
|
17962
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17963
|
+
return sha256Hex(
|
|
17964
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17965
|
+
);
|
|
17966
|
+
}
|
|
17967
|
+
|
|
17813
17968
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17814
17969
|
function escapeLikePattern(s) {
|
|
17815
17970
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17906,39 +18061,81 @@ function evidenceExists(db, object2) {
|
|
|
17906
18061
|
return schemaObjectExists(db, "table", object2.name);
|
|
17907
18062
|
}
|
|
17908
18063
|
|
|
17909
|
-
// ../../packages/persistence/src/
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
18064
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18065
|
+
function allRows(stmt, params) {
|
|
18066
|
+
if (params === void 0) return stmt.all();
|
|
18067
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18068
|
+
return stmt.all(params);
|
|
17913
18069
|
}
|
|
17914
|
-
function
|
|
17915
|
-
|
|
18070
|
+
function getRow(stmt, params) {
|
|
18071
|
+
if (params === void 0) return stmt.get();
|
|
18072
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18073
|
+
return stmt.get(params);
|
|
17916
18074
|
}
|
|
17917
|
-
function
|
|
17918
|
-
return
|
|
18075
|
+
function intToBool(raw) {
|
|
18076
|
+
return raw === 1 || raw === true;
|
|
17919
18077
|
}
|
|
17920
|
-
function
|
|
17921
|
-
return
|
|
18078
|
+
function boolToInt(b) {
|
|
18079
|
+
return b ? 1 : 0;
|
|
17922
18080
|
}
|
|
17923
|
-
function
|
|
17924
|
-
|
|
18081
|
+
function bindParams(row) {
|
|
18082
|
+
const out = {};
|
|
18083
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18084
|
+
out[key] = value === void 0 ? null : value;
|
|
18085
|
+
}
|
|
18086
|
+
return out;
|
|
17925
18087
|
}
|
|
17926
|
-
function
|
|
17927
|
-
return
|
|
18088
|
+
function countScalar(db, sql, params) {
|
|
18089
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17928
18090
|
}
|
|
17929
|
-
function
|
|
17930
|
-
|
|
18091
|
+
function countBy(db, sql, params) {
|
|
18092
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18093
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18094
|
+
map2.set(row.k, row.n);
|
|
18095
|
+
}
|
|
18096
|
+
return map2;
|
|
17931
18097
|
}
|
|
17932
|
-
function
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
18098
|
+
function mapRowsTolerant(rows, map2) {
|
|
18099
|
+
const out = [];
|
|
18100
|
+
for (const row of rows) {
|
|
18101
|
+
try {
|
|
18102
|
+
out.push(map2(row));
|
|
18103
|
+
} catch {
|
|
18104
|
+
}
|
|
18105
|
+
}
|
|
18106
|
+
return out;
|
|
18107
|
+
}
|
|
18108
|
+
|
|
18109
|
+
// ../../packages/persistence/src/paths.ts
|
|
18110
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18111
|
+
var DATA_DIR_MODE = 448;
|
|
18112
|
+
var DATA_FILE_MODE = 384;
|
|
18113
|
+
var DB_FILENAME = "aka.db";
|
|
18114
|
+
function chmodBestEffort(path, mode) {
|
|
18115
|
+
try {
|
|
18116
|
+
chmodSync(path, mode);
|
|
18117
|
+
} catch {
|
|
18118
|
+
}
|
|
18119
|
+
}
|
|
18120
|
+
function tightenDir(dir) {
|
|
18121
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18122
|
+
}
|
|
18123
|
+
function ensureDataDirSync(dir) {
|
|
18124
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18125
|
+
tightenDir(dir);
|
|
18126
|
+
}
|
|
18127
|
+
function dbSidecars(file2) {
|
|
18128
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18129
|
+
}
|
|
18130
|
+
function tightenFile(file2) {
|
|
18131
|
+
try {
|
|
18132
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18133
|
+
} catch {
|
|
18134
|
+
}
|
|
18135
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18136
|
+
}
|
|
18137
|
+
function tightenPerms(file2) {
|
|
18138
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
17942
18139
|
}
|
|
17943
18140
|
|
|
17944
18141
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17952,7 +18149,8 @@ function createdIndexName(statement) {
|
|
|
17952
18149
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17953
18150
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17954
18151
|
}
|
|
17955
|
-
|
|
18152
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18153
|
+
function applyMigrations(db, file2) {
|
|
17956
18154
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17957
18155
|
db.exec(
|
|
17958
18156
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17966,6 +18164,7 @@ function applyMigrations(db) {
|
|
|
17966
18164
|
);
|
|
17967
18165
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17968
18166
|
if (applied.has(migration.tag)) continue;
|
|
18167
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17969
18168
|
const evidence = evidenceObjects(migration.sql);
|
|
17970
18169
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17971
18170
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18010,7 +18209,6 @@ function applyMigrations(db) {
|
|
|
18010
18209
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18011
18210
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18012
18211
|
}
|
|
18013
|
-
ensureSyncedAtColumn(db, "events");
|
|
18014
18212
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18015
18213
|
ensureScanLedgerTable(db);
|
|
18016
18214
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18018,6 +18216,47 @@ function applyMigrations(db) {
|
|
|
18018
18216
|
ensureWriteGateTrigger(db);
|
|
18019
18217
|
ensureTokenUsageColumns(db);
|
|
18020
18218
|
reconcileSourceProjectIds(db);
|
|
18219
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18220
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18221
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18222
|
+
}
|
|
18223
|
+
}
|
|
18224
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18225
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18226
|
+
if (!migration) return;
|
|
18227
|
+
if (file2) {
|
|
18228
|
+
try {
|
|
18229
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18230
|
+
} catch (error51) {
|
|
18231
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18232
|
+
return;
|
|
18233
|
+
}
|
|
18234
|
+
}
|
|
18235
|
+
try {
|
|
18236
|
+
withTransaction(
|
|
18237
|
+
db,
|
|
18238
|
+
() => {
|
|
18239
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18240
|
+
if (alreadyDropped) return;
|
|
18241
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18242
|
+
db.exec(statement);
|
|
18243
|
+
}
|
|
18244
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18245
|
+
migration.tag,
|
|
18246
|
+
Date.now()
|
|
18247
|
+
);
|
|
18248
|
+
},
|
|
18249
|
+
"IMMEDIATE"
|
|
18250
|
+
);
|
|
18251
|
+
} catch (error51) {
|
|
18252
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18253
|
+
}
|
|
18254
|
+
}
|
|
18255
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18256
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18257
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18258
|
+
tightenFile(backup);
|
|
18259
|
+
return backup;
|
|
18021
18260
|
}
|
|
18022
18261
|
var TOKEN_USAGE_COLUMNS = [
|
|
18023
18262
|
{
|
|
@@ -18046,6 +18285,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18046
18285
|
}
|
|
18047
18286
|
];
|
|
18048
18287
|
function ensureTokenUsageColumns(db) {
|
|
18288
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18049
18289
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18050
18290
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18051
18291
|
if (!existing.has(column.name)) {
|
|
@@ -18111,11 +18351,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18111
18351
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18112
18352
|
}
|
|
18113
18353
|
}
|
|
18354
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18355
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18356
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18357
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18358
|
+
return row?.lastRowid ?? 0;
|
|
18359
|
+
}
|
|
18360
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18361
|
+
db.prepare(
|
|
18362
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18363
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18364
|
+
).run(source, lastRowid);
|
|
18365
|
+
}
|
|
18366
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18367
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18368
|
+
let processed = 0;
|
|
18369
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18370
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18371
|
+
if (rows.length === 0) return true;
|
|
18372
|
+
withTransaction(
|
|
18373
|
+
db,
|
|
18374
|
+
() => {
|
|
18375
|
+
handleRows(rows);
|
|
18376
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18377
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18378
|
+
},
|
|
18379
|
+
"IMMEDIATE"
|
|
18380
|
+
);
|
|
18381
|
+
processed += rows.length;
|
|
18382
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18383
|
+
}
|
|
18384
|
+
return false;
|
|
18385
|
+
}
|
|
18386
|
+
function parseLegacyEventMetadata(raw) {
|
|
18387
|
+
if (raw === null) return void 0;
|
|
18388
|
+
try {
|
|
18389
|
+
return JSON.parse(raw);
|
|
18390
|
+
} catch {
|
|
18391
|
+
return void 0;
|
|
18392
|
+
}
|
|
18393
|
+
}
|
|
18394
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18395
|
+
return JSON.stringify(
|
|
18396
|
+
toCaptureAttributes({
|
|
18397
|
+
id: row.id,
|
|
18398
|
+
sourceTool: row.sourceTool,
|
|
18399
|
+
kind: row.kind,
|
|
18400
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18401
|
+
contentHash: row.contentHash,
|
|
18402
|
+
content: row.content,
|
|
18403
|
+
metadata: row.metadata
|
|
18404
|
+
})
|
|
18405
|
+
);
|
|
18406
|
+
}
|
|
18407
|
+
function copyLegacyEvents(db) {
|
|
18408
|
+
const selectStmt = db.prepare(
|
|
18409
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18410
|
+
content_hash AS contentHash, content, metadata
|
|
18411
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18412
|
+
);
|
|
18413
|
+
const insertStmt = db.prepare(
|
|
18414
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18415
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18416
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18417
|
+
);
|
|
18418
|
+
const stubRootStmt = db.prepare(
|
|
18419
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18420
|
+
);
|
|
18421
|
+
return drainLegacyTable(
|
|
18422
|
+
db,
|
|
18423
|
+
"events",
|
|
18424
|
+
selectStmt,
|
|
18425
|
+
(rows) => {
|
|
18426
|
+
for (const row of rows) {
|
|
18427
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18428
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18429
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18430
|
+
insertStmt.run(
|
|
18431
|
+
bindParams({
|
|
18432
|
+
id: row.id,
|
|
18433
|
+
parentId: sessionId,
|
|
18434
|
+
rootSessionId: sessionId,
|
|
18435
|
+
eventType: row.kind,
|
|
18436
|
+
startedAt: row.occurredAt,
|
|
18437
|
+
content: row.content,
|
|
18438
|
+
contentHash: row.contentHash,
|
|
18439
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18440
|
+
})
|
|
18441
|
+
);
|
|
18442
|
+
}
|
|
18443
|
+
}
|
|
18444
|
+
);
|
|
18445
|
+
}
|
|
18446
|
+
function copyLegacyFindings(db) {
|
|
18447
|
+
const selectStmt = db.prepare(
|
|
18448
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18449
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18450
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18451
|
+
first_detected_at AS firstDetectedAt
|
|
18452
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18453
|
+
);
|
|
18454
|
+
const definitionStmt = db.prepare(
|
|
18455
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18456
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18457
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18458
|
+
);
|
|
18459
|
+
const findingStmt = db.prepare(
|
|
18460
|
+
`INSERT INTO inspection_findings
|
|
18461
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18462
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18463
|
+
finding_key, first_detected_at)
|
|
18464
|
+
VALUES
|
|
18465
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18466
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18467
|
+
:findingKey, :firstDetectedAt)
|
|
18468
|
+
ON CONFLICT(id) DO NOTHING
|
|
18469
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18470
|
+
first_detected_at = CASE
|
|
18471
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18472
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18473
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18474
|
+
END`
|
|
18475
|
+
);
|
|
18476
|
+
return drainLegacyTable(
|
|
18477
|
+
db,
|
|
18478
|
+
"findings",
|
|
18479
|
+
selectStmt,
|
|
18480
|
+
(rows) => {
|
|
18481
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18482
|
+
for (const row of rows) {
|
|
18483
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18484
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18485
|
+
if (definitionId === void 0) {
|
|
18486
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18487
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18488
|
+
definitionStmt.run(
|
|
18489
|
+
bindParams({
|
|
18490
|
+
id: definitionId,
|
|
18491
|
+
ruleId: row.ruleId,
|
|
18492
|
+
name: row.ruleId,
|
|
18493
|
+
category: row.category,
|
|
18494
|
+
severity: row.severity,
|
|
18495
|
+
definition: "",
|
|
18496
|
+
version: version2
|
|
18497
|
+
})
|
|
18498
|
+
);
|
|
18499
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18500
|
+
}
|
|
18501
|
+
findingStmt.run(
|
|
18502
|
+
bindParams({
|
|
18503
|
+
id: row.id,
|
|
18504
|
+
auditEventId: row.eventId,
|
|
18505
|
+
inspectionDefinitionId: definitionId,
|
|
18506
|
+
spanStart: row.spanStart,
|
|
18507
|
+
spanEnd: row.spanEnd,
|
|
18508
|
+
maskedMatch: row.maskedMatch,
|
|
18509
|
+
actionTaken: row.actionTaken,
|
|
18510
|
+
confidence: row.confidence,
|
|
18511
|
+
findingKey: row.findingKey,
|
|
18512
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18513
|
+
})
|
|
18514
|
+
);
|
|
18515
|
+
}
|
|
18516
|
+
}
|
|
18517
|
+
);
|
|
18518
|
+
}
|
|
18519
|
+
function runLegacyHistoryBackfill(db) {
|
|
18520
|
+
try {
|
|
18521
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18522
|
+
if (!eventsCaughtUp) return false;
|
|
18523
|
+
return copyLegacyFindings(db);
|
|
18524
|
+
} catch (error51) {
|
|
18525
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18526
|
+
return false;
|
|
18527
|
+
}
|
|
18528
|
+
}
|
|
18114
18529
|
function isForeignSqliteLineage(db) {
|
|
18115
18530
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18116
18531
|
return columnNames(db, "events").includes("tenant_id");
|
|
18117
18532
|
}
|
|
18118
18533
|
function ensureSyncedAtColumn(db, table2) {
|
|
18534
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18119
18535
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18120
18536
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18121
18537
|
}
|
|
@@ -18136,6 +18552,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18136
18552
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18137
18553
|
)`);
|
|
18138
18554
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18555
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18139
18556
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18140
18557
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18141
18558
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18163,30 +18580,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18163
18580
|
)`);
|
|
18164
18581
|
}
|
|
18165
18582
|
|
|
18166
|
-
// ../../packages/persistence/src/paths.ts
|
|
18167
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18168
|
-
var DATA_DIR_MODE = 448;
|
|
18169
|
-
var DATA_FILE_MODE = 384;
|
|
18170
|
-
var DB_FILENAME = "aka.db";
|
|
18171
|
-
function ensureDataDirSync(dir) {
|
|
18172
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18173
|
-
try {
|
|
18174
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18175
|
-
} catch {
|
|
18176
|
-
}
|
|
18177
|
-
}
|
|
18178
|
-
function walSidecars(file2) {
|
|
18179
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18180
|
-
}
|
|
18181
|
-
function tightenPerms(file2) {
|
|
18182
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18183
|
-
try {
|
|
18184
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18185
|
-
} catch {
|
|
18186
|
-
}
|
|
18187
|
-
}
|
|
18188
|
-
}
|
|
18189
|
-
|
|
18190
18583
|
// ../../packages/persistence/src/internal/json.ts
|
|
18191
18584
|
function safeJson(s, fallback) {
|
|
18192
18585
|
if (s == null) return fallback;
|
|
@@ -18206,51 +18599,6 @@ function parseJsonObject(s) {
|
|
|
18206
18599
|
return void 0;
|
|
18207
18600
|
}
|
|
18208
18601
|
|
|
18209
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18210
|
-
function allRows(stmt, params) {
|
|
18211
|
-
if (params === void 0) return stmt.all();
|
|
18212
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18213
|
-
return stmt.all(params);
|
|
18214
|
-
}
|
|
18215
|
-
function getRow(stmt, params) {
|
|
18216
|
-
if (params === void 0) return stmt.get();
|
|
18217
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18218
|
-
return stmt.get(params);
|
|
18219
|
-
}
|
|
18220
|
-
function intToBool(raw) {
|
|
18221
|
-
return raw === 1 || raw === true;
|
|
18222
|
-
}
|
|
18223
|
-
function boolToInt(b) {
|
|
18224
|
-
return b ? 1 : 0;
|
|
18225
|
-
}
|
|
18226
|
-
function bindParams(row) {
|
|
18227
|
-
const out = {};
|
|
18228
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18229
|
-
out[key] = value === void 0 ? null : value;
|
|
18230
|
-
}
|
|
18231
|
-
return out;
|
|
18232
|
-
}
|
|
18233
|
-
function countScalar(db, sql, params) {
|
|
18234
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18235
|
-
}
|
|
18236
|
-
function countBy(db, sql, params) {
|
|
18237
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18238
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18239
|
-
map2.set(row.k, row.n);
|
|
18240
|
-
}
|
|
18241
|
-
return map2;
|
|
18242
|
-
}
|
|
18243
|
-
function mapRowsTolerant(rows, map2) {
|
|
18244
|
-
const out = [];
|
|
18245
|
-
for (const row of rows) {
|
|
18246
|
-
try {
|
|
18247
|
-
out.push(map2(row));
|
|
18248
|
-
} catch {
|
|
18249
|
-
}
|
|
18250
|
-
}
|
|
18251
|
-
return out;
|
|
18252
|
-
}
|
|
18253
|
-
|
|
18254
18602
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18255
18603
|
var DAY_MS = 864e5;
|
|
18256
18604
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18867,6 +19215,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18867
19215
|
})
|
|
18868
19216
|
);
|
|
18869
19217
|
}
|
|
19218
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19219
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19220
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19221
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19222
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19223
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19224
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19225
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19226
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19227
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19228
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19229
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19230
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19231
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19232
|
+
}
|
|
18870
19233
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18871
19234
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18872
19235
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19328,8 +19691,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19328
19691
|
)
|
|
19329
19692
|
);
|
|
19330
19693
|
}
|
|
19331
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19332
|
-
//
|
|
19694
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19695
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19696
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19697
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19698
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19699
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19700
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19701
|
+
// to the old findings⋈events one.
|
|
19333
19702
|
countFindingsLast30d(ruleIds) {
|
|
19334
19703
|
if (ruleIds.length === 0) return 0;
|
|
19335
19704
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19337,8 +19706,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19337
19706
|
return countScalar(
|
|
19338
19707
|
this.db,
|
|
19339
19708
|
`SELECT count(*) AS n
|
|
19340
|
-
FROM
|
|
19341
|
-
|
|
19709
|
+
FROM inspection_findings f
|
|
19710
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19711
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19712
|
+
WHERE e.started_at >= ?
|
|
19713
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19714
|
+
AND d.rule_id IN (${inClause})`,
|
|
19342
19715
|
[since, ...ruleIds]
|
|
19343
19716
|
);
|
|
19344
19717
|
}
|
|
@@ -19348,35 +19721,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19348
19721
|
var SqliteEventsRepository = class {
|
|
19349
19722
|
constructor(db) {
|
|
19350
19723
|
this.db = db;
|
|
19351
|
-
this.insertStmt = db.prepare(
|
|
19352
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19353
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19354
|
-
);
|
|
19355
19724
|
}
|
|
19356
19725
|
db;
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
this.insertStmt.run(
|
|
19361
|
-
bindParams({
|
|
19362
|
-
id: row.id,
|
|
19363
|
-
sourceTool: row.sourceTool,
|
|
19364
|
-
kind: row.kind,
|
|
19365
|
-
occurredAt: row.occurredAt,
|
|
19366
|
-
contentHash: row.contentHash,
|
|
19367
|
-
content: row.content,
|
|
19368
|
-
metadata: row.metadata
|
|
19369
|
-
})
|
|
19370
|
-
);
|
|
19371
|
-
}
|
|
19372
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19373
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19374
|
-
// never duplicates findings.
|
|
19726
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19727
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19728
|
+
// scan never duplicates findings.
|
|
19375
19729
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19376
19730
|
// async EventsReadPort contract.
|
|
19731
|
+
//
|
|
19732
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19733
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19734
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19735
|
+
// index-friendly rather than walking rows that can never match.
|
|
19377
19736
|
contentHashes() {
|
|
19378
19737
|
const rows = allRows(
|
|
19379
|
-
this.db.prepare(
|
|
19738
|
+
this.db.prepare(
|
|
19739
|
+
`SELECT content_hash FROM audit_events
|
|
19740
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19741
|
+
)
|
|
19380
19742
|
);
|
|
19381
19743
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19382
19744
|
}
|
|
@@ -19712,17 +20074,20 @@ function parseExceptionRow(row) {
|
|
|
19712
20074
|
}
|
|
19713
20075
|
|
|
19714
20076
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19715
|
-
function
|
|
20077
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19716
20078
|
return `(
|
|
19717
|
-
SELECT fr
|
|
20079
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19718
20080
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19719
20081
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19720
20082
|
LIMIT 1
|
|
19721
20083
|
)`;
|
|
19722
20084
|
}
|
|
20085
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20086
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20087
|
+
}
|
|
19723
20088
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19724
|
-
SELECT finding_key, status FROM (
|
|
19725
|
-
SELECT fr.finding_key, fr.status,
|
|
20089
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20090
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19726
20091
|
ROW_NUMBER() OVER (
|
|
19727
20092
|
PARTITION BY fr.finding_key
|
|
19728
20093
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19749,68 +20114,21 @@ var DAY_MS3 = 864e5;
|
|
|
19749
20114
|
var SqliteFindingsRepository = class {
|
|
19750
20115
|
constructor(db) {
|
|
19751
20116
|
this.db = db;
|
|
19752
|
-
this.insertStmt = db.prepare(
|
|
19753
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19754
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19755
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19756
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19757
|
-
event_id = excluded.event_id,
|
|
19758
|
-
category = excluded.category,
|
|
19759
|
-
severity = excluded.severity,
|
|
19760
|
-
span_start = excluded.span_start,
|
|
19761
|
-
span_end = excluded.span_end,
|
|
19762
|
-
masked_match = excluded.masked_match,
|
|
19763
|
-
action_taken = excluded.action_taken,
|
|
19764
|
-
confidence = excluded.confidence`
|
|
19765
|
-
);
|
|
19766
|
-
this.sessionDupStmt = db.prepare(
|
|
19767
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19768
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19769
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19770
|
-
LIMIT 1`
|
|
19771
|
-
);
|
|
19772
20117
|
}
|
|
19773
20118
|
db;
|
|
19774
|
-
insertStmt;
|
|
19775
|
-
sessionDupStmt;
|
|
19776
|
-
insertFindings(findings, scope = {}) {
|
|
19777
|
-
for (const finding of findings) {
|
|
19778
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19779
|
-
const row = toFindingRow(finding);
|
|
19780
|
-
this.insertStmt.run({
|
|
19781
|
-
id: row.id,
|
|
19782
|
-
eventId: row.eventId,
|
|
19783
|
-
ruleId: row.ruleId,
|
|
19784
|
-
category: row.category,
|
|
19785
|
-
severity: row.severity,
|
|
19786
|
-
spanStart: row.spanStart,
|
|
19787
|
-
spanEnd: row.spanEnd,
|
|
19788
|
-
maskedMatch: row.maskedMatch,
|
|
19789
|
-
actionTaken: row.actionTaken,
|
|
19790
|
-
confidence: row.confidence,
|
|
19791
|
-
findingKey: row.findingKey ?? null
|
|
19792
|
-
});
|
|
19793
|
-
}
|
|
19794
|
-
}
|
|
19795
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19796
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19797
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19798
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19799
|
-
const hit = this.sessionDupStmt.get({
|
|
19800
|
-
ruleId: finding.ruleId,
|
|
19801
|
-
maskedMatch: finding.maskedMatch,
|
|
19802
|
-
sessionId
|
|
19803
|
-
});
|
|
19804
|
-
return hit !== void 0;
|
|
19805
|
-
}
|
|
19806
20119
|
recentFindings(opts) {
|
|
19807
20120
|
const limit = opts?.limit ?? 50;
|
|
19808
20121
|
const rows = allRows(
|
|
19809
20122
|
this.db.prepare(
|
|
19810
|
-
`SELECT f.id, f.event_id,
|
|
19811
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19812
|
-
|
|
19813
|
-
|
|
20123
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20124
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20125
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20126
|
+
e.event_type AS kind
|
|
20127
|
+
FROM inspection_findings f
|
|
20128
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20129
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20130
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20131
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19814
20132
|
LIMIT :limit`
|
|
19815
20133
|
),
|
|
19816
20134
|
{ limit }
|
|
@@ -19832,25 +20150,34 @@ var SqliteFindingsRepository = class {
|
|
|
19832
20150
|
);
|
|
19833
20151
|
}
|
|
19834
20152
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19835
|
-
* session-stamped
|
|
20153
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19836
20154
|
* page can label its findings link without the grouped pipeline. */
|
|
19837
20155
|
sessionFindingsCount(sessionId) {
|
|
19838
20156
|
if (!sessionId) return Promise.resolve(0);
|
|
19839
20157
|
return Promise.resolve(
|
|
19840
20158
|
countScalar(
|
|
19841
20159
|
this.db,
|
|
19842
|
-
`SELECT count(*) AS n FROM
|
|
19843
|
-
JOIN
|
|
19844
|
-
WHERE
|
|
20160
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20161
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20162
|
+
WHERE e.root_session_id = :sessionId
|
|
20163
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19845
20164
|
{ sessionId }
|
|
19846
20165
|
)
|
|
19847
20166
|
);
|
|
19848
20167
|
}
|
|
19849
|
-
/** Per-rule transcript firing tally for one session —
|
|
19850
|
-
*
|
|
19851
|
-
*
|
|
19852
|
-
*
|
|
19853
|
-
*
|
|
20168
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20169
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20170
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20171
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20172
|
+
* tally with the deduped groups it lists.
|
|
20173
|
+
*
|
|
20174
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20175
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20176
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20177
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20178
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20179
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20180
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19854
20181
|
sessionFirings(sessionId) {
|
|
19855
20182
|
return Object.fromEntries(
|
|
19856
20183
|
countBy(
|
|
@@ -19860,18 +20187,25 @@ var SqliteFindingsRepository = class {
|
|
|
19860
20187
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19861
20188
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19862
20189
|
WHERE e.root_session_id = :sessionId
|
|
20190
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19863
20191
|
GROUP BY d.rule_id`,
|
|
19864
20192
|
{ sessionId }
|
|
19865
20193
|
)
|
|
19866
20194
|
);
|
|
19867
20195
|
}
|
|
19868
20196
|
/**
|
|
19869
|
-
* Grouped findings for the dashboard — joins
|
|
19870
|
-
* toolName from
|
|
19871
|
-
*
|
|
20197
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20198
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20199
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20200
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20201
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20202
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20203
|
+
* severity then recency. Filtering
|
|
19872
20204
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19873
20205
|
* reflect the full filtered set; `items` is the requested
|
|
19874
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20206
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20207
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20208
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19875
20209
|
*
|
|
19876
20210
|
* Two reads, neither of which materializes a row per finding:
|
|
19877
20211
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19884,10 +20218,11 @@ var SqliteFindingsRepository = class {
|
|
|
19884
20218
|
* rule is ever restated in SQL.
|
|
19885
20219
|
*/
|
|
19886
20220
|
listGroupedFindings(query) {
|
|
19887
|
-
const sessionPredicate = query.sessionId ? `
|
|
20221
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20222
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19888
20223
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19889
20224
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19890
|
-
predicate
|
|
20225
|
+
predicate,
|
|
19891
20226
|
params: sessionParams
|
|
19892
20227
|
});
|
|
19893
20228
|
const rows = allRows(
|
|
@@ -19895,24 +20230,26 @@ var SqliteFindingsRepository = class {
|
|
|
19895
20230
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19896
20231
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19897
20232
|
FROM (
|
|
19898
|
-
SELECT f.id AS id,
|
|
19899
|
-
|
|
20233
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20234
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19900
20235
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19901
|
-
e.
|
|
19902
|
-
json_extract(e.
|
|
19903
|
-
json_extract(e.
|
|
19904
|
-
json_extract(e.
|
|
19905
|
-
e.
|
|
20236
|
+
e.started_at AS occurred_at,
|
|
20237
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20238
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20239
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20240
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20241
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19906
20242
|
latest.status AS latest_status,
|
|
19907
20243
|
ROW_NUMBER() OVER (
|
|
19908
|
-
PARTITION BY
|
|
19909
|
-
ORDER BY e.
|
|
20244
|
+
PARTITION BY d.rule_id
|
|
20245
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19910
20246
|
) AS rn
|
|
19911
|
-
FROM
|
|
19912
|
-
JOIN
|
|
20247
|
+
FROM inspection_findings f
|
|
20248
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20249
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19913
20250
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19914
20251
|
ON latest.finding_key = f.finding_key
|
|
19915
|
-
${
|
|
20252
|
+
${predicate}
|
|
19916
20253
|
)
|
|
19917
20254
|
WHERE rn <= :cap
|
|
19918
20255
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19939,17 +20276,29 @@ var SqliteFindingsRepository = class {
|
|
|
19939
20276
|
severity: query.severity,
|
|
19940
20277
|
providers: query.provider,
|
|
19941
20278
|
actions: query.action,
|
|
20279
|
+
statuses: query.status,
|
|
19942
20280
|
subtype: query.subtype,
|
|
19943
20281
|
q: query.q
|
|
19944
20282
|
};
|
|
19945
20283
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19946
20284
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20285
|
+
const statusFilter = query.status ?? [];
|
|
19947
20286
|
const totals = {
|
|
19948
|
-
findings: sorted.reduce((acc, g) =>
|
|
20287
|
+
findings: sorted.reduce((acc, g) => {
|
|
20288
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20289
|
+
const agg = aggregates.get(g.id);
|
|
20290
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20291
|
+
}, 0),
|
|
19949
20292
|
groups: sorted.length
|
|
19950
20293
|
};
|
|
19951
20294
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19952
|
-
const
|
|
20295
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20296
|
+
const items = sorted.slice(0, limit).map(
|
|
20297
|
+
(g) => statusSet ? {
|
|
20298
|
+
...g,
|
|
20299
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20300
|
+
} : g
|
|
20301
|
+
);
|
|
19953
20302
|
return Promise.resolve({
|
|
19954
20303
|
totals,
|
|
19955
20304
|
facets,
|
|
@@ -19963,45 +20312,62 @@ var SqliteFindingsRepository = class {
|
|
|
19963
20312
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19964
20313
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19965
20314
|
*
|
|
19966
|
-
*
|
|
19967
|
-
*
|
|
19968
|
-
*
|
|
19969
|
-
* status
|
|
19970
|
-
*
|
|
19971
|
-
*
|
|
20315
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20316
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20317
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20318
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20319
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20320
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20321
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20322
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20323
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20324
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20325
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19972
20326
|
* a group's row stays small however many findings it holds.
|
|
19973
20327
|
*
|
|
19974
20328
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19975
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19976
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19977
|
-
* that is a string proportional to the store (~8MB over
|
|
19978
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19979
|
-
* match an instance outside the preview, which searching
|
|
19980
|
-
* would silently lose, so it is fetched only when the
|
|
19981
|
-
* carries a `q`.
|
|
20329
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20330
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20331
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20332
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20333
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20334
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20335
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20336
|
+
* path repeating across tuples.)
|
|
19982
20337
|
*/
|
|
19983
20338
|
groupAggregates(withSearchText, scope) {
|
|
19984
|
-
const
|
|
19985
|
-
group_concat(DISTINCT json_extract(e.
|
|
19986
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20339
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20340
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20341
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19987
20342
|
const rows = this.db.prepare(
|
|
19988
|
-
`SELECT
|
|
19989
|
-
|
|
19990
|
-
max(
|
|
19991
|
-
group_concat(
|
|
19992
|
-
group_concat(
|
|
19993
|
-
group_concat(
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20343
|
+
`SELECT rule_id,
|
|
20344
|
+
sum(tuple_count) AS instance_count,
|
|
20345
|
+
max(latest_at) AS latest_at,
|
|
20346
|
+
group_concat(source_tools) AS source_tools,
|
|
20347
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20348
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20349
|
+
group_concat(repos) AS repos,
|
|
20350
|
+
group_concat(files) AS files,
|
|
20351
|
+
group_concat(tool_names) AS tool_names
|
|
20352
|
+
FROM (
|
|
20353
|
+
SELECT d.rule_id AS rule_id,
|
|
20354
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20355
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20356
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20357
|
+
count(*) AS tuple_count,
|
|
20358
|
+
max(e.started_at) AS latest_at,
|
|
20359
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20360
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20361
|
+
${innerSearchColumns}
|
|
20362
|
+
FROM inspection_findings f
|
|
20363
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20364
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20365
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20366
|
+
ON latest.finding_key = f.finding_key
|
|
20367
|
+
${scope.predicate}
|
|
20368
|
+
GROUP BY d.rule_id, status_tuple
|
|
20369
|
+
)
|
|
20370
|
+
GROUP BY rule_id`
|
|
20005
20371
|
).all(scope.params);
|
|
20006
20372
|
return new Map(
|
|
20007
20373
|
rows.map((r) => [
|
|
@@ -20011,13 +20377,14 @@ var SqliteFindingsRepository = class {
|
|
|
20011
20377
|
sourceTools: splitConcat(r.source_tools),
|
|
20012
20378
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20013
20379
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20014
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20380
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20015
20381
|
return {
|
|
20016
20382
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20017
20383
|
// so the marker stands in for the key itself (never rendered).
|
|
20018
20384
|
kind,
|
|
20019
20385
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20020
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20386
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20387
|
+
count: Number(count)
|
|
20021
20388
|
};
|
|
20022
20389
|
}),
|
|
20023
20390
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20034,10 +20401,21 @@ var SqliteFindingsRepository = class {
|
|
|
20034
20401
|
);
|
|
20035
20402
|
}
|
|
20036
20403
|
healthSummary() {
|
|
20037
|
-
const total = countScalar(
|
|
20404
|
+
const total = countScalar(
|
|
20405
|
+
this.db,
|
|
20406
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20407
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20408
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20409
|
+
);
|
|
20038
20410
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20039
20411
|
const grouped = allRows(
|
|
20040
|
-
this.db.prepare(
|
|
20412
|
+
this.db.prepare(
|
|
20413
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20414
|
+
FROM inspection_findings f
|
|
20415
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20416
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20417
|
+
GROUP BY f.action_taken`
|
|
20418
|
+
)
|
|
20041
20419
|
);
|
|
20042
20420
|
for (const row of grouped) {
|
|
20043
20421
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20045,12 +20423,15 @@ var SqliteFindingsRepository = class {
|
|
|
20045
20423
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20046
20424
|
const sevRows = allRows(
|
|
20047
20425
|
this.db.prepare(
|
|
20048
|
-
`SELECT
|
|
20049
|
-
FROM
|
|
20426
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20427
|
+
FROM inspection_findings f
|
|
20428
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20429
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20050
20430
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20051
20431
|
ON latest.finding_key = f.finding_key
|
|
20052
|
-
WHERE
|
|
20053
|
-
|
|
20432
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20433
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20434
|
+
GROUP BY d.severity`
|
|
20054
20435
|
)
|
|
20055
20436
|
);
|
|
20056
20437
|
for (const row of sevRows) {
|
|
@@ -20071,9 +20452,11 @@ var SqliteFindingsRepository = class {
|
|
|
20071
20452
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20072
20453
|
const rows = allRows(
|
|
20073
20454
|
this.db.prepare(
|
|
20074
|
-
`SELECT date(e.
|
|
20075
|
-
FROM
|
|
20076
|
-
|
|
20455
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20456
|
+
FROM inspection_findings f
|
|
20457
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20458
|
+
WHERE e.started_at >= :since
|
|
20459
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20077
20460
|
GROUP BY day, f.action_taken`
|
|
20078
20461
|
),
|
|
20079
20462
|
{ since }
|
|
@@ -20138,15 +20521,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20138
20521
|
this.insertStmt = db.prepare(
|
|
20139
20522
|
`INSERT INTO inspection_findings
|
|
20140
20523
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20141
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20524
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20525
|
+
finding_key, first_detected_at)
|
|
20142
20526
|
VALUES
|
|
20143
20527
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20144
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20145
|
-
|
|
20528
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20529
|
+
:findingKey,
|
|
20530
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20531
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20532
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20533
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20534
|
+
audit_event_id = excluded.audit_event_id,
|
|
20535
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20536
|
+
classified_data_id = excluded.classified_data_id,
|
|
20537
|
+
span_start = excluded.span_start,
|
|
20538
|
+
span_end = excluded.span_end,
|
|
20539
|
+
masked_match = excluded.masked_match,
|
|
20540
|
+
action_taken = excluded.action_taken,
|
|
20541
|
+
confidence = excluded.confidence`
|
|
20542
|
+
);
|
|
20543
|
+
this.sessionDupStmt = db.prepare(
|
|
20544
|
+
`SELECT 1 FROM inspection_findings f
|
|
20545
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20546
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20547
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20548
|
+
AND e.root_session_id = :sessionId
|
|
20549
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20550
|
+
LIMIT 1`
|
|
20551
|
+
);
|
|
20552
|
+
this.eventDupStmt = db.prepare(
|
|
20553
|
+
`SELECT 1 FROM inspection_findings f
|
|
20554
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20555
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20556
|
+
AND f.masked_match = :maskedMatch
|
|
20557
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20558
|
+
LIMIT 1`
|
|
20146
20559
|
);
|
|
20147
20560
|
}
|
|
20148
20561
|
db;
|
|
20149
20562
|
insertStmt;
|
|
20563
|
+
sessionDupStmt;
|
|
20564
|
+
eventDupStmt;
|
|
20565
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20566
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20567
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20568
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20569
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20570
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20571
|
+
}
|
|
20572
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20573
|
+
// recorded against the given audit event.
|
|
20574
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20575
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20576
|
+
}
|
|
20150
20577
|
insertFinding(input) {
|
|
20151
20578
|
const row = toInspectionFindingRow(input);
|
|
20152
20579
|
this.insertStmt.run(
|
|
@@ -20159,7 +20586,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20159
20586
|
spanEnd: row.spanEnd,
|
|
20160
20587
|
maskedMatch: row.maskedMatch,
|
|
20161
20588
|
actionTaken: row.actionTaken,
|
|
20162
|
-
confidence: row.confidence
|
|
20589
|
+
confidence: row.confidence,
|
|
20590
|
+
findingKey: row.findingKey,
|
|
20591
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20163
20592
|
})
|
|
20164
20593
|
);
|
|
20165
20594
|
}
|
|
@@ -20431,7 +20860,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20431
20860
|
installedRuleset() {
|
|
20432
20861
|
const rows = allRows(
|
|
20433
20862
|
this.db.prepare(
|
|
20434
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20863
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20435
20864
|
)
|
|
20436
20865
|
);
|
|
20437
20866
|
const out = {
|
|
@@ -20439,7 +20868,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20439
20868
|
enabledPacks: 0,
|
|
20440
20869
|
rules: [],
|
|
20441
20870
|
invalidRules: 0,
|
|
20442
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20871
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20872
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20443
20873
|
};
|
|
20444
20874
|
for (const row of rows) {
|
|
20445
20875
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20461,6 +20891,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20461
20891
|
if (parsed.success) {
|
|
20462
20892
|
out.rules.push(parsed.data);
|
|
20463
20893
|
out.ruleActions.set(parsed.data.id, action);
|
|
20894
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20464
20895
|
} else out.invalidRules += 1;
|
|
20465
20896
|
}
|
|
20466
20897
|
}
|
|
@@ -21635,19 +22066,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21635
22066
|
);
|
|
21636
22067
|
this.openAtRestStmt = db.prepare(
|
|
21637
22068
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21638
|
-
FROM
|
|
21639
|
-
JOIN
|
|
21640
|
-
WHERE e.
|
|
21641
|
-
AND json_extract(e.
|
|
22069
|
+
FROM inspection_findings f
|
|
22070
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22071
|
+
WHERE e.event_type = 'code_change'
|
|
22072
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21642
22073
|
AND f.finding_key IS NOT NULL
|
|
21643
22074
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21644
22075
|
);
|
|
21645
22076
|
this.resolvedAtRestStmt = db.prepare(
|
|
21646
22077
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21647
|
-
FROM
|
|
21648
|
-
JOIN
|
|
21649
|
-
WHERE e.
|
|
21650
|
-
AND json_extract(e.
|
|
22078
|
+
FROM inspection_findings f
|
|
22079
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22080
|
+
WHERE e.event_type = 'code_change'
|
|
22081
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21651
22082
|
AND f.finding_key IS NOT NULL
|
|
21652
22083
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21653
22084
|
);
|
|
@@ -21870,25 +22301,27 @@ var SqliteSecurityRepository = class {
|
|
|
21870
22301
|
severitySummary() {
|
|
21871
22302
|
const rows = allRows(
|
|
21872
22303
|
this.db.prepare(
|
|
21873
|
-
`SELECT
|
|
22304
|
+
`SELECT d.severity AS severity,
|
|
21874
22305
|
COUNT(*) AS count,
|
|
21875
22306
|
SUM(CASE
|
|
21876
|
-
WHEN e.
|
|
22307
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21877
22308
|
WHEN f.finding_key IS NULL THEN 0
|
|
21878
22309
|
WHEN latest.status = 'resolved' THEN 1
|
|
21879
22310
|
ELSE 0
|
|
21880
22311
|
END) AS caught,
|
|
21881
22312
|
SUM(CASE
|
|
21882
|
-
WHEN e.
|
|
22313
|
+
WHEN e.event_type = 'code_change'
|
|
21883
22314
|
AND f.finding_key IS NOT NULL
|
|
21884
22315
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21885
22316
|
ELSE 0
|
|
21886
22317
|
END) AS open_at_rest
|
|
21887
|
-
FROM
|
|
21888
|
-
JOIN
|
|
22318
|
+
FROM inspection_findings f
|
|
22319
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22320
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21889
22321
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21890
22322
|
ON latest.finding_key = f.finding_key
|
|
21891
|
-
|
|
22323
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22324
|
+
GROUP BY d.severity`
|
|
21892
22325
|
)
|
|
21893
22326
|
);
|
|
21894
22327
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21954,7 +22387,7 @@ var SqliteSecurityRepository = class {
|
|
|
21954
22387
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21955
22388
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21956
22389
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21957
|
-
// (
|
|
22390
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21958
22391
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21959
22392
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21960
22393
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21979,30 +22412,20 @@ var SqliteSecurityRepository = class {
|
|
|
21979
22412
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21980
22413
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21981
22414
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21982
|
-
//
|
|
21983
|
-
// the parent event's
|
|
21984
|
-
// backfill left null.
|
|
21985
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
WHERE fr.finding_key = f.finding_key
|
|
21995
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21996
|
-
LIMIT 1
|
|
21997
|
-
) AS latest_method,
|
|
21998
|
-
(
|
|
21999
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22000
|
-
WHERE fr.finding_key = f.finding_key
|
|
22001
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22002
|
-
LIMIT 1
|
|
22003
|
-
) AS latest_resolved_at
|
|
22004
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22415
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22416
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22417
|
+
// legacy/edge row the backfill left null.
|
|
22418
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22419
|
+
latest.status AS latest_status,
|
|
22420
|
+
latest.method AS latest_method,
|
|
22421
|
+
latest.resolved_at AS latest_resolved_at
|
|
22422
|
+
FROM inspection_findings f
|
|
22423
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22424
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22425
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22426
|
+
ON latest.finding_key = f.finding_key
|
|
22005
22427
|
WHERE f.finding_key IS NOT NULL
|
|
22428
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22006
22429
|
AND EXISTS (
|
|
22007
22430
|
SELECT 1 FROM finding_resolution fr
|
|
22008
22431
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22049,11 +22472,13 @@ var SqliteSecurityRepository = class {
|
|
|
22049
22472
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22050
22473
|
const rows = allRows(
|
|
22051
22474
|
this.db.prepare(
|
|
22052
|
-
`SELECT json_extract(e.
|
|
22053
|
-
FROM
|
|
22054
|
-
|
|
22055
|
-
|
|
22056
|
-
AND
|
|
22475
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22476
|
+
FROM inspection_findings f
|
|
22477
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22478
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22479
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22480
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22481
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22057
22482
|
GROUP BY repo
|
|
22058
22483
|
ORDER BY c DESC, repo
|
|
22059
22484
|
LIMIT :limit`
|
|
@@ -22077,44 +22502,28 @@ var SqliteSecurityRepository = class {
|
|
|
22077
22502
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22078
22503
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22079
22504
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22080
|
-
// finding's parent event (
|
|
22081
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22082
|
-
// capped at `limit`.
|
|
22505
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22506
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22507
|
+
// DESC, capped at `limit`.
|
|
22083
22508
|
recentlyResolved(limit = 20) {
|
|
22084
22509
|
const rows = allRows(
|
|
22085
22510
|
this.db.prepare(
|
|
22086
22511
|
`SELECT f.finding_key AS finding_key,
|
|
22087
|
-
|
|
22088
|
-
|
|
22089
|
-
json_extract(e.
|
|
22090
|
-
COALESCE(f.first_detected_at, e.
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22098
|
-
WHERE e.kind = 'code_change'
|
|
22512
|
+
d.rule_id AS rule_id,
|
|
22513
|
+
d.severity AS severity,
|
|
22514
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22515
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22516
|
+
latest.resolved_at AS latest_resolved_at
|
|
22517
|
+
FROM inspection_findings f
|
|
22518
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22519
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22520
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22521
|
+
ON latest.finding_key = f.finding_key
|
|
22522
|
+
WHERE e.event_type = 'code_change'
|
|
22099
22523
|
AND f.finding_key IS NOT NULL
|
|
22100
|
-
AND
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22104
|
-
LIMIT 1
|
|
22105
|
-
) = 'resolved'
|
|
22106
|
-
AND (
|
|
22107
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22108
|
-
WHERE fr.finding_key = f.finding_key
|
|
22109
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22110
|
-
LIMIT 1
|
|
22111
|
-
) = 'fixed-at-source'
|
|
22112
|
-
AND (
|
|
22113
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22114
|
-
WHERE fr.finding_key = f.finding_key
|
|
22115
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22116
|
-
LIMIT 1
|
|
22117
|
-
) IS NOT NULL
|
|
22524
|
+
AND latest.status = 'resolved'
|
|
22525
|
+
AND latest.method = 'fixed-at-source'
|
|
22526
|
+
AND latest.resolved_at IS NOT NULL
|
|
22118
22527
|
ORDER BY latest_resolved_at DESC
|
|
22119
22528
|
LIMIT :limit`
|
|
22120
22529
|
),
|
|
@@ -22133,15 +22542,18 @@ var SqliteSecurityRepository = class {
|
|
|
22133
22542
|
return Promise.resolve({ items });
|
|
22134
22543
|
}
|
|
22135
22544
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22136
|
-
// epoch-millis timestamp.
|
|
22545
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22137
22546
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22138
22547
|
findingsInRange(fromMs, toMs) {
|
|
22139
22548
|
const rows = allRows(
|
|
22140
22549
|
this.db.prepare(
|
|
22141
|
-
`SELECT e.
|
|
22142
|
-
FROM
|
|
22143
|
-
|
|
22144
|
-
|
|
22550
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22551
|
+
FROM inspection_findings f
|
|
22552
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22553
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22554
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22555
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22556
|
+
ORDER BY e.started_at`
|
|
22145
22557
|
),
|
|
22146
22558
|
{ from: fromMs, to: toMs }
|
|
22147
22559
|
);
|
|
@@ -22965,9 +23377,10 @@ function openWithPragmas(file2) {
|
|
|
22965
23377
|
}
|
|
22966
23378
|
function backupLegacyStore(file2) {
|
|
22967
23379
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
23380
|
+
renameSync2(file2, backup);
|
|
23381
|
+
tightenFile(backup);
|
|
23382
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23383
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22971
23384
|
}
|
|
22972
23385
|
return backup;
|
|
22973
23386
|
}
|
|
@@ -22983,7 +23396,7 @@ function openLocalDatabase(dir) {
|
|
|
22983
23396
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22984
23397
|
);
|
|
22985
23398
|
}
|
|
22986
|
-
applyMigrations(db);
|
|
23399
|
+
applyMigrations(db, file2);
|
|
22987
23400
|
tightenPerms(file2);
|
|
22988
23401
|
const events = new SqliteEventsRepository(db);
|
|
22989
23402
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -23010,9 +23423,56 @@ function openLocalDatabase(dir) {
|
|
|
23010
23423
|
policies.seedDefaults();
|
|
23011
23424
|
function recordCapture(event, detected) {
|
|
23012
23425
|
failOpenTransaction(db, () => {
|
|
23013
|
-
events.insertEvent(event);
|
|
23014
23426
|
const sessionId = event.metadata?.sessionId;
|
|
23015
|
-
|
|
23427
|
+
if (sessionId) {
|
|
23428
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23429
|
+
}
|
|
23430
|
+
const auditEventId = captureId(
|
|
23431
|
+
sessionId ?? null,
|
|
23432
|
+
event.contentHash,
|
|
23433
|
+
event.metadata?.filePath ?? null
|
|
23434
|
+
);
|
|
23435
|
+
auditEvents.insertAuditEvent({
|
|
23436
|
+
id: auditEventId,
|
|
23437
|
+
eventType: event.kind,
|
|
23438
|
+
startedAt: event.occurredAt,
|
|
23439
|
+
parentId: sessionId,
|
|
23440
|
+
rootSessionId: sessionId,
|
|
23441
|
+
content: event.content,
|
|
23442
|
+
contentHash: event.contentHash,
|
|
23443
|
+
attributes: toCaptureAttributes(event)
|
|
23444
|
+
});
|
|
23445
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23446
|
+
for (const finding of detected) {
|
|
23447
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23448
|
+
continue;
|
|
23449
|
+
}
|
|
23450
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23451
|
+
auditEventId,
|
|
23452
|
+
finding.ruleId,
|
|
23453
|
+
finding.maskedMatch,
|
|
23454
|
+
finding.span.start,
|
|
23455
|
+
finding.span.end
|
|
23456
|
+
)) {
|
|
23457
|
+
continue;
|
|
23458
|
+
}
|
|
23459
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23460
|
+
let definitionId = definitionIds.get(key);
|
|
23461
|
+
if (!definitionId) {
|
|
23462
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23463
|
+
definitionIds.set(key, definitionId);
|
|
23464
|
+
}
|
|
23465
|
+
inspectionFindings.insertFinding({
|
|
23466
|
+
id: finding.id,
|
|
23467
|
+
auditEventId,
|
|
23468
|
+
inspectionDefinitionId: definitionId,
|
|
23469
|
+
span: finding.span,
|
|
23470
|
+
maskedMatch: finding.maskedMatch,
|
|
23471
|
+
actionTaken: finding.actionTaken,
|
|
23472
|
+
confidence: finding.confidence,
|
|
23473
|
+
findingKey: finding.findingKey ?? void 0
|
|
23474
|
+
});
|
|
23475
|
+
}
|
|
23016
23476
|
});
|
|
23017
23477
|
}
|
|
23018
23478
|
function ensureInventory(ctx) {
|
|
@@ -23160,9 +23620,12 @@ function openLocalDatabase(dir) {
|
|
|
23160
23620
|
};
|
|
23161
23621
|
}
|
|
23162
23622
|
|
|
23623
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23624
|
+
import { createHash as createHash3 } from "crypto";
|
|
23625
|
+
|
|
23163
23626
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23164
23627
|
import { createHmac, randomBytes } from "crypto";
|
|
23165
|
-
import {
|
|
23628
|
+
import { readFileSync } from "fs";
|
|
23166
23629
|
import { join as join2 } from "path";
|
|
23167
23630
|
var KEY_FILENAME = "exception.key";
|
|
23168
23631
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -23199,8 +23662,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
23199
23662
|
}
|
|
23200
23663
|
|
|
23201
23664
|
// ../../packages/persistence/src/local-layout.ts
|
|
23202
|
-
import {
|
|
23203
|
-
import {
|
|
23665
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23666
|
+
import { mkdir } from "fs/promises";
|
|
23204
23667
|
import { homedir } from "os";
|
|
23205
23668
|
import { join as join3 } from "path";
|
|
23206
23669
|
function defaultDataDir() {
|
|
@@ -23215,6 +23678,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23215
23678
|
function dbPath(base = defaultDataDir()) {
|
|
23216
23679
|
return join3(dataDir(base), "aka.db");
|
|
23217
23680
|
}
|
|
23681
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23682
|
+
ensureDataDirSync(dir);
|
|
23683
|
+
}
|
|
23218
23684
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23219
23685
|
const moves = [
|
|
23220
23686
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23222,19 +23688,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23222
23688
|
];
|
|
23223
23689
|
for (const { name, dest } of moves) {
|
|
23224
23690
|
try {
|
|
23225
|
-
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
}
|
|
23230
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23691
|
+
ensureDataDirSync(dest);
|
|
23692
|
+
const moved = join3(dest, name);
|
|
23693
|
+
renameSync3(join3(base, name), moved);
|
|
23694
|
+
tightenFile(moved);
|
|
23231
23695
|
} catch {
|
|
23232
23696
|
}
|
|
23233
23697
|
}
|
|
23234
23698
|
}
|
|
23235
23699
|
|
|
23236
23700
|
// ../../packages/persistence/src/settings.ts
|
|
23237
|
-
import { readFileSync as readFileSync2
|
|
23701
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23238
23702
|
import { join as join4 } from "path";
|
|
23239
23703
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23240
23704
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23256,7 +23720,7 @@ function readJson(file2) {
|
|
|
23256
23720
|
}
|
|
23257
23721
|
|
|
23258
23722
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23259
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23723
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
23260
23724
|
import { join as join5 } from "path";
|
|
23261
23725
|
var MARKER = "warn-era-capped";
|
|
23262
23726
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -23264,11 +23728,15 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
23264
23728
|
const marker = join5(dataDir2, MARKER);
|
|
23265
23729
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
23266
23730
|
const capped = db.policies.capCategoryActions();
|
|
23267
|
-
|
|
23731
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23268
23732
|
`, { mode: DATA_FILE_MODE });
|
|
23269
23733
|
return { capped };
|
|
23270
23734
|
}
|
|
23271
23735
|
|
|
23736
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23737
|
+
import { existsSync as existsSync3 } from "fs";
|
|
23738
|
+
import { join as join6 } from "path";
|
|
23739
|
+
|
|
23272
23740
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
23273
23741
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
23274
23742
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -23319,6 +23787,12 @@ function resolveProvider() {
|
|
|
23319
23787
|
|
|
23320
23788
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23321
23789
|
function loadConfig(base = defaultDataDir()) {
|
|
23790
|
+
try {
|
|
23791
|
+
ensureLayoutDirSync(base);
|
|
23792
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23793
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23794
|
+
} catch {
|
|
23795
|
+
}
|
|
23322
23796
|
migrateLegacyLayout(base);
|
|
23323
23797
|
const settings = readWorkspaceSettings(base);
|
|
23324
23798
|
return {
|
|
@@ -23341,7 +23815,7 @@ function resolveProviderSafe() {
|
|
|
23341
23815
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23342
23816
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23343
23817
|
import { homedir as homedir2 } from "os";
|
|
23344
|
-
import { basename as basename2, join as
|
|
23818
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23345
23819
|
|
|
23346
23820
|
// ../../packages/detections/src/egress/registry.ts
|
|
23347
23821
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -26077,21 +26551,18 @@ function bundledDetections() {
|
|
|
26077
26551
|
}
|
|
26078
26552
|
|
|
26079
26553
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26080
|
-
import { existsSync as
|
|
26081
|
-
import { basename, dirname, isAbsolute, join as
|
|
26554
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26555
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26082
26556
|
|
|
26083
26557
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26084
|
-
import { createHash as
|
|
26085
|
-
|
|
26086
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
26087
|
-
import { createHash as createHash4 } from "crypto";
|
|
26558
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26088
26559
|
|
|
26089
26560
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26090
26561
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
26091
26562
|
|
|
26092
26563
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26093
|
-
import { mkdirSync as
|
|
26094
|
-
import { join as
|
|
26564
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26565
|
+
import { join as join9 } from "path";
|
|
26095
26566
|
|
|
26096
26567
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
26097
26568
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -26099,8 +26570,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
|
26099
26570
|
|
|
26100
26571
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
26101
26572
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
26102
|
-
import { existsSync as
|
|
26103
|
-
import { basename as basename4, join as
|
|
26573
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26574
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
26104
26575
|
|
|
26105
26576
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
26106
26577
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -26109,8 +26580,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
26109
26580
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26110
26581
|
|
|
26111
26582
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
26112
|
-
import { mkdirSync as
|
|
26113
|
-
import { join as
|
|
26583
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26584
|
+
import { join as join11 } from "path";
|
|
26114
26585
|
|
|
26115
26586
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
26116
26587
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -26141,7 +26612,8 @@ var StandaloneDataGateway = class {
|
|
|
26141
26612
|
}
|
|
26142
26613
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
26143
26614
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
26144
|
-
// hands the natural key across.
|
|
26615
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
26616
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
26145
26617
|
recordLlmCall(input) {
|
|
26146
26618
|
this.db.auditEvents.insertLlmCall(input);
|
|
26147
26619
|
return Promise.resolve();
|
|
@@ -26183,7 +26655,9 @@ var StandaloneDataGateway = class {
|
|
|
26183
26655
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
26184
26656
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
26185
26657
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
26186
|
-
// content-addressed
|
|
26658
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
26659
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
26660
|
+
// than no-opping.
|
|
26187
26661
|
writeToolCall(input) {
|
|
26188
26662
|
this.db.auditEvents.insertToolCall(input);
|
|
26189
26663
|
if (input.inspections.length === 0) return;
|
|
@@ -26203,7 +26677,7 @@ var StandaloneDataGateway = class {
|
|
|
26203
26677
|
});
|
|
26204
26678
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
26205
26679
|
this.db.inspectionFindings.insertFinding({
|
|
26206
|
-
id: inspectionFindingId(auditEventId,
|
|
26680
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
26207
26681
|
auditEventId,
|
|
26208
26682
|
inspectionDefinitionId: definitionId,
|
|
26209
26683
|
classifiedDataId: classifiedDataId2,
|
|
@@ -26252,10 +26726,17 @@ var StandaloneDataGateway = class {
|
|
|
26252
26726
|
try {
|
|
26253
26727
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
26254
26728
|
if (snapshot.installedPacks === 0) return void 0;
|
|
26255
|
-
if (snapshot.enabledPacks === 0)
|
|
26729
|
+
if (snapshot.enabledPacks === 0) {
|
|
26730
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
26731
|
+
}
|
|
26256
26732
|
if (snapshot.invalidRules > 0) return void 0;
|
|
26257
26733
|
if (snapshot.rules.length === 0) return void 0;
|
|
26258
|
-
return {
|
|
26734
|
+
return {
|
|
26735
|
+
rules: snapshot.rules,
|
|
26736
|
+
ruleActions: snapshot.ruleActions,
|
|
26737
|
+
ruleVersions: snapshot.ruleVersions,
|
|
26738
|
+
complete: true
|
|
26739
|
+
};
|
|
26259
26740
|
} catch {
|
|
26260
26741
|
return void 0;
|
|
26261
26742
|
}
|
|
@@ -26283,6 +26764,7 @@ var StandaloneDataGateway = class {
|
|
|
26283
26764
|
policies: [...policies, ...rulePolicies],
|
|
26284
26765
|
rules: installed ? installed.rules : [],
|
|
26285
26766
|
...installed ? { rulesComplete: true } : {},
|
|
26767
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
26286
26768
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
26287
26769
|
customKeywords,
|
|
26288
26770
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|