@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/session-start.js
CHANGED
|
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
|
|
|
494
494
|
// src/hooks/session-start.ts
|
|
495
495
|
import { readFileSync as readFileSync8 } from "fs";
|
|
496
496
|
|
|
497
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
498
|
+
import { existsSync as existsSync3 } from "fs";
|
|
499
|
+
import { join as join6 } from "path";
|
|
500
|
+
|
|
497
501
|
// ../../packages/persistence/src/database.ts
|
|
498
502
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
499
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
503
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
500
504
|
import { join, sep } from "path";
|
|
501
505
|
import { DatabaseSync } from "node:sqlite";
|
|
502
506
|
|
|
@@ -549,6 +553,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
549
553
|
{
|
|
550
554
|
tag: "0011_egress_writer",
|
|
551
555
|
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
tag: "0012_handy_the_captain",
|
|
559
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
tag: "0013_legacy_history_backfill_support",
|
|
563
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
tag: "0014_drop_legacy_events_findings",
|
|
567
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
552
568
|
}
|
|
553
569
|
];
|
|
554
570
|
|
|
@@ -15379,7 +15395,12 @@ var FindingFacets = external_exports.object({
|
|
|
15379
15395
|
severity: external_exports.array(FindingFacetItem),
|
|
15380
15396
|
subtype: external_exports.array(FindingFacetItem),
|
|
15381
15397
|
provider: external_exports.array(FindingFacetItem),
|
|
15382
|
-
action: external_exports.array(FindingFacetItem)
|
|
15398
|
+
action: external_exports.array(FindingFacetItem),
|
|
15399
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15400
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15401
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15402
|
+
// counted under no value.
|
|
15403
|
+
status: external_exports.array(FindingFacetItem)
|
|
15383
15404
|
}).meta({ id: "FindingFacets" });
|
|
15384
15405
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15385
15406
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15389,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15389
15410
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15390
15411
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15391
15412
|
action: external_exports.array(FindingAction).optional(),
|
|
15413
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15414
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15415
|
+
// one of the requested values.
|
|
15416
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15392
15417
|
q: external_exports.string().optional(),
|
|
15393
15418
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15394
15419
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15579,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15579
15604
|
parent_uuid: external_exports.string().optional(),
|
|
15580
15605
|
run_key: external_exports.string().optional()
|
|
15581
15606
|
}).catchall(external_exports.unknown());
|
|
15607
|
+
var CaptureAttributes = external_exports.object({
|
|
15608
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15609
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15610
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15611
|
+
source_tool: external_exports.string().optional(),
|
|
15612
|
+
file_path: external_exports.string().optional(),
|
|
15613
|
+
repo: external_exports.string().optional(),
|
|
15614
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15615
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15616
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15617
|
+
tool_name: external_exports.string().optional(),
|
|
15618
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15619
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15620
|
+
gitignored: external_exports.boolean().optional(),
|
|
15621
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15622
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15623
|
+
whole_file: external_exports.boolean().optional(),
|
|
15624
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15625
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15626
|
+
// trace id when telemetry is enabled.
|
|
15627
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15628
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15629
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15630
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15631
|
+
// authorized the bypass.
|
|
15632
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15633
|
+
}).catchall(external_exports.unknown());
|
|
15582
15634
|
var ToolCallInspection = external_exports.object({
|
|
15583
15635
|
ruleId: external_exports.string().min(1),
|
|
15584
15636
|
ruleName: external_exports.string(),
|
|
@@ -15665,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15665
15717
|
span: Span,
|
|
15666
15718
|
maskedMatch: external_exports.string(),
|
|
15667
15719
|
actionTaken: ActionTaken,
|
|
15668
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15720
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15721
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15722
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15723
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15724
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15725
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15726
|
+
findingKey: external_exports.string().optional(),
|
|
15727
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15728
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15729
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15730
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15731
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15669
15732
|
});
|
|
15670
15733
|
var InventoryContext = external_exports.object({
|
|
15671
15734
|
host: InventoryInput.optional(),
|
|
@@ -15867,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15867
15930
|
|
|
15868
15931
|
// ../../packages/schema/src/zod/event.ts
|
|
15869
15932
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15933
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15870
15934
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15871
15935
|
var EventMetadata = external_exports.object({
|
|
15872
15936
|
sessionId: external_exports.string().optional(),
|
|
@@ -16356,6 +16420,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16356
16420
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16357
16421
|
// `bundle.exceptions ?? []`.
|
|
16358
16422
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16423
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16424
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16425
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16426
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16427
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16428
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16359
16429
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16360
16430
|
fetchedAt: external_exports.iso.datetime()
|
|
16361
16431
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17269,6 +17339,15 @@ function groupActions(g) {
|
|
|
17269
17339
|
actionsCache.set(g, actions);
|
|
17270
17340
|
return actions;
|
|
17271
17341
|
}
|
|
17342
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17343
|
+
const statusSet = new Set(statuses);
|
|
17344
|
+
let sum = 0;
|
|
17345
|
+
for (const input of statusInputs) {
|
|
17346
|
+
if (input.count === void 0) return null;
|
|
17347
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17348
|
+
}
|
|
17349
|
+
return sum;
|
|
17350
|
+
}
|
|
17272
17351
|
function applyFindingFilters(groups, opts) {
|
|
17273
17352
|
let filtered = groups;
|
|
17274
17353
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17287,6 +17366,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17287
17366
|
const subtypeSet = new Set(opts.subtype);
|
|
17288
17367
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17289
17368
|
}
|
|
17369
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17370
|
+
const statusSet = new Set(opts.statuses);
|
|
17371
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17372
|
+
}
|
|
17290
17373
|
if (opts.q) {
|
|
17291
17374
|
const q = opts.q.toLowerCase();
|
|
17292
17375
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17308,6 +17391,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17308
17391
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17309
17392
|
providers: opts.providers,
|
|
17310
17393
|
actions: opts.actions,
|
|
17394
|
+
statuses: opts.statuses,
|
|
17311
17395
|
q: opts.q,
|
|
17312
17396
|
subtype: opts.subtype
|
|
17313
17397
|
});
|
|
@@ -17317,6 +17401,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17317
17401
|
}
|
|
17318
17402
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17319
17403
|
actions: opts.actions,
|
|
17404
|
+
statuses: opts.statuses,
|
|
17320
17405
|
q: opts.q,
|
|
17321
17406
|
subtype: opts.subtype,
|
|
17322
17407
|
severity: opts.severity
|
|
@@ -17327,6 +17412,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17327
17412
|
}
|
|
17328
17413
|
const forAction = applyFindingFilters(allGroups, {
|
|
17329
17414
|
providers: opts.providers,
|
|
17415
|
+
statuses: opts.statuses,
|
|
17330
17416
|
q: opts.q,
|
|
17331
17417
|
subtype: opts.subtype,
|
|
17332
17418
|
severity: opts.severity
|
|
@@ -17338,17 +17424,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17338
17424
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17339
17425
|
providers: opts.providers,
|
|
17340
17426
|
actions: opts.actions,
|
|
17427
|
+
statuses: opts.statuses,
|
|
17341
17428
|
q: opts.q,
|
|
17342
17429
|
severity: opts.severity
|
|
17343
17430
|
});
|
|
17344
17431
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17345
17432
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17433
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17434
|
+
providers: opts.providers,
|
|
17435
|
+
actions: opts.actions,
|
|
17436
|
+
q: opts.q,
|
|
17437
|
+
subtype: opts.subtype,
|
|
17438
|
+
severity: opts.severity
|
|
17439
|
+
});
|
|
17440
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17441
|
+
for (const g of forStatus) {
|
|
17442
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17443
|
+
}
|
|
17346
17444
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17347
17445
|
return {
|
|
17348
17446
|
severity: toItems(severityMap),
|
|
17349
17447
|
provider: toItems(providerMap),
|
|
17350
17448
|
action: toItems(actionMap),
|
|
17351
|
-
subtype: toItems(subtypeMap)
|
|
17449
|
+
subtype: toItems(subtypeMap),
|
|
17450
|
+
status: toItems(statusMap)
|
|
17352
17451
|
};
|
|
17353
17452
|
}
|
|
17354
17453
|
|
|
@@ -17383,10 +17482,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17383
17482
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17384
17483
|
|
|
17385
17484
|
// ../../packages/schema/src/zod/local.ts
|
|
17386
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17485
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17387
17486
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17388
17487
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17389
17488
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17489
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17490
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17491
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17492
|
+
});
|
|
17390
17493
|
var WorkspaceSettings = external_exports.object({
|
|
17391
17494
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17392
17495
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17402,37 +17505,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17402
17505
|
// Shares writes.
|
|
17403
17506
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17404
17507
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17405
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17508
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17509
|
+
// Records that the user consented to sending findings to the model API for
|
|
17510
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17511
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17512
|
+
// covers the current payload and must be re-granted.
|
|
17513
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17406
17514
|
});
|
|
17407
17515
|
function defaultWorkspaceSettings() {
|
|
17408
17516
|
return WorkspaceSettings.parse({});
|
|
17409
17517
|
}
|
|
17410
|
-
function toEventRow(event) {
|
|
17411
|
-
return {
|
|
17412
|
-
id: event.id,
|
|
17413
|
-
sourceTool: event.sourceTool,
|
|
17414
|
-
kind: event.kind,
|
|
17415
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17416
|
-
contentHash: event.contentHash,
|
|
17417
|
-
content: event.content,
|
|
17418
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17419
|
-
};
|
|
17420
|
-
}
|
|
17421
|
-
function toFindingRow(finding2) {
|
|
17422
|
-
return {
|
|
17423
|
-
id: finding2.id,
|
|
17424
|
-
eventId: finding2.eventId,
|
|
17425
|
-
ruleId: finding2.ruleId,
|
|
17426
|
-
category: finding2.category,
|
|
17427
|
-
severity: finding2.severity,
|
|
17428
|
-
spanStart: finding2.span.start,
|
|
17429
|
-
spanEnd: finding2.span.end,
|
|
17430
|
-
maskedMatch: finding2.maskedMatch,
|
|
17431
|
-
actionTaken: finding2.actionTaken,
|
|
17432
|
-
confidence: finding2.confidence,
|
|
17433
|
-
findingKey: finding2.findingKey ?? null
|
|
17434
|
-
};
|
|
17435
|
-
}
|
|
17436
17518
|
function toInventoryRow(input, id, now) {
|
|
17437
17519
|
return {
|
|
17438
17520
|
id,
|
|
@@ -17502,7 +17584,42 @@ function toInspectionFindingRow(input) {
|
|
|
17502
17584
|
spanEnd: input.span.end,
|
|
17503
17585
|
maskedMatch: input.maskedMatch,
|
|
17504
17586
|
actionTaken: input.actionTaken,
|
|
17505
|
-
confidence: input.confidence
|
|
17587
|
+
confidence: input.confidence,
|
|
17588
|
+
findingKey: input.findingKey ?? null,
|
|
17589
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17590
|
+
};
|
|
17591
|
+
}
|
|
17592
|
+
function toCaptureAttributes(event) {
|
|
17593
|
+
const metadata = event.metadata;
|
|
17594
|
+
return {
|
|
17595
|
+
source_tool: event.sourceTool,
|
|
17596
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17597
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17598
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17599
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17600
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17601
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17602
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17603
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17604
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17605
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17606
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17607
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17608
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17609
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17610
|
+
};
|
|
17611
|
+
}
|
|
17612
|
+
function captureDefinitionVersion(finding2) {
|
|
17613
|
+
return `capture/${finding2.category}/${finding2.severity}`;
|
|
17614
|
+
}
|
|
17615
|
+
function toCaptureDefinitionInput(finding2) {
|
|
17616
|
+
return {
|
|
17617
|
+
ruleId: finding2.ruleId,
|
|
17618
|
+
version: captureDefinitionVersion(finding2),
|
|
17619
|
+
name: finding2.ruleId,
|
|
17620
|
+
category: finding2.category,
|
|
17621
|
+
severity: finding2.severity,
|
|
17622
|
+
definition: JSON.stringify({ ruleId: finding2.ruleId })
|
|
17506
17623
|
};
|
|
17507
17624
|
}
|
|
17508
17625
|
|
|
@@ -17930,6 +18047,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17930
18047
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17931
18048
|
}
|
|
17932
18049
|
|
|
18050
|
+
// ../../packages/persistence/src/ids.ts
|
|
18051
|
+
import { createHash } from "crypto";
|
|
18052
|
+
function sha256Hex(input) {
|
|
18053
|
+
return createHash("sha256").update(input).digest("hex");
|
|
18054
|
+
}
|
|
18055
|
+
function inventoryId(objectType, identityKey) {
|
|
18056
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
18057
|
+
}
|
|
18058
|
+
function sourceProjectId(url2) {
|
|
18059
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
18060
|
+
}
|
|
18061
|
+
function classifiedDataId(cls) {
|
|
18062
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
18063
|
+
}
|
|
18064
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
18065
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
18066
|
+
}
|
|
18067
|
+
function llmCallId(sessionId, messageId) {
|
|
18068
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
18069
|
+
}
|
|
18070
|
+
function toolCallId(sessionId, toolUseId) {
|
|
18071
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
18072
|
+
}
|
|
18073
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
18074
|
+
return sha256Hex(
|
|
18075
|
+
canonicalIdentity([
|
|
18076
|
+
"inspection_finding",
|
|
18077
|
+
auditEventId,
|
|
18078
|
+
ruleId,
|
|
18079
|
+
String(spanStart),
|
|
18080
|
+
String(spanEnd)
|
|
18081
|
+
])
|
|
18082
|
+
);
|
|
18083
|
+
}
|
|
18084
|
+
var NO_SESSION = "no_session";
|
|
18085
|
+
var NO_PATH = "no_path";
|
|
18086
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
18087
|
+
return sha256Hex(
|
|
18088
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
18089
|
+
);
|
|
18090
|
+
}
|
|
18091
|
+
|
|
17933
18092
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17934
18093
|
function escapeLikePattern(s) {
|
|
17935
18094
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -18026,39 +18185,81 @@ function evidenceExists(db, object2) {
|
|
|
18026
18185
|
return schemaObjectExists(db, "table", object2.name);
|
|
18027
18186
|
}
|
|
18028
18187
|
|
|
18029
|
-
// ../../packages/persistence/src/
|
|
18030
|
-
|
|
18031
|
-
|
|
18032
|
-
|
|
18188
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18189
|
+
function allRows(stmt, params) {
|
|
18190
|
+
if (params === void 0) return stmt.all();
|
|
18191
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18192
|
+
return stmt.all(params);
|
|
18033
18193
|
}
|
|
18034
|
-
function
|
|
18035
|
-
|
|
18194
|
+
function getRow(stmt, params) {
|
|
18195
|
+
if (params === void 0) return stmt.get();
|
|
18196
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18197
|
+
return stmt.get(params);
|
|
18036
18198
|
}
|
|
18037
|
-
function
|
|
18038
|
-
return
|
|
18199
|
+
function intToBool(raw) {
|
|
18200
|
+
return raw === 1 || raw === true;
|
|
18039
18201
|
}
|
|
18040
|
-
function
|
|
18041
|
-
return
|
|
18202
|
+
function boolToInt(b) {
|
|
18203
|
+
return b ? 1 : 0;
|
|
18042
18204
|
}
|
|
18043
|
-
function
|
|
18044
|
-
|
|
18205
|
+
function bindParams(row) {
|
|
18206
|
+
const out = {};
|
|
18207
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18208
|
+
out[key] = value === void 0 ? null : value;
|
|
18209
|
+
}
|
|
18210
|
+
return out;
|
|
18045
18211
|
}
|
|
18046
|
-
function
|
|
18047
|
-
return
|
|
18212
|
+
function countScalar(db, sql, params) {
|
|
18213
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18048
18214
|
}
|
|
18049
|
-
function
|
|
18050
|
-
|
|
18215
|
+
function countBy(db, sql, params) {
|
|
18216
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18217
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18218
|
+
map2.set(row.k, row.n);
|
|
18219
|
+
}
|
|
18220
|
+
return map2;
|
|
18051
18221
|
}
|
|
18052
|
-
function
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
|
|
18057
|
-
|
|
18058
|
-
|
|
18059
|
-
|
|
18060
|
-
|
|
18061
|
-
|
|
18222
|
+
function mapRowsTolerant(rows, map2) {
|
|
18223
|
+
const out = [];
|
|
18224
|
+
for (const row of rows) {
|
|
18225
|
+
try {
|
|
18226
|
+
out.push(map2(row));
|
|
18227
|
+
} catch {
|
|
18228
|
+
}
|
|
18229
|
+
}
|
|
18230
|
+
return out;
|
|
18231
|
+
}
|
|
18232
|
+
|
|
18233
|
+
// ../../packages/persistence/src/paths.ts
|
|
18234
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18235
|
+
var DATA_DIR_MODE = 448;
|
|
18236
|
+
var DATA_FILE_MODE = 384;
|
|
18237
|
+
var DB_FILENAME = "aka.db";
|
|
18238
|
+
function chmodBestEffort(path, mode) {
|
|
18239
|
+
try {
|
|
18240
|
+
chmodSync(path, mode);
|
|
18241
|
+
} catch {
|
|
18242
|
+
}
|
|
18243
|
+
}
|
|
18244
|
+
function tightenDir(dir) {
|
|
18245
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18246
|
+
}
|
|
18247
|
+
function ensureDataDirSync(dir) {
|
|
18248
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18249
|
+
tightenDir(dir);
|
|
18250
|
+
}
|
|
18251
|
+
function dbSidecars(file2) {
|
|
18252
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18253
|
+
}
|
|
18254
|
+
function tightenFile(file2) {
|
|
18255
|
+
try {
|
|
18256
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18257
|
+
} catch {
|
|
18258
|
+
}
|
|
18259
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18260
|
+
}
|
|
18261
|
+
function tightenPerms(file2) {
|
|
18262
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18062
18263
|
}
|
|
18063
18264
|
|
|
18064
18265
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -18072,7 +18273,8 @@ function createdIndexName(statement) {
|
|
|
18072
18273
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
18073
18274
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
18074
18275
|
}
|
|
18075
|
-
|
|
18276
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18277
|
+
function applyMigrations(db, file2) {
|
|
18076
18278
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
18077
18279
|
db.exec(
|
|
18078
18280
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -18086,6 +18288,7 @@ function applyMigrations(db) {
|
|
|
18086
18288
|
);
|
|
18087
18289
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
18088
18290
|
if (applied.has(migration.tag)) continue;
|
|
18291
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
18089
18292
|
const evidence = evidenceObjects(migration.sql);
|
|
18090
18293
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
18091
18294
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18130,7 +18333,6 @@ function applyMigrations(db) {
|
|
|
18130
18333
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18131
18334
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18132
18335
|
}
|
|
18133
|
-
ensureSyncedAtColumn(db, "events");
|
|
18134
18336
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18135
18337
|
ensureScanLedgerTable(db);
|
|
18136
18338
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18138,6 +18340,47 @@ function applyMigrations(db) {
|
|
|
18138
18340
|
ensureWriteGateTrigger(db);
|
|
18139
18341
|
ensureTokenUsageColumns(db);
|
|
18140
18342
|
reconcileSourceProjectIds(db);
|
|
18343
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18344
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18345
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18346
|
+
}
|
|
18347
|
+
}
|
|
18348
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18349
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18350
|
+
if (!migration) return;
|
|
18351
|
+
if (file2) {
|
|
18352
|
+
try {
|
|
18353
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18354
|
+
} catch (error51) {
|
|
18355
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18356
|
+
return;
|
|
18357
|
+
}
|
|
18358
|
+
}
|
|
18359
|
+
try {
|
|
18360
|
+
withTransaction(
|
|
18361
|
+
db,
|
|
18362
|
+
() => {
|
|
18363
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18364
|
+
if (alreadyDropped) return;
|
|
18365
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18366
|
+
db.exec(statement);
|
|
18367
|
+
}
|
|
18368
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18369
|
+
migration.tag,
|
|
18370
|
+
Date.now()
|
|
18371
|
+
);
|
|
18372
|
+
},
|
|
18373
|
+
"IMMEDIATE"
|
|
18374
|
+
);
|
|
18375
|
+
} catch (error51) {
|
|
18376
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18377
|
+
}
|
|
18378
|
+
}
|
|
18379
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18380
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18381
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18382
|
+
tightenFile(backup);
|
|
18383
|
+
return backup;
|
|
18141
18384
|
}
|
|
18142
18385
|
var TOKEN_USAGE_COLUMNS = [
|
|
18143
18386
|
{
|
|
@@ -18166,6 +18409,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18166
18409
|
}
|
|
18167
18410
|
];
|
|
18168
18411
|
function ensureTokenUsageColumns(db) {
|
|
18412
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18169
18413
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18170
18414
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18171
18415
|
if (!existing.has(column.name)) {
|
|
@@ -18231,11 +18475,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18231
18475
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18232
18476
|
}
|
|
18233
18477
|
}
|
|
18478
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18479
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18480
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18481
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18482
|
+
return row?.lastRowid ?? 0;
|
|
18483
|
+
}
|
|
18484
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18485
|
+
db.prepare(
|
|
18486
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18487
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18488
|
+
).run(source, lastRowid);
|
|
18489
|
+
}
|
|
18490
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18491
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18492
|
+
let processed = 0;
|
|
18493
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18494
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18495
|
+
if (rows.length === 0) return true;
|
|
18496
|
+
withTransaction(
|
|
18497
|
+
db,
|
|
18498
|
+
() => {
|
|
18499
|
+
handleRows(rows);
|
|
18500
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18501
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18502
|
+
},
|
|
18503
|
+
"IMMEDIATE"
|
|
18504
|
+
);
|
|
18505
|
+
processed += rows.length;
|
|
18506
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18507
|
+
}
|
|
18508
|
+
return false;
|
|
18509
|
+
}
|
|
18510
|
+
function parseLegacyEventMetadata(raw) {
|
|
18511
|
+
if (raw === null) return void 0;
|
|
18512
|
+
try {
|
|
18513
|
+
return JSON.parse(raw);
|
|
18514
|
+
} catch {
|
|
18515
|
+
return void 0;
|
|
18516
|
+
}
|
|
18517
|
+
}
|
|
18518
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18519
|
+
return JSON.stringify(
|
|
18520
|
+
toCaptureAttributes({
|
|
18521
|
+
id: row.id,
|
|
18522
|
+
sourceTool: row.sourceTool,
|
|
18523
|
+
kind: row.kind,
|
|
18524
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18525
|
+
contentHash: row.contentHash,
|
|
18526
|
+
content: row.content,
|
|
18527
|
+
metadata: row.metadata
|
|
18528
|
+
})
|
|
18529
|
+
);
|
|
18530
|
+
}
|
|
18531
|
+
function copyLegacyEvents(db) {
|
|
18532
|
+
const selectStmt = db.prepare(
|
|
18533
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18534
|
+
content_hash AS contentHash, content, metadata
|
|
18535
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18536
|
+
);
|
|
18537
|
+
const insertStmt = db.prepare(
|
|
18538
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18539
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18540
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18541
|
+
);
|
|
18542
|
+
const stubRootStmt = db.prepare(
|
|
18543
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18544
|
+
);
|
|
18545
|
+
return drainLegacyTable(
|
|
18546
|
+
db,
|
|
18547
|
+
"events",
|
|
18548
|
+
selectStmt,
|
|
18549
|
+
(rows) => {
|
|
18550
|
+
for (const row of rows) {
|
|
18551
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18552
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18553
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18554
|
+
insertStmt.run(
|
|
18555
|
+
bindParams({
|
|
18556
|
+
id: row.id,
|
|
18557
|
+
parentId: sessionId,
|
|
18558
|
+
rootSessionId: sessionId,
|
|
18559
|
+
eventType: row.kind,
|
|
18560
|
+
startedAt: row.occurredAt,
|
|
18561
|
+
content: row.content,
|
|
18562
|
+
contentHash: row.contentHash,
|
|
18563
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18564
|
+
})
|
|
18565
|
+
);
|
|
18566
|
+
}
|
|
18567
|
+
}
|
|
18568
|
+
);
|
|
18569
|
+
}
|
|
18570
|
+
function copyLegacyFindings(db) {
|
|
18571
|
+
const selectStmt = db.prepare(
|
|
18572
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18573
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18574
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18575
|
+
first_detected_at AS firstDetectedAt
|
|
18576
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18577
|
+
);
|
|
18578
|
+
const definitionStmt = db.prepare(
|
|
18579
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18580
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18581
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18582
|
+
);
|
|
18583
|
+
const findingStmt = db.prepare(
|
|
18584
|
+
`INSERT INTO inspection_findings
|
|
18585
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18586
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18587
|
+
finding_key, first_detected_at)
|
|
18588
|
+
VALUES
|
|
18589
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18590
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18591
|
+
:findingKey, :firstDetectedAt)
|
|
18592
|
+
ON CONFLICT(id) DO NOTHING
|
|
18593
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18594
|
+
first_detected_at = CASE
|
|
18595
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18596
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18597
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18598
|
+
END`
|
|
18599
|
+
);
|
|
18600
|
+
return drainLegacyTable(
|
|
18601
|
+
db,
|
|
18602
|
+
"findings",
|
|
18603
|
+
selectStmt,
|
|
18604
|
+
(rows) => {
|
|
18605
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18606
|
+
for (const row of rows) {
|
|
18607
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18608
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18609
|
+
if (definitionId === void 0) {
|
|
18610
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18611
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18612
|
+
definitionStmt.run(
|
|
18613
|
+
bindParams({
|
|
18614
|
+
id: definitionId,
|
|
18615
|
+
ruleId: row.ruleId,
|
|
18616
|
+
name: row.ruleId,
|
|
18617
|
+
category: row.category,
|
|
18618
|
+
severity: row.severity,
|
|
18619
|
+
definition: "",
|
|
18620
|
+
version: version2
|
|
18621
|
+
})
|
|
18622
|
+
);
|
|
18623
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18624
|
+
}
|
|
18625
|
+
findingStmt.run(
|
|
18626
|
+
bindParams({
|
|
18627
|
+
id: row.id,
|
|
18628
|
+
auditEventId: row.eventId,
|
|
18629
|
+
inspectionDefinitionId: definitionId,
|
|
18630
|
+
spanStart: row.spanStart,
|
|
18631
|
+
spanEnd: row.spanEnd,
|
|
18632
|
+
maskedMatch: row.maskedMatch,
|
|
18633
|
+
actionTaken: row.actionTaken,
|
|
18634
|
+
confidence: row.confidence,
|
|
18635
|
+
findingKey: row.findingKey,
|
|
18636
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18637
|
+
})
|
|
18638
|
+
);
|
|
18639
|
+
}
|
|
18640
|
+
}
|
|
18641
|
+
);
|
|
18642
|
+
}
|
|
18643
|
+
function runLegacyHistoryBackfill(db) {
|
|
18644
|
+
try {
|
|
18645
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18646
|
+
if (!eventsCaughtUp) return false;
|
|
18647
|
+
return copyLegacyFindings(db);
|
|
18648
|
+
} catch (error51) {
|
|
18649
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18650
|
+
return false;
|
|
18651
|
+
}
|
|
18652
|
+
}
|
|
18234
18653
|
function isForeignSqliteLineage(db) {
|
|
18235
18654
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18236
18655
|
return columnNames(db, "events").includes("tenant_id");
|
|
18237
18656
|
}
|
|
18238
18657
|
function ensureSyncedAtColumn(db, table) {
|
|
18658
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
18239
18659
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
18240
18660
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18241
18661
|
}
|
|
@@ -18256,6 +18676,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18256
18676
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18257
18677
|
)`);
|
|
18258
18678
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18679
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18259
18680
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18260
18681
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18261
18682
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18283,30 +18704,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18283
18704
|
)`);
|
|
18284
18705
|
}
|
|
18285
18706
|
|
|
18286
|
-
// ../../packages/persistence/src/paths.ts
|
|
18287
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18288
|
-
var DATA_DIR_MODE = 448;
|
|
18289
|
-
var DATA_FILE_MODE = 384;
|
|
18290
|
-
var DB_FILENAME = "aka.db";
|
|
18291
|
-
function ensureDataDirSync(dir) {
|
|
18292
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18293
|
-
try {
|
|
18294
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18295
|
-
} catch {
|
|
18296
|
-
}
|
|
18297
|
-
}
|
|
18298
|
-
function walSidecars(file2) {
|
|
18299
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18300
|
-
}
|
|
18301
|
-
function tightenPerms(file2) {
|
|
18302
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18303
|
-
try {
|
|
18304
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18305
|
-
} catch {
|
|
18306
|
-
}
|
|
18307
|
-
}
|
|
18308
|
-
}
|
|
18309
|
-
|
|
18310
18707
|
// ../../packages/persistence/src/internal/json.ts
|
|
18311
18708
|
function safeJson(s, fallback) {
|
|
18312
18709
|
if (s == null) return fallback;
|
|
@@ -18326,51 +18723,6 @@ function parseJsonObject(s) {
|
|
|
18326
18723
|
return void 0;
|
|
18327
18724
|
}
|
|
18328
18725
|
|
|
18329
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18330
|
-
function allRows(stmt, params) {
|
|
18331
|
-
if (params === void 0) return stmt.all();
|
|
18332
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18333
|
-
return stmt.all(params);
|
|
18334
|
-
}
|
|
18335
|
-
function getRow(stmt, params) {
|
|
18336
|
-
if (params === void 0) return stmt.get();
|
|
18337
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18338
|
-
return stmt.get(params);
|
|
18339
|
-
}
|
|
18340
|
-
function intToBool(raw) {
|
|
18341
|
-
return raw === 1 || raw === true;
|
|
18342
|
-
}
|
|
18343
|
-
function boolToInt(b) {
|
|
18344
|
-
return b ? 1 : 0;
|
|
18345
|
-
}
|
|
18346
|
-
function bindParams(row) {
|
|
18347
|
-
const out = {};
|
|
18348
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18349
|
-
out[key] = value === void 0 ? null : value;
|
|
18350
|
-
}
|
|
18351
|
-
return out;
|
|
18352
|
-
}
|
|
18353
|
-
function countScalar(db, sql, params) {
|
|
18354
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18355
|
-
}
|
|
18356
|
-
function countBy(db, sql, params) {
|
|
18357
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18358
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18359
|
-
map2.set(row.k, row.n);
|
|
18360
|
-
}
|
|
18361
|
-
return map2;
|
|
18362
|
-
}
|
|
18363
|
-
function mapRowsTolerant(rows, map2) {
|
|
18364
|
-
const out = [];
|
|
18365
|
-
for (const row of rows) {
|
|
18366
|
-
try {
|
|
18367
|
-
out.push(map2(row));
|
|
18368
|
-
} catch {
|
|
18369
|
-
}
|
|
18370
|
-
}
|
|
18371
|
-
return out;
|
|
18372
|
-
}
|
|
18373
|
-
|
|
18374
18726
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18375
18727
|
var DAY_MS = 864e5;
|
|
18376
18728
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18987,6 +19339,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18987
19339
|
})
|
|
18988
19340
|
);
|
|
18989
19341
|
}
|
|
19342
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19343
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19344
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19345
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19346
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19347
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19348
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19349
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19350
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19351
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19352
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19353
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19354
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19355
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19356
|
+
}
|
|
18990
19357
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18991
19358
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18992
19359
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19448,8 +19815,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19448
19815
|
)
|
|
19449
19816
|
);
|
|
19450
19817
|
}
|
|
19451
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19452
|
-
//
|
|
19818
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19819
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19820
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19821
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19822
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19823
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19824
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19825
|
+
// to the old findings⋈events one.
|
|
19453
19826
|
countFindingsLast30d(ruleIds) {
|
|
19454
19827
|
if (ruleIds.length === 0) return 0;
|
|
19455
19828
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19457,8 +19830,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19457
19830
|
return countScalar(
|
|
19458
19831
|
this.db,
|
|
19459
19832
|
`SELECT count(*) AS n
|
|
19460
|
-
FROM
|
|
19461
|
-
|
|
19833
|
+
FROM inspection_findings f
|
|
19834
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19835
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19836
|
+
WHERE e.started_at >= ?
|
|
19837
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19838
|
+
AND d.rule_id IN (${inClause})`,
|
|
19462
19839
|
[since, ...ruleIds]
|
|
19463
19840
|
);
|
|
19464
19841
|
}
|
|
@@ -19468,35 +19845,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19468
19845
|
var SqliteEventsRepository = class {
|
|
19469
19846
|
constructor(db) {
|
|
19470
19847
|
this.db = db;
|
|
19471
|
-
this.insertStmt = db.prepare(
|
|
19472
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19473
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19474
|
-
);
|
|
19475
19848
|
}
|
|
19476
19849
|
db;
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
this.insertStmt.run(
|
|
19481
|
-
bindParams({
|
|
19482
|
-
id: row.id,
|
|
19483
|
-
sourceTool: row.sourceTool,
|
|
19484
|
-
kind: row.kind,
|
|
19485
|
-
occurredAt: row.occurredAt,
|
|
19486
|
-
contentHash: row.contentHash,
|
|
19487
|
-
content: row.content,
|
|
19488
|
-
metadata: row.metadata
|
|
19489
|
-
})
|
|
19490
|
-
);
|
|
19491
|
-
}
|
|
19492
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19493
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19494
|
-
// never duplicates findings.
|
|
19850
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19851
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19852
|
+
// scan never duplicates findings.
|
|
19495
19853
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19496
19854
|
// async EventsReadPort contract.
|
|
19855
|
+
//
|
|
19856
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19857
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19858
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19859
|
+
// index-friendly rather than walking rows that can never match.
|
|
19497
19860
|
contentHashes() {
|
|
19498
19861
|
const rows = allRows(
|
|
19499
|
-
this.db.prepare(
|
|
19862
|
+
this.db.prepare(
|
|
19863
|
+
`SELECT content_hash FROM audit_events
|
|
19864
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19865
|
+
)
|
|
19500
19866
|
);
|
|
19501
19867
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19502
19868
|
}
|
|
@@ -19832,17 +20198,20 @@ function parseExceptionRow(row) {
|
|
|
19832
20198
|
}
|
|
19833
20199
|
|
|
19834
20200
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19835
|
-
function
|
|
20201
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19836
20202
|
return `(
|
|
19837
|
-
SELECT fr
|
|
20203
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19838
20204
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19839
20205
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19840
20206
|
LIMIT 1
|
|
19841
20207
|
)`;
|
|
19842
20208
|
}
|
|
20209
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20210
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20211
|
+
}
|
|
19843
20212
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19844
|
-
SELECT finding_key, status FROM (
|
|
19845
|
-
SELECT fr.finding_key, fr.status,
|
|
20213
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20214
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19846
20215
|
ROW_NUMBER() OVER (
|
|
19847
20216
|
PARTITION BY fr.finding_key
|
|
19848
20217
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19869,68 +20238,21 @@ var DAY_MS3 = 864e5;
|
|
|
19869
20238
|
var SqliteFindingsRepository = class {
|
|
19870
20239
|
constructor(db) {
|
|
19871
20240
|
this.db = db;
|
|
19872
|
-
this.insertStmt = db.prepare(
|
|
19873
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19874
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19875
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19876
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19877
|
-
event_id = excluded.event_id,
|
|
19878
|
-
category = excluded.category,
|
|
19879
|
-
severity = excluded.severity,
|
|
19880
|
-
span_start = excluded.span_start,
|
|
19881
|
-
span_end = excluded.span_end,
|
|
19882
|
-
masked_match = excluded.masked_match,
|
|
19883
|
-
action_taken = excluded.action_taken,
|
|
19884
|
-
confidence = excluded.confidence`
|
|
19885
|
-
);
|
|
19886
|
-
this.sessionDupStmt = db.prepare(
|
|
19887
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19888
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19889
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19890
|
-
LIMIT 1`
|
|
19891
|
-
);
|
|
19892
20241
|
}
|
|
19893
20242
|
db;
|
|
19894
|
-
insertStmt;
|
|
19895
|
-
sessionDupStmt;
|
|
19896
|
-
insertFindings(findings, scope = {}) {
|
|
19897
|
-
for (const finding2 of findings) {
|
|
19898
|
-
if (scope.sessionId && this.isSessionDuplicate(finding2, scope.sessionId)) continue;
|
|
19899
|
-
const row = toFindingRow(finding2);
|
|
19900
|
-
this.insertStmt.run({
|
|
19901
|
-
id: row.id,
|
|
19902
|
-
eventId: row.eventId,
|
|
19903
|
-
ruleId: row.ruleId,
|
|
19904
|
-
category: row.category,
|
|
19905
|
-
severity: row.severity,
|
|
19906
|
-
spanStart: row.spanStart,
|
|
19907
|
-
spanEnd: row.spanEnd,
|
|
19908
|
-
maskedMatch: row.maskedMatch,
|
|
19909
|
-
actionTaken: row.actionTaken,
|
|
19910
|
-
confidence: row.confidence,
|
|
19911
|
-
findingKey: row.findingKey ?? null
|
|
19912
|
-
});
|
|
19913
|
-
}
|
|
19914
|
-
}
|
|
19915
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19916
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19917
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19918
|
-
isSessionDuplicate(finding2, sessionId) {
|
|
19919
|
-
const hit = this.sessionDupStmt.get({
|
|
19920
|
-
ruleId: finding2.ruleId,
|
|
19921
|
-
maskedMatch: finding2.maskedMatch,
|
|
19922
|
-
sessionId
|
|
19923
|
-
});
|
|
19924
|
-
return hit !== void 0;
|
|
19925
|
-
}
|
|
19926
20243
|
recentFindings(opts) {
|
|
19927
20244
|
const limit = opts?.limit ?? 50;
|
|
19928
20245
|
const rows = allRows(
|
|
19929
20246
|
this.db.prepare(
|
|
19930
|
-
`SELECT f.id, f.event_id,
|
|
19931
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19932
|
-
|
|
19933
|
-
|
|
20247
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20248
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20249
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20250
|
+
e.event_type AS kind
|
|
20251
|
+
FROM inspection_findings f
|
|
20252
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20253
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20254
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20255
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19934
20256
|
LIMIT :limit`
|
|
19935
20257
|
),
|
|
19936
20258
|
{ limit }
|
|
@@ -19952,25 +20274,34 @@ var SqliteFindingsRepository = class {
|
|
|
19952
20274
|
);
|
|
19953
20275
|
}
|
|
19954
20276
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19955
|
-
* session-stamped
|
|
20277
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19956
20278
|
* page can label its findings link without the grouped pipeline. */
|
|
19957
20279
|
sessionFindingsCount(sessionId) {
|
|
19958
20280
|
if (!sessionId) return Promise.resolve(0);
|
|
19959
20281
|
return Promise.resolve(
|
|
19960
20282
|
countScalar(
|
|
19961
20283
|
this.db,
|
|
19962
|
-
`SELECT count(*) AS n FROM
|
|
19963
|
-
JOIN
|
|
19964
|
-
WHERE
|
|
20284
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20285
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20286
|
+
WHERE e.root_session_id = :sessionId
|
|
20287
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19965
20288
|
{ sessionId }
|
|
19966
20289
|
)
|
|
19967
20290
|
);
|
|
19968
20291
|
}
|
|
19969
|
-
/** Per-rule transcript firing tally for one session —
|
|
19970
|
-
*
|
|
19971
|
-
*
|
|
19972
|
-
*
|
|
19973
|
-
*
|
|
20292
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20293
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20294
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20295
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20296
|
+
* tally with the deduped groups it lists.
|
|
20297
|
+
*
|
|
20298
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20299
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20300
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20301
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20302
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20303
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20304
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19974
20305
|
sessionFirings(sessionId) {
|
|
19975
20306
|
return Object.fromEntries(
|
|
19976
20307
|
countBy(
|
|
@@ -19980,18 +20311,25 @@ var SqliteFindingsRepository = class {
|
|
|
19980
20311
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19981
20312
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19982
20313
|
WHERE e.root_session_id = :sessionId
|
|
20314
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19983
20315
|
GROUP BY d.rule_id`,
|
|
19984
20316
|
{ sessionId }
|
|
19985
20317
|
)
|
|
19986
20318
|
);
|
|
19987
20319
|
}
|
|
19988
20320
|
/**
|
|
19989
|
-
* Grouped findings for the dashboard — joins
|
|
19990
|
-
* toolName from
|
|
19991
|
-
*
|
|
20321
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20322
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20323
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20324
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20325
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20326
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20327
|
+
* severity then recency. Filtering
|
|
19992
20328
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19993
20329
|
* reflect the full filtered set; `items` is the requested
|
|
19994
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20330
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20331
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20332
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19995
20333
|
*
|
|
19996
20334
|
* Two reads, neither of which materializes a row per finding:
|
|
19997
20335
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -20004,10 +20342,11 @@ var SqliteFindingsRepository = class {
|
|
|
20004
20342
|
* rule is ever restated in SQL.
|
|
20005
20343
|
*/
|
|
20006
20344
|
listGroupedFindings(query) {
|
|
20007
|
-
const sessionPredicate = query.sessionId ? `
|
|
20345
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20346
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
20008
20347
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
20009
20348
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
20010
|
-
predicate
|
|
20349
|
+
predicate,
|
|
20011
20350
|
params: sessionParams
|
|
20012
20351
|
});
|
|
20013
20352
|
const rows = allRows(
|
|
@@ -20015,24 +20354,26 @@ var SqliteFindingsRepository = class {
|
|
|
20015
20354
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
20016
20355
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
20017
20356
|
FROM (
|
|
20018
|
-
SELECT f.id AS id,
|
|
20019
|
-
|
|
20357
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20358
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
20020
20359
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
20021
|
-
e.
|
|
20022
|
-
json_extract(e.
|
|
20023
|
-
json_extract(e.
|
|
20024
|
-
json_extract(e.
|
|
20025
|
-
e.
|
|
20360
|
+
e.started_at AS occurred_at,
|
|
20361
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20362
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20363
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20364
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20365
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
20026
20366
|
latest.status AS latest_status,
|
|
20027
20367
|
ROW_NUMBER() OVER (
|
|
20028
|
-
PARTITION BY
|
|
20029
|
-
ORDER BY e.
|
|
20368
|
+
PARTITION BY d.rule_id
|
|
20369
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
20030
20370
|
) AS rn
|
|
20031
|
-
FROM
|
|
20032
|
-
JOIN
|
|
20371
|
+
FROM inspection_findings f
|
|
20372
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20373
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20033
20374
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20034
20375
|
ON latest.finding_key = f.finding_key
|
|
20035
|
-
${
|
|
20376
|
+
${predicate}
|
|
20036
20377
|
)
|
|
20037
20378
|
WHERE rn <= :cap
|
|
20038
20379
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -20059,17 +20400,29 @@ var SqliteFindingsRepository = class {
|
|
|
20059
20400
|
severity: query.severity,
|
|
20060
20401
|
providers: query.provider,
|
|
20061
20402
|
actions: query.action,
|
|
20403
|
+
statuses: query.status,
|
|
20062
20404
|
subtype: query.subtype,
|
|
20063
20405
|
q: query.q
|
|
20064
20406
|
};
|
|
20065
20407
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
20066
20408
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20409
|
+
const statusFilter = query.status ?? [];
|
|
20067
20410
|
const totals = {
|
|
20068
|
-
findings: sorted.reduce((acc, g) =>
|
|
20411
|
+
findings: sorted.reduce((acc, g) => {
|
|
20412
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20413
|
+
const agg = aggregates.get(g.id);
|
|
20414
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20415
|
+
}, 0),
|
|
20069
20416
|
groups: sorted.length
|
|
20070
20417
|
};
|
|
20071
20418
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
20072
|
-
const
|
|
20419
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20420
|
+
const items = sorted.slice(0, limit).map(
|
|
20421
|
+
(g) => statusSet ? {
|
|
20422
|
+
...g,
|
|
20423
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20424
|
+
} : g
|
|
20425
|
+
);
|
|
20073
20426
|
return Promise.resolve({
|
|
20074
20427
|
totals,
|
|
20075
20428
|
facets,
|
|
@@ -20083,45 +20436,62 @@ var SqliteFindingsRepository = class {
|
|
|
20083
20436
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
20084
20437
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
20085
20438
|
*
|
|
20086
|
-
*
|
|
20087
|
-
*
|
|
20088
|
-
*
|
|
20089
|
-
* status
|
|
20090
|
-
*
|
|
20091
|
-
*
|
|
20439
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20440
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20441
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20442
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20443
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20444
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20445
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20446
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20447
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20448
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20449
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
20092
20450
|
* a group's row stays small however many findings it holds.
|
|
20093
20451
|
*
|
|
20094
20452
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
20095
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
20096
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
20097
|
-
* that is a string proportional to the store (~8MB over
|
|
20098
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
20099
|
-
* match an instance outside the preview, which searching
|
|
20100
|
-
* would silently lose, so it is fetched only when the
|
|
20101
|
-
* carries a `q`.
|
|
20453
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20454
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20455
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20456
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20457
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20458
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20459
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20460
|
+
* path repeating across tuples.)
|
|
20102
20461
|
*/
|
|
20103
20462
|
groupAggregates(withSearchText, scope) {
|
|
20104
|
-
const
|
|
20105
|
-
group_concat(DISTINCT json_extract(e.
|
|
20106
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20463
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20464
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20465
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
20107
20466
|
const rows = this.db.prepare(
|
|
20108
|
-
`SELECT
|
|
20109
|
-
|
|
20110
|
-
max(
|
|
20111
|
-
group_concat(
|
|
20112
|
-
group_concat(
|
|
20113
|
-
group_concat(
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20123
|
-
|
|
20124
|
-
|
|
20467
|
+
`SELECT rule_id,
|
|
20468
|
+
sum(tuple_count) AS instance_count,
|
|
20469
|
+
max(latest_at) AS latest_at,
|
|
20470
|
+
group_concat(source_tools) AS source_tools,
|
|
20471
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20472
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20473
|
+
group_concat(repos) AS repos,
|
|
20474
|
+
group_concat(files) AS files,
|
|
20475
|
+
group_concat(tool_names) AS tool_names
|
|
20476
|
+
FROM (
|
|
20477
|
+
SELECT d.rule_id AS rule_id,
|
|
20478
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20479
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20480
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20481
|
+
count(*) AS tuple_count,
|
|
20482
|
+
max(e.started_at) AS latest_at,
|
|
20483
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20484
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20485
|
+
${innerSearchColumns}
|
|
20486
|
+
FROM inspection_findings f
|
|
20487
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20488
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20489
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20490
|
+
ON latest.finding_key = f.finding_key
|
|
20491
|
+
${scope.predicate}
|
|
20492
|
+
GROUP BY d.rule_id, status_tuple
|
|
20493
|
+
)
|
|
20494
|
+
GROUP BY rule_id`
|
|
20125
20495
|
).all(scope.params);
|
|
20126
20496
|
return new Map(
|
|
20127
20497
|
rows.map((r) => [
|
|
@@ -20131,13 +20501,14 @@ var SqliteFindingsRepository = class {
|
|
|
20131
20501
|
sourceTools: splitConcat(r.source_tools),
|
|
20132
20502
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20133
20503
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20134
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20504
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20135
20505
|
return {
|
|
20136
20506
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20137
20507
|
// so the marker stands in for the key itself (never rendered).
|
|
20138
20508
|
kind,
|
|
20139
20509
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20140
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20510
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20511
|
+
count: Number(count)
|
|
20141
20512
|
};
|
|
20142
20513
|
}),
|
|
20143
20514
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20154,10 +20525,21 @@ var SqliteFindingsRepository = class {
|
|
|
20154
20525
|
);
|
|
20155
20526
|
}
|
|
20156
20527
|
healthSummary() {
|
|
20157
|
-
const total = countScalar(
|
|
20528
|
+
const total = countScalar(
|
|
20529
|
+
this.db,
|
|
20530
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20531
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20532
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20533
|
+
);
|
|
20158
20534
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20159
20535
|
const grouped = allRows(
|
|
20160
|
-
this.db.prepare(
|
|
20536
|
+
this.db.prepare(
|
|
20537
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20538
|
+
FROM inspection_findings f
|
|
20539
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20540
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20541
|
+
GROUP BY f.action_taken`
|
|
20542
|
+
)
|
|
20161
20543
|
);
|
|
20162
20544
|
for (const row of grouped) {
|
|
20163
20545
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20165,12 +20547,15 @@ var SqliteFindingsRepository = class {
|
|
|
20165
20547
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20166
20548
|
const sevRows = allRows(
|
|
20167
20549
|
this.db.prepare(
|
|
20168
|
-
`SELECT
|
|
20169
|
-
FROM
|
|
20550
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20551
|
+
FROM inspection_findings f
|
|
20552
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20553
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20170
20554
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20171
20555
|
ON latest.finding_key = f.finding_key
|
|
20172
|
-
WHERE
|
|
20173
|
-
|
|
20556
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20557
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20558
|
+
GROUP BY d.severity`
|
|
20174
20559
|
)
|
|
20175
20560
|
);
|
|
20176
20561
|
for (const row of sevRows) {
|
|
@@ -20191,9 +20576,11 @@ var SqliteFindingsRepository = class {
|
|
|
20191
20576
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20192
20577
|
const rows = allRows(
|
|
20193
20578
|
this.db.prepare(
|
|
20194
|
-
`SELECT date(e.
|
|
20195
|
-
FROM
|
|
20196
|
-
|
|
20579
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20580
|
+
FROM inspection_findings f
|
|
20581
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20582
|
+
WHERE e.started_at >= :since
|
|
20583
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20197
20584
|
GROUP BY day, f.action_taken`
|
|
20198
20585
|
),
|
|
20199
20586
|
{ since }
|
|
@@ -20258,15 +20645,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20258
20645
|
this.insertStmt = db.prepare(
|
|
20259
20646
|
`INSERT INTO inspection_findings
|
|
20260
20647
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20261
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20648
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20649
|
+
finding_key, first_detected_at)
|
|
20262
20650
|
VALUES
|
|
20263
20651
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20264
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20265
|
-
|
|
20652
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20653
|
+
:findingKey,
|
|
20654
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20655
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20656
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20657
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20658
|
+
audit_event_id = excluded.audit_event_id,
|
|
20659
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20660
|
+
classified_data_id = excluded.classified_data_id,
|
|
20661
|
+
span_start = excluded.span_start,
|
|
20662
|
+
span_end = excluded.span_end,
|
|
20663
|
+
masked_match = excluded.masked_match,
|
|
20664
|
+
action_taken = excluded.action_taken,
|
|
20665
|
+
confidence = excluded.confidence`
|
|
20666
|
+
);
|
|
20667
|
+
this.sessionDupStmt = db.prepare(
|
|
20668
|
+
`SELECT 1 FROM inspection_findings f
|
|
20669
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20670
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20671
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20672
|
+
AND e.root_session_id = :sessionId
|
|
20673
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20674
|
+
LIMIT 1`
|
|
20675
|
+
);
|
|
20676
|
+
this.eventDupStmt = db.prepare(
|
|
20677
|
+
`SELECT 1 FROM inspection_findings f
|
|
20678
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20679
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20680
|
+
AND f.masked_match = :maskedMatch
|
|
20681
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20682
|
+
LIMIT 1`
|
|
20266
20683
|
);
|
|
20267
20684
|
}
|
|
20268
20685
|
db;
|
|
20269
20686
|
insertStmt;
|
|
20687
|
+
sessionDupStmt;
|
|
20688
|
+
eventDupStmt;
|
|
20689
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20690
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20691
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20692
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20693
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20694
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20695
|
+
}
|
|
20696
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20697
|
+
// recorded against the given audit event.
|
|
20698
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20699
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20700
|
+
}
|
|
20270
20701
|
insertFinding(input) {
|
|
20271
20702
|
const row = toInspectionFindingRow(input);
|
|
20272
20703
|
this.insertStmt.run(
|
|
@@ -20279,7 +20710,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20279
20710
|
spanEnd: row.spanEnd,
|
|
20280
20711
|
maskedMatch: row.maskedMatch,
|
|
20281
20712
|
actionTaken: row.actionTaken,
|
|
20282
|
-
confidence: row.confidence
|
|
20713
|
+
confidence: row.confidence,
|
|
20714
|
+
findingKey: row.findingKey,
|
|
20715
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20283
20716
|
})
|
|
20284
20717
|
);
|
|
20285
20718
|
}
|
|
@@ -20551,7 +20984,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20551
20984
|
installedRuleset() {
|
|
20552
20985
|
const rows = allRows(
|
|
20553
20986
|
this.db.prepare(
|
|
20554
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20987
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20555
20988
|
)
|
|
20556
20989
|
);
|
|
20557
20990
|
const out = {
|
|
@@ -20559,7 +20992,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20559
20992
|
enabledPacks: 0,
|
|
20560
20993
|
rules: [],
|
|
20561
20994
|
invalidRules: 0,
|
|
20562
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20995
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20996
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20563
20997
|
};
|
|
20564
20998
|
for (const row of rows) {
|
|
20565
20999
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20581,6 +21015,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20581
21015
|
if (parsed.success) {
|
|
20582
21016
|
out.rules.push(parsed.data);
|
|
20583
21017
|
out.ruleActions.set(parsed.data.id, action);
|
|
21018
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20584
21019
|
} else out.invalidRules += 1;
|
|
20585
21020
|
}
|
|
20586
21021
|
}
|
|
@@ -21755,19 +22190,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21755
22190
|
);
|
|
21756
22191
|
this.openAtRestStmt = db.prepare(
|
|
21757
22192
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21758
|
-
FROM
|
|
21759
|
-
JOIN
|
|
21760
|
-
WHERE e.
|
|
21761
|
-
AND json_extract(e.
|
|
22193
|
+
FROM inspection_findings f
|
|
22194
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22195
|
+
WHERE e.event_type = 'code_change'
|
|
22196
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21762
22197
|
AND f.finding_key IS NOT NULL
|
|
21763
22198
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21764
22199
|
);
|
|
21765
22200
|
this.resolvedAtRestStmt = db.prepare(
|
|
21766
22201
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21767
|
-
FROM
|
|
21768
|
-
JOIN
|
|
21769
|
-
WHERE e.
|
|
21770
|
-
AND json_extract(e.
|
|
22202
|
+
FROM inspection_findings f
|
|
22203
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22204
|
+
WHERE e.event_type = 'code_change'
|
|
22205
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21771
22206
|
AND f.finding_key IS NOT NULL
|
|
21772
22207
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21773
22208
|
);
|
|
@@ -21990,25 +22425,27 @@ var SqliteSecurityRepository = class {
|
|
|
21990
22425
|
severitySummary() {
|
|
21991
22426
|
const rows = allRows(
|
|
21992
22427
|
this.db.prepare(
|
|
21993
|
-
`SELECT
|
|
22428
|
+
`SELECT d.severity AS severity,
|
|
21994
22429
|
COUNT(*) AS count,
|
|
21995
22430
|
SUM(CASE
|
|
21996
|
-
WHEN e.
|
|
22431
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21997
22432
|
WHEN f.finding_key IS NULL THEN 0
|
|
21998
22433
|
WHEN latest.status = 'resolved' THEN 1
|
|
21999
22434
|
ELSE 0
|
|
22000
22435
|
END) AS caught,
|
|
22001
22436
|
SUM(CASE
|
|
22002
|
-
WHEN e.
|
|
22437
|
+
WHEN e.event_type = 'code_change'
|
|
22003
22438
|
AND f.finding_key IS NOT NULL
|
|
22004
22439
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
22005
22440
|
ELSE 0
|
|
22006
22441
|
END) AS open_at_rest
|
|
22007
|
-
FROM
|
|
22008
|
-
JOIN
|
|
22442
|
+
FROM inspection_findings f
|
|
22443
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22444
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22009
22445
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22010
22446
|
ON latest.finding_key = f.finding_key
|
|
22011
|
-
|
|
22447
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22448
|
+
GROUP BY d.severity`
|
|
22012
22449
|
)
|
|
22013
22450
|
);
|
|
22014
22451
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -22074,7 +22511,7 @@ var SqliteSecurityRepository = class {
|
|
|
22074
22511
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
22075
22512
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
22076
22513
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
22077
|
-
// (
|
|
22514
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
22078
22515
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
22079
22516
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
22080
22517
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -22099,30 +22536,20 @@ var SqliteSecurityRepository = class {
|
|
|
22099
22536
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
22100
22537
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
22101
22538
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
22102
|
-
//
|
|
22103
|
-
// the parent event's
|
|
22104
|
-
// backfill left null.
|
|
22105
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22114
|
-
WHERE fr.finding_key = f.finding_key
|
|
22115
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22116
|
-
LIMIT 1
|
|
22117
|
-
) AS latest_method,
|
|
22118
|
-
(
|
|
22119
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22120
|
-
WHERE fr.finding_key = f.finding_key
|
|
22121
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22122
|
-
LIMIT 1
|
|
22123
|
-
) AS latest_resolved_at
|
|
22124
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22539
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22540
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22541
|
+
// legacy/edge row the backfill left null.
|
|
22542
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22543
|
+
latest.status AS latest_status,
|
|
22544
|
+
latest.method AS latest_method,
|
|
22545
|
+
latest.resolved_at AS latest_resolved_at
|
|
22546
|
+
FROM inspection_findings f
|
|
22547
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22548
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22549
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22550
|
+
ON latest.finding_key = f.finding_key
|
|
22125
22551
|
WHERE f.finding_key IS NOT NULL
|
|
22552
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22126
22553
|
AND EXISTS (
|
|
22127
22554
|
SELECT 1 FROM finding_resolution fr
|
|
22128
22555
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22169,11 +22596,13 @@ var SqliteSecurityRepository = class {
|
|
|
22169
22596
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22170
22597
|
const rows = allRows(
|
|
22171
22598
|
this.db.prepare(
|
|
22172
|
-
`SELECT json_extract(e.
|
|
22173
|
-
FROM
|
|
22174
|
-
|
|
22175
|
-
|
|
22176
|
-
AND
|
|
22599
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22600
|
+
FROM inspection_findings f
|
|
22601
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22602
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22603
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22604
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22605
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22177
22606
|
GROUP BY repo
|
|
22178
22607
|
ORDER BY c DESC, repo
|
|
22179
22608
|
LIMIT :limit`
|
|
@@ -22197,44 +22626,28 @@ var SqliteSecurityRepository = class {
|
|
|
22197
22626
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22198
22627
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22199
22628
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22200
|
-
// finding's parent event (
|
|
22201
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22202
|
-
// capped at `limit`.
|
|
22629
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22630
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22631
|
+
// DESC, capped at `limit`.
|
|
22203
22632
|
recentlyResolved(limit = 20) {
|
|
22204
22633
|
const rows = allRows(
|
|
22205
22634
|
this.db.prepare(
|
|
22206
22635
|
`SELECT f.finding_key AS finding_key,
|
|
22207
|
-
|
|
22208
|
-
|
|
22209
|
-
json_extract(e.
|
|
22210
|
-
COALESCE(f.first_detected_at, e.
|
|
22211
|
-
|
|
22212
|
-
|
|
22213
|
-
|
|
22214
|
-
|
|
22215
|
-
|
|
22216
|
-
|
|
22217
|
-
|
|
22218
|
-
WHERE e.kind = 'code_change'
|
|
22636
|
+
d.rule_id AS rule_id,
|
|
22637
|
+
d.severity AS severity,
|
|
22638
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22639
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22640
|
+
latest.resolved_at AS latest_resolved_at
|
|
22641
|
+
FROM inspection_findings f
|
|
22642
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22643
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22644
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22645
|
+
ON latest.finding_key = f.finding_key
|
|
22646
|
+
WHERE e.event_type = 'code_change'
|
|
22219
22647
|
AND f.finding_key IS NOT NULL
|
|
22220
|
-
AND
|
|
22221
|
-
|
|
22222
|
-
|
|
22223
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22224
|
-
LIMIT 1
|
|
22225
|
-
) = 'resolved'
|
|
22226
|
-
AND (
|
|
22227
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22228
|
-
WHERE fr.finding_key = f.finding_key
|
|
22229
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22230
|
-
LIMIT 1
|
|
22231
|
-
) = 'fixed-at-source'
|
|
22232
|
-
AND (
|
|
22233
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22234
|
-
WHERE fr.finding_key = f.finding_key
|
|
22235
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22236
|
-
LIMIT 1
|
|
22237
|
-
) IS NOT NULL
|
|
22648
|
+
AND latest.status = 'resolved'
|
|
22649
|
+
AND latest.method = 'fixed-at-source'
|
|
22650
|
+
AND latest.resolved_at IS NOT NULL
|
|
22238
22651
|
ORDER BY latest_resolved_at DESC
|
|
22239
22652
|
LIMIT :limit`
|
|
22240
22653
|
),
|
|
@@ -22253,15 +22666,18 @@ var SqliteSecurityRepository = class {
|
|
|
22253
22666
|
return Promise.resolve({ items });
|
|
22254
22667
|
}
|
|
22255
22668
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22256
|
-
// epoch-millis timestamp.
|
|
22669
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22257
22670
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22258
22671
|
findingsInRange(fromMs, toMs) {
|
|
22259
22672
|
const rows = allRows(
|
|
22260
22673
|
this.db.prepare(
|
|
22261
|
-
`SELECT e.
|
|
22262
|
-
FROM
|
|
22263
|
-
|
|
22264
|
-
|
|
22674
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22675
|
+
FROM inspection_findings f
|
|
22676
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22677
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22678
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22679
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22680
|
+
ORDER BY e.started_at`
|
|
22265
22681
|
),
|
|
22266
22682
|
{ from: fromMs, to: toMs }
|
|
22267
22683
|
);
|
|
@@ -23085,9 +23501,10 @@ function openWithPragmas(file2) {
|
|
|
23085
23501
|
}
|
|
23086
23502
|
function backupLegacyStore(file2) {
|
|
23087
23503
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23504
|
+
renameSync2(file2, backup);
|
|
23505
|
+
tightenFile(backup);
|
|
23506
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23507
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
23091
23508
|
}
|
|
23092
23509
|
return backup;
|
|
23093
23510
|
}
|
|
@@ -23103,7 +23520,7 @@ function openLocalDatabase(dir) {
|
|
|
23103
23520
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
23104
23521
|
);
|
|
23105
23522
|
}
|
|
23106
|
-
applyMigrations(db);
|
|
23523
|
+
applyMigrations(db, file2);
|
|
23107
23524
|
tightenPerms(file2);
|
|
23108
23525
|
const events = new SqliteEventsRepository(db);
|
|
23109
23526
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -23130,9 +23547,56 @@ function openLocalDatabase(dir) {
|
|
|
23130
23547
|
policies.seedDefaults();
|
|
23131
23548
|
function recordCapture(event, detected) {
|
|
23132
23549
|
failOpenTransaction(db, () => {
|
|
23133
|
-
events.insertEvent(event);
|
|
23134
23550
|
const sessionId = event.metadata?.sessionId;
|
|
23135
|
-
|
|
23551
|
+
if (sessionId) {
|
|
23552
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23553
|
+
}
|
|
23554
|
+
const auditEventId = captureId(
|
|
23555
|
+
sessionId ?? null,
|
|
23556
|
+
event.contentHash,
|
|
23557
|
+
event.metadata?.filePath ?? null
|
|
23558
|
+
);
|
|
23559
|
+
auditEvents.insertAuditEvent({
|
|
23560
|
+
id: auditEventId,
|
|
23561
|
+
eventType: event.kind,
|
|
23562
|
+
startedAt: event.occurredAt,
|
|
23563
|
+
parentId: sessionId,
|
|
23564
|
+
rootSessionId: sessionId,
|
|
23565
|
+
content: event.content,
|
|
23566
|
+
contentHash: event.contentHash,
|
|
23567
|
+
attributes: toCaptureAttributes(event)
|
|
23568
|
+
});
|
|
23569
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23570
|
+
for (const finding2 of detected) {
|
|
23571
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding2.ruleId, finding2.maskedMatch, sessionId)) {
|
|
23572
|
+
continue;
|
|
23573
|
+
}
|
|
23574
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23575
|
+
auditEventId,
|
|
23576
|
+
finding2.ruleId,
|
|
23577
|
+
finding2.maskedMatch,
|
|
23578
|
+
finding2.span.start,
|
|
23579
|
+
finding2.span.end
|
|
23580
|
+
)) {
|
|
23581
|
+
continue;
|
|
23582
|
+
}
|
|
23583
|
+
const key = `${finding2.ruleId}@${captureDefinitionVersion(finding2)}`;
|
|
23584
|
+
let definitionId = definitionIds.get(key);
|
|
23585
|
+
if (!definitionId) {
|
|
23586
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding2));
|
|
23587
|
+
definitionIds.set(key, definitionId);
|
|
23588
|
+
}
|
|
23589
|
+
inspectionFindings.insertFinding({
|
|
23590
|
+
id: finding2.id,
|
|
23591
|
+
auditEventId,
|
|
23592
|
+
inspectionDefinitionId: definitionId,
|
|
23593
|
+
span: finding2.span,
|
|
23594
|
+
maskedMatch: finding2.maskedMatch,
|
|
23595
|
+
actionTaken: finding2.actionTaken,
|
|
23596
|
+
confidence: finding2.confidence,
|
|
23597
|
+
findingKey: finding2.findingKey ?? void 0
|
|
23598
|
+
});
|
|
23599
|
+
}
|
|
23136
23600
|
});
|
|
23137
23601
|
}
|
|
23138
23602
|
function ensureInventory(ctx) {
|
|
@@ -23280,9 +23744,12 @@ function openLocalDatabase(dir) {
|
|
|
23280
23744
|
};
|
|
23281
23745
|
}
|
|
23282
23746
|
|
|
23747
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23748
|
+
import { createHash as createHash3 } from "crypto";
|
|
23749
|
+
|
|
23283
23750
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23284
23751
|
import { createHmac, randomBytes } from "crypto";
|
|
23285
|
-
import {
|
|
23752
|
+
import { readFileSync } from "fs";
|
|
23286
23753
|
import { join as join2 } from "path";
|
|
23287
23754
|
var KEY_FILENAME = "exception.key";
|
|
23288
23755
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -23319,8 +23786,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
23319
23786
|
}
|
|
23320
23787
|
|
|
23321
23788
|
// ../../packages/persistence/src/local-layout.ts
|
|
23322
|
-
import {
|
|
23323
|
-
import {
|
|
23789
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23790
|
+
import { mkdir } from "fs/promises";
|
|
23324
23791
|
import { homedir } from "os";
|
|
23325
23792
|
import { join as join3 } from "path";
|
|
23326
23793
|
function defaultDataDir() {
|
|
@@ -23335,6 +23802,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23335
23802
|
function dbPath(base = defaultDataDir()) {
|
|
23336
23803
|
return join3(dataDir(base), "aka.db");
|
|
23337
23804
|
}
|
|
23805
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23806
|
+
ensureDataDirSync(dir);
|
|
23807
|
+
}
|
|
23338
23808
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23339
23809
|
const moves = [
|
|
23340
23810
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23342,19 +23812,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23342
23812
|
];
|
|
23343
23813
|
for (const { name, dest } of moves) {
|
|
23344
23814
|
try {
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
}
|
|
23350
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23815
|
+
ensureDataDirSync(dest);
|
|
23816
|
+
const moved = join3(dest, name);
|
|
23817
|
+
renameSync3(join3(base, name), moved);
|
|
23818
|
+
tightenFile(moved);
|
|
23351
23819
|
} catch {
|
|
23352
23820
|
}
|
|
23353
23821
|
}
|
|
23354
23822
|
}
|
|
23355
23823
|
|
|
23356
23824
|
// ../../packages/persistence/src/settings.ts
|
|
23357
|
-
import { readFileSync as readFileSync2
|
|
23825
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23358
23826
|
import { join as join4 } from "path";
|
|
23359
23827
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23360
23828
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23376,7 +23844,7 @@ function readJson(file2) {
|
|
|
23376
23844
|
}
|
|
23377
23845
|
|
|
23378
23846
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23379
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23847
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
23380
23848
|
import { join as join5 } from "path";
|
|
23381
23849
|
var MARKER = "warn-era-capped";
|
|
23382
23850
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -23384,7 +23852,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
23384
23852
|
const marker = join5(dataDir2, MARKER);
|
|
23385
23853
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
23386
23854
|
const capped = db.policies.capCategoryActions();
|
|
23387
|
-
|
|
23855
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
23388
23856
|
`, { mode: DATA_FILE_MODE });
|
|
23389
23857
|
return { capped };
|
|
23390
23858
|
}
|
|
@@ -23439,6 +23907,12 @@ function resolveProvider() {
|
|
|
23439
23907
|
|
|
23440
23908
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23441
23909
|
function loadConfig(base = defaultDataDir()) {
|
|
23910
|
+
try {
|
|
23911
|
+
ensureLayoutDirSync(base);
|
|
23912
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23913
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23914
|
+
} catch {
|
|
23915
|
+
}
|
|
23442
23916
|
migrateLegacyLayout(base);
|
|
23443
23917
|
const settings = readWorkspaceSettings(base);
|
|
23444
23918
|
return {
|
|
@@ -23461,7 +23935,7 @@ function resolveProviderSafe() {
|
|
|
23461
23935
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23462
23936
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23463
23937
|
import { homedir as homedir2 } from "os";
|
|
23464
|
-
import { basename as basename3, join as
|
|
23938
|
+
import { basename as basename3, join as join8 } from "path";
|
|
23465
23939
|
|
|
23466
23940
|
// ../../packages/detections/src/egress/registry.ts
|
|
23467
23941
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -26494,7 +26968,7 @@ function ensureBundledPacks() {
|
|
|
26494
26968
|
return false;
|
|
26495
26969
|
}
|
|
26496
26970
|
}
|
|
26497
|
-
function scanText(text) {
|
|
26971
|
+
function scanText(text, ruleVersions) {
|
|
26498
26972
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
26499
26973
|
try {
|
|
26500
26974
|
const rules = getLoadedRules();
|
|
@@ -26506,7 +26980,7 @@ function scanText(text) {
|
|
|
26506
26980
|
return {
|
|
26507
26981
|
ruleId: m.ruleId,
|
|
26508
26982
|
ruleName: rule?.name ?? m.ruleId,
|
|
26509
|
-
ruleVersion: String(rule?.specVersion ?? 1),
|
|
26983
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
26510
26984
|
category: m.category,
|
|
26511
26985
|
severity: m.severity,
|
|
26512
26986
|
span: m.span,
|
|
@@ -26524,8 +26998,8 @@ function maskText(text) {
|
|
|
26524
26998
|
}
|
|
26525
26999
|
|
|
26526
27000
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26527
|
-
import { existsSync as
|
|
26528
|
-
import { basename as basename2, dirname, isAbsolute, join as
|
|
27001
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
27002
|
+
import { basename as basename2, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26529
27003
|
function resolveRepoIdentity(cwd) {
|
|
26530
27004
|
try {
|
|
26531
27005
|
const root = findGitRoot(cwd);
|
|
@@ -26575,7 +27049,7 @@ function resolveGitBranch(cwd) {
|
|
|
26575
27049
|
try {
|
|
26576
27050
|
const root = findGitRoot(cwd);
|
|
26577
27051
|
if (!root) return void 0;
|
|
26578
|
-
const dotGit =
|
|
27052
|
+
const dotGit = join7(root, ".git");
|
|
26579
27053
|
let gitdir;
|
|
26580
27054
|
try {
|
|
26581
27055
|
gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
|
|
@@ -26583,7 +27057,7 @@ function resolveGitBranch(cwd) {
|
|
|
26583
27057
|
return void 0;
|
|
26584
27058
|
}
|
|
26585
27059
|
if (gitdir === void 0) return void 0;
|
|
26586
|
-
const head = safeRead(
|
|
27060
|
+
const head = safeRead(join7(gitdir, "HEAD"));
|
|
26587
27061
|
if (!head) return void 0;
|
|
26588
27062
|
return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
|
|
26589
27063
|
} catch {
|
|
@@ -26593,37 +27067,37 @@ function resolveGitBranch(cwd) {
|
|
|
26593
27067
|
function resolveWorktreeGitdir(root, dotGitFile) {
|
|
26594
27068
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
|
|
26595
27069
|
if (!target) return void 0;
|
|
26596
|
-
return isAbsolute(target) ? target :
|
|
27070
|
+
return isAbsolute(target) ? target : join7(root, target);
|
|
26597
27071
|
}
|
|
26598
27072
|
function findGitRoot(start) {
|
|
26599
27073
|
let dir = start;
|
|
26600
27074
|
for (; ; ) {
|
|
26601
|
-
if (
|
|
27075
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
26602
27076
|
const parent = dirname(dir);
|
|
26603
27077
|
if (parent === dir) return void 0;
|
|
26604
27078
|
dir = parent;
|
|
26605
27079
|
}
|
|
26606
27080
|
}
|
|
26607
27081
|
function resolveGitContext(root) {
|
|
26608
|
-
const dotGit =
|
|
27082
|
+
const dotGit = join7(root, ".git");
|
|
26609
27083
|
try {
|
|
26610
27084
|
if (statSync(dotGit).isDirectory()) {
|
|
26611
|
-
return { configPath:
|
|
27085
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
26612
27086
|
}
|
|
26613
27087
|
} catch {
|
|
26614
27088
|
return void 0;
|
|
26615
27089
|
}
|
|
26616
27090
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
26617
27091
|
if (!target) return void 0;
|
|
26618
|
-
const gitdir = isAbsolute(target) ? target :
|
|
26619
|
-
if (
|
|
26620
|
-
return { configPath:
|
|
27092
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
27093
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
27094
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
26621
27095
|
}
|
|
26622
|
-
const commonRaw = safeRead(
|
|
27096
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
26623
27097
|
if (!commonRaw) return void 0;
|
|
26624
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
27098
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
26625
27099
|
const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
26626
|
-
return { configPath:
|
|
27100
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
26627
27101
|
}
|
|
26628
27102
|
function safeRead(path) {
|
|
26629
27103
|
try {
|
|
@@ -26689,31 +27163,31 @@ function resolveConfigInventory(input) {
|
|
|
26689
27163
|
};
|
|
26690
27164
|
try {
|
|
26691
27165
|
const home = input.homeDir ?? homedir2();
|
|
26692
|
-
const claudeDir =
|
|
27166
|
+
const claudeDir = join8(home, ".claude");
|
|
26693
27167
|
const repo = resolveRepoIdentity(input.cwd);
|
|
26694
27168
|
const repoIdentity = repo?.url ?? input.cwd;
|
|
26695
27169
|
const projectSource = `project:${repoIdentity}`;
|
|
26696
|
-
collectSettingsHooks(scan2,
|
|
26697
|
-
collectSettingsHooks(scan2,
|
|
26698
|
-
collectSettingsHooks(scan2,
|
|
27170
|
+
collectSettingsHooks(scan2, join8(claudeDir, "settings.json"), "user");
|
|
27171
|
+
collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.json"), "project");
|
|
27172
|
+
collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.local.json"), "local");
|
|
26699
27173
|
const projectOrigin = { scope: "project", project: repoIdentity };
|
|
26700
|
-
collectMcpFile(scan2,
|
|
26701
|
-
collectUserClaudeJson(scan2,
|
|
26702
|
-
collectMcpFile(scan2,
|
|
26703
|
-
collectMcpFile(scan2,
|
|
26704
|
-
collectMcpFile(scan2,
|
|
27174
|
+
collectMcpFile(scan2, join8(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
|
|
27175
|
+
collectUserClaudeJson(scan2, join8(home, ".claude.json"), input.cwd, repoIdentity);
|
|
27176
|
+
collectMcpFile(scan2, join8(claudeDir, "settings.json"), { scope: "user" });
|
|
27177
|
+
collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.json"), projectOrigin);
|
|
27178
|
+
collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.local.json"), {
|
|
26705
27179
|
scope: "local",
|
|
26706
27180
|
project: repoIdentity
|
|
26707
27181
|
});
|
|
26708
27182
|
collectConfigFiles(scan2, claudeDir, input.cwd);
|
|
26709
|
-
collectSkillsDir(scan2,
|
|
26710
|
-
collectSkillsDir(scan2,
|
|
27183
|
+
collectSkillsDir(scan2, join8(claudeDir, "skills"), { source: "local", scope: "user" });
|
|
27184
|
+
collectSkillsDir(scan2, join8(input.cwd, ".claude", "skills"), {
|
|
26711
27185
|
source: projectSource,
|
|
26712
27186
|
scope: "project"
|
|
26713
27187
|
});
|
|
26714
27188
|
collectInstalledPlugins(scan2, claudeDir);
|
|
26715
27189
|
collectMarketplaceSkills(scan2, claudeDir);
|
|
26716
|
-
collectSkillsDir(scan2,
|
|
27190
|
+
collectSkillsDir(scan2, join8(input.cwd, "skills"), { source: projectSource, scope: "project" });
|
|
26717
27191
|
scan2.skills = dedupeSkills(scan2.skills);
|
|
26718
27192
|
scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
|
|
26719
27193
|
} catch (err) {
|
|
@@ -26842,7 +27316,7 @@ function projectEntryFor(projects, cwd) {
|
|
|
26842
27316
|
return void 0;
|
|
26843
27317
|
}
|
|
26844
27318
|
function collectPluginManifestMcp(scan2, installPath, origin) {
|
|
26845
|
-
const manifestPath =
|
|
27319
|
+
const manifestPath = join8(installPath, ".claude-plugin", "plugin.json");
|
|
26846
27320
|
const raw = readOptional(manifestPath);
|
|
26847
27321
|
if (raw === void 0) return;
|
|
26848
27322
|
try {
|
|
@@ -26850,7 +27324,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
|
|
|
26850
27324
|
if (typeof parsed !== "object" || parsed === null) return;
|
|
26851
27325
|
const declared = parsed.mcpServers;
|
|
26852
27326
|
if (typeof declared === "string" && declared.length > 0) {
|
|
26853
|
-
collectMcpFile(scan2,
|
|
27327
|
+
collectMcpFile(scan2, join8(installPath, declared), origin, { recordErrors: true });
|
|
26854
27328
|
} else {
|
|
26855
27329
|
collectMcpObject(scan2, declared, manifestPath, origin);
|
|
26856
27330
|
}
|
|
@@ -26867,14 +27341,14 @@ var SETTINGS_KEY_LABELS = [
|
|
|
26867
27341
|
["statusLine", "status line"]
|
|
26868
27342
|
];
|
|
26869
27343
|
function collectConfigFiles(scan2, claudeDir, cwd) {
|
|
26870
|
-
settingsConfigFile(scan2,
|
|
26871
|
-
settingsConfigFile(scan2,
|
|
26872
|
-
settingsConfigFile(scan2,
|
|
26873
|
-
memoryConfigFile(scan2,
|
|
26874
|
-
memoryConfigFile(scan2,
|
|
26875
|
-
mcpJsonConfigFile(scan2,
|
|
26876
|
-
dirConfigFile(scan2,
|
|
26877
|
-
dirConfigFile(scan2,
|
|
27344
|
+
settingsConfigFile(scan2, join8(claudeDir, "settings.json"), "user", "User settings");
|
|
27345
|
+
settingsConfigFile(scan2, join8(cwd, ".claude", "settings.json"), "project", "Project settings");
|
|
27346
|
+
settingsConfigFile(scan2, join8(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
|
|
27347
|
+
memoryConfigFile(scan2, join8(claudeDir, "CLAUDE.md"), "user", "User memory");
|
|
27348
|
+
memoryConfigFile(scan2, join8(cwd, "CLAUDE.md"), "project", "Project memory");
|
|
27349
|
+
mcpJsonConfigFile(scan2, join8(cwd, ".mcp.json"));
|
|
27350
|
+
dirConfigFile(scan2, join8(cwd, ".claude", "commands"), "Slash commands", "command");
|
|
27351
|
+
dirConfigFile(scan2, join8(cwd, ".claude", "agents"), "Subagents", "subagent");
|
|
26878
27352
|
}
|
|
26879
27353
|
function configFileEntry(path, scope, kind) {
|
|
26880
27354
|
try {
|
|
@@ -26949,7 +27423,7 @@ function countMarkdownFiles(dir, depth) {
|
|
|
26949
27423
|
let count = 0;
|
|
26950
27424
|
for (const dirent of readdirSync(dir, { withFileTypes: true })) {
|
|
26951
27425
|
if (dirent.name.startsWith(".")) continue;
|
|
26952
|
-
if (dirent.isDirectory()) count += countMarkdownFiles(
|
|
27426
|
+
if (dirent.isDirectory()) count += countMarkdownFiles(join8(dir, dirent.name), depth + 1);
|
|
26953
27427
|
else if (dirent.name.endsWith(".md")) count += 1;
|
|
26954
27428
|
}
|
|
26955
27429
|
return count;
|
|
@@ -26962,7 +27436,7 @@ function collectSkillsDir(scan2, dir, origin) {
|
|
|
26962
27436
|
return;
|
|
26963
27437
|
}
|
|
26964
27438
|
for (const name of names) {
|
|
26965
|
-
const skillFile =
|
|
27439
|
+
const skillFile = join8(dir, name, "SKILL.md");
|
|
26966
27440
|
try {
|
|
26967
27441
|
const raw = readOptional(skillFile);
|
|
26968
27442
|
if (raw === void 0) continue;
|
|
@@ -26971,7 +27445,7 @@ function collectSkillsDir(scan2, dir, origin) {
|
|
|
26971
27445
|
name: front.name ?? name,
|
|
26972
27446
|
source: origin.source,
|
|
26973
27447
|
scope: origin.scope,
|
|
26974
|
-
location:
|
|
27448
|
+
location: join8(dir, name),
|
|
26975
27449
|
updatedAt: statSync2(skillFile).mtime.toISOString()
|
|
26976
27450
|
};
|
|
26977
27451
|
const version2 = front.version ?? origin.defaultVersion;
|
|
@@ -27002,7 +27476,7 @@ function parseFrontmatter(raw) {
|
|
|
27002
27476
|
return out;
|
|
27003
27477
|
}
|
|
27004
27478
|
function collectInstalledPlugins(scan2, claudeDir) {
|
|
27005
|
-
const manifestPath =
|
|
27479
|
+
const manifestPath = join8(claudeDir, "plugins", "installed_plugins.json");
|
|
27006
27480
|
const raw = readOptional(manifestPath);
|
|
27007
27481
|
if (raw === void 0) return;
|
|
27008
27482
|
let plugins;
|
|
@@ -27027,7 +27501,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
|
|
|
27027
27501
|
if (typeof installPath !== "string" || seen.has(installPath)) continue;
|
|
27028
27502
|
seen.add(installPath);
|
|
27029
27503
|
const version2 = install.version;
|
|
27030
|
-
const hooksPath =
|
|
27504
|
+
const hooksPath = join8(installPath, "hooks", "hooks.json");
|
|
27031
27505
|
const hooksRaw = readOptional(hooksPath);
|
|
27032
27506
|
if (hooksRaw !== void 0) {
|
|
27033
27507
|
try {
|
|
@@ -27047,22 +27521,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
|
|
|
27047
27521
|
}
|
|
27048
27522
|
const origin = { source: marketplace, scope: "plugin", pluginName };
|
|
27049
27523
|
if (typeof version2 === "string") origin.defaultVersion = version2;
|
|
27050
|
-
collectSkillsDir(scan2,
|
|
27524
|
+
collectSkillsDir(scan2, join8(installPath, "skills"), origin);
|
|
27051
27525
|
const mcpOrigin = { scope: "plugin", pluginName, marketplace };
|
|
27052
|
-
collectMcpFile(scan2,
|
|
27526
|
+
collectMcpFile(scan2, join8(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
|
|
27053
27527
|
collectPluginManifestMcp(scan2, installPath, mcpOrigin);
|
|
27054
27528
|
}
|
|
27055
27529
|
}
|
|
27056
27530
|
}
|
|
27057
27531
|
function collectMarketplaceSkills(scan2, claudeDir) {
|
|
27058
|
-
for (const mp of readMarketplaces(
|
|
27532
|
+
for (const mp of readMarketplaces(join8(claudeDir, "plugins", "known_marketplaces.json"))) {
|
|
27059
27533
|
if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
|
|
27060
|
-
collectSkillsDir(scan2,
|
|
27534
|
+
collectSkillsDir(scan2, join8(mp.installLocation, "skills"), {
|
|
27061
27535
|
source: mp.name,
|
|
27062
27536
|
scope: "plugin"
|
|
27063
27537
|
});
|
|
27064
|
-
collectPluginSkillDirs(scan2,
|
|
27065
|
-
collectPluginSkillDirs(scan2,
|
|
27538
|
+
collectPluginSkillDirs(scan2, join8(mp.installLocation, "plugins"), mp.name);
|
|
27539
|
+
collectPluginSkillDirs(scan2, join8(mp.installLocation, "external_plugins"), mp.name);
|
|
27066
27540
|
}
|
|
27067
27541
|
}
|
|
27068
27542
|
function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
|
|
@@ -27073,7 +27547,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
|
|
|
27073
27547
|
return;
|
|
27074
27548
|
}
|
|
27075
27549
|
for (const plugin of plugins) {
|
|
27076
|
-
collectSkillsDir(scan2,
|
|
27550
|
+
collectSkillsDir(scan2, join8(pluginsDir, plugin, "skills"), {
|
|
27077
27551
|
source: marketplace,
|
|
27078
27552
|
scope: "plugin",
|
|
27079
27553
|
pluginName: plugin
|
|
@@ -27147,10 +27621,7 @@ function str2(value) {
|
|
|
27147
27621
|
}
|
|
27148
27622
|
|
|
27149
27623
|
// ../../packages/plugin-sdk/src/events.ts
|
|
27150
|
-
import { createHash as
|
|
27151
|
-
|
|
27152
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
27153
|
-
import { createHash as createHash4 } from "crypto";
|
|
27624
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
27154
27625
|
|
|
27155
27626
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
27156
27627
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
@@ -27182,22 +27653,22 @@ function resolveInventoryContext(input) {
|
|
|
27182
27653
|
}
|
|
27183
27654
|
|
|
27184
27655
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
27185
|
-
import { mkdirSync as
|
|
27186
|
-
import { join as
|
|
27656
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
27657
|
+
import { join as join9 } from "path";
|
|
27187
27658
|
var SESSION_START_MARKER = "session-start-last";
|
|
27188
27659
|
function claimSessionStart(dataDir2, sessionId) {
|
|
27189
27660
|
return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
|
|
27190
27661
|
}
|
|
27191
27662
|
function claimOncePerSession(dataDir2, marker, sessionId) {
|
|
27192
27663
|
if (!sessionId) return true;
|
|
27193
|
-
const path =
|
|
27664
|
+
const path = join9(dataDir2, marker);
|
|
27194
27665
|
try {
|
|
27195
27666
|
if (readFileSync5(path, "utf8") === sessionId) return false;
|
|
27196
27667
|
} catch {
|
|
27197
27668
|
}
|
|
27198
27669
|
try {
|
|
27199
|
-
|
|
27200
|
-
|
|
27670
|
+
mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
|
|
27671
|
+
writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
|
|
27201
27672
|
} catch {
|
|
27202
27673
|
}
|
|
27203
27674
|
return true;
|
|
@@ -27209,8 +27680,8 @@ import { basename as basename4, dirname as dirname2, sep as sep3 } from "path";
|
|
|
27209
27680
|
|
|
27210
27681
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
27211
27682
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
27212
|
-
import { existsSync as
|
|
27213
|
-
import { basename as basename5, join as
|
|
27683
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27684
|
+
import { basename as basename5, join as join10, relative, sep as sep4 } from "path";
|
|
27214
27685
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
27215
27686
|
".git",
|
|
27216
27687
|
"node_modules",
|
|
@@ -27230,7 +27701,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
27230
27701
|
var MAX_FILES = 2e4;
|
|
27231
27702
|
function readIgnoreLayer(dir) {
|
|
27232
27703
|
try {
|
|
27233
|
-
const content = readFileSync6(
|
|
27704
|
+
const content = readFileSync6(join10(dir, ".gitignore"), "utf8");
|
|
27234
27705
|
return { base: dir, matcher: (0, import_ignore.default)().add(content) };
|
|
27235
27706
|
} catch {
|
|
27236
27707
|
return void 0;
|
|
@@ -27308,10 +27779,10 @@ function resolveProjectFiles(cwd) {
|
|
|
27308
27779
|
const layer = readIgnoreLayer(dir);
|
|
27309
27780
|
const dirLayers = layer ? [...layers, layer] : layers;
|
|
27310
27781
|
for (const entry of dirents) {
|
|
27311
|
-
const fullPath =
|
|
27782
|
+
const fullPath = join10(dir, entry.name);
|
|
27312
27783
|
if (entry.isDirectory()) {
|
|
27313
27784
|
if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
|
|
27314
|
-
if (
|
|
27785
|
+
if (existsSync5(join10(fullPath, ".git"))) continue;
|
|
27315
27786
|
if (visit2(fullPath, dirLayers)) return true;
|
|
27316
27787
|
continue;
|
|
27317
27788
|
}
|
|
@@ -27352,17 +27823,17 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
27352
27823
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27353
27824
|
|
|
27354
27825
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
27355
|
-
import { mkdirSync as
|
|
27356
|
-
import { join as
|
|
27826
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
27827
|
+
import { join as join11 } from "path";
|
|
27357
27828
|
function throttled(dataDir2, markerName, windowMs) {
|
|
27358
|
-
const marker =
|
|
27829
|
+
const marker = join11(dataDir2, markerName);
|
|
27359
27830
|
try {
|
|
27360
27831
|
if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
|
|
27361
27832
|
} catch {
|
|
27362
27833
|
}
|
|
27363
27834
|
try {
|
|
27364
|
-
|
|
27365
|
-
|
|
27835
|
+
mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
|
|
27836
|
+
writeFileSync4(marker, String(Date.now()), { mode: DATA_FILE_MODE });
|
|
27366
27837
|
} catch {
|
|
27367
27838
|
}
|
|
27368
27839
|
return false;
|
|
@@ -27400,7 +27871,8 @@ var StandaloneDataGateway = class {
|
|
|
27400
27871
|
}
|
|
27401
27872
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
27402
27873
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
27403
|
-
// hands the natural key across.
|
|
27874
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27875
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
27404
27876
|
recordLlmCall(input) {
|
|
27405
27877
|
this.db.auditEvents.insertLlmCall(input);
|
|
27406
27878
|
return Promise.resolve();
|
|
@@ -27442,7 +27914,9 @@ var StandaloneDataGateway = class {
|
|
|
27442
27914
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
27443
27915
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
27444
27916
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
27445
|
-
// content-addressed
|
|
27917
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27918
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27919
|
+
// than no-opping.
|
|
27446
27920
|
writeToolCall(input) {
|
|
27447
27921
|
this.db.auditEvents.insertToolCall(input);
|
|
27448
27922
|
if (input.inspections.length === 0) return;
|
|
@@ -27462,7 +27936,7 @@ var StandaloneDataGateway = class {
|
|
|
27462
27936
|
});
|
|
27463
27937
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
27464
27938
|
this.db.inspectionFindings.insertFinding({
|
|
27465
|
-
id: inspectionFindingId(auditEventId,
|
|
27939
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
27466
27940
|
auditEventId,
|
|
27467
27941
|
inspectionDefinitionId: definitionId,
|
|
27468
27942
|
classifiedDataId: classifiedDataId2,
|
|
@@ -27511,10 +27985,17 @@ var StandaloneDataGateway = class {
|
|
|
27511
27985
|
try {
|
|
27512
27986
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
27513
27987
|
if (snapshot.installedPacks === 0) return void 0;
|
|
27514
|
-
if (snapshot.enabledPacks === 0)
|
|
27988
|
+
if (snapshot.enabledPacks === 0) {
|
|
27989
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27990
|
+
}
|
|
27515
27991
|
if (snapshot.invalidRules > 0) return void 0;
|
|
27516
27992
|
if (snapshot.rules.length === 0) return void 0;
|
|
27517
|
-
return {
|
|
27993
|
+
return {
|
|
27994
|
+
rules: snapshot.rules,
|
|
27995
|
+
ruleActions: snapshot.ruleActions,
|
|
27996
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27997
|
+
complete: true
|
|
27998
|
+
};
|
|
27518
27999
|
} catch {
|
|
27519
28000
|
return void 0;
|
|
27520
28001
|
}
|
|
@@ -27542,6 +28023,7 @@ var StandaloneDataGateway = class {
|
|
|
27542
28023
|
policies: [...policies, ...rulePolicies],
|
|
27543
28024
|
rules: installed ? installed.rules : [],
|
|
27544
28025
|
...installed ? { rulesComplete: true } : {},
|
|
28026
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
27545
28027
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
27546
28028
|
customKeywords,
|
|
27547
28029
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -27807,7 +28289,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
|
|
|
27807
28289
|
|
|
27808
28290
|
// src/history/reconcile-trigger.ts
|
|
27809
28291
|
import { spawn } from "child_process";
|
|
27810
|
-
import { dirname as dirname3, join as
|
|
28292
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
27811
28293
|
import { fileURLToPath } from "url";
|
|
27812
28294
|
|
|
27813
28295
|
// src/history/tail.ts
|
|
@@ -27815,13 +28297,13 @@ import { createHash as createHash5 } from "crypto";
|
|
|
27815
28297
|
import {
|
|
27816
28298
|
closeSync,
|
|
27817
28299
|
fstatSync,
|
|
27818
|
-
mkdirSync as
|
|
28300
|
+
mkdirSync as mkdirSync4,
|
|
27819
28301
|
openSync,
|
|
27820
28302
|
readFileSync as readFileSync7,
|
|
27821
28303
|
readSync,
|
|
27822
|
-
writeFileSync as
|
|
28304
|
+
writeFileSync as writeFileSync5
|
|
27823
28305
|
} from "fs";
|
|
27824
|
-
import { join as
|
|
28306
|
+
import { join as join12 } from "path";
|
|
27825
28307
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
27826
28308
|
function safeSessionId(sessionId) {
|
|
27827
28309
|
if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
|
|
@@ -27838,7 +28320,7 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
|
|
|
27838
28320
|
const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
|
|
27839
28321
|
if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
|
|
27840
28322
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
27841
|
-
const child = spawn(process.execPath, [
|
|
28323
|
+
const child = spawn(process.execPath, [join13(here, "reconcile.js"), sessionId, transcriptPath], {
|
|
27842
28324
|
detached: true,
|
|
27843
28325
|
stdio: "ignore"
|
|
27844
28326
|
});
|