@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/commands/setup.md +101 -19
- package/package.json +3 -3
- package/scripts/apply-suppressions.js +3251 -497
- package/scripts/backfill.js +1081 -493
- package/scripts/filescan.js +1069 -487
- package/scripts/firstrun.js +981 -450
- package/scripts/intro.js +92 -24
- package/scripts/onboard.js +1006 -453
- package/scripts/post-tool-use.js +1062 -480
- package/scripts/pre-tool-use.js +1067 -485
- package/scripts/query.js +981 -450
- package/scripts/reconcile.js +1010 -473
- package/scripts/remediate.js +1072 -490
- package/scripts/session-start.js +1043 -512
- package/scripts/start-light.js +92 -24
- package/scripts/statusline.js +981 -450
- package/scripts/stop.js +133 -38
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +1070 -488
package/scripts/onboard.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" });
|
|
@@ -17154,6 +17220,15 @@ function groupActions(g) {
|
|
|
17154
17220
|
actionsCache.set(g, actions);
|
|
17155
17221
|
return actions;
|
|
17156
17222
|
}
|
|
17223
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17224
|
+
const statusSet = new Set(statuses);
|
|
17225
|
+
let sum = 0;
|
|
17226
|
+
for (const input of statusInputs) {
|
|
17227
|
+
if (input.count === void 0) return null;
|
|
17228
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17229
|
+
}
|
|
17230
|
+
return sum;
|
|
17231
|
+
}
|
|
17157
17232
|
function applyFindingFilters(groups, opts) {
|
|
17158
17233
|
let filtered = groups;
|
|
17159
17234
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17172,6 +17247,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17172
17247
|
const subtypeSet = new Set(opts.subtype);
|
|
17173
17248
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17174
17249
|
}
|
|
17250
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17251
|
+
const statusSet = new Set(opts.statuses);
|
|
17252
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17253
|
+
}
|
|
17175
17254
|
if (opts.q) {
|
|
17176
17255
|
const q = opts.q.toLowerCase();
|
|
17177
17256
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17193,6 +17272,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17193
17272
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17194
17273
|
providers: opts.providers,
|
|
17195
17274
|
actions: opts.actions,
|
|
17275
|
+
statuses: opts.statuses,
|
|
17196
17276
|
q: opts.q,
|
|
17197
17277
|
subtype: opts.subtype
|
|
17198
17278
|
});
|
|
@@ -17202,6 +17282,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17202
17282
|
}
|
|
17203
17283
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17204
17284
|
actions: opts.actions,
|
|
17285
|
+
statuses: opts.statuses,
|
|
17205
17286
|
q: opts.q,
|
|
17206
17287
|
subtype: opts.subtype,
|
|
17207
17288
|
severity: opts.severity
|
|
@@ -17212,6 +17293,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17212
17293
|
}
|
|
17213
17294
|
const forAction = applyFindingFilters(allGroups, {
|
|
17214
17295
|
providers: opts.providers,
|
|
17296
|
+
statuses: opts.statuses,
|
|
17215
17297
|
q: opts.q,
|
|
17216
17298
|
subtype: opts.subtype,
|
|
17217
17299
|
severity: opts.severity
|
|
@@ -17223,17 +17305,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17223
17305
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17224
17306
|
providers: opts.providers,
|
|
17225
17307
|
actions: opts.actions,
|
|
17308
|
+
statuses: opts.statuses,
|
|
17226
17309
|
q: opts.q,
|
|
17227
17310
|
severity: opts.severity
|
|
17228
17311
|
});
|
|
17229
17312
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17230
17313
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17314
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17315
|
+
providers: opts.providers,
|
|
17316
|
+
actions: opts.actions,
|
|
17317
|
+
q: opts.q,
|
|
17318
|
+
subtype: opts.subtype,
|
|
17319
|
+
severity: opts.severity
|
|
17320
|
+
});
|
|
17321
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17322
|
+
for (const g of forStatus) {
|
|
17323
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17324
|
+
}
|
|
17231
17325
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17232
17326
|
return {
|
|
17233
17327
|
severity: toItems(severityMap),
|
|
17234
17328
|
provider: toItems(providerMap),
|
|
17235
17329
|
action: toItems(actionMap),
|
|
17236
|
-
subtype: toItems(subtypeMap)
|
|
17330
|
+
subtype: toItems(subtypeMap),
|
|
17331
|
+
status: toItems(statusMap)
|
|
17237
17332
|
};
|
|
17238
17333
|
}
|
|
17239
17334
|
|
|
@@ -17268,10 +17363,15 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17268
17363
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17269
17364
|
|
|
17270
17365
|
// ../../packages/schema/src/zod/local.ts
|
|
17271
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17366
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17367
|
+
var MODEL_JUDGE_PAYLOAD_VERSION = 1;
|
|
17272
17368
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17273
17369
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17274
17370
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17371
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17372
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17373
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17374
|
+
});
|
|
17275
17375
|
var WorkspaceSettings = external_exports.object({
|
|
17276
17376
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17277
17377
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17287,37 +17387,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17287
17387
|
// Shares writes.
|
|
17288
17388
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17289
17389
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17290
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17390
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17391
|
+
// Records that the user consented to sending findings to the model API for
|
|
17392
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17393
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17394
|
+
// covers the current payload and must be re-granted.
|
|
17395
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17291
17396
|
});
|
|
17292
17397
|
function defaultWorkspaceSettings() {
|
|
17293
17398
|
return WorkspaceSettings.parse({});
|
|
17294
17399
|
}
|
|
17295
|
-
function toEventRow(event) {
|
|
17296
|
-
return {
|
|
17297
|
-
id: event.id,
|
|
17298
|
-
sourceTool: event.sourceTool,
|
|
17299
|
-
kind: event.kind,
|
|
17300
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17301
|
-
contentHash: event.contentHash,
|
|
17302
|
-
content: event.content,
|
|
17303
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17304
|
-
};
|
|
17305
|
-
}
|
|
17306
|
-
function toFindingRow(finding) {
|
|
17307
|
-
return {
|
|
17308
|
-
id: finding.id,
|
|
17309
|
-
eventId: finding.eventId,
|
|
17310
|
-
ruleId: finding.ruleId,
|
|
17311
|
-
category: finding.category,
|
|
17312
|
-
severity: finding.severity,
|
|
17313
|
-
spanStart: finding.span.start,
|
|
17314
|
-
spanEnd: finding.span.end,
|
|
17315
|
-
maskedMatch: finding.maskedMatch,
|
|
17316
|
-
actionTaken: finding.actionTaken,
|
|
17317
|
-
confidence: finding.confidence,
|
|
17318
|
-
findingKey: finding.findingKey ?? null
|
|
17319
|
-
};
|
|
17320
|
-
}
|
|
17321
17400
|
function toInventoryRow(input, id, now) {
|
|
17322
17401
|
return {
|
|
17323
17402
|
id,
|
|
@@ -17387,7 +17466,42 @@ function toInspectionFindingRow(input) {
|
|
|
17387
17466
|
spanEnd: input.span.end,
|
|
17388
17467
|
maskedMatch: input.maskedMatch,
|
|
17389
17468
|
actionTaken: input.actionTaken,
|
|
17390
|
-
confidence: input.confidence
|
|
17469
|
+
confidence: input.confidence,
|
|
17470
|
+
findingKey: input.findingKey ?? null,
|
|
17471
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17472
|
+
};
|
|
17473
|
+
}
|
|
17474
|
+
function toCaptureAttributes(event) {
|
|
17475
|
+
const metadata = event.metadata;
|
|
17476
|
+
return {
|
|
17477
|
+
source_tool: event.sourceTool,
|
|
17478
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17479
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17480
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17481
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17482
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17483
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17484
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17485
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17486
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17487
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17488
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17489
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17490
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17491
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17492
|
+
};
|
|
17493
|
+
}
|
|
17494
|
+
function captureDefinitionVersion(finding) {
|
|
17495
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17496
|
+
}
|
|
17497
|
+
function toCaptureDefinitionInput(finding) {
|
|
17498
|
+
return {
|
|
17499
|
+
ruleId: finding.ruleId,
|
|
17500
|
+
version: captureDefinitionVersion(finding),
|
|
17501
|
+
name: finding.ruleId,
|
|
17502
|
+
category: finding.category,
|
|
17503
|
+
severity: finding.severity,
|
|
17504
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17391
17505
|
};
|
|
17392
17506
|
}
|
|
17393
17507
|
|
|
@@ -17815,6 +17929,37 @@ function reviewSeverityRank(reasons) {
|
|
|
17815
17929
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17816
17930
|
}
|
|
17817
17931
|
|
|
17932
|
+
// ../../packages/persistence/src/ids.ts
|
|
17933
|
+
import { createHash } from "crypto";
|
|
17934
|
+
function sha256Hex(input) {
|
|
17935
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17936
|
+
}
|
|
17937
|
+
function inventoryId(objectType, identityKey) {
|
|
17938
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17939
|
+
}
|
|
17940
|
+
function sourceProjectId(url2) {
|
|
17941
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17942
|
+
}
|
|
17943
|
+
function classifiedDataId(cls) {
|
|
17944
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17945
|
+
}
|
|
17946
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17947
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17948
|
+
}
|
|
17949
|
+
function llmCallId(sessionId, messageId) {
|
|
17950
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17951
|
+
}
|
|
17952
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17953
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17954
|
+
}
|
|
17955
|
+
var NO_SESSION = "no_session";
|
|
17956
|
+
var NO_PATH = "no_path";
|
|
17957
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17958
|
+
return sha256Hex(
|
|
17959
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17960
|
+
);
|
|
17961
|
+
}
|
|
17962
|
+
|
|
17818
17963
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17819
17964
|
function escapeLikePattern(s) {
|
|
17820
17965
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17911,28 +18056,98 @@ function evidenceExists(db, object2) {
|
|
|
17911
18056
|
return schemaObjectExists(db, "table", object2.name);
|
|
17912
18057
|
}
|
|
17913
18058
|
|
|
17914
|
-
// ../../packages/persistence/src/
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
18059
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18060
|
+
function allRows(stmt, params) {
|
|
18061
|
+
if (params === void 0) return stmt.all();
|
|
18062
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18063
|
+
return stmt.all(params);
|
|
17918
18064
|
}
|
|
17919
|
-
function
|
|
17920
|
-
|
|
18065
|
+
function getRow(stmt, params) {
|
|
18066
|
+
if (params === void 0) return stmt.get();
|
|
18067
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18068
|
+
return stmt.get(params);
|
|
17921
18069
|
}
|
|
17922
|
-
function
|
|
17923
|
-
return
|
|
18070
|
+
function intToBool(raw) {
|
|
18071
|
+
return raw === 1 || raw === true;
|
|
17924
18072
|
}
|
|
17925
|
-
function
|
|
17926
|
-
return
|
|
18073
|
+
function boolToInt(b) {
|
|
18074
|
+
return b ? 1 : 0;
|
|
17927
18075
|
}
|
|
17928
|
-
function
|
|
17929
|
-
|
|
18076
|
+
function bindParams(row) {
|
|
18077
|
+
const out = {};
|
|
18078
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18079
|
+
out[key] = value === void 0 ? null : value;
|
|
18080
|
+
}
|
|
18081
|
+
return out;
|
|
17930
18082
|
}
|
|
17931
|
-
function
|
|
17932
|
-
return
|
|
18083
|
+
function countScalar(db, sql, params) {
|
|
18084
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17933
18085
|
}
|
|
17934
|
-
function
|
|
17935
|
-
|
|
18086
|
+
function countBy(db, sql, params) {
|
|
18087
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18088
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18089
|
+
map2.set(row.k, row.n);
|
|
18090
|
+
}
|
|
18091
|
+
return map2;
|
|
18092
|
+
}
|
|
18093
|
+
function mapRowsTolerant(rows, map2) {
|
|
18094
|
+
const out = [];
|
|
18095
|
+
for (const row of rows) {
|
|
18096
|
+
try {
|
|
18097
|
+
out.push(map2(row));
|
|
18098
|
+
} catch {
|
|
18099
|
+
}
|
|
18100
|
+
}
|
|
18101
|
+
return out;
|
|
18102
|
+
}
|
|
18103
|
+
|
|
18104
|
+
// ../../packages/persistence/src/paths.ts
|
|
18105
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18106
|
+
var DATA_DIR_MODE = 448;
|
|
18107
|
+
var DATA_FILE_MODE = 384;
|
|
18108
|
+
var DB_FILENAME = "aka.db";
|
|
18109
|
+
function chmodBestEffort(path, mode) {
|
|
18110
|
+
try {
|
|
18111
|
+
chmodSync(path, mode);
|
|
18112
|
+
} catch {
|
|
18113
|
+
}
|
|
18114
|
+
}
|
|
18115
|
+
function tightenDir(dir) {
|
|
18116
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18117
|
+
}
|
|
18118
|
+
function ensureDataDirSync(dir) {
|
|
18119
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18120
|
+
tightenDir(dir);
|
|
18121
|
+
}
|
|
18122
|
+
function dbSidecars(file2) {
|
|
18123
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18124
|
+
}
|
|
18125
|
+
function tightenFile(file2) {
|
|
18126
|
+
try {
|
|
18127
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18128
|
+
} catch {
|
|
18129
|
+
}
|
|
18130
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18131
|
+
}
|
|
18132
|
+
function tightenPerms(file2) {
|
|
18133
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18134
|
+
}
|
|
18135
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
18136
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
18137
|
+
try {
|
|
18138
|
+
rmSync(tmp, { force: true });
|
|
18139
|
+
} catch {
|
|
18140
|
+
}
|
|
18141
|
+
try {
|
|
18142
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18143
|
+
renameSync(tmp, file2);
|
|
18144
|
+
} finally {
|
|
18145
|
+
try {
|
|
18146
|
+
rmSync(tmp, { force: true });
|
|
18147
|
+
} catch {
|
|
18148
|
+
}
|
|
18149
|
+
}
|
|
18150
|
+
tightenFile(file2);
|
|
17936
18151
|
}
|
|
17937
18152
|
|
|
17938
18153
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17946,7 +18161,8 @@ function createdIndexName(statement) {
|
|
|
17946
18161
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17947
18162
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17948
18163
|
}
|
|
17949
|
-
|
|
18164
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18165
|
+
function applyMigrations(db, file2) {
|
|
17950
18166
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17951
18167
|
db.exec(
|
|
17952
18168
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17960,6 +18176,7 @@ function applyMigrations(db) {
|
|
|
17960
18176
|
);
|
|
17961
18177
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17962
18178
|
if (applied.has(migration.tag)) continue;
|
|
18179
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17963
18180
|
const evidence = evidenceObjects(migration.sql);
|
|
17964
18181
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17965
18182
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18004,7 +18221,6 @@ function applyMigrations(db) {
|
|
|
18004
18221
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18005
18222
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18006
18223
|
}
|
|
18007
|
-
ensureSyncedAtColumn(db, "events");
|
|
18008
18224
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18009
18225
|
ensureScanLedgerTable(db);
|
|
18010
18226
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18012,6 +18228,47 @@ function applyMigrations(db) {
|
|
|
18012
18228
|
ensureWriteGateTrigger(db);
|
|
18013
18229
|
ensureTokenUsageColumns(db);
|
|
18014
18230
|
reconcileSourceProjectIds(db);
|
|
18231
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18232
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18233
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18234
|
+
}
|
|
18235
|
+
}
|
|
18236
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18237
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18238
|
+
if (!migration) return;
|
|
18239
|
+
if (file2) {
|
|
18240
|
+
try {
|
|
18241
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18242
|
+
} catch (error51) {
|
|
18243
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18244
|
+
return;
|
|
18245
|
+
}
|
|
18246
|
+
}
|
|
18247
|
+
try {
|
|
18248
|
+
withTransaction(
|
|
18249
|
+
db,
|
|
18250
|
+
() => {
|
|
18251
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18252
|
+
if (alreadyDropped) return;
|
|
18253
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18254
|
+
db.exec(statement);
|
|
18255
|
+
}
|
|
18256
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18257
|
+
migration.tag,
|
|
18258
|
+
Date.now()
|
|
18259
|
+
);
|
|
18260
|
+
},
|
|
18261
|
+
"IMMEDIATE"
|
|
18262
|
+
);
|
|
18263
|
+
} catch (error51) {
|
|
18264
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18265
|
+
}
|
|
18266
|
+
}
|
|
18267
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18268
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18269
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18270
|
+
tightenFile(backup);
|
|
18271
|
+
return backup;
|
|
18015
18272
|
}
|
|
18016
18273
|
var TOKEN_USAGE_COLUMNS = [
|
|
18017
18274
|
{
|
|
@@ -18040,6 +18297,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18040
18297
|
}
|
|
18041
18298
|
];
|
|
18042
18299
|
function ensureTokenUsageColumns(db) {
|
|
18300
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18043
18301
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18044
18302
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18045
18303
|
if (!existing.has(column.name)) {
|
|
@@ -18105,11 +18363,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18105
18363
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18106
18364
|
}
|
|
18107
18365
|
}
|
|
18366
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18367
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18368
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18369
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18370
|
+
return row?.lastRowid ?? 0;
|
|
18371
|
+
}
|
|
18372
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18373
|
+
db.prepare(
|
|
18374
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18375
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18376
|
+
).run(source, lastRowid);
|
|
18377
|
+
}
|
|
18378
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18379
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18380
|
+
let processed = 0;
|
|
18381
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18382
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18383
|
+
if (rows.length === 0) return true;
|
|
18384
|
+
withTransaction(
|
|
18385
|
+
db,
|
|
18386
|
+
() => {
|
|
18387
|
+
handleRows(rows);
|
|
18388
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18389
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18390
|
+
},
|
|
18391
|
+
"IMMEDIATE"
|
|
18392
|
+
);
|
|
18393
|
+
processed += rows.length;
|
|
18394
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18395
|
+
}
|
|
18396
|
+
return false;
|
|
18397
|
+
}
|
|
18398
|
+
function parseLegacyEventMetadata(raw) {
|
|
18399
|
+
if (raw === null) return void 0;
|
|
18400
|
+
try {
|
|
18401
|
+
return JSON.parse(raw);
|
|
18402
|
+
} catch {
|
|
18403
|
+
return void 0;
|
|
18404
|
+
}
|
|
18405
|
+
}
|
|
18406
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18407
|
+
return JSON.stringify(
|
|
18408
|
+
toCaptureAttributes({
|
|
18409
|
+
id: row.id,
|
|
18410
|
+
sourceTool: row.sourceTool,
|
|
18411
|
+
kind: row.kind,
|
|
18412
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18413
|
+
contentHash: row.contentHash,
|
|
18414
|
+
content: row.content,
|
|
18415
|
+
metadata: row.metadata
|
|
18416
|
+
})
|
|
18417
|
+
);
|
|
18418
|
+
}
|
|
18419
|
+
function copyLegacyEvents(db) {
|
|
18420
|
+
const selectStmt = db.prepare(
|
|
18421
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18422
|
+
content_hash AS contentHash, content, metadata
|
|
18423
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18424
|
+
);
|
|
18425
|
+
const insertStmt = db.prepare(
|
|
18426
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18427
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18428
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18429
|
+
);
|
|
18430
|
+
const stubRootStmt = db.prepare(
|
|
18431
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18432
|
+
);
|
|
18433
|
+
return drainLegacyTable(
|
|
18434
|
+
db,
|
|
18435
|
+
"events",
|
|
18436
|
+
selectStmt,
|
|
18437
|
+
(rows) => {
|
|
18438
|
+
for (const row of rows) {
|
|
18439
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18440
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18441
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18442
|
+
insertStmt.run(
|
|
18443
|
+
bindParams({
|
|
18444
|
+
id: row.id,
|
|
18445
|
+
parentId: sessionId,
|
|
18446
|
+
rootSessionId: sessionId,
|
|
18447
|
+
eventType: row.kind,
|
|
18448
|
+
startedAt: row.occurredAt,
|
|
18449
|
+
content: row.content,
|
|
18450
|
+
contentHash: row.contentHash,
|
|
18451
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18452
|
+
})
|
|
18453
|
+
);
|
|
18454
|
+
}
|
|
18455
|
+
}
|
|
18456
|
+
);
|
|
18457
|
+
}
|
|
18458
|
+
function copyLegacyFindings(db) {
|
|
18459
|
+
const selectStmt = db.prepare(
|
|
18460
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18461
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18462
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18463
|
+
first_detected_at AS firstDetectedAt
|
|
18464
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18465
|
+
);
|
|
18466
|
+
const definitionStmt = db.prepare(
|
|
18467
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18468
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18469
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18470
|
+
);
|
|
18471
|
+
const findingStmt = db.prepare(
|
|
18472
|
+
`INSERT INTO inspection_findings
|
|
18473
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18474
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18475
|
+
finding_key, first_detected_at)
|
|
18476
|
+
VALUES
|
|
18477
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18478
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18479
|
+
:findingKey, :firstDetectedAt)
|
|
18480
|
+
ON CONFLICT(id) DO NOTHING
|
|
18481
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18482
|
+
first_detected_at = CASE
|
|
18483
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18484
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18485
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18486
|
+
END`
|
|
18487
|
+
);
|
|
18488
|
+
return drainLegacyTable(
|
|
18489
|
+
db,
|
|
18490
|
+
"findings",
|
|
18491
|
+
selectStmt,
|
|
18492
|
+
(rows) => {
|
|
18493
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18494
|
+
for (const row of rows) {
|
|
18495
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18496
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18497
|
+
if (definitionId === void 0) {
|
|
18498
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18499
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18500
|
+
definitionStmt.run(
|
|
18501
|
+
bindParams({
|
|
18502
|
+
id: definitionId,
|
|
18503
|
+
ruleId: row.ruleId,
|
|
18504
|
+
name: row.ruleId,
|
|
18505
|
+
category: row.category,
|
|
18506
|
+
severity: row.severity,
|
|
18507
|
+
definition: "",
|
|
18508
|
+
version: version2
|
|
18509
|
+
})
|
|
18510
|
+
);
|
|
18511
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18512
|
+
}
|
|
18513
|
+
findingStmt.run(
|
|
18514
|
+
bindParams({
|
|
18515
|
+
id: row.id,
|
|
18516
|
+
auditEventId: row.eventId,
|
|
18517
|
+
inspectionDefinitionId: definitionId,
|
|
18518
|
+
spanStart: row.spanStart,
|
|
18519
|
+
spanEnd: row.spanEnd,
|
|
18520
|
+
maskedMatch: row.maskedMatch,
|
|
18521
|
+
actionTaken: row.actionTaken,
|
|
18522
|
+
confidence: row.confidence,
|
|
18523
|
+
findingKey: row.findingKey,
|
|
18524
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18525
|
+
})
|
|
18526
|
+
);
|
|
18527
|
+
}
|
|
18528
|
+
}
|
|
18529
|
+
);
|
|
18530
|
+
}
|
|
18531
|
+
function runLegacyHistoryBackfill(db) {
|
|
18532
|
+
try {
|
|
18533
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18534
|
+
if (!eventsCaughtUp) return false;
|
|
18535
|
+
return copyLegacyFindings(db);
|
|
18536
|
+
} catch (error51) {
|
|
18537
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18538
|
+
return false;
|
|
18539
|
+
}
|
|
18540
|
+
}
|
|
18108
18541
|
function isForeignSqliteLineage(db) {
|
|
18109
18542
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18110
18543
|
return columnNames(db, "events").includes("tenant_id");
|
|
18111
18544
|
}
|
|
18112
18545
|
function ensureSyncedAtColumn(db, table2) {
|
|
18546
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18113
18547
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18114
18548
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18115
18549
|
}
|
|
@@ -18130,6 +18564,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18130
18564
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18131
18565
|
)`);
|
|
18132
18566
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18567
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18133
18568
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18134
18569
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18135
18570
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18157,30 +18592,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18157
18592
|
)`);
|
|
18158
18593
|
}
|
|
18159
18594
|
|
|
18160
|
-
// ../../packages/persistence/src/paths.ts
|
|
18161
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18162
|
-
var DATA_DIR_MODE = 448;
|
|
18163
|
-
var DATA_FILE_MODE = 384;
|
|
18164
|
-
var DB_FILENAME = "aka.db";
|
|
18165
|
-
function ensureDataDirSync(dir) {
|
|
18166
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18167
|
-
try {
|
|
18168
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18169
|
-
} catch {
|
|
18170
|
-
}
|
|
18171
|
-
}
|
|
18172
|
-
function walSidecars(file2) {
|
|
18173
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18174
|
-
}
|
|
18175
|
-
function tightenPerms(file2) {
|
|
18176
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18177
|
-
try {
|
|
18178
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18179
|
-
} catch {
|
|
18180
|
-
}
|
|
18181
|
-
}
|
|
18182
|
-
}
|
|
18183
|
-
|
|
18184
18595
|
// ../../packages/persistence/src/internal/json.ts
|
|
18185
18596
|
function safeJson(s, fallback) {
|
|
18186
18597
|
if (s == null) return fallback;
|
|
@@ -18200,51 +18611,6 @@ function parseJsonObject(s) {
|
|
|
18200
18611
|
return void 0;
|
|
18201
18612
|
}
|
|
18202
18613
|
|
|
18203
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18204
|
-
function allRows(stmt, params) {
|
|
18205
|
-
if (params === void 0) return stmt.all();
|
|
18206
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18207
|
-
return stmt.all(params);
|
|
18208
|
-
}
|
|
18209
|
-
function getRow(stmt, params) {
|
|
18210
|
-
if (params === void 0) return stmt.get();
|
|
18211
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18212
|
-
return stmt.get(params);
|
|
18213
|
-
}
|
|
18214
|
-
function intToBool(raw) {
|
|
18215
|
-
return raw === 1 || raw === true;
|
|
18216
|
-
}
|
|
18217
|
-
function boolToInt(b) {
|
|
18218
|
-
return b ? 1 : 0;
|
|
18219
|
-
}
|
|
18220
|
-
function bindParams(row) {
|
|
18221
|
-
const out = {};
|
|
18222
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18223
|
-
out[key] = value === void 0 ? null : value;
|
|
18224
|
-
}
|
|
18225
|
-
return out;
|
|
18226
|
-
}
|
|
18227
|
-
function countScalar(db, sql, params) {
|
|
18228
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18229
|
-
}
|
|
18230
|
-
function countBy(db, sql, params) {
|
|
18231
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18232
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18233
|
-
map2.set(row.k, row.n);
|
|
18234
|
-
}
|
|
18235
|
-
return map2;
|
|
18236
|
-
}
|
|
18237
|
-
function mapRowsTolerant(rows, map2) {
|
|
18238
|
-
const out = [];
|
|
18239
|
-
for (const row of rows) {
|
|
18240
|
-
try {
|
|
18241
|
-
out.push(map2(row));
|
|
18242
|
-
} catch {
|
|
18243
|
-
}
|
|
18244
|
-
}
|
|
18245
|
-
return out;
|
|
18246
|
-
}
|
|
18247
|
-
|
|
18248
18614
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18249
18615
|
var DAY_MS = 864e5;
|
|
18250
18616
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18861,6 +19227,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18861
19227
|
})
|
|
18862
19228
|
);
|
|
18863
19229
|
}
|
|
19230
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19231
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19232
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19233
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19234
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19235
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19236
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19237
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19238
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19239
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19240
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19241
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19242
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19243
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19244
|
+
}
|
|
18864
19245
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18865
19246
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18866
19247
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19322,8 +19703,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19322
19703
|
)
|
|
19323
19704
|
);
|
|
19324
19705
|
}
|
|
19325
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19326
|
-
//
|
|
19706
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19707
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19708
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19709
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19710
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19711
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19712
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19713
|
+
// to the old findings⋈events one.
|
|
19327
19714
|
countFindingsLast30d(ruleIds) {
|
|
19328
19715
|
if (ruleIds.length === 0) return 0;
|
|
19329
19716
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19331,8 +19718,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19331
19718
|
return countScalar(
|
|
19332
19719
|
this.db,
|
|
19333
19720
|
`SELECT count(*) AS n
|
|
19334
|
-
FROM
|
|
19335
|
-
|
|
19721
|
+
FROM inspection_findings f
|
|
19722
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19723
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19724
|
+
WHERE e.started_at >= ?
|
|
19725
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19726
|
+
AND d.rule_id IN (${inClause})`,
|
|
19336
19727
|
[since, ...ruleIds]
|
|
19337
19728
|
);
|
|
19338
19729
|
}
|
|
@@ -19342,35 +19733,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19342
19733
|
var SqliteEventsRepository = class {
|
|
19343
19734
|
constructor(db) {
|
|
19344
19735
|
this.db = db;
|
|
19345
|
-
this.insertStmt = db.prepare(
|
|
19346
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19347
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19348
|
-
);
|
|
19349
19736
|
}
|
|
19350
19737
|
db;
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
this.insertStmt.run(
|
|
19355
|
-
bindParams({
|
|
19356
|
-
id: row.id,
|
|
19357
|
-
sourceTool: row.sourceTool,
|
|
19358
|
-
kind: row.kind,
|
|
19359
|
-
occurredAt: row.occurredAt,
|
|
19360
|
-
contentHash: row.contentHash,
|
|
19361
|
-
content: row.content,
|
|
19362
|
-
metadata: row.metadata
|
|
19363
|
-
})
|
|
19364
|
-
);
|
|
19365
|
-
}
|
|
19366
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19367
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19368
|
-
// never duplicates findings.
|
|
19738
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19739
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19740
|
+
// scan never duplicates findings.
|
|
19369
19741
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19370
19742
|
// async EventsReadPort contract.
|
|
19743
|
+
//
|
|
19744
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19745
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19746
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19747
|
+
// index-friendly rather than walking rows that can never match.
|
|
19371
19748
|
contentHashes() {
|
|
19372
19749
|
const rows = allRows(
|
|
19373
|
-
this.db.prepare(
|
|
19750
|
+
this.db.prepare(
|
|
19751
|
+
`SELECT content_hash FROM audit_events
|
|
19752
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19753
|
+
)
|
|
19374
19754
|
);
|
|
19375
19755
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19376
19756
|
}
|
|
@@ -19706,17 +20086,20 @@ function parseExceptionRow(row) {
|
|
|
19706
20086
|
}
|
|
19707
20087
|
|
|
19708
20088
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19709
|
-
function
|
|
20089
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19710
20090
|
return `(
|
|
19711
|
-
SELECT fr
|
|
20091
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19712
20092
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19713
20093
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19714
20094
|
LIMIT 1
|
|
19715
20095
|
)`;
|
|
19716
20096
|
}
|
|
20097
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20098
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20099
|
+
}
|
|
19717
20100
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19718
|
-
SELECT finding_key, status FROM (
|
|
19719
|
-
SELECT fr.finding_key, fr.status,
|
|
20101
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20102
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19720
20103
|
ROW_NUMBER() OVER (
|
|
19721
20104
|
PARTITION BY fr.finding_key
|
|
19722
20105
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19743,68 +20126,21 @@ var DAY_MS3 = 864e5;
|
|
|
19743
20126
|
var SqliteFindingsRepository = class {
|
|
19744
20127
|
constructor(db) {
|
|
19745
20128
|
this.db = db;
|
|
19746
|
-
this.insertStmt = db.prepare(
|
|
19747
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19748
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19749
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19750
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19751
|
-
event_id = excluded.event_id,
|
|
19752
|
-
category = excluded.category,
|
|
19753
|
-
severity = excluded.severity,
|
|
19754
|
-
span_start = excluded.span_start,
|
|
19755
|
-
span_end = excluded.span_end,
|
|
19756
|
-
masked_match = excluded.masked_match,
|
|
19757
|
-
action_taken = excluded.action_taken,
|
|
19758
|
-
confidence = excluded.confidence`
|
|
19759
|
-
);
|
|
19760
|
-
this.sessionDupStmt = db.prepare(
|
|
19761
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19762
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19763
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19764
|
-
LIMIT 1`
|
|
19765
|
-
);
|
|
19766
20129
|
}
|
|
19767
20130
|
db;
|
|
19768
|
-
insertStmt;
|
|
19769
|
-
sessionDupStmt;
|
|
19770
|
-
insertFindings(findings, scope = {}) {
|
|
19771
|
-
for (const finding of findings) {
|
|
19772
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19773
|
-
const row = toFindingRow(finding);
|
|
19774
|
-
this.insertStmt.run({
|
|
19775
|
-
id: row.id,
|
|
19776
|
-
eventId: row.eventId,
|
|
19777
|
-
ruleId: row.ruleId,
|
|
19778
|
-
category: row.category,
|
|
19779
|
-
severity: row.severity,
|
|
19780
|
-
spanStart: row.spanStart,
|
|
19781
|
-
spanEnd: row.spanEnd,
|
|
19782
|
-
maskedMatch: row.maskedMatch,
|
|
19783
|
-
actionTaken: row.actionTaken,
|
|
19784
|
-
confidence: row.confidence,
|
|
19785
|
-
findingKey: row.findingKey ?? null
|
|
19786
|
-
});
|
|
19787
|
-
}
|
|
19788
|
-
}
|
|
19789
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19790
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19791
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19792
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19793
|
-
const hit = this.sessionDupStmt.get({
|
|
19794
|
-
ruleId: finding.ruleId,
|
|
19795
|
-
maskedMatch: finding.maskedMatch,
|
|
19796
|
-
sessionId
|
|
19797
|
-
});
|
|
19798
|
-
return hit !== void 0;
|
|
19799
|
-
}
|
|
19800
20131
|
recentFindings(opts) {
|
|
19801
20132
|
const limit = opts?.limit ?? 50;
|
|
19802
20133
|
const rows = allRows(
|
|
19803
20134
|
this.db.prepare(
|
|
19804
|
-
`SELECT f.id, f.event_id,
|
|
19805
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19806
|
-
|
|
19807
|
-
|
|
20135
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20136
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20137
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20138
|
+
e.event_type AS kind
|
|
20139
|
+
FROM inspection_findings f
|
|
20140
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20141
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20142
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20143
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19808
20144
|
LIMIT :limit`
|
|
19809
20145
|
),
|
|
19810
20146
|
{ limit }
|
|
@@ -19826,25 +20162,34 @@ var SqliteFindingsRepository = class {
|
|
|
19826
20162
|
);
|
|
19827
20163
|
}
|
|
19828
20164
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19829
|
-
* session-stamped
|
|
20165
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19830
20166
|
* page can label its findings link without the grouped pipeline. */
|
|
19831
20167
|
sessionFindingsCount(sessionId) {
|
|
19832
20168
|
if (!sessionId) return Promise.resolve(0);
|
|
19833
20169
|
return Promise.resolve(
|
|
19834
20170
|
countScalar(
|
|
19835
20171
|
this.db,
|
|
19836
|
-
`SELECT count(*) AS n FROM
|
|
19837
|
-
JOIN
|
|
19838
|
-
WHERE
|
|
20172
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20173
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20174
|
+
WHERE e.root_session_id = :sessionId
|
|
20175
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19839
20176
|
{ sessionId }
|
|
19840
20177
|
)
|
|
19841
20178
|
);
|
|
19842
20179
|
}
|
|
19843
|
-
/** Per-rule transcript firing tally for one session —
|
|
19844
|
-
*
|
|
19845
|
-
*
|
|
19846
|
-
*
|
|
19847
|
-
*
|
|
20180
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20181
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20182
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20183
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20184
|
+
* tally with the deduped groups it lists.
|
|
20185
|
+
*
|
|
20186
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20187
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20188
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20189
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20190
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20191
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20192
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19848
20193
|
sessionFirings(sessionId) {
|
|
19849
20194
|
return Object.fromEntries(
|
|
19850
20195
|
countBy(
|
|
@@ -19854,18 +20199,25 @@ var SqliteFindingsRepository = class {
|
|
|
19854
20199
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19855
20200
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19856
20201
|
WHERE e.root_session_id = :sessionId
|
|
20202
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19857
20203
|
GROUP BY d.rule_id`,
|
|
19858
20204
|
{ sessionId }
|
|
19859
20205
|
)
|
|
19860
20206
|
);
|
|
19861
20207
|
}
|
|
19862
20208
|
/**
|
|
19863
|
-
* Grouped findings for the dashboard — joins
|
|
19864
|
-
* toolName from
|
|
19865
|
-
*
|
|
20209
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20210
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20211
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20212
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20213
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20214
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20215
|
+
* severity then recency. Filtering
|
|
19866
20216
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19867
20217
|
* reflect the full filtered set; `items` is the requested
|
|
19868
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20218
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20219
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20220
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19869
20221
|
*
|
|
19870
20222
|
* Two reads, neither of which materializes a row per finding:
|
|
19871
20223
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19878,10 +20230,11 @@ var SqliteFindingsRepository = class {
|
|
|
19878
20230
|
* rule is ever restated in SQL.
|
|
19879
20231
|
*/
|
|
19880
20232
|
listGroupedFindings(query) {
|
|
19881
|
-
const sessionPredicate = query.sessionId ? `
|
|
20233
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20234
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19882
20235
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19883
20236
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19884
|
-
predicate
|
|
20237
|
+
predicate,
|
|
19885
20238
|
params: sessionParams
|
|
19886
20239
|
});
|
|
19887
20240
|
const rows = allRows(
|
|
@@ -19889,24 +20242,26 @@ var SqliteFindingsRepository = class {
|
|
|
19889
20242
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19890
20243
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19891
20244
|
FROM (
|
|
19892
|
-
SELECT f.id AS id,
|
|
19893
|
-
|
|
20245
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20246
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19894
20247
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19895
|
-
e.
|
|
19896
|
-
json_extract(e.
|
|
19897
|
-
json_extract(e.
|
|
19898
|
-
json_extract(e.
|
|
19899
|
-
e.
|
|
20248
|
+
e.started_at AS occurred_at,
|
|
20249
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20250
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20251
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20252
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20253
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19900
20254
|
latest.status AS latest_status,
|
|
19901
20255
|
ROW_NUMBER() OVER (
|
|
19902
|
-
PARTITION BY
|
|
19903
|
-
ORDER BY e.
|
|
20256
|
+
PARTITION BY d.rule_id
|
|
20257
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19904
20258
|
) AS rn
|
|
19905
|
-
FROM
|
|
19906
|
-
JOIN
|
|
20259
|
+
FROM inspection_findings f
|
|
20260
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20261
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19907
20262
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19908
20263
|
ON latest.finding_key = f.finding_key
|
|
19909
|
-
${
|
|
20264
|
+
${predicate}
|
|
19910
20265
|
)
|
|
19911
20266
|
WHERE rn <= :cap
|
|
19912
20267
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19933,17 +20288,29 @@ var SqliteFindingsRepository = class {
|
|
|
19933
20288
|
severity: query.severity,
|
|
19934
20289
|
providers: query.provider,
|
|
19935
20290
|
actions: query.action,
|
|
20291
|
+
statuses: query.status,
|
|
19936
20292
|
subtype: query.subtype,
|
|
19937
20293
|
q: query.q
|
|
19938
20294
|
};
|
|
19939
20295
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19940
20296
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20297
|
+
const statusFilter = query.status ?? [];
|
|
19941
20298
|
const totals = {
|
|
19942
|
-
findings: sorted.reduce((acc, g) =>
|
|
20299
|
+
findings: sorted.reduce((acc, g) => {
|
|
20300
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20301
|
+
const agg = aggregates.get(g.id);
|
|
20302
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20303
|
+
}, 0),
|
|
19943
20304
|
groups: sorted.length
|
|
19944
20305
|
};
|
|
19945
20306
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19946
|
-
const
|
|
20307
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20308
|
+
const items = sorted.slice(0, limit).map(
|
|
20309
|
+
(g) => statusSet ? {
|
|
20310
|
+
...g,
|
|
20311
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20312
|
+
} : g
|
|
20313
|
+
);
|
|
19947
20314
|
return Promise.resolve({
|
|
19948
20315
|
totals,
|
|
19949
20316
|
facets,
|
|
@@ -19957,45 +20324,62 @@ var SqliteFindingsRepository = class {
|
|
|
19957
20324
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19958
20325
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19959
20326
|
*
|
|
19960
|
-
*
|
|
19961
|
-
*
|
|
19962
|
-
*
|
|
19963
|
-
* status
|
|
19964
|
-
*
|
|
19965
|
-
*
|
|
20327
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20328
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20329
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20330
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20331
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20332
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20333
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20334
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20335
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20336
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20337
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19966
20338
|
* a group's row stays small however many findings it holds.
|
|
19967
20339
|
*
|
|
19968
20340
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19969
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19970
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19971
|
-
* that is a string proportional to the store (~8MB over
|
|
19972
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19973
|
-
* match an instance outside the preview, which searching
|
|
19974
|
-
* would silently lose, so it is fetched only when the
|
|
19975
|
-
* carries a `q`.
|
|
20341
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20342
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20343
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20344
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20345
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20346
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20347
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20348
|
+
* path repeating across tuples.)
|
|
19976
20349
|
*/
|
|
19977
20350
|
groupAggregates(withSearchText, scope) {
|
|
19978
|
-
const
|
|
19979
|
-
group_concat(DISTINCT json_extract(e.
|
|
19980
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20351
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20352
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20353
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19981
20354
|
const rows = this.db.prepare(
|
|
19982
|
-
`SELECT
|
|
19983
|
-
|
|
19984
|
-
max(
|
|
19985
|
-
group_concat(
|
|
19986
|
-
group_concat(
|
|
19987
|
-
group_concat(
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
20355
|
+
`SELECT rule_id,
|
|
20356
|
+
sum(tuple_count) AS instance_count,
|
|
20357
|
+
max(latest_at) AS latest_at,
|
|
20358
|
+
group_concat(source_tools) AS source_tools,
|
|
20359
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20360
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20361
|
+
group_concat(repos) AS repos,
|
|
20362
|
+
group_concat(files) AS files,
|
|
20363
|
+
group_concat(tool_names) AS tool_names
|
|
20364
|
+
FROM (
|
|
20365
|
+
SELECT d.rule_id AS rule_id,
|
|
20366
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20367
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20368
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20369
|
+
count(*) AS tuple_count,
|
|
20370
|
+
max(e.started_at) AS latest_at,
|
|
20371
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20372
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20373
|
+
${innerSearchColumns}
|
|
20374
|
+
FROM inspection_findings f
|
|
20375
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20376
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20377
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20378
|
+
ON latest.finding_key = f.finding_key
|
|
20379
|
+
${scope.predicate}
|
|
20380
|
+
GROUP BY d.rule_id, status_tuple
|
|
20381
|
+
)
|
|
20382
|
+
GROUP BY rule_id`
|
|
19999
20383
|
).all(scope.params);
|
|
20000
20384
|
return new Map(
|
|
20001
20385
|
rows.map((r) => [
|
|
@@ -20005,13 +20389,14 @@ var SqliteFindingsRepository = class {
|
|
|
20005
20389
|
sourceTools: splitConcat(r.source_tools),
|
|
20006
20390
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20007
20391
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20008
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20392
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20009
20393
|
return {
|
|
20010
20394
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20011
20395
|
// so the marker stands in for the key itself (never rendered).
|
|
20012
20396
|
kind,
|
|
20013
20397
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20014
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20398
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20399
|
+
count: Number(count)
|
|
20015
20400
|
};
|
|
20016
20401
|
}),
|
|
20017
20402
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20028,10 +20413,21 @@ var SqliteFindingsRepository = class {
|
|
|
20028
20413
|
);
|
|
20029
20414
|
}
|
|
20030
20415
|
healthSummary() {
|
|
20031
|
-
const total = countScalar(
|
|
20416
|
+
const total = countScalar(
|
|
20417
|
+
this.db,
|
|
20418
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20419
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20420
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20421
|
+
);
|
|
20032
20422
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20033
20423
|
const grouped = allRows(
|
|
20034
|
-
this.db.prepare(
|
|
20424
|
+
this.db.prepare(
|
|
20425
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20426
|
+
FROM inspection_findings f
|
|
20427
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20428
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20429
|
+
GROUP BY f.action_taken`
|
|
20430
|
+
)
|
|
20035
20431
|
);
|
|
20036
20432
|
for (const row of grouped) {
|
|
20037
20433
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20039,12 +20435,15 @@ var SqliteFindingsRepository = class {
|
|
|
20039
20435
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20040
20436
|
const sevRows = allRows(
|
|
20041
20437
|
this.db.prepare(
|
|
20042
|
-
`SELECT
|
|
20043
|
-
FROM
|
|
20438
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20439
|
+
FROM inspection_findings f
|
|
20440
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20441
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20044
20442
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20045
20443
|
ON latest.finding_key = f.finding_key
|
|
20046
|
-
WHERE
|
|
20047
|
-
|
|
20444
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20445
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20446
|
+
GROUP BY d.severity`
|
|
20048
20447
|
)
|
|
20049
20448
|
);
|
|
20050
20449
|
for (const row of sevRows) {
|
|
@@ -20065,9 +20464,11 @@ var SqliteFindingsRepository = class {
|
|
|
20065
20464
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20066
20465
|
const rows = allRows(
|
|
20067
20466
|
this.db.prepare(
|
|
20068
|
-
`SELECT date(e.
|
|
20069
|
-
FROM
|
|
20070
|
-
|
|
20467
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20468
|
+
FROM inspection_findings f
|
|
20469
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20470
|
+
WHERE e.started_at >= :since
|
|
20471
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20071
20472
|
GROUP BY day, f.action_taken`
|
|
20072
20473
|
),
|
|
20073
20474
|
{ since }
|
|
@@ -20132,15 +20533,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20132
20533
|
this.insertStmt = db.prepare(
|
|
20133
20534
|
`INSERT INTO inspection_findings
|
|
20134
20535
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20135
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20536
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20537
|
+
finding_key, first_detected_at)
|
|
20136
20538
|
VALUES
|
|
20137
20539
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20138
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20139
|
-
|
|
20540
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20541
|
+
:findingKey,
|
|
20542
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20543
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20544
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20545
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20546
|
+
audit_event_id = excluded.audit_event_id,
|
|
20547
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20548
|
+
classified_data_id = excluded.classified_data_id,
|
|
20549
|
+
span_start = excluded.span_start,
|
|
20550
|
+
span_end = excluded.span_end,
|
|
20551
|
+
masked_match = excluded.masked_match,
|
|
20552
|
+
action_taken = excluded.action_taken,
|
|
20553
|
+
confidence = excluded.confidence`
|
|
20554
|
+
);
|
|
20555
|
+
this.sessionDupStmt = db.prepare(
|
|
20556
|
+
`SELECT 1 FROM inspection_findings f
|
|
20557
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20558
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20559
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20560
|
+
AND e.root_session_id = :sessionId
|
|
20561
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20562
|
+
LIMIT 1`
|
|
20563
|
+
);
|
|
20564
|
+
this.eventDupStmt = db.prepare(
|
|
20565
|
+
`SELECT 1 FROM inspection_findings f
|
|
20566
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20567
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20568
|
+
AND f.masked_match = :maskedMatch
|
|
20569
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20570
|
+
LIMIT 1`
|
|
20140
20571
|
);
|
|
20141
20572
|
}
|
|
20142
20573
|
db;
|
|
20143
20574
|
insertStmt;
|
|
20575
|
+
sessionDupStmt;
|
|
20576
|
+
eventDupStmt;
|
|
20577
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20578
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20579
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20580
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20581
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20582
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20583
|
+
}
|
|
20584
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20585
|
+
// recorded against the given audit event.
|
|
20586
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20587
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20588
|
+
}
|
|
20144
20589
|
insertFinding(input) {
|
|
20145
20590
|
const row = toInspectionFindingRow(input);
|
|
20146
20591
|
this.insertStmt.run(
|
|
@@ -20153,7 +20598,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20153
20598
|
spanEnd: row.spanEnd,
|
|
20154
20599
|
maskedMatch: row.maskedMatch,
|
|
20155
20600
|
actionTaken: row.actionTaken,
|
|
20156
|
-
confidence: row.confidence
|
|
20601
|
+
confidence: row.confidence,
|
|
20602
|
+
findingKey: row.findingKey,
|
|
20603
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20157
20604
|
})
|
|
20158
20605
|
);
|
|
20159
20606
|
}
|
|
@@ -20425,7 +20872,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20425
20872
|
installedRuleset() {
|
|
20426
20873
|
const rows = allRows(
|
|
20427
20874
|
this.db.prepare(
|
|
20428
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20875
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20429
20876
|
)
|
|
20430
20877
|
);
|
|
20431
20878
|
const out = {
|
|
@@ -20433,7 +20880,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20433
20880
|
enabledPacks: 0,
|
|
20434
20881
|
rules: [],
|
|
20435
20882
|
invalidRules: 0,
|
|
20436
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20883
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20884
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20437
20885
|
};
|
|
20438
20886
|
for (const row of rows) {
|
|
20439
20887
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20455,6 +20903,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20455
20903
|
if (parsed.success) {
|
|
20456
20904
|
out.rules.push(parsed.data);
|
|
20457
20905
|
out.ruleActions.set(parsed.data.id, action);
|
|
20906
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20458
20907
|
} else out.invalidRules += 1;
|
|
20459
20908
|
}
|
|
20460
20909
|
}
|
|
@@ -21629,19 +22078,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21629
22078
|
);
|
|
21630
22079
|
this.openAtRestStmt = db.prepare(
|
|
21631
22080
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21632
|
-
FROM
|
|
21633
|
-
JOIN
|
|
21634
|
-
WHERE e.
|
|
21635
|
-
AND json_extract(e.
|
|
22081
|
+
FROM inspection_findings f
|
|
22082
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22083
|
+
WHERE e.event_type = 'code_change'
|
|
22084
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21636
22085
|
AND f.finding_key IS NOT NULL
|
|
21637
22086
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21638
22087
|
);
|
|
21639
22088
|
this.resolvedAtRestStmt = db.prepare(
|
|
21640
22089
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21641
|
-
FROM
|
|
21642
|
-
JOIN
|
|
21643
|
-
WHERE e.
|
|
21644
|
-
AND json_extract(e.
|
|
22090
|
+
FROM inspection_findings f
|
|
22091
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22092
|
+
WHERE e.event_type = 'code_change'
|
|
22093
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21645
22094
|
AND f.finding_key IS NOT NULL
|
|
21646
22095
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21647
22096
|
);
|
|
@@ -21864,25 +22313,27 @@ var SqliteSecurityRepository = class {
|
|
|
21864
22313
|
severitySummary() {
|
|
21865
22314
|
const rows = allRows(
|
|
21866
22315
|
this.db.prepare(
|
|
21867
|
-
`SELECT
|
|
22316
|
+
`SELECT d.severity AS severity,
|
|
21868
22317
|
COUNT(*) AS count,
|
|
21869
22318
|
SUM(CASE
|
|
21870
|
-
WHEN e.
|
|
22319
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21871
22320
|
WHEN f.finding_key IS NULL THEN 0
|
|
21872
22321
|
WHEN latest.status = 'resolved' THEN 1
|
|
21873
22322
|
ELSE 0
|
|
21874
22323
|
END) AS caught,
|
|
21875
22324
|
SUM(CASE
|
|
21876
|
-
WHEN e.
|
|
22325
|
+
WHEN e.event_type = 'code_change'
|
|
21877
22326
|
AND f.finding_key IS NOT NULL
|
|
21878
22327
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21879
22328
|
ELSE 0
|
|
21880
22329
|
END) AS open_at_rest
|
|
21881
|
-
FROM
|
|
21882
|
-
JOIN
|
|
22330
|
+
FROM inspection_findings f
|
|
22331
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22332
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21883
22333
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21884
22334
|
ON latest.finding_key = f.finding_key
|
|
21885
|
-
|
|
22335
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22336
|
+
GROUP BY d.severity`
|
|
21886
22337
|
)
|
|
21887
22338
|
);
|
|
21888
22339
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21948,7 +22399,7 @@ var SqliteSecurityRepository = class {
|
|
|
21948
22399
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21949
22400
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21950
22401
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21951
|
-
// (
|
|
22402
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21952
22403
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21953
22404
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21954
22405
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21973,30 +22424,20 @@ var SqliteSecurityRepository = class {
|
|
|
21973
22424
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21974
22425
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21975
22426
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21976
|
-
//
|
|
21977
|
-
// the parent event's
|
|
21978
|
-
// backfill left null.
|
|
21979
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
WHERE fr.finding_key = f.finding_key
|
|
21989
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21990
|
-
LIMIT 1
|
|
21991
|
-
) AS latest_method,
|
|
21992
|
-
(
|
|
21993
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21994
|
-
WHERE fr.finding_key = f.finding_key
|
|
21995
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21996
|
-
LIMIT 1
|
|
21997
|
-
) AS latest_resolved_at
|
|
21998
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22427
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22428
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22429
|
+
// legacy/edge row the backfill left null.
|
|
22430
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22431
|
+
latest.status AS latest_status,
|
|
22432
|
+
latest.method AS latest_method,
|
|
22433
|
+
latest.resolved_at AS latest_resolved_at
|
|
22434
|
+
FROM inspection_findings f
|
|
22435
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22436
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22437
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22438
|
+
ON latest.finding_key = f.finding_key
|
|
21999
22439
|
WHERE f.finding_key IS NOT NULL
|
|
22440
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22000
22441
|
AND EXISTS (
|
|
22001
22442
|
SELECT 1 FROM finding_resolution fr
|
|
22002
22443
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22043,11 +22484,13 @@ var SqliteSecurityRepository = class {
|
|
|
22043
22484
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22044
22485
|
const rows = allRows(
|
|
22045
22486
|
this.db.prepare(
|
|
22046
|
-
`SELECT json_extract(e.
|
|
22047
|
-
FROM
|
|
22048
|
-
|
|
22049
|
-
|
|
22050
|
-
AND
|
|
22487
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22488
|
+
FROM inspection_findings f
|
|
22489
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22490
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22491
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22492
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22493
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22051
22494
|
GROUP BY repo
|
|
22052
22495
|
ORDER BY c DESC, repo
|
|
22053
22496
|
LIMIT :limit`
|
|
@@ -22071,44 +22514,28 @@ var SqliteSecurityRepository = class {
|
|
|
22071
22514
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22072
22515
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22073
22516
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22074
|
-
// finding's parent event (
|
|
22075
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22076
|
-
// capped at `limit`.
|
|
22517
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22518
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22519
|
+
// DESC, capped at `limit`.
|
|
22077
22520
|
recentlyResolved(limit = 20) {
|
|
22078
22521
|
const rows = allRows(
|
|
22079
22522
|
this.db.prepare(
|
|
22080
22523
|
`SELECT f.finding_key AS finding_key,
|
|
22081
|
-
|
|
22082
|
-
|
|
22083
|
-
json_extract(e.
|
|
22084
|
-
COALESCE(f.first_detected_at, e.
|
|
22085
|
-
|
|
22086
|
-
|
|
22087
|
-
|
|
22088
|
-
|
|
22089
|
-
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
WHERE e.kind = 'code_change'
|
|
22524
|
+
d.rule_id AS rule_id,
|
|
22525
|
+
d.severity AS severity,
|
|
22526
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22527
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22528
|
+
latest.resolved_at AS latest_resolved_at
|
|
22529
|
+
FROM inspection_findings f
|
|
22530
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22531
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22532
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22533
|
+
ON latest.finding_key = f.finding_key
|
|
22534
|
+
WHERE e.event_type = 'code_change'
|
|
22093
22535
|
AND f.finding_key IS NOT NULL
|
|
22094
|
-
AND
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22098
|
-
LIMIT 1
|
|
22099
|
-
) = 'resolved'
|
|
22100
|
-
AND (
|
|
22101
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22102
|
-
WHERE fr.finding_key = f.finding_key
|
|
22103
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22104
|
-
LIMIT 1
|
|
22105
|
-
) = 'fixed-at-source'
|
|
22106
|
-
AND (
|
|
22107
|
-
SELECT fr.resolved_at 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
|
-
) IS NOT NULL
|
|
22536
|
+
AND latest.status = 'resolved'
|
|
22537
|
+
AND latest.method = 'fixed-at-source'
|
|
22538
|
+
AND latest.resolved_at IS NOT NULL
|
|
22112
22539
|
ORDER BY latest_resolved_at DESC
|
|
22113
22540
|
LIMIT :limit`
|
|
22114
22541
|
),
|
|
@@ -22127,15 +22554,18 @@ var SqliteSecurityRepository = class {
|
|
|
22127
22554
|
return Promise.resolve({ items });
|
|
22128
22555
|
}
|
|
22129
22556
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22130
|
-
// epoch-millis timestamp.
|
|
22557
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22131
22558
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22132
22559
|
findingsInRange(fromMs, toMs) {
|
|
22133
22560
|
const rows = allRows(
|
|
22134
22561
|
this.db.prepare(
|
|
22135
|
-
`SELECT e.
|
|
22136
|
-
FROM
|
|
22137
|
-
|
|
22138
|
-
|
|
22562
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22563
|
+
FROM inspection_findings f
|
|
22564
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22565
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22566
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22567
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22568
|
+
ORDER BY e.started_at`
|
|
22139
22569
|
),
|
|
22140
22570
|
{ from: fromMs, to: toMs }
|
|
22141
22571
|
);
|
|
@@ -22950,63 +23380,159 @@ function purgeSampleData(db) {
|
|
|
22950
23380
|
function linkHost(input, hostId) {
|
|
22951
23381
|
return hostId ? { ...input, hostId } : input;
|
|
22952
23382
|
}
|
|
23383
|
+
function closeQuietly(db) {
|
|
23384
|
+
try {
|
|
23385
|
+
db.close();
|
|
23386
|
+
} catch {
|
|
23387
|
+
}
|
|
23388
|
+
}
|
|
22953
23389
|
function openWithPragmas(file2) {
|
|
22954
23390
|
const db = new DatabaseSync(file2);
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
23391
|
+
try {
|
|
23392
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
23393
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
23394
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
23395
|
+
} catch (err) {
|
|
23396
|
+
closeQuietly(db);
|
|
23397
|
+
throw err;
|
|
23398
|
+
}
|
|
22958
23399
|
return db;
|
|
22959
23400
|
}
|
|
22960
23401
|
function backupLegacyStore(file2) {
|
|
22961
23402
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
23403
|
+
renameSync2(file2, backup);
|
|
23404
|
+
tightenFile(backup);
|
|
23405
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23406
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22965
23407
|
}
|
|
22966
23408
|
return backup;
|
|
22967
23409
|
}
|
|
23410
|
+
function openAndInitialize(file2) {
|
|
23411
|
+
let db = openWithPragmas(file2);
|
|
23412
|
+
try {
|
|
23413
|
+
if (isForeignSqliteLineage(db)) {
|
|
23414
|
+
db.close();
|
|
23415
|
+
const backup = backupLegacyStore(file2);
|
|
23416
|
+
db = openWithPragmas(file2);
|
|
23417
|
+
akaWarn(
|
|
23418
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
23419
|
+
);
|
|
23420
|
+
}
|
|
23421
|
+
applyMigrations(db, file2);
|
|
23422
|
+
tightenPerms(file2);
|
|
23423
|
+
const policies = new SqlitePoliciesRepository(db);
|
|
23424
|
+
const installedPacks = new SqliteInstalledPacksRepository(db);
|
|
23425
|
+
const repositories = {
|
|
23426
|
+
events: new SqliteEventsRepository(db),
|
|
23427
|
+
findings: new SqliteFindingsRepository(db),
|
|
23428
|
+
policies,
|
|
23429
|
+
installedPacks,
|
|
23430
|
+
scanLedger: new SqliteScanLedgerRepository(db),
|
|
23431
|
+
exceptions: new SqliteExceptionsRepository(db),
|
|
23432
|
+
resolutions: new SqliteResolutionsRepository(db),
|
|
23433
|
+
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
23434
|
+
security: new SqliteSecurityRepository(db),
|
|
23435
|
+
detections: new SqliteDetectionsRepository(db),
|
|
23436
|
+
shares: new SqliteSharesRepository(db),
|
|
23437
|
+
policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
|
|
23438
|
+
inventory: new SqliteInventoryRepository(db),
|
|
23439
|
+
inventoryAssets: new SqliteInventoryAssetsRepository(db),
|
|
23440
|
+
projectFiles: new SqliteProjectFilesRepository(db),
|
|
23441
|
+
activity: new SqliteActivityRepository(db),
|
|
23442
|
+
sourceProject: new SqliteSourceProjectRepository(db),
|
|
23443
|
+
auditEvents: new SqliteAuditEventsRepository(db),
|
|
23444
|
+
classifiedData: new SqliteClassifiedDataRepository(db),
|
|
23445
|
+
inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
|
|
23446
|
+
inspectionFindings: new SqliteInspectionFindingsRepository(db),
|
|
23447
|
+
configInventory: new SqliteConfigInventoryRepository(db)
|
|
23448
|
+
};
|
|
23449
|
+
policies.seedDefaults();
|
|
23450
|
+
return { db, ...repositories };
|
|
23451
|
+
} catch (err) {
|
|
23452
|
+
closeQuietly(db);
|
|
23453
|
+
throw err;
|
|
23454
|
+
}
|
|
23455
|
+
}
|
|
22968
23456
|
function openLocalDatabase(dir) {
|
|
22969
23457
|
ensureDataDirSync(dir);
|
|
22970
23458
|
const file2 = join(dir, DB_FILENAME);
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
const projectFiles = new SqliteProjectFilesRepository(db);
|
|
22997
|
-
const activity = new SqliteActivityRepository(db);
|
|
22998
|
-
const sourceProject = new SqliteSourceProjectRepository(db);
|
|
22999
|
-
const auditEvents = new SqliteAuditEventsRepository(db);
|
|
23000
|
-
const classifiedData = new SqliteClassifiedDataRepository(db);
|
|
23001
|
-
const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
|
|
23002
|
-
const inspectionFindings = new SqliteInspectionFindingsRepository(db);
|
|
23003
|
-
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
23004
|
-
policies.seedDefaults();
|
|
23459
|
+
const {
|
|
23460
|
+
db,
|
|
23461
|
+
events,
|
|
23462
|
+
findings,
|
|
23463
|
+
policies,
|
|
23464
|
+
installedPacks,
|
|
23465
|
+
scanLedger,
|
|
23466
|
+
exceptions,
|
|
23467
|
+
resolutions,
|
|
23468
|
+
ruleProbeCache,
|
|
23469
|
+
security,
|
|
23470
|
+
detections,
|
|
23471
|
+
shares,
|
|
23472
|
+
policyCatalog,
|
|
23473
|
+
inventory,
|
|
23474
|
+
inventoryAssets,
|
|
23475
|
+
projectFiles,
|
|
23476
|
+
activity,
|
|
23477
|
+
sourceProject,
|
|
23478
|
+
auditEvents,
|
|
23479
|
+
classifiedData,
|
|
23480
|
+
inspectionDefinitions,
|
|
23481
|
+
inspectionFindings,
|
|
23482
|
+
configInventory
|
|
23483
|
+
} = openAndInitialize(file2);
|
|
23005
23484
|
function recordCapture(event, detected) {
|
|
23006
23485
|
failOpenTransaction(db, () => {
|
|
23007
|
-
events.insertEvent(event);
|
|
23008
23486
|
const sessionId = event.metadata?.sessionId;
|
|
23009
|
-
|
|
23487
|
+
if (sessionId) {
|
|
23488
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23489
|
+
}
|
|
23490
|
+
const auditEventId = captureId(
|
|
23491
|
+
sessionId ?? null,
|
|
23492
|
+
event.contentHash,
|
|
23493
|
+
event.metadata?.filePath ?? null
|
|
23494
|
+
);
|
|
23495
|
+
auditEvents.insertAuditEvent({
|
|
23496
|
+
id: auditEventId,
|
|
23497
|
+
eventType: event.kind,
|
|
23498
|
+
startedAt: event.occurredAt,
|
|
23499
|
+
parentId: sessionId,
|
|
23500
|
+
rootSessionId: sessionId,
|
|
23501
|
+
content: event.content,
|
|
23502
|
+
contentHash: event.contentHash,
|
|
23503
|
+
attributes: toCaptureAttributes(event)
|
|
23504
|
+
});
|
|
23505
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23506
|
+
for (const finding of detected) {
|
|
23507
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23508
|
+
continue;
|
|
23509
|
+
}
|
|
23510
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23511
|
+
auditEventId,
|
|
23512
|
+
finding.ruleId,
|
|
23513
|
+
finding.maskedMatch,
|
|
23514
|
+
finding.span.start,
|
|
23515
|
+
finding.span.end
|
|
23516
|
+
)) {
|
|
23517
|
+
continue;
|
|
23518
|
+
}
|
|
23519
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23520
|
+
let definitionId = definitionIds.get(key);
|
|
23521
|
+
if (!definitionId) {
|
|
23522
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23523
|
+
definitionIds.set(key, definitionId);
|
|
23524
|
+
}
|
|
23525
|
+
inspectionFindings.insertFinding({
|
|
23526
|
+
id: finding.id,
|
|
23527
|
+
auditEventId,
|
|
23528
|
+
inspectionDefinitionId: definitionId,
|
|
23529
|
+
span: finding.span,
|
|
23530
|
+
maskedMatch: finding.maskedMatch,
|
|
23531
|
+
actionTaken: finding.actionTaken,
|
|
23532
|
+
confidence: finding.confidence,
|
|
23533
|
+
findingKey: finding.findingKey ?? void 0
|
|
23534
|
+
});
|
|
23535
|
+
}
|
|
23010
23536
|
});
|
|
23011
23537
|
}
|
|
23012
23538
|
function ensureInventory(ctx) {
|
|
@@ -23154,14 +23680,18 @@ function openLocalDatabase(dir) {
|
|
|
23154
23680
|
};
|
|
23155
23681
|
}
|
|
23156
23682
|
|
|
23683
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23684
|
+
import { createHash as createHash3 } from "crypto";
|
|
23685
|
+
|
|
23157
23686
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23158
23687
|
import { createHmac, randomBytes } from "crypto";
|
|
23159
|
-
import {
|
|
23688
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
23160
23689
|
import { join as join2 } from "path";
|
|
23690
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
23161
23691
|
|
|
23162
23692
|
// ../../packages/persistence/src/local-layout.ts
|
|
23163
|
-
import {
|
|
23164
|
-
import {
|
|
23693
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23694
|
+
import { mkdir } from "fs/promises";
|
|
23165
23695
|
import { homedir } from "os";
|
|
23166
23696
|
import { join as join3 } from "path";
|
|
23167
23697
|
function defaultDataDir() {
|
|
@@ -23176,6 +23706,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23176
23706
|
function dbPath(base = defaultDataDir()) {
|
|
23177
23707
|
return join3(dataDir(base), "aka.db");
|
|
23178
23708
|
}
|
|
23709
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23710
|
+
ensureDataDirSync(dir);
|
|
23711
|
+
}
|
|
23179
23712
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23180
23713
|
const moves = [
|
|
23181
23714
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23183,19 +23716,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23183
23716
|
];
|
|
23184
23717
|
for (const { name, dest } of moves) {
|
|
23185
23718
|
try {
|
|
23186
|
-
|
|
23187
|
-
|
|
23188
|
-
|
|
23189
|
-
|
|
23190
|
-
}
|
|
23191
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23719
|
+
ensureDataDirSync(dest);
|
|
23720
|
+
const moved = join3(dest, name);
|
|
23721
|
+
renameSync3(join3(base, name), moved);
|
|
23722
|
+
tightenFile(moved);
|
|
23192
23723
|
} catch {
|
|
23193
23724
|
}
|
|
23194
23725
|
}
|
|
23195
23726
|
}
|
|
23196
23727
|
|
|
23197
23728
|
// ../../packages/persistence/src/settings.ts
|
|
23198
|
-
import { readFileSync as readFileSync2
|
|
23729
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23199
23730
|
import { join as join4 } from "path";
|
|
23200
23731
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23201
23732
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23217,10 +23748,8 @@ function applyOnboarding(answers2, base = defaultDataDir()) {
|
|
|
23217
23748
|
});
|
|
23218
23749
|
ensureDataDirSync(dir);
|
|
23219
23750
|
const file2 = join4(dir, "settings.json");
|
|
23220
|
-
|
|
23221
|
-
|
|
23222
|
-
`, { mode: DATA_FILE_MODE });
|
|
23223
|
-
renameSync4(tmp, file2);
|
|
23751
|
+
writeOwnerOnlyFileSync(file2, `${JSON.stringify(merged, null, 2)}
|
|
23752
|
+
`);
|
|
23224
23753
|
return merged;
|
|
23225
23754
|
}
|
|
23226
23755
|
function readJson(file2) {
|
|
@@ -23234,19 +23763,23 @@ function readJson(file2) {
|
|
|
23234
23763
|
}
|
|
23235
23764
|
|
|
23236
23765
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23237
|
-
import { existsSync as
|
|
23766
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
23238
23767
|
import { join as join5 } from "path";
|
|
23239
23768
|
var MARKER = "warn-era-capped";
|
|
23240
23769
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
23241
23770
|
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
23242
23771
|
const marker = join5(dataDir2, MARKER);
|
|
23243
|
-
if (
|
|
23772
|
+
if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
|
|
23244
23773
|
const capped = db.policies.capCategoryActions();
|
|
23245
|
-
|
|
23774
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23246
23775
|
`, { mode: DATA_FILE_MODE });
|
|
23247
23776
|
return { capped };
|
|
23248
23777
|
}
|
|
23249
23778
|
|
|
23779
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23780
|
+
import { existsSync as existsSync4 } from "fs";
|
|
23781
|
+
import { join as join6 } from "path";
|
|
23782
|
+
|
|
23250
23783
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
23251
23784
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
23252
23785
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -23297,6 +23830,12 @@ function resolveProvider() {
|
|
|
23297
23830
|
|
|
23298
23831
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23299
23832
|
function loadConfig(base = defaultDataDir()) {
|
|
23833
|
+
try {
|
|
23834
|
+
ensureLayoutDirSync(base);
|
|
23835
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23836
|
+
if (existsSync4(settingsFile)) tightenFile(settingsFile);
|
|
23837
|
+
} catch {
|
|
23838
|
+
}
|
|
23300
23839
|
migrateLegacyLayout(base);
|
|
23301
23840
|
const settings = readWorkspaceSettings(base);
|
|
23302
23841
|
return {
|
|
@@ -23319,7 +23858,7 @@ function resolveProviderSafe() {
|
|
|
23319
23858
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23320
23859
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23321
23860
|
import { homedir as homedir2 } from "os";
|
|
23322
|
-
import { basename as basename2, join as
|
|
23861
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23323
23862
|
|
|
23324
23863
|
// ../../packages/detections/src/egress/registry.ts
|
|
23325
23864
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -24030,21 +24569,18 @@ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].m
|
|
|
24030
24569
|
);
|
|
24031
24570
|
|
|
24032
24571
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24033
|
-
import { existsSync as
|
|
24034
|
-
import { basename, dirname, isAbsolute, join as
|
|
24572
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
|
|
24573
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
24035
24574
|
|
|
24036
24575
|
// ../../packages/plugin-sdk/src/events.ts
|
|
24037
|
-
import { createHash as
|
|
24038
|
-
|
|
24039
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
24040
|
-
import { createHash as createHash4 } from "crypto";
|
|
24576
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
24041
24577
|
|
|
24042
24578
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
24043
24579
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
24044
24580
|
|
|
24045
24581
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24046
|
-
import { mkdirSync as
|
|
24047
|
-
import { join as
|
|
24582
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
24583
|
+
import { join as join9 } from "path";
|
|
24048
24584
|
|
|
24049
24585
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
24050
24586
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -24062,8 +24598,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
24062
24598
|
|
|
24063
24599
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24064
24600
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24065
|
-
import { existsSync as
|
|
24066
|
-
import { basename as basename4, join as
|
|
24601
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
24602
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
24067
24603
|
|
|
24068
24604
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24069
24605
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -24072,8 +24608,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
24072
24608
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
24073
24609
|
|
|
24074
24610
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24075
|
-
import { mkdirSync as
|
|
24076
|
-
import { join as
|
|
24611
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
24612
|
+
import { join as join11 } from "path";
|
|
24077
24613
|
|
|
24078
24614
|
// src/onboard-posture.ts
|
|
24079
24615
|
function parsePosture(json2) {
|
|
@@ -24195,6 +24731,12 @@ if (rawHistorical !== void 0) {
|
|
|
24195
24731
|
fail(`invalid --historical "${rawHistorical}" (expected full or session-only)`);
|
|
24196
24732
|
else answers.historicalAccess = parsed.data;
|
|
24197
24733
|
}
|
|
24734
|
+
if (process.argv.includes("--model-judge-consent")) {
|
|
24735
|
+
answers.modelJudgeConsent = {
|
|
24736
|
+
acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24737
|
+
payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
|
|
24738
|
+
};
|
|
24739
|
+
}
|
|
24198
24740
|
var rawPosture = flags.get("posture");
|
|
24199
24741
|
var useFloor = process.argv.includes("--floor");
|
|
24200
24742
|
var recalibrate = process.argv.includes("--recalibrate");
|
|
@@ -24202,26 +24744,37 @@ if (useFloor && rawPosture !== void 0) fail("--floor and --posture are mutually
|
|
|
24202
24744
|
if (Object.keys(answers).length === 0 && rawPosture === void 0 && !useFloor) {
|
|
24203
24745
|
fail("nothing to save \u2014 pass --policy, --historical, --posture and/or --floor");
|
|
24204
24746
|
}
|
|
24747
|
+
var wroteConsent = answers.modelJudgeConsent !== void 0;
|
|
24748
|
+
var wrotePosture = answers.policy !== void 0 || answers.historicalAccess !== void 0;
|
|
24205
24749
|
if (Object.keys(answers).length > 0) {
|
|
24206
24750
|
try {
|
|
24207
24751
|
const settings = applyOnboarding(answers);
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24752
|
+
if (wrotePosture) {
|
|
24753
|
+
process.stdout.write(show("Got it \u2014 I'll look over Claude's recent work to tune things."));
|
|
24754
|
+
}
|
|
24755
|
+
if (wroteConsent) {
|
|
24756
|
+
process.stdout.write(
|
|
24757
|
+
show("Noted \u2014 I'll send findings to the model to rate them. You can revoke that anytime.")
|
|
24758
|
+
);
|
|
24759
|
+
}
|
|
24760
|
+
if (wrotePosture) {
|
|
24212
24761
|
try {
|
|
24213
|
-
const
|
|
24214
|
-
|
|
24215
|
-
|
|
24216
|
-
|
|
24217
|
-
|
|
24218
|
-
|
|
24219
|
-
|
|
24762
|
+
const dataDir2 = loadConfig().dataDir;
|
|
24763
|
+
const db = openLocalDatabase(dataDir2);
|
|
24764
|
+
try {
|
|
24765
|
+
const { capped } = capWarnEraEnforcementOnce(db, settings.policy, dataDir2);
|
|
24766
|
+
if (capped > 0) {
|
|
24767
|
+
process.stdout.write(
|
|
24768
|
+
show(
|
|
24769
|
+
`I eased ${String(capped)} detection level${capped === 1 ? "" : "s"} back to "warn" to match the new defaults \u2014 you can raise any of them again in this setup.`
|
|
24770
|
+
)
|
|
24771
|
+
);
|
|
24772
|
+
}
|
|
24773
|
+
} finally {
|
|
24774
|
+
db.close();
|
|
24220
24775
|
}
|
|
24221
|
-
}
|
|
24222
|
-
db.close();
|
|
24776
|
+
} catch {
|
|
24223
24777
|
}
|
|
24224
|
-
} catch {
|
|
24225
24778
|
}
|
|
24226
24779
|
} catch (err) {
|
|
24227
24780
|
fail(err instanceof Error ? err.message : "could not save your settings");
|