@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/commands/setup.md +101 -19
- package/package.json +3 -3
- package/scripts/apply-suppressions.js +3192 -487
- package/scripts/backfill.js +951 -455
- package/scripts/filescan.js +939 -449
- package/scripts/firstrun.js +895 -413
- package/scripts/intro.js +91 -24
- package/scripts/onboard.js +920 -416
- package/scripts/post-tool-use.js +932 -442
- package/scripts/pre-tool-use.js +937 -447
- package/scripts/query.js +895 -413
- package/scripts/reconcile.js +924 -436
- package/scripts/remediate.js +942 -452
- package/scripts/session-start.js +957 -475
- package/scripts/start-light.js +91 -24
- package/scripts/statusline.js +895 -413
- package/scripts/stop.js +132 -38
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +940 -450
package/scripts/remediate.js
CHANGED
|
@@ -497,7 +497,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
497
497
|
|
|
498
498
|
// ../../packages/persistence/src/database.ts
|
|
499
499
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
500
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
500
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
501
501
|
import { join, sep } from "path";
|
|
502
502
|
import { DatabaseSync } from "node:sqlite";
|
|
503
503
|
|
|
@@ -550,6 +550,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
550
550
|
{
|
|
551
551
|
tag: "0011_egress_writer",
|
|
552
552
|
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0012_handy_the_captain",
|
|
556
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0013_legacy_history_backfill_support",
|
|
560
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
tag: "0014_drop_legacy_events_findings",
|
|
564
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
553
565
|
}
|
|
554
566
|
];
|
|
555
567
|
|
|
@@ -15380,7 +15392,12 @@ var FindingFacets = external_exports.object({
|
|
|
15380
15392
|
severity: external_exports.array(FindingFacetItem),
|
|
15381
15393
|
subtype: external_exports.array(FindingFacetItem),
|
|
15382
15394
|
provider: external_exports.array(FindingFacetItem),
|
|
15383
|
-
action: external_exports.array(FindingFacetItem)
|
|
15395
|
+
action: external_exports.array(FindingFacetItem),
|
|
15396
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15397
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15398
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15399
|
+
// counted under no value.
|
|
15400
|
+
status: external_exports.array(FindingFacetItem)
|
|
15384
15401
|
}).meta({ id: "FindingFacets" });
|
|
15385
15402
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15386
15403
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15390,6 +15407,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15390
15407
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15391
15408
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15392
15409
|
action: external_exports.array(FindingAction).optional(),
|
|
15410
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15411
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15412
|
+
// one of the requested values.
|
|
15413
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15393
15414
|
q: external_exports.string().optional(),
|
|
15394
15415
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15395
15416
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15577,6 +15598,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15577
15598
|
parent_uuid: external_exports.string().optional(),
|
|
15578
15599
|
run_key: external_exports.string().optional()
|
|
15579
15600
|
}).catchall(external_exports.unknown());
|
|
15601
|
+
var CaptureAttributes = external_exports.object({
|
|
15602
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15603
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15604
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15605
|
+
source_tool: external_exports.string().optional(),
|
|
15606
|
+
file_path: external_exports.string().optional(),
|
|
15607
|
+
repo: external_exports.string().optional(),
|
|
15608
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15609
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15610
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15611
|
+
tool_name: external_exports.string().optional(),
|
|
15612
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15613
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15614
|
+
gitignored: external_exports.boolean().optional(),
|
|
15615
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15616
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15617
|
+
whole_file: external_exports.boolean().optional(),
|
|
15618
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15619
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15620
|
+
// trace id when telemetry is enabled.
|
|
15621
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15622
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15623
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15624
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15625
|
+
// authorized the bypass.
|
|
15626
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15627
|
+
}).catchall(external_exports.unknown());
|
|
15580
15628
|
var ToolCallInspection = external_exports.object({
|
|
15581
15629
|
ruleId: external_exports.string().min(1),
|
|
15582
15630
|
ruleName: external_exports.string(),
|
|
@@ -15663,7 +15711,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15663
15711
|
span: Span,
|
|
15664
15712
|
maskedMatch: external_exports.string(),
|
|
15665
15713
|
actionTaken: ActionTaken,
|
|
15666
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15714
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15715
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15716
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15717
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15718
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15719
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15720
|
+
findingKey: external_exports.string().optional(),
|
|
15721
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15722
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15723
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15724
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15725
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15667
15726
|
});
|
|
15668
15727
|
var InventoryContext = external_exports.object({
|
|
15669
15728
|
host: InventoryInput.optional(),
|
|
@@ -15865,6 +15924,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15865
15924
|
|
|
15866
15925
|
// ../../packages/schema/src/zod/event.ts
|
|
15867
15926
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15927
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15868
15928
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15869
15929
|
var EventMetadata = external_exports.object({
|
|
15870
15930
|
sessionId: external_exports.string().optional(),
|
|
@@ -16354,6 +16414,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16354
16414
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16355
16415
|
// `bundle.exceptions ?? []`.
|
|
16356
16416
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16417
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16418
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16419
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16420
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16421
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16422
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16357
16423
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16358
16424
|
fetchedAt: external_exports.iso.datetime()
|
|
16359
16425
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17153,6 +17219,15 @@ function groupActions(g) {
|
|
|
17153
17219
|
actionsCache.set(g, actions);
|
|
17154
17220
|
return actions;
|
|
17155
17221
|
}
|
|
17222
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17223
|
+
const statusSet = new Set(statuses);
|
|
17224
|
+
let sum = 0;
|
|
17225
|
+
for (const input of statusInputs) {
|
|
17226
|
+
if (input.count === void 0) return null;
|
|
17227
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17228
|
+
}
|
|
17229
|
+
return sum;
|
|
17230
|
+
}
|
|
17156
17231
|
function applyFindingFilters(groups, opts) {
|
|
17157
17232
|
let filtered = groups;
|
|
17158
17233
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17171,6 +17246,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17171
17246
|
const subtypeSet = new Set(opts.subtype);
|
|
17172
17247
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17173
17248
|
}
|
|
17249
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17250
|
+
const statusSet = new Set(opts.statuses);
|
|
17251
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17252
|
+
}
|
|
17174
17253
|
if (opts.q) {
|
|
17175
17254
|
const q = opts.q.toLowerCase();
|
|
17176
17255
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17192,6 +17271,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17192
17271
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17193
17272
|
providers: opts.providers,
|
|
17194
17273
|
actions: opts.actions,
|
|
17274
|
+
statuses: opts.statuses,
|
|
17195
17275
|
q: opts.q,
|
|
17196
17276
|
subtype: opts.subtype
|
|
17197
17277
|
});
|
|
@@ -17201,6 +17281,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17201
17281
|
}
|
|
17202
17282
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17203
17283
|
actions: opts.actions,
|
|
17284
|
+
statuses: opts.statuses,
|
|
17204
17285
|
q: opts.q,
|
|
17205
17286
|
subtype: opts.subtype,
|
|
17206
17287
|
severity: opts.severity
|
|
@@ -17211,6 +17292,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17211
17292
|
}
|
|
17212
17293
|
const forAction = applyFindingFilters(allGroups, {
|
|
17213
17294
|
providers: opts.providers,
|
|
17295
|
+
statuses: opts.statuses,
|
|
17214
17296
|
q: opts.q,
|
|
17215
17297
|
subtype: opts.subtype,
|
|
17216
17298
|
severity: opts.severity
|
|
@@ -17222,17 +17304,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17222
17304
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17223
17305
|
providers: opts.providers,
|
|
17224
17306
|
actions: opts.actions,
|
|
17307
|
+
statuses: opts.statuses,
|
|
17225
17308
|
q: opts.q,
|
|
17226
17309
|
severity: opts.severity
|
|
17227
17310
|
});
|
|
17228
17311
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17229
17312
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17313
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17314
|
+
providers: opts.providers,
|
|
17315
|
+
actions: opts.actions,
|
|
17316
|
+
q: opts.q,
|
|
17317
|
+
subtype: opts.subtype,
|
|
17318
|
+
severity: opts.severity
|
|
17319
|
+
});
|
|
17320
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17321
|
+
for (const g of forStatus) {
|
|
17322
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17323
|
+
}
|
|
17230
17324
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17231
17325
|
return {
|
|
17232
17326
|
severity: toItems(severityMap),
|
|
17233
17327
|
provider: toItems(providerMap),
|
|
17234
17328
|
action: toItems(actionMap),
|
|
17235
|
-
subtype: toItems(subtypeMap)
|
|
17329
|
+
subtype: toItems(subtypeMap),
|
|
17330
|
+
status: toItems(statusMap)
|
|
17236
17331
|
};
|
|
17237
17332
|
}
|
|
17238
17333
|
|
|
@@ -17267,10 +17362,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17267
17362
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17268
17363
|
|
|
17269
17364
|
// ../../packages/schema/src/zod/local.ts
|
|
17270
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17365
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17271
17366
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17272
17367
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17273
17368
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17369
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17370
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17371
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17372
|
+
});
|
|
17274
17373
|
var WorkspaceSettings = external_exports.object({
|
|
17275
17374
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17276
17375
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17286,37 +17385,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17286
17385
|
// Shares writes.
|
|
17287
17386
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17288
17387
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17289
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17388
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17389
|
+
// Records that the user consented to sending findings to the model API for
|
|
17390
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17391
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17392
|
+
// covers the current payload and must be re-granted.
|
|
17393
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17290
17394
|
});
|
|
17291
17395
|
function defaultWorkspaceSettings() {
|
|
17292
17396
|
return WorkspaceSettings.parse({});
|
|
17293
17397
|
}
|
|
17294
|
-
function toEventRow(event) {
|
|
17295
|
-
return {
|
|
17296
|
-
id: event.id,
|
|
17297
|
-
sourceTool: event.sourceTool,
|
|
17298
|
-
kind: event.kind,
|
|
17299
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17300
|
-
contentHash: event.contentHash,
|
|
17301
|
-
content: event.content,
|
|
17302
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17303
|
-
};
|
|
17304
|
-
}
|
|
17305
|
-
function toFindingRow(finding) {
|
|
17306
|
-
return {
|
|
17307
|
-
id: finding.id,
|
|
17308
|
-
eventId: finding.eventId,
|
|
17309
|
-
ruleId: finding.ruleId,
|
|
17310
|
-
category: finding.category,
|
|
17311
|
-
severity: finding.severity,
|
|
17312
|
-
spanStart: finding.span.start,
|
|
17313
|
-
spanEnd: finding.span.end,
|
|
17314
|
-
maskedMatch: finding.maskedMatch,
|
|
17315
|
-
actionTaken: finding.actionTaken,
|
|
17316
|
-
confidence: finding.confidence,
|
|
17317
|
-
findingKey: finding.findingKey ?? null
|
|
17318
|
-
};
|
|
17319
|
-
}
|
|
17320
17398
|
function toInventoryRow(input, id, now) {
|
|
17321
17399
|
return {
|
|
17322
17400
|
id,
|
|
@@ -17386,7 +17464,42 @@ function toInspectionFindingRow(input) {
|
|
|
17386
17464
|
spanEnd: input.span.end,
|
|
17387
17465
|
maskedMatch: input.maskedMatch,
|
|
17388
17466
|
actionTaken: input.actionTaken,
|
|
17389
|
-
confidence: input.confidence
|
|
17467
|
+
confidence: input.confidence,
|
|
17468
|
+
findingKey: input.findingKey ?? null,
|
|
17469
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17470
|
+
};
|
|
17471
|
+
}
|
|
17472
|
+
function toCaptureAttributes(event) {
|
|
17473
|
+
const metadata = event.metadata;
|
|
17474
|
+
return {
|
|
17475
|
+
source_tool: event.sourceTool,
|
|
17476
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17477
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17478
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17479
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17480
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17481
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17482
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17483
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17484
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17485
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17486
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17487
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17488
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17489
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17490
|
+
};
|
|
17491
|
+
}
|
|
17492
|
+
function captureDefinitionVersion(finding) {
|
|
17493
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17494
|
+
}
|
|
17495
|
+
function toCaptureDefinitionInput(finding) {
|
|
17496
|
+
return {
|
|
17497
|
+
ruleId: finding.ruleId,
|
|
17498
|
+
version: captureDefinitionVersion(finding),
|
|
17499
|
+
name: finding.ruleId,
|
|
17500
|
+
category: finding.category,
|
|
17501
|
+
severity: finding.severity,
|
|
17502
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17390
17503
|
};
|
|
17391
17504
|
}
|
|
17392
17505
|
|
|
@@ -17814,6 +17927,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17814
17927
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17815
17928
|
}
|
|
17816
17929
|
|
|
17930
|
+
// ../../packages/persistence/src/ids.ts
|
|
17931
|
+
import { createHash } from "crypto";
|
|
17932
|
+
function sha256Hex(input) {
|
|
17933
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17934
|
+
}
|
|
17935
|
+
function inventoryId(objectType, identityKey) {
|
|
17936
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17937
|
+
}
|
|
17938
|
+
function sourceProjectId(url2) {
|
|
17939
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17940
|
+
}
|
|
17941
|
+
function classifiedDataId(cls) {
|
|
17942
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17943
|
+
}
|
|
17944
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17945
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17946
|
+
}
|
|
17947
|
+
function llmCallId(sessionId, messageId) {
|
|
17948
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17949
|
+
}
|
|
17950
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17951
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17952
|
+
}
|
|
17953
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17954
|
+
return sha256Hex(
|
|
17955
|
+
canonicalIdentity([
|
|
17956
|
+
"inspection_finding",
|
|
17957
|
+
auditEventId,
|
|
17958
|
+
ruleId,
|
|
17959
|
+
String(spanStart),
|
|
17960
|
+
String(spanEnd)
|
|
17961
|
+
])
|
|
17962
|
+
);
|
|
17963
|
+
}
|
|
17964
|
+
var NO_SESSION = "no_session";
|
|
17965
|
+
var NO_PATH = "no_path";
|
|
17966
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17967
|
+
return sha256Hex(
|
|
17968
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17969
|
+
);
|
|
17970
|
+
}
|
|
17971
|
+
|
|
17817
17972
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17818
17973
|
function escapeLikePattern(s) {
|
|
17819
17974
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17910,39 +18065,98 @@ function evidenceExists(db, object2) {
|
|
|
17910
18065
|
return schemaObjectExists(db, "table", object2.name);
|
|
17911
18066
|
}
|
|
17912
18067
|
|
|
17913
|
-
// ../../packages/persistence/src/
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
|
|
18068
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18069
|
+
function allRows(stmt, params) {
|
|
18070
|
+
if (params === void 0) return stmt.all();
|
|
18071
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18072
|
+
return stmt.all(params);
|
|
17917
18073
|
}
|
|
17918
|
-
function
|
|
17919
|
-
|
|
18074
|
+
function getRow(stmt, params) {
|
|
18075
|
+
if (params === void 0) return stmt.get();
|
|
18076
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18077
|
+
return stmt.get(params);
|
|
17920
18078
|
}
|
|
17921
|
-
function
|
|
17922
|
-
return
|
|
18079
|
+
function intToBool(raw) {
|
|
18080
|
+
return raw === 1 || raw === true;
|
|
17923
18081
|
}
|
|
17924
|
-
function
|
|
17925
|
-
return
|
|
18082
|
+
function boolToInt(b) {
|
|
18083
|
+
return b ? 1 : 0;
|
|
17926
18084
|
}
|
|
17927
|
-
function
|
|
17928
|
-
|
|
18085
|
+
function bindParams(row) {
|
|
18086
|
+
const out = {};
|
|
18087
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18088
|
+
out[key] = value === void 0 ? null : value;
|
|
18089
|
+
}
|
|
18090
|
+
return out;
|
|
17929
18091
|
}
|
|
17930
|
-
function
|
|
17931
|
-
return
|
|
18092
|
+
function countScalar(db, sql, params) {
|
|
18093
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17932
18094
|
}
|
|
17933
|
-
function
|
|
17934
|
-
|
|
18095
|
+
function countBy(db, sql, params) {
|
|
18096
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18097
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18098
|
+
map2.set(row.k, row.n);
|
|
18099
|
+
}
|
|
18100
|
+
return map2;
|
|
17935
18101
|
}
|
|
17936
|
-
function
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
18102
|
+
function mapRowsTolerant(rows, map2) {
|
|
18103
|
+
const out = [];
|
|
18104
|
+
for (const row of rows) {
|
|
18105
|
+
try {
|
|
18106
|
+
out.push(map2(row));
|
|
18107
|
+
} catch {
|
|
18108
|
+
}
|
|
18109
|
+
}
|
|
18110
|
+
return out;
|
|
18111
|
+
}
|
|
18112
|
+
|
|
18113
|
+
// ../../packages/persistence/src/paths.ts
|
|
18114
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18115
|
+
var DATA_DIR_MODE = 448;
|
|
18116
|
+
var DATA_FILE_MODE = 384;
|
|
18117
|
+
var DB_FILENAME = "aka.db";
|
|
18118
|
+
function chmodBestEffort(path, mode) {
|
|
18119
|
+
try {
|
|
18120
|
+
chmodSync(path, mode);
|
|
18121
|
+
} catch {
|
|
18122
|
+
}
|
|
18123
|
+
}
|
|
18124
|
+
function tightenDir(dir) {
|
|
18125
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18126
|
+
}
|
|
18127
|
+
function ensureDataDirSync(dir) {
|
|
18128
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18129
|
+
tightenDir(dir);
|
|
18130
|
+
}
|
|
18131
|
+
function dbSidecars(file2) {
|
|
18132
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18133
|
+
}
|
|
18134
|
+
function tightenFile(file2) {
|
|
18135
|
+
try {
|
|
18136
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18137
|
+
} catch {
|
|
18138
|
+
}
|
|
18139
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18140
|
+
}
|
|
18141
|
+
function tightenPerms(file2) {
|
|
18142
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18143
|
+
}
|
|
18144
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
18145
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
18146
|
+
try {
|
|
18147
|
+
rmSync(tmp, { force: true });
|
|
18148
|
+
} catch {
|
|
18149
|
+
}
|
|
18150
|
+
try {
|
|
18151
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18152
|
+
renameSync(tmp, file2);
|
|
18153
|
+
} finally {
|
|
18154
|
+
try {
|
|
18155
|
+
rmSync(tmp, { force: true });
|
|
18156
|
+
} catch {
|
|
18157
|
+
}
|
|
18158
|
+
}
|
|
18159
|
+
tightenFile(file2);
|
|
17946
18160
|
}
|
|
17947
18161
|
|
|
17948
18162
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17956,7 +18170,8 @@ function createdIndexName(statement) {
|
|
|
17956
18170
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17957
18171
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17958
18172
|
}
|
|
17959
|
-
|
|
18173
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18174
|
+
function applyMigrations(db, file2) {
|
|
17960
18175
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17961
18176
|
db.exec(
|
|
17962
18177
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17970,6 +18185,7 @@ function applyMigrations(db) {
|
|
|
17970
18185
|
);
|
|
17971
18186
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17972
18187
|
if (applied.has(migration.tag)) continue;
|
|
18188
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17973
18189
|
const evidence = evidenceObjects(migration.sql);
|
|
17974
18190
|
const present2 = evidence.filter((o) => evidenceExists(db, o));
|
|
17975
18191
|
if (present2.length > 0 && present2.length < evidence.length) {
|
|
@@ -18014,7 +18230,6 @@ function applyMigrations(db) {
|
|
|
18014
18230
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18015
18231
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18016
18232
|
}
|
|
18017
|
-
ensureSyncedAtColumn(db, "events");
|
|
18018
18233
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18019
18234
|
ensureScanLedgerTable(db);
|
|
18020
18235
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18022,6 +18237,47 @@ function applyMigrations(db) {
|
|
|
18022
18237
|
ensureWriteGateTrigger(db);
|
|
18023
18238
|
ensureTokenUsageColumns(db);
|
|
18024
18239
|
reconcileSourceProjectIds(db);
|
|
18240
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18241
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18242
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18245
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18246
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18247
|
+
if (!migration) return;
|
|
18248
|
+
if (file2) {
|
|
18249
|
+
try {
|
|
18250
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18251
|
+
} catch (error51) {
|
|
18252
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18253
|
+
return;
|
|
18254
|
+
}
|
|
18255
|
+
}
|
|
18256
|
+
try {
|
|
18257
|
+
withTransaction(
|
|
18258
|
+
db,
|
|
18259
|
+
() => {
|
|
18260
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18261
|
+
if (alreadyDropped) return;
|
|
18262
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18263
|
+
db.exec(statement);
|
|
18264
|
+
}
|
|
18265
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18266
|
+
migration.tag,
|
|
18267
|
+
Date.now()
|
|
18268
|
+
);
|
|
18269
|
+
},
|
|
18270
|
+
"IMMEDIATE"
|
|
18271
|
+
);
|
|
18272
|
+
} catch (error51) {
|
|
18273
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18274
|
+
}
|
|
18275
|
+
}
|
|
18276
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18277
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18278
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18279
|
+
tightenFile(backup);
|
|
18280
|
+
return backup;
|
|
18025
18281
|
}
|
|
18026
18282
|
var TOKEN_USAGE_COLUMNS = [
|
|
18027
18283
|
{
|
|
@@ -18050,6 +18306,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18050
18306
|
}
|
|
18051
18307
|
];
|
|
18052
18308
|
function ensureTokenUsageColumns(db) {
|
|
18309
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18053
18310
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18054
18311
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18055
18312
|
if (!existing.has(column.name)) {
|
|
@@ -18115,11 +18372,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18115
18372
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18116
18373
|
}
|
|
18117
18374
|
}
|
|
18375
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18376
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18377
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18378
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18379
|
+
return row?.lastRowid ?? 0;
|
|
18380
|
+
}
|
|
18381
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18382
|
+
db.prepare(
|
|
18383
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18384
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18385
|
+
).run(source, lastRowid);
|
|
18386
|
+
}
|
|
18387
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18388
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18389
|
+
let processed = 0;
|
|
18390
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18391
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18392
|
+
if (rows.length === 0) return true;
|
|
18393
|
+
withTransaction(
|
|
18394
|
+
db,
|
|
18395
|
+
() => {
|
|
18396
|
+
handleRows(rows);
|
|
18397
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18398
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18399
|
+
},
|
|
18400
|
+
"IMMEDIATE"
|
|
18401
|
+
);
|
|
18402
|
+
processed += rows.length;
|
|
18403
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18404
|
+
}
|
|
18405
|
+
return false;
|
|
18406
|
+
}
|
|
18407
|
+
function parseLegacyEventMetadata(raw) {
|
|
18408
|
+
if (raw === null) return void 0;
|
|
18409
|
+
try {
|
|
18410
|
+
return JSON.parse(raw);
|
|
18411
|
+
} catch {
|
|
18412
|
+
return void 0;
|
|
18413
|
+
}
|
|
18414
|
+
}
|
|
18415
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18416
|
+
return JSON.stringify(
|
|
18417
|
+
toCaptureAttributes({
|
|
18418
|
+
id: row.id,
|
|
18419
|
+
sourceTool: row.sourceTool,
|
|
18420
|
+
kind: row.kind,
|
|
18421
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18422
|
+
contentHash: row.contentHash,
|
|
18423
|
+
content: row.content,
|
|
18424
|
+
metadata: row.metadata
|
|
18425
|
+
})
|
|
18426
|
+
);
|
|
18427
|
+
}
|
|
18428
|
+
function copyLegacyEvents(db) {
|
|
18429
|
+
const selectStmt = db.prepare(
|
|
18430
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18431
|
+
content_hash AS contentHash, content, metadata
|
|
18432
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18433
|
+
);
|
|
18434
|
+
const insertStmt = db.prepare(
|
|
18435
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18436
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18437
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18438
|
+
);
|
|
18439
|
+
const stubRootStmt = db.prepare(
|
|
18440
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18441
|
+
);
|
|
18442
|
+
return drainLegacyTable(
|
|
18443
|
+
db,
|
|
18444
|
+
"events",
|
|
18445
|
+
selectStmt,
|
|
18446
|
+
(rows) => {
|
|
18447
|
+
for (const row of rows) {
|
|
18448
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18449
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18450
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18451
|
+
insertStmt.run(
|
|
18452
|
+
bindParams({
|
|
18453
|
+
id: row.id,
|
|
18454
|
+
parentId: sessionId,
|
|
18455
|
+
rootSessionId: sessionId,
|
|
18456
|
+
eventType: row.kind,
|
|
18457
|
+
startedAt: row.occurredAt,
|
|
18458
|
+
content: row.content,
|
|
18459
|
+
contentHash: row.contentHash,
|
|
18460
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18461
|
+
})
|
|
18462
|
+
);
|
|
18463
|
+
}
|
|
18464
|
+
}
|
|
18465
|
+
);
|
|
18466
|
+
}
|
|
18467
|
+
function copyLegacyFindings(db) {
|
|
18468
|
+
const selectStmt = db.prepare(
|
|
18469
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18470
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18471
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18472
|
+
first_detected_at AS firstDetectedAt
|
|
18473
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18474
|
+
);
|
|
18475
|
+
const definitionStmt = db.prepare(
|
|
18476
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18477
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18478
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18479
|
+
);
|
|
18480
|
+
const findingStmt = db.prepare(
|
|
18481
|
+
`INSERT INTO inspection_findings
|
|
18482
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18483
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18484
|
+
finding_key, first_detected_at)
|
|
18485
|
+
VALUES
|
|
18486
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18487
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18488
|
+
:findingKey, :firstDetectedAt)
|
|
18489
|
+
ON CONFLICT(id) DO NOTHING
|
|
18490
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18491
|
+
first_detected_at = CASE
|
|
18492
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18493
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18494
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18495
|
+
END`
|
|
18496
|
+
);
|
|
18497
|
+
return drainLegacyTable(
|
|
18498
|
+
db,
|
|
18499
|
+
"findings",
|
|
18500
|
+
selectStmt,
|
|
18501
|
+
(rows) => {
|
|
18502
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18503
|
+
for (const row of rows) {
|
|
18504
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18505
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18506
|
+
if (definitionId === void 0) {
|
|
18507
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18508
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18509
|
+
definitionStmt.run(
|
|
18510
|
+
bindParams({
|
|
18511
|
+
id: definitionId,
|
|
18512
|
+
ruleId: row.ruleId,
|
|
18513
|
+
name: row.ruleId,
|
|
18514
|
+
category: row.category,
|
|
18515
|
+
severity: row.severity,
|
|
18516
|
+
definition: "",
|
|
18517
|
+
version: version2
|
|
18518
|
+
})
|
|
18519
|
+
);
|
|
18520
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18521
|
+
}
|
|
18522
|
+
findingStmt.run(
|
|
18523
|
+
bindParams({
|
|
18524
|
+
id: row.id,
|
|
18525
|
+
auditEventId: row.eventId,
|
|
18526
|
+
inspectionDefinitionId: definitionId,
|
|
18527
|
+
spanStart: row.spanStart,
|
|
18528
|
+
spanEnd: row.spanEnd,
|
|
18529
|
+
maskedMatch: row.maskedMatch,
|
|
18530
|
+
actionTaken: row.actionTaken,
|
|
18531
|
+
confidence: row.confidence,
|
|
18532
|
+
findingKey: row.findingKey,
|
|
18533
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18534
|
+
})
|
|
18535
|
+
);
|
|
18536
|
+
}
|
|
18537
|
+
}
|
|
18538
|
+
);
|
|
18539
|
+
}
|
|
18540
|
+
function runLegacyHistoryBackfill(db) {
|
|
18541
|
+
try {
|
|
18542
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18543
|
+
if (!eventsCaughtUp) return false;
|
|
18544
|
+
return copyLegacyFindings(db);
|
|
18545
|
+
} catch (error51) {
|
|
18546
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18547
|
+
return false;
|
|
18548
|
+
}
|
|
18549
|
+
}
|
|
18118
18550
|
function isForeignSqliteLineage(db) {
|
|
18119
18551
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18120
18552
|
return columnNames(db, "events").includes("tenant_id");
|
|
18121
18553
|
}
|
|
18122
18554
|
function ensureSyncedAtColumn(db, table2) {
|
|
18555
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18123
18556
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18124
18557
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18125
18558
|
}
|
|
@@ -18140,6 +18573,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18140
18573
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18141
18574
|
)`);
|
|
18142
18575
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18576
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18143
18577
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18144
18578
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18145
18579
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18167,30 +18601,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18167
18601
|
)`);
|
|
18168
18602
|
}
|
|
18169
18603
|
|
|
18170
|
-
// ../../packages/persistence/src/paths.ts
|
|
18171
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18172
|
-
var DATA_DIR_MODE = 448;
|
|
18173
|
-
var DATA_FILE_MODE = 384;
|
|
18174
|
-
var DB_FILENAME = "aka.db";
|
|
18175
|
-
function ensureDataDirSync(dir) {
|
|
18176
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18177
|
-
try {
|
|
18178
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18179
|
-
} catch {
|
|
18180
|
-
}
|
|
18181
|
-
}
|
|
18182
|
-
function walSidecars(file2) {
|
|
18183
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18184
|
-
}
|
|
18185
|
-
function tightenPerms(file2) {
|
|
18186
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18187
|
-
try {
|
|
18188
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18189
|
-
} catch {
|
|
18190
|
-
}
|
|
18191
|
-
}
|
|
18192
|
-
}
|
|
18193
|
-
|
|
18194
18604
|
// ../../packages/persistence/src/internal/json.ts
|
|
18195
18605
|
function safeJson(s, fallback) {
|
|
18196
18606
|
if (s == null) return fallback;
|
|
@@ -18210,51 +18620,6 @@ function parseJsonObject(s) {
|
|
|
18210
18620
|
return void 0;
|
|
18211
18621
|
}
|
|
18212
18622
|
|
|
18213
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18214
|
-
function allRows(stmt, params) {
|
|
18215
|
-
if (params === void 0) return stmt.all();
|
|
18216
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18217
|
-
return stmt.all(params);
|
|
18218
|
-
}
|
|
18219
|
-
function getRow(stmt, params) {
|
|
18220
|
-
if (params === void 0) return stmt.get();
|
|
18221
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18222
|
-
return stmt.get(params);
|
|
18223
|
-
}
|
|
18224
|
-
function intToBool(raw) {
|
|
18225
|
-
return raw === 1 || raw === true;
|
|
18226
|
-
}
|
|
18227
|
-
function boolToInt(b) {
|
|
18228
|
-
return b ? 1 : 0;
|
|
18229
|
-
}
|
|
18230
|
-
function bindParams(row) {
|
|
18231
|
-
const out = {};
|
|
18232
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18233
|
-
out[key] = value === void 0 ? null : value;
|
|
18234
|
-
}
|
|
18235
|
-
return out;
|
|
18236
|
-
}
|
|
18237
|
-
function countScalar(db, sql, params) {
|
|
18238
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18239
|
-
}
|
|
18240
|
-
function countBy(db, sql, params) {
|
|
18241
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18242
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18243
|
-
map2.set(row.k, row.n);
|
|
18244
|
-
}
|
|
18245
|
-
return map2;
|
|
18246
|
-
}
|
|
18247
|
-
function mapRowsTolerant(rows, map2) {
|
|
18248
|
-
const out = [];
|
|
18249
|
-
for (const row of rows) {
|
|
18250
|
-
try {
|
|
18251
|
-
out.push(map2(row));
|
|
18252
|
-
} catch {
|
|
18253
|
-
}
|
|
18254
|
-
}
|
|
18255
|
-
return out;
|
|
18256
|
-
}
|
|
18257
|
-
|
|
18258
18623
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18259
18624
|
var DAY_MS = 864e5;
|
|
18260
18625
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18871,6 +19236,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18871
19236
|
})
|
|
18872
19237
|
);
|
|
18873
19238
|
}
|
|
19239
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19240
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19241
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19242
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19243
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19244
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19245
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19246
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19247
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19248
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19249
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19250
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19251
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19252
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19253
|
+
}
|
|
18874
19254
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18875
19255
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18876
19256
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19332,8 +19712,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19332
19712
|
)
|
|
19333
19713
|
);
|
|
19334
19714
|
}
|
|
19335
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19336
|
-
//
|
|
19715
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19716
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19717
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19718
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19719
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19720
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19721
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19722
|
+
// to the old findings⋈events one.
|
|
19337
19723
|
countFindingsLast30d(ruleIds) {
|
|
19338
19724
|
if (ruleIds.length === 0) return 0;
|
|
19339
19725
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19341,8 +19727,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19341
19727
|
return countScalar(
|
|
19342
19728
|
this.db,
|
|
19343
19729
|
`SELECT count(*) AS n
|
|
19344
|
-
FROM
|
|
19345
|
-
|
|
19730
|
+
FROM inspection_findings f
|
|
19731
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19732
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19733
|
+
WHERE e.started_at >= ?
|
|
19734
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19735
|
+
AND d.rule_id IN (${inClause})`,
|
|
19346
19736
|
[since, ...ruleIds]
|
|
19347
19737
|
);
|
|
19348
19738
|
}
|
|
@@ -19352,35 +19742,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19352
19742
|
var SqliteEventsRepository = class {
|
|
19353
19743
|
constructor(db) {
|
|
19354
19744
|
this.db = db;
|
|
19355
|
-
this.insertStmt = db.prepare(
|
|
19356
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19357
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19358
|
-
);
|
|
19359
19745
|
}
|
|
19360
19746
|
db;
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19364
|
-
this.insertStmt.run(
|
|
19365
|
-
bindParams({
|
|
19366
|
-
id: row.id,
|
|
19367
|
-
sourceTool: row.sourceTool,
|
|
19368
|
-
kind: row.kind,
|
|
19369
|
-
occurredAt: row.occurredAt,
|
|
19370
|
-
contentHash: row.contentHash,
|
|
19371
|
-
content: row.content,
|
|
19372
|
-
metadata: row.metadata
|
|
19373
|
-
})
|
|
19374
|
-
);
|
|
19375
|
-
}
|
|
19376
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19377
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19378
|
-
// never duplicates findings.
|
|
19747
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19748
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19749
|
+
// scan never duplicates findings.
|
|
19379
19750
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19380
19751
|
// async EventsReadPort contract.
|
|
19752
|
+
//
|
|
19753
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19754
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19755
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19756
|
+
// index-friendly rather than walking rows that can never match.
|
|
19381
19757
|
contentHashes() {
|
|
19382
19758
|
const rows = allRows(
|
|
19383
|
-
this.db.prepare(
|
|
19759
|
+
this.db.prepare(
|
|
19760
|
+
`SELECT content_hash FROM audit_events
|
|
19761
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19762
|
+
)
|
|
19384
19763
|
);
|
|
19385
19764
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19386
19765
|
}
|
|
@@ -19716,17 +20095,20 @@ function parseExceptionRow(row) {
|
|
|
19716
20095
|
}
|
|
19717
20096
|
|
|
19718
20097
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19719
|
-
function
|
|
20098
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19720
20099
|
return `(
|
|
19721
|
-
SELECT fr
|
|
20100
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19722
20101
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19723
20102
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19724
20103
|
LIMIT 1
|
|
19725
20104
|
)`;
|
|
19726
20105
|
}
|
|
20106
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20107
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20108
|
+
}
|
|
19727
20109
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19728
|
-
SELECT finding_key, status FROM (
|
|
19729
|
-
SELECT fr.finding_key, fr.status,
|
|
20110
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20111
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19730
20112
|
ROW_NUMBER() OVER (
|
|
19731
20113
|
PARTITION BY fr.finding_key
|
|
19732
20114
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19753,68 +20135,21 @@ var DAY_MS3 = 864e5;
|
|
|
19753
20135
|
var SqliteFindingsRepository = class {
|
|
19754
20136
|
constructor(db) {
|
|
19755
20137
|
this.db = db;
|
|
19756
|
-
this.insertStmt = db.prepare(
|
|
19757
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19758
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19759
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19760
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19761
|
-
event_id = excluded.event_id,
|
|
19762
|
-
category = excluded.category,
|
|
19763
|
-
severity = excluded.severity,
|
|
19764
|
-
span_start = excluded.span_start,
|
|
19765
|
-
span_end = excluded.span_end,
|
|
19766
|
-
masked_match = excluded.masked_match,
|
|
19767
|
-
action_taken = excluded.action_taken,
|
|
19768
|
-
confidence = excluded.confidence`
|
|
19769
|
-
);
|
|
19770
|
-
this.sessionDupStmt = db.prepare(
|
|
19771
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19772
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19773
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19774
|
-
LIMIT 1`
|
|
19775
|
-
);
|
|
19776
20138
|
}
|
|
19777
20139
|
db;
|
|
19778
|
-
insertStmt;
|
|
19779
|
-
sessionDupStmt;
|
|
19780
|
-
insertFindings(findings, scope = {}) {
|
|
19781
|
-
for (const finding of findings) {
|
|
19782
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19783
|
-
const row = toFindingRow(finding);
|
|
19784
|
-
this.insertStmt.run({
|
|
19785
|
-
id: row.id,
|
|
19786
|
-
eventId: row.eventId,
|
|
19787
|
-
ruleId: row.ruleId,
|
|
19788
|
-
category: row.category,
|
|
19789
|
-
severity: row.severity,
|
|
19790
|
-
spanStart: row.spanStart,
|
|
19791
|
-
spanEnd: row.spanEnd,
|
|
19792
|
-
maskedMatch: row.maskedMatch,
|
|
19793
|
-
actionTaken: row.actionTaken,
|
|
19794
|
-
confidence: row.confidence,
|
|
19795
|
-
findingKey: row.findingKey ?? null
|
|
19796
|
-
});
|
|
19797
|
-
}
|
|
19798
|
-
}
|
|
19799
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19800
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19801
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19802
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19803
|
-
const hit = this.sessionDupStmt.get({
|
|
19804
|
-
ruleId: finding.ruleId,
|
|
19805
|
-
maskedMatch: finding.maskedMatch,
|
|
19806
|
-
sessionId
|
|
19807
|
-
});
|
|
19808
|
-
return hit !== void 0;
|
|
19809
|
-
}
|
|
19810
20140
|
recentFindings(opts) {
|
|
19811
20141
|
const limit = opts?.limit ?? 50;
|
|
19812
20142
|
const rows = allRows(
|
|
19813
20143
|
this.db.prepare(
|
|
19814
|
-
`SELECT f.id, f.event_id,
|
|
19815
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19816
|
-
|
|
19817
|
-
|
|
20144
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20145
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20146
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20147
|
+
e.event_type AS kind
|
|
20148
|
+
FROM inspection_findings f
|
|
20149
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20150
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20151
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20152
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19818
20153
|
LIMIT :limit`
|
|
19819
20154
|
),
|
|
19820
20155
|
{ limit }
|
|
@@ -19836,25 +20171,34 @@ var SqliteFindingsRepository = class {
|
|
|
19836
20171
|
);
|
|
19837
20172
|
}
|
|
19838
20173
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19839
|
-
* session-stamped
|
|
20174
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19840
20175
|
* page can label its findings link without the grouped pipeline. */
|
|
19841
20176
|
sessionFindingsCount(sessionId) {
|
|
19842
20177
|
if (!sessionId) return Promise.resolve(0);
|
|
19843
20178
|
return Promise.resolve(
|
|
19844
20179
|
countScalar(
|
|
19845
20180
|
this.db,
|
|
19846
|
-
`SELECT count(*) AS n FROM
|
|
19847
|
-
JOIN
|
|
19848
|
-
WHERE
|
|
20181
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20182
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20183
|
+
WHERE e.root_session_id = :sessionId
|
|
20184
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19849
20185
|
{ sessionId }
|
|
19850
20186
|
)
|
|
19851
20187
|
);
|
|
19852
20188
|
}
|
|
19853
|
-
/** Per-rule transcript firing tally for one session —
|
|
19854
|
-
*
|
|
19855
|
-
*
|
|
19856
|
-
*
|
|
19857
|
-
*
|
|
20189
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20190
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20191
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20192
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20193
|
+
* tally with the deduped groups it lists.
|
|
20194
|
+
*
|
|
20195
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20196
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20197
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20198
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20199
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20200
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20201
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19858
20202
|
sessionFirings(sessionId) {
|
|
19859
20203
|
return Object.fromEntries(
|
|
19860
20204
|
countBy(
|
|
@@ -19864,18 +20208,25 @@ var SqliteFindingsRepository = class {
|
|
|
19864
20208
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19865
20209
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19866
20210
|
WHERE e.root_session_id = :sessionId
|
|
20211
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19867
20212
|
GROUP BY d.rule_id`,
|
|
19868
20213
|
{ sessionId }
|
|
19869
20214
|
)
|
|
19870
20215
|
);
|
|
19871
20216
|
}
|
|
19872
20217
|
/**
|
|
19873
|
-
* Grouped findings for the dashboard — joins
|
|
19874
|
-
* toolName from
|
|
19875
|
-
*
|
|
20218
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20219
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20220
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20221
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20222
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20223
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20224
|
+
* severity then recency. Filtering
|
|
19876
20225
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19877
20226
|
* reflect the full filtered set; `items` is the requested
|
|
19878
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20227
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20228
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20229
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19879
20230
|
*
|
|
19880
20231
|
* Two reads, neither of which materializes a row per finding:
|
|
19881
20232
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19888,10 +20239,11 @@ var SqliteFindingsRepository = class {
|
|
|
19888
20239
|
* rule is ever restated in SQL.
|
|
19889
20240
|
*/
|
|
19890
20241
|
listGroupedFindings(query) {
|
|
19891
|
-
const sessionPredicate = query.sessionId ? `
|
|
20242
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20243
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19892
20244
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19893
20245
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19894
|
-
predicate
|
|
20246
|
+
predicate,
|
|
19895
20247
|
params: sessionParams
|
|
19896
20248
|
});
|
|
19897
20249
|
const rows = allRows(
|
|
@@ -19899,24 +20251,26 @@ var SqliteFindingsRepository = class {
|
|
|
19899
20251
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19900
20252
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19901
20253
|
FROM (
|
|
19902
|
-
SELECT f.id AS id,
|
|
19903
|
-
|
|
20254
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20255
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19904
20256
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19905
|
-
e.
|
|
19906
|
-
json_extract(e.
|
|
19907
|
-
json_extract(e.
|
|
19908
|
-
json_extract(e.
|
|
19909
|
-
e.
|
|
20257
|
+
e.started_at AS occurred_at,
|
|
20258
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20259
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20260
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20261
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20262
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19910
20263
|
latest.status AS latest_status,
|
|
19911
20264
|
ROW_NUMBER() OVER (
|
|
19912
|
-
PARTITION BY
|
|
19913
|
-
ORDER BY e.
|
|
20265
|
+
PARTITION BY d.rule_id
|
|
20266
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19914
20267
|
) AS rn
|
|
19915
|
-
FROM
|
|
19916
|
-
JOIN
|
|
20268
|
+
FROM inspection_findings f
|
|
20269
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20270
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19917
20271
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19918
20272
|
ON latest.finding_key = f.finding_key
|
|
19919
|
-
${
|
|
20273
|
+
${predicate}
|
|
19920
20274
|
)
|
|
19921
20275
|
WHERE rn <= :cap
|
|
19922
20276
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19943,17 +20297,29 @@ var SqliteFindingsRepository = class {
|
|
|
19943
20297
|
severity: query.severity,
|
|
19944
20298
|
providers: query.provider,
|
|
19945
20299
|
actions: query.action,
|
|
20300
|
+
statuses: query.status,
|
|
19946
20301
|
subtype: query.subtype,
|
|
19947
20302
|
q: query.q
|
|
19948
20303
|
};
|
|
19949
20304
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19950
20305
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20306
|
+
const statusFilter = query.status ?? [];
|
|
19951
20307
|
const totals = {
|
|
19952
|
-
findings: sorted.reduce((acc, g) =>
|
|
20308
|
+
findings: sorted.reduce((acc, g) => {
|
|
20309
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20310
|
+
const agg = aggregates.get(g.id);
|
|
20311
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20312
|
+
}, 0),
|
|
19953
20313
|
groups: sorted.length
|
|
19954
20314
|
};
|
|
19955
20315
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19956
|
-
const
|
|
20316
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20317
|
+
const items = sorted.slice(0, limit).map(
|
|
20318
|
+
(g) => statusSet ? {
|
|
20319
|
+
...g,
|
|
20320
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20321
|
+
} : g
|
|
20322
|
+
);
|
|
19957
20323
|
return Promise.resolve({
|
|
19958
20324
|
totals,
|
|
19959
20325
|
facets,
|
|
@@ -19967,45 +20333,62 @@ var SqliteFindingsRepository = class {
|
|
|
19967
20333
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19968
20334
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19969
20335
|
*
|
|
19970
|
-
*
|
|
19971
|
-
*
|
|
19972
|
-
*
|
|
19973
|
-
* status
|
|
19974
|
-
*
|
|
19975
|
-
*
|
|
20336
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20337
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20338
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20339
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20340
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20341
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20342
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20343
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20344
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20345
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20346
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19976
20347
|
* a group's row stays small however many findings it holds.
|
|
19977
20348
|
*
|
|
19978
20349
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19979
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19980
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19981
|
-
* that is a string proportional to the store (~8MB over
|
|
19982
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19983
|
-
* match an instance outside the preview, which searching
|
|
19984
|
-
* would silently lose, so it is fetched only when the
|
|
19985
|
-
* carries a `q`.
|
|
20350
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20351
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20352
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20353
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20354
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20355
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20356
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20357
|
+
* path repeating across tuples.)
|
|
19986
20358
|
*/
|
|
19987
20359
|
groupAggregates(withSearchText, scope) {
|
|
19988
|
-
const
|
|
19989
|
-
group_concat(DISTINCT json_extract(e.
|
|
19990
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20360
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20361
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20362
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19991
20363
|
const rows = this.db.prepare(
|
|
19992
|
-
`SELECT
|
|
19993
|
-
|
|
19994
|
-
max(
|
|
19995
|
-
group_concat(
|
|
19996
|
-
group_concat(
|
|
19997
|
-
group_concat(
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20364
|
+
`SELECT rule_id,
|
|
20365
|
+
sum(tuple_count) AS instance_count,
|
|
20366
|
+
max(latest_at) AS latest_at,
|
|
20367
|
+
group_concat(source_tools) AS source_tools,
|
|
20368
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20369
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20370
|
+
group_concat(repos) AS repos,
|
|
20371
|
+
group_concat(files) AS files,
|
|
20372
|
+
group_concat(tool_names) AS tool_names
|
|
20373
|
+
FROM (
|
|
20374
|
+
SELECT d.rule_id AS rule_id,
|
|
20375
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20376
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20377
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20378
|
+
count(*) AS tuple_count,
|
|
20379
|
+
max(e.started_at) AS latest_at,
|
|
20380
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20381
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20382
|
+
${innerSearchColumns}
|
|
20383
|
+
FROM inspection_findings f
|
|
20384
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20385
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20386
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20387
|
+
ON latest.finding_key = f.finding_key
|
|
20388
|
+
${scope.predicate}
|
|
20389
|
+
GROUP BY d.rule_id, status_tuple
|
|
20390
|
+
)
|
|
20391
|
+
GROUP BY rule_id`
|
|
20009
20392
|
).all(scope.params);
|
|
20010
20393
|
return new Map(
|
|
20011
20394
|
rows.map((r) => [
|
|
@@ -20015,13 +20398,14 @@ var SqliteFindingsRepository = class {
|
|
|
20015
20398
|
sourceTools: splitConcat(r.source_tools),
|
|
20016
20399
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20017
20400
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20018
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20401
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20019
20402
|
return {
|
|
20020
20403
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20021
20404
|
// so the marker stands in for the key itself (never rendered).
|
|
20022
20405
|
kind,
|
|
20023
20406
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20024
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20407
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20408
|
+
count: Number(count)
|
|
20025
20409
|
};
|
|
20026
20410
|
}),
|
|
20027
20411
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20038,10 +20422,21 @@ var SqliteFindingsRepository = class {
|
|
|
20038
20422
|
);
|
|
20039
20423
|
}
|
|
20040
20424
|
healthSummary() {
|
|
20041
|
-
const total = countScalar(
|
|
20425
|
+
const total = countScalar(
|
|
20426
|
+
this.db,
|
|
20427
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20428
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20429
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20430
|
+
);
|
|
20042
20431
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20043
20432
|
const grouped = allRows(
|
|
20044
|
-
this.db.prepare(
|
|
20433
|
+
this.db.prepare(
|
|
20434
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20435
|
+
FROM inspection_findings f
|
|
20436
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20437
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20438
|
+
GROUP BY f.action_taken`
|
|
20439
|
+
)
|
|
20045
20440
|
);
|
|
20046
20441
|
for (const row of grouped) {
|
|
20047
20442
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20049,12 +20444,15 @@ var SqliteFindingsRepository = class {
|
|
|
20049
20444
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20050
20445
|
const sevRows = allRows(
|
|
20051
20446
|
this.db.prepare(
|
|
20052
|
-
`SELECT
|
|
20053
|
-
FROM
|
|
20447
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20448
|
+
FROM inspection_findings f
|
|
20449
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20450
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20054
20451
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20055
20452
|
ON latest.finding_key = f.finding_key
|
|
20056
|
-
WHERE
|
|
20057
|
-
|
|
20453
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20454
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20455
|
+
GROUP BY d.severity`
|
|
20058
20456
|
)
|
|
20059
20457
|
);
|
|
20060
20458
|
for (const row of sevRows) {
|
|
@@ -20075,9 +20473,11 @@ var SqliteFindingsRepository = class {
|
|
|
20075
20473
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20076
20474
|
const rows = allRows(
|
|
20077
20475
|
this.db.prepare(
|
|
20078
|
-
`SELECT date(e.
|
|
20079
|
-
FROM
|
|
20080
|
-
|
|
20476
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20477
|
+
FROM inspection_findings f
|
|
20478
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20479
|
+
WHERE e.started_at >= :since
|
|
20480
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20081
20481
|
GROUP BY day, f.action_taken`
|
|
20082
20482
|
),
|
|
20083
20483
|
{ since }
|
|
@@ -20142,15 +20542,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20142
20542
|
this.insertStmt = db.prepare(
|
|
20143
20543
|
`INSERT INTO inspection_findings
|
|
20144
20544
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20145
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20545
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20546
|
+
finding_key, first_detected_at)
|
|
20146
20547
|
VALUES
|
|
20147
20548
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20148
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20149
|
-
|
|
20549
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20550
|
+
:findingKey,
|
|
20551
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20552
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20553
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20554
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20555
|
+
audit_event_id = excluded.audit_event_id,
|
|
20556
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20557
|
+
classified_data_id = excluded.classified_data_id,
|
|
20558
|
+
span_start = excluded.span_start,
|
|
20559
|
+
span_end = excluded.span_end,
|
|
20560
|
+
masked_match = excluded.masked_match,
|
|
20561
|
+
action_taken = excluded.action_taken,
|
|
20562
|
+
confidence = excluded.confidence`
|
|
20563
|
+
);
|
|
20564
|
+
this.sessionDupStmt = db.prepare(
|
|
20565
|
+
`SELECT 1 FROM inspection_findings f
|
|
20566
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20567
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20568
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20569
|
+
AND e.root_session_id = :sessionId
|
|
20570
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20571
|
+
LIMIT 1`
|
|
20572
|
+
);
|
|
20573
|
+
this.eventDupStmt = db.prepare(
|
|
20574
|
+
`SELECT 1 FROM inspection_findings f
|
|
20575
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20576
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20577
|
+
AND f.masked_match = :maskedMatch
|
|
20578
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20579
|
+
LIMIT 1`
|
|
20150
20580
|
);
|
|
20151
20581
|
}
|
|
20152
20582
|
db;
|
|
20153
20583
|
insertStmt;
|
|
20584
|
+
sessionDupStmt;
|
|
20585
|
+
eventDupStmt;
|
|
20586
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20587
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20588
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20589
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20590
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20591
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20592
|
+
}
|
|
20593
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20594
|
+
// recorded against the given audit event.
|
|
20595
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20596
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20597
|
+
}
|
|
20154
20598
|
insertFinding(input) {
|
|
20155
20599
|
const row = toInspectionFindingRow(input);
|
|
20156
20600
|
this.insertStmt.run(
|
|
@@ -20163,7 +20607,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20163
20607
|
spanEnd: row.spanEnd,
|
|
20164
20608
|
maskedMatch: row.maskedMatch,
|
|
20165
20609
|
actionTaken: row.actionTaken,
|
|
20166
|
-
confidence: row.confidence
|
|
20610
|
+
confidence: row.confidence,
|
|
20611
|
+
findingKey: row.findingKey,
|
|
20612
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20167
20613
|
})
|
|
20168
20614
|
);
|
|
20169
20615
|
}
|
|
@@ -20435,7 +20881,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20435
20881
|
installedRuleset() {
|
|
20436
20882
|
const rows = allRows(
|
|
20437
20883
|
this.db.prepare(
|
|
20438
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20884
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20439
20885
|
)
|
|
20440
20886
|
);
|
|
20441
20887
|
const out = {
|
|
@@ -20443,7 +20889,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20443
20889
|
enabledPacks: 0,
|
|
20444
20890
|
rules: [],
|
|
20445
20891
|
invalidRules: 0,
|
|
20446
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20892
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20893
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20447
20894
|
};
|
|
20448
20895
|
for (const row of rows) {
|
|
20449
20896
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20465,6 +20912,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20465
20912
|
if (parsed.success) {
|
|
20466
20913
|
out.rules.push(parsed.data);
|
|
20467
20914
|
out.ruleActions.set(parsed.data.id, action);
|
|
20915
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20468
20916
|
} else out.invalidRules += 1;
|
|
20469
20917
|
}
|
|
20470
20918
|
}
|
|
@@ -21639,19 +22087,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21639
22087
|
);
|
|
21640
22088
|
this.openAtRestStmt = db.prepare(
|
|
21641
22089
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21642
|
-
FROM
|
|
21643
|
-
JOIN
|
|
21644
|
-
WHERE e.
|
|
21645
|
-
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
|
|
21646
22094
|
AND f.finding_key IS NOT NULL
|
|
21647
22095
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21648
22096
|
);
|
|
21649
22097
|
this.resolvedAtRestStmt = db.prepare(
|
|
21650
22098
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21651
|
-
FROM
|
|
21652
|
-
JOIN
|
|
21653
|
-
WHERE e.
|
|
21654
|
-
AND json_extract(e.
|
|
22099
|
+
FROM inspection_findings f
|
|
22100
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22101
|
+
WHERE e.event_type = 'code_change'
|
|
22102
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21655
22103
|
AND f.finding_key IS NOT NULL
|
|
21656
22104
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21657
22105
|
);
|
|
@@ -21874,25 +22322,27 @@ var SqliteSecurityRepository = class {
|
|
|
21874
22322
|
severitySummary() {
|
|
21875
22323
|
const rows = allRows(
|
|
21876
22324
|
this.db.prepare(
|
|
21877
|
-
`SELECT
|
|
22325
|
+
`SELECT d.severity AS severity,
|
|
21878
22326
|
COUNT(*) AS count,
|
|
21879
22327
|
SUM(CASE
|
|
21880
|
-
WHEN e.
|
|
22328
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21881
22329
|
WHEN f.finding_key IS NULL THEN 0
|
|
21882
22330
|
WHEN latest.status = 'resolved' THEN 1
|
|
21883
22331
|
ELSE 0
|
|
21884
22332
|
END) AS caught,
|
|
21885
22333
|
SUM(CASE
|
|
21886
|
-
WHEN e.
|
|
22334
|
+
WHEN e.event_type = 'code_change'
|
|
21887
22335
|
AND f.finding_key IS NOT NULL
|
|
21888
22336
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21889
22337
|
ELSE 0
|
|
21890
22338
|
END) AS open_at_rest
|
|
21891
|
-
FROM
|
|
21892
|
-
JOIN
|
|
22339
|
+
FROM inspection_findings f
|
|
22340
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22341
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21893
22342
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21894
22343
|
ON latest.finding_key = f.finding_key
|
|
21895
|
-
|
|
22344
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22345
|
+
GROUP BY d.severity`
|
|
21896
22346
|
)
|
|
21897
22347
|
);
|
|
21898
22348
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21958,7 +22408,7 @@ var SqliteSecurityRepository = class {
|
|
|
21958
22408
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21959
22409
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21960
22410
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21961
|
-
// (
|
|
22411
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21962
22412
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21963
22413
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21964
22414
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21983,30 +22433,20 @@ var SqliteSecurityRepository = class {
|
|
|
21983
22433
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21984
22434
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21985
22435
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21986
|
-
//
|
|
21987
|
-
// the parent event's
|
|
21988
|
-
// backfill left null.
|
|
21989
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
|
|
21998
|
-
WHERE fr.finding_key = f.finding_key
|
|
21999
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22000
|
-
LIMIT 1
|
|
22001
|
-
) AS latest_method,
|
|
22002
|
-
(
|
|
22003
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22004
|
-
WHERE fr.finding_key = f.finding_key
|
|
22005
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22006
|
-
LIMIT 1
|
|
22007
|
-
) AS latest_resolved_at
|
|
22008
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22436
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22437
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22438
|
+
// legacy/edge row the backfill left null.
|
|
22439
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22440
|
+
latest.status AS latest_status,
|
|
22441
|
+
latest.method AS latest_method,
|
|
22442
|
+
latest.resolved_at AS latest_resolved_at
|
|
22443
|
+
FROM inspection_findings f
|
|
22444
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22445
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22446
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22447
|
+
ON latest.finding_key = f.finding_key
|
|
22009
22448
|
WHERE f.finding_key IS NOT NULL
|
|
22449
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22010
22450
|
AND EXISTS (
|
|
22011
22451
|
SELECT 1 FROM finding_resolution fr
|
|
22012
22452
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22053,11 +22493,13 @@ var SqliteSecurityRepository = class {
|
|
|
22053
22493
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22054
22494
|
const rows = allRows(
|
|
22055
22495
|
this.db.prepare(
|
|
22056
|
-
`SELECT json_extract(e.
|
|
22057
|
-
FROM
|
|
22058
|
-
|
|
22059
|
-
|
|
22060
|
-
AND
|
|
22496
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22497
|
+
FROM inspection_findings f
|
|
22498
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22499
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22500
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22501
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22502
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22061
22503
|
GROUP BY repo
|
|
22062
22504
|
ORDER BY c DESC, repo
|
|
22063
22505
|
LIMIT :limit`
|
|
@@ -22081,44 +22523,28 @@ var SqliteSecurityRepository = class {
|
|
|
22081
22523
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22082
22524
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22083
22525
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22084
|
-
// finding's parent event (
|
|
22085
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22086
|
-
// capped at `limit`.
|
|
22526
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22527
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22528
|
+
// DESC, capped at `limit`.
|
|
22087
22529
|
recentlyResolved(limit = 20) {
|
|
22088
22530
|
const rows = allRows(
|
|
22089
22531
|
this.db.prepare(
|
|
22090
22532
|
`SELECT f.finding_key AS finding_key,
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
json_extract(e.
|
|
22094
|
-
COALESCE(f.first_detected_at, e.
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
WHERE e.kind = 'code_change'
|
|
22533
|
+
d.rule_id AS rule_id,
|
|
22534
|
+
d.severity AS severity,
|
|
22535
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22536
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22537
|
+
latest.resolved_at AS latest_resolved_at
|
|
22538
|
+
FROM inspection_findings f
|
|
22539
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22540
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22541
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22542
|
+
ON latest.finding_key = f.finding_key
|
|
22543
|
+
WHERE e.event_type = 'code_change'
|
|
22103
22544
|
AND f.finding_key IS NOT NULL
|
|
22104
|
-
AND
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22108
|
-
LIMIT 1
|
|
22109
|
-
) = 'resolved'
|
|
22110
|
-
AND (
|
|
22111
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22112
|
-
WHERE fr.finding_key = f.finding_key
|
|
22113
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22114
|
-
LIMIT 1
|
|
22115
|
-
) = 'fixed-at-source'
|
|
22116
|
-
AND (
|
|
22117
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22118
|
-
WHERE fr.finding_key = f.finding_key
|
|
22119
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22120
|
-
LIMIT 1
|
|
22121
|
-
) IS NOT NULL
|
|
22545
|
+
AND latest.status = 'resolved'
|
|
22546
|
+
AND latest.method = 'fixed-at-source'
|
|
22547
|
+
AND latest.resolved_at IS NOT NULL
|
|
22122
22548
|
ORDER BY latest_resolved_at DESC
|
|
22123
22549
|
LIMIT :limit`
|
|
22124
22550
|
),
|
|
@@ -22137,15 +22563,18 @@ var SqliteSecurityRepository = class {
|
|
|
22137
22563
|
return Promise.resolve({ items });
|
|
22138
22564
|
}
|
|
22139
22565
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22140
|
-
// epoch-millis timestamp.
|
|
22566
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22141
22567
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22142
22568
|
findingsInRange(fromMs, toMs) {
|
|
22143
22569
|
const rows = allRows(
|
|
22144
22570
|
this.db.prepare(
|
|
22145
|
-
`SELECT e.
|
|
22146
|
-
FROM
|
|
22147
|
-
|
|
22148
|
-
|
|
22571
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22572
|
+
FROM inspection_findings f
|
|
22573
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22574
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22575
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22576
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22577
|
+
ORDER BY e.started_at`
|
|
22149
22578
|
),
|
|
22150
22579
|
{ from: fromMs, to: toMs }
|
|
22151
22580
|
);
|
|
@@ -22969,9 +23398,10 @@ function openWithPragmas(file2) {
|
|
|
22969
23398
|
}
|
|
22970
23399
|
function backupLegacyStore(file2) {
|
|
22971
23400
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
23401
|
+
renameSync2(file2, backup);
|
|
23402
|
+
tightenFile(backup);
|
|
23403
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23404
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22975
23405
|
}
|
|
22976
23406
|
return backup;
|
|
22977
23407
|
}
|
|
@@ -22987,7 +23417,7 @@ function openLocalDatabase(dir) {
|
|
|
22987
23417
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22988
23418
|
);
|
|
22989
23419
|
}
|
|
22990
|
-
applyMigrations(db);
|
|
23420
|
+
applyMigrations(db, file2);
|
|
22991
23421
|
tightenPerms(file2);
|
|
22992
23422
|
const events = new SqliteEventsRepository(db);
|
|
22993
23423
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -23014,9 +23444,56 @@ function openLocalDatabase(dir) {
|
|
|
23014
23444
|
policies.seedDefaults();
|
|
23015
23445
|
function recordCapture(event, detected) {
|
|
23016
23446
|
failOpenTransaction(db, () => {
|
|
23017
|
-
events.insertEvent(event);
|
|
23018
23447
|
const sessionId = event.metadata?.sessionId;
|
|
23019
|
-
|
|
23448
|
+
if (sessionId) {
|
|
23449
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23450
|
+
}
|
|
23451
|
+
const auditEventId = captureId(
|
|
23452
|
+
sessionId ?? null,
|
|
23453
|
+
event.contentHash,
|
|
23454
|
+
event.metadata?.filePath ?? null
|
|
23455
|
+
);
|
|
23456
|
+
auditEvents.insertAuditEvent({
|
|
23457
|
+
id: auditEventId,
|
|
23458
|
+
eventType: event.kind,
|
|
23459
|
+
startedAt: event.occurredAt,
|
|
23460
|
+
parentId: sessionId,
|
|
23461
|
+
rootSessionId: sessionId,
|
|
23462
|
+
content: event.content,
|
|
23463
|
+
contentHash: event.contentHash,
|
|
23464
|
+
attributes: toCaptureAttributes(event)
|
|
23465
|
+
});
|
|
23466
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23467
|
+
for (const finding of detected) {
|
|
23468
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23469
|
+
continue;
|
|
23470
|
+
}
|
|
23471
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23472
|
+
auditEventId,
|
|
23473
|
+
finding.ruleId,
|
|
23474
|
+
finding.maskedMatch,
|
|
23475
|
+
finding.span.start,
|
|
23476
|
+
finding.span.end
|
|
23477
|
+
)) {
|
|
23478
|
+
continue;
|
|
23479
|
+
}
|
|
23480
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23481
|
+
let definitionId = definitionIds.get(key);
|
|
23482
|
+
if (!definitionId) {
|
|
23483
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23484
|
+
definitionIds.set(key, definitionId);
|
|
23485
|
+
}
|
|
23486
|
+
inspectionFindings.insertFinding({
|
|
23487
|
+
id: finding.id,
|
|
23488
|
+
auditEventId,
|
|
23489
|
+
inspectionDefinitionId: definitionId,
|
|
23490
|
+
span: finding.span,
|
|
23491
|
+
maskedMatch: finding.maskedMatch,
|
|
23492
|
+
actionTaken: finding.actionTaken,
|
|
23493
|
+
confidence: finding.confidence,
|
|
23494
|
+
findingKey: finding.findingKey ?? void 0
|
|
23495
|
+
});
|
|
23496
|
+
}
|
|
23020
23497
|
});
|
|
23021
23498
|
}
|
|
23022
23499
|
function ensureInventory(ctx) {
|
|
@@ -23164,9 +23641,19 @@ function openLocalDatabase(dir) {
|
|
|
23164
23641
|
};
|
|
23165
23642
|
}
|
|
23166
23643
|
|
|
23644
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23645
|
+
import { createHash as createHash3 } from "crypto";
|
|
23646
|
+
function normalizeFilePath(filePath) {
|
|
23647
|
+
return filePath.replaceAll("\\", "/");
|
|
23648
|
+
}
|
|
23649
|
+
function computeFindingKey(input) {
|
|
23650
|
+
const normalizedPath = normalizeFilePath(input.filePath);
|
|
23651
|
+
return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
23652
|
+
}
|
|
23653
|
+
|
|
23167
23654
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23168
23655
|
import { createHmac, randomBytes } from "crypto";
|
|
23169
|
-
import {
|
|
23656
|
+
import { readFileSync } from "fs";
|
|
23170
23657
|
import { join as join2 } from "path";
|
|
23171
23658
|
var KEY_FILENAME = "exception.key";
|
|
23172
23659
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -23194,15 +23681,9 @@ function parseKeyFile(raw) {
|
|
|
23194
23681
|
function writeKeyFile(dataDir2, key) {
|
|
23195
23682
|
ensureDataDirSync(dataDir2);
|
|
23196
23683
|
const file2 = keyFilePath(dataDir2);
|
|
23197
|
-
const tmp = `${file2}.tmp`;
|
|
23198
23684
|
const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
renameSync2(tmp, file2);
|
|
23202
|
-
try {
|
|
23203
|
-
chmodSync2(file2, DATA_FILE_MODE);
|
|
23204
|
-
} catch {
|
|
23205
|
-
}
|
|
23685
|
+
writeOwnerOnlyFileSync(file2, `${body}
|
|
23686
|
+
`);
|
|
23206
23687
|
return key;
|
|
23207
23688
|
}
|
|
23208
23689
|
function readFingerprintKey(dataDir2) {
|
|
@@ -23218,10 +23699,7 @@ function readFingerprintKey(dataDir2) {
|
|
|
23218
23699
|
function loadOrCreateFingerprintKey(dataDir2) {
|
|
23219
23700
|
const existing = readFingerprintKey(dataDir2);
|
|
23220
23701
|
if (existing) {
|
|
23221
|
-
|
|
23222
|
-
chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
|
|
23223
|
-
} catch {
|
|
23224
|
-
}
|
|
23702
|
+
tightenFile(keyFilePath(dataDir2));
|
|
23225
23703
|
return existing;
|
|
23226
23704
|
}
|
|
23227
23705
|
return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
|
|
@@ -23231,8 +23709,8 @@ function fingerprintValue(key, raw) {
|
|
|
23231
23709
|
}
|
|
23232
23710
|
|
|
23233
23711
|
// ../../packages/persistence/src/local-layout.ts
|
|
23234
|
-
import {
|
|
23235
|
-
import {
|
|
23712
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23713
|
+
import { mkdir } from "fs/promises";
|
|
23236
23714
|
import { homedir } from "os";
|
|
23237
23715
|
import { join as join3 } from "path";
|
|
23238
23716
|
function defaultDataDir() {
|
|
@@ -23247,6 +23725,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23247
23725
|
function dbPath(base = defaultDataDir()) {
|
|
23248
23726
|
return join3(dataDir(base), "aka.db");
|
|
23249
23727
|
}
|
|
23728
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23729
|
+
ensureDataDirSync(dir);
|
|
23730
|
+
}
|
|
23250
23731
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23251
23732
|
const moves = [
|
|
23252
23733
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23254,19 +23735,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23254
23735
|
];
|
|
23255
23736
|
for (const { name, dest } of moves) {
|
|
23256
23737
|
try {
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
}
|
|
23262
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23738
|
+
ensureDataDirSync(dest);
|
|
23739
|
+
const moved = join3(dest, name);
|
|
23740
|
+
renameSync3(join3(base, name), moved);
|
|
23741
|
+
tightenFile(moved);
|
|
23263
23742
|
} catch {
|
|
23264
23743
|
}
|
|
23265
23744
|
}
|
|
23266
23745
|
}
|
|
23267
23746
|
|
|
23268
23747
|
// ../../packages/persistence/src/settings.ts
|
|
23269
|
-
import { readFileSync as readFileSync2
|
|
23748
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23270
23749
|
import { join as join4 } from "path";
|
|
23271
23750
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23272
23751
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23288,7 +23767,7 @@ function readJson(file2) {
|
|
|
23288
23767
|
}
|
|
23289
23768
|
|
|
23290
23769
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23291
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23770
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
23292
23771
|
import { join as join5 } from "path";
|
|
23293
23772
|
var MARKER = "warn-era-capped";
|
|
23294
23773
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -23296,11 +23775,15 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
23296
23775
|
const marker = join5(dataDir2, MARKER);
|
|
23297
23776
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
23298
23777
|
const capped = db.policies.capCategoryActions();
|
|
23299
|
-
|
|
23778
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23300
23779
|
`, { mode: DATA_FILE_MODE });
|
|
23301
23780
|
return { capped };
|
|
23302
23781
|
}
|
|
23303
23782
|
|
|
23783
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23784
|
+
import { existsSync as existsSync3 } from "fs";
|
|
23785
|
+
import { join as join6 } from "path";
|
|
23786
|
+
|
|
23304
23787
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
23305
23788
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
23306
23789
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -23351,6 +23834,12 @@ function resolveProvider() {
|
|
|
23351
23834
|
|
|
23352
23835
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23353
23836
|
function loadConfig(base = defaultDataDir()) {
|
|
23837
|
+
try {
|
|
23838
|
+
ensureLayoutDirSync(base);
|
|
23839
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23840
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23841
|
+
} catch {
|
|
23842
|
+
}
|
|
23354
23843
|
migrateLegacyLayout(base);
|
|
23355
23844
|
const settings = readWorkspaceSettings(base);
|
|
23356
23845
|
return {
|
|
@@ -23373,7 +23862,7 @@ function resolveProviderSafe() {
|
|
|
23373
23862
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23374
23863
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23375
23864
|
import { homedir as homedir2 } from "os";
|
|
23376
|
-
import { basename as basename2, join as
|
|
23865
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23377
23866
|
|
|
23378
23867
|
// ../../packages/detections/src/egress/registry.ts
|
|
23379
23868
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -26391,8 +26880,8 @@ function bundledDetections() {
|
|
|
26391
26880
|
}
|
|
26392
26881
|
|
|
26393
26882
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26394
|
-
import { existsSync as
|
|
26395
|
-
import { basename, dirname, isAbsolute, join as
|
|
26883
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26884
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26396
26885
|
function resolveRepo(cwd) {
|
|
26397
26886
|
try {
|
|
26398
26887
|
const root = findGitRoot(cwd);
|
|
@@ -26414,32 +26903,32 @@ function resolveWorktreeRoot(cwd) {
|
|
|
26414
26903
|
function findGitRoot(start) {
|
|
26415
26904
|
let dir = start;
|
|
26416
26905
|
for (; ; ) {
|
|
26417
|
-
if (
|
|
26906
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
26418
26907
|
const parent = dirname(dir);
|
|
26419
26908
|
if (parent === dir) return void 0;
|
|
26420
26909
|
dir = parent;
|
|
26421
26910
|
}
|
|
26422
26911
|
}
|
|
26423
26912
|
function resolveGitContext(root) {
|
|
26424
|
-
const dotGit =
|
|
26913
|
+
const dotGit = join7(root, ".git");
|
|
26425
26914
|
try {
|
|
26426
26915
|
if (statSync(dotGit).isDirectory()) {
|
|
26427
|
-
return { configPath:
|
|
26916
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
26428
26917
|
}
|
|
26429
26918
|
} catch {
|
|
26430
26919
|
return void 0;
|
|
26431
26920
|
}
|
|
26432
26921
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
26433
26922
|
if (!target) return void 0;
|
|
26434
|
-
const gitdir = isAbsolute(target) ? target :
|
|
26435
|
-
if (
|
|
26436
|
-
return { configPath:
|
|
26923
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
26924
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
26925
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
26437
26926
|
}
|
|
26438
|
-
const commonRaw = safeRead(
|
|
26927
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
26439
26928
|
if (!commonRaw) return void 0;
|
|
26440
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
26929
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
26441
26930
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
26442
|
-
return { configPath:
|
|
26931
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
26443
26932
|
}
|
|
26444
26933
|
function safeRead(path) {
|
|
26445
26934
|
try {
|
|
@@ -26481,9 +26970,9 @@ function slugFromUrl(url2) {
|
|
|
26481
26970
|
}
|
|
26482
26971
|
|
|
26483
26972
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26484
|
-
import { createHash as
|
|
26973
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26485
26974
|
function contentHashOf(text) {
|
|
26486
|
-
return
|
|
26975
|
+
return createHash4("sha256").update(text).digest("hex");
|
|
26487
26976
|
}
|
|
26488
26977
|
function buildIngestEvent(input) {
|
|
26489
26978
|
return {
|
|
@@ -26503,22 +26992,12 @@ function buildIngestEvent(input) {
|
|
|
26503
26992
|
};
|
|
26504
26993
|
}
|
|
26505
26994
|
|
|
26506
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
26507
|
-
import { createHash as createHash4 } from "crypto";
|
|
26508
|
-
function normalizeFilePath(filePath) {
|
|
26509
|
-
return filePath.replaceAll("\\", "/");
|
|
26510
|
-
}
|
|
26511
|
-
function computeFindingKey(input) {
|
|
26512
|
-
const normalizedPath = normalizeFilePath(input.filePath);
|
|
26513
|
-
return createHash4("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
26514
|
-
}
|
|
26515
|
-
|
|
26516
26995
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26517
26996
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
26518
26997
|
|
|
26519
26998
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26520
|
-
import { mkdirSync as
|
|
26521
|
-
import { join as
|
|
26999
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
27000
|
+
import { join as join9 } from "path";
|
|
26522
27001
|
|
|
26523
27002
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
26524
27003
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -26536,8 +27015,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
26536
27015
|
|
|
26537
27016
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
26538
27017
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
26539
|
-
import { existsSync as
|
|
26540
|
-
import { basename as basename4, join as
|
|
27018
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27019
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
26541
27020
|
|
|
26542
27021
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
26543
27022
|
var MIN_RAW_LEN = 4;
|
|
@@ -26897,8 +27376,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
26897
27376
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26898
27377
|
|
|
26899
27378
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
26900
|
-
import { mkdirSync as
|
|
26901
|
-
import { join as
|
|
27379
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
27380
|
+
import { join as join11 } from "path";
|
|
26902
27381
|
|
|
26903
27382
|
// src/command-registry.ts
|
|
26904
27383
|
import { readdirSync as readdirSync4 } from "fs";
|
|
@@ -27065,8 +27544,8 @@ function routeRemediationOption(option, handlers) {
|
|
|
27065
27544
|
}
|
|
27066
27545
|
|
|
27067
27546
|
// src/remediation/rotation-checklist.ts
|
|
27068
|
-
import { writeFileSync as
|
|
27069
|
-
import { join as
|
|
27547
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
27548
|
+
import { join as join12 } from "path";
|
|
27070
27549
|
var GENERIC_CONSOLE_PATH = "rotate via the provider's own console";
|
|
27071
27550
|
var CONSOLE_PATHS = {
|
|
27072
27551
|
anthropic: "console.anthropic.com \u2192 Settings \u2192 API keys",
|
|
@@ -27151,7 +27630,7 @@ function renderRotationChecklistResolvedLine(location) {
|
|
|
27151
27630
|
return `\u2713 I drafted a rotation checklist for you (${location}).`;
|
|
27152
27631
|
}
|
|
27153
27632
|
function writeRotationChecklist(entries, targetDirectory) {
|
|
27154
|
-
|
|
27633
|
+
writeFileSync5(
|
|
27155
27634
|
`${targetDirectory}/rotation-checklist.md`,
|
|
27156
27635
|
renderChecklistMarkdown(entries),
|
|
27157
27636
|
"utf8"
|
|
@@ -27166,7 +27645,7 @@ function generateRotationChecklist(input) {
|
|
|
27166
27645
|
try {
|
|
27167
27646
|
const target = resolveRotationChecklistTarget(input.cwd);
|
|
27168
27647
|
targetDirectory = target.directory;
|
|
27169
|
-
const filePath =
|
|
27648
|
+
const filePath = join12(target.directory, "rotation-checklist.md");
|
|
27170
27649
|
writeRotationChecklist(input.entries, target.directory);
|
|
27171
27650
|
return {
|
|
27172
27651
|
status: "written",
|
|
@@ -27313,7 +27792,8 @@ var StandaloneDataGateway = class {
|
|
|
27313
27792
|
}
|
|
27314
27793
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
27315
27794
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
27316
|
-
// hands the natural key across.
|
|
27795
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27796
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
27317
27797
|
recordLlmCall(input) {
|
|
27318
27798
|
this.db.auditEvents.insertLlmCall(input);
|
|
27319
27799
|
return Promise.resolve();
|
|
@@ -27355,7 +27835,9 @@ var StandaloneDataGateway = class {
|
|
|
27355
27835
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
27356
27836
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
27357
27837
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
27358
|
-
// content-addressed
|
|
27838
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27839
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27840
|
+
// than no-opping.
|
|
27359
27841
|
writeToolCall(input) {
|
|
27360
27842
|
this.db.auditEvents.insertToolCall(input);
|
|
27361
27843
|
if (input.inspections.length === 0) return;
|
|
@@ -27375,7 +27857,7 @@ var StandaloneDataGateway = class {
|
|
|
27375
27857
|
});
|
|
27376
27858
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
27377
27859
|
this.db.inspectionFindings.insertFinding({
|
|
27378
|
-
id: inspectionFindingId(auditEventId,
|
|
27860
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
27379
27861
|
auditEventId,
|
|
27380
27862
|
inspectionDefinitionId: definitionId,
|
|
27381
27863
|
classifiedDataId: classifiedDataId2,
|
|
@@ -27424,10 +27906,17 @@ var StandaloneDataGateway = class {
|
|
|
27424
27906
|
try {
|
|
27425
27907
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
27426
27908
|
if (snapshot.installedPacks === 0) return void 0;
|
|
27427
|
-
if (snapshot.enabledPacks === 0)
|
|
27909
|
+
if (snapshot.enabledPacks === 0) {
|
|
27910
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27911
|
+
}
|
|
27428
27912
|
if (snapshot.invalidRules > 0) return void 0;
|
|
27429
27913
|
if (snapshot.rules.length === 0) return void 0;
|
|
27430
|
-
return {
|
|
27914
|
+
return {
|
|
27915
|
+
rules: snapshot.rules,
|
|
27916
|
+
ruleActions: snapshot.ruleActions,
|
|
27917
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27918
|
+
complete: true
|
|
27919
|
+
};
|
|
27431
27920
|
} catch {
|
|
27432
27921
|
return void 0;
|
|
27433
27922
|
}
|
|
@@ -27455,6 +27944,7 @@ var StandaloneDataGateway = class {
|
|
|
27455
27944
|
policies: [...policies, ...rulePolicies],
|
|
27456
27945
|
rules: installed ? installed.rules : [],
|
|
27457
27946
|
...installed ? { rulesComplete: true } : {},
|
|
27947
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
27458
27948
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
27459
27949
|
customKeywords,
|
|
27460
27950
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -27595,9 +28085,9 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
|
27595
28085
|
// src/history/transcripts.ts
|
|
27596
28086
|
import { readdirSync as readdirSync5, readFileSync as readFileSync7 } from "fs";
|
|
27597
28087
|
import { homedir as homedir3 } from "os";
|
|
27598
|
-
import { join as
|
|
28088
|
+
import { join as join13 } from "path";
|
|
27599
28089
|
function transcriptsDir(home) {
|
|
27600
|
-
return
|
|
28090
|
+
return join13(home ?? homedir3(), ".claude", "projects");
|
|
27601
28091
|
}
|
|
27602
28092
|
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
27603
28093
|
|
|
@@ -27609,7 +28099,7 @@ function deriveProvider(ruleId) {
|
|
|
27609
28099
|
}
|
|
27610
28100
|
|
|
27611
28101
|
// src/remediation/redact.ts
|
|
27612
|
-
import { readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as
|
|
28102
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
27613
28103
|
import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
|
|
27614
28104
|
var REDACTED_PLACEHOLDER = "[REDACTED:SECRET]";
|
|
27615
28105
|
function platformRedactionScope(home) {
|
|
@@ -27667,11 +28157,11 @@ function redactLeakedKeysDetailed(targets, scope = platformRedactionScope()) {
|
|
|
27667
28157
|
if (struckHere.length === 0) continue;
|
|
27668
28158
|
const tmpPath = `${filePath}.aka-redact.tmp`;
|
|
27669
28159
|
try {
|
|
27670
|
-
|
|
27671
|
-
|
|
28160
|
+
writeFileSync6(tmpPath, content);
|
|
28161
|
+
renameSync4(tmpPath, filePath);
|
|
27672
28162
|
} catch {
|
|
27673
28163
|
try {
|
|
27674
|
-
|
|
28164
|
+
rmSync3(tmpPath, { force: true, recursive: true });
|
|
27675
28165
|
} catch {
|
|
27676
28166
|
}
|
|
27677
28167
|
continue;
|