@akasecurity/ai-tc-claude-code 0.9.0 → 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 +112 -29
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +4607 -848
- package/scripts/backfill.js +2346 -639
- package/scripts/filescan.js +3554 -756
- package/scripts/firstrun.js +2132 -583
- package/scripts/intro.js +908 -168
- package/scripts/onboard.js +2140 -582
- package/scripts/post-tool-use.js +2344 -628
- package/scripts/pre-tool-use.js +2349 -633
- package/scripts/query.js +2133 -584
- package/scripts/reconcile.js +2158 -603
- package/scripts/remediate.js +2341 -640
- package/scripts/session-start.js +2216 -657
- package/scripts/start-light.js +911 -171
- package/scripts/statusline.js +2146 -587
- package/scripts/stop.js +965 -188
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +2352 -636
package/scripts/backfill.js
CHANGED
|
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
|
|
|
494
494
|
// src/backfill.ts
|
|
495
495
|
import { fileURLToPath } from "url";
|
|
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
|
|
|
@@ -545,6 +549,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
545
549
|
{
|
|
546
550
|
tag: "0010_events_session_expression_index",
|
|
547
551
|
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
tag: "0011_egress_writer",
|
|
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"
|
|
548
568
|
}
|
|
549
569
|
];
|
|
550
570
|
|
|
@@ -15375,7 +15395,12 @@ var FindingFacets = external_exports.object({
|
|
|
15375
15395
|
severity: external_exports.array(FindingFacetItem),
|
|
15376
15396
|
subtype: external_exports.array(FindingFacetItem),
|
|
15377
15397
|
provider: external_exports.array(FindingFacetItem),
|
|
15378
|
-
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)
|
|
15379
15404
|
}).meta({ id: "FindingFacets" });
|
|
15380
15405
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15381
15406
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15385,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15385
15410
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15386
15411
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15387
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(),
|
|
15388
15417
|
q: external_exports.string().optional(),
|
|
15389
15418
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15390
15419
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15575,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15575
15604
|
parent_uuid: external_exports.string().optional(),
|
|
15576
15605
|
run_key: external_exports.string().optional()
|
|
15577
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());
|
|
15578
15634
|
var ToolCallInspection = external_exports.object({
|
|
15579
15635
|
ruleId: external_exports.string().min(1),
|
|
15580
15636
|
ruleName: external_exports.string(),
|
|
@@ -15661,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15661
15717
|
span: Span,
|
|
15662
15718
|
maskedMatch: external_exports.string(),
|
|
15663
15719
|
actionTaken: ActionTaken,
|
|
15664
|
-
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()
|
|
15665
15732
|
});
|
|
15666
15733
|
var InventoryContext = external_exports.object({
|
|
15667
15734
|
host: InventoryInput.optional(),
|
|
@@ -15863,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15863
15930
|
|
|
15864
15931
|
// ../../packages/schema/src/zod/event.ts
|
|
15865
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(",");
|
|
15866
15934
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15867
15935
|
var EventMetadata = external_exports.object({
|
|
15868
15936
|
sessionId: external_exports.string().optional(),
|
|
@@ -16203,6 +16271,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16203
16271
|
|
|
16204
16272
|
// ../../packages/schema/src/zod/rule.ts
|
|
16205
16273
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16274
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16206
16275
|
var KeywordMatcher = external_exports.object({
|
|
16207
16276
|
type: external_exports.literal("keyword"),
|
|
16208
16277
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16227,9 +16296,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16227
16296
|
return false;
|
|
16228
16297
|
}
|
|
16229
16298
|
}
|
|
16299
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16230
16300
|
var RegexMatcher = external_exports.object({
|
|
16231
16301
|
type: external_exports.literal("regex"),
|
|
16232
|
-
pattern: external_exports.string(),
|
|
16302
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16233
16303
|
flags: external_exports.string().default("gi"),
|
|
16234
16304
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16235
16305
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16350,6 +16420,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16350
16420
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16351
16421
|
// `bundle.exceptions ?? []`.
|
|
16352
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(),
|
|
16353
16429
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16354
16430
|
fetchedAt: external_exports.iso.datetime()
|
|
16355
16431
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16796,6 +16872,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16796
16872
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16797
16873
|
}
|
|
16798
16874
|
|
|
16875
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16876
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16877
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16878
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16879
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16880
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16881
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16882
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16883
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16884
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16885
|
+
var ReviewInfo = external_exports.object({
|
|
16886
|
+
needsReview: external_exports.boolean(),
|
|
16887
|
+
reasons: external_exports.array(ReviewReason)
|
|
16888
|
+
}).meta({ id: "ReviewInfo" });
|
|
16889
|
+
var DestinationNetwork = external_exports.object({
|
|
16890
|
+
port: external_exports.number().int().nullable(),
|
|
16891
|
+
geo: external_exports.string().nullable(),
|
|
16892
|
+
ptr: external_exports.string().nullable()
|
|
16893
|
+
}).meta({ id: "DestinationNetwork" });
|
|
16894
|
+
var EndpointSummary = external_exports.object({
|
|
16895
|
+
id: external_exports.string(),
|
|
16896
|
+
method: HttpMethod,
|
|
16897
|
+
transport: Transport,
|
|
16898
|
+
url: external_exports.string(),
|
|
16899
|
+
template: external_exports.boolean(),
|
|
16900
|
+
dataClass: DataClass,
|
|
16901
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16902
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
16903
|
+
}).meta({ id: "EndpointSummary" });
|
|
16904
|
+
var CallSite = external_exports.object({
|
|
16905
|
+
id: external_exports.string(),
|
|
16906
|
+
project: external_exports.string(),
|
|
16907
|
+
file: external_exports.string(),
|
|
16908
|
+
line: external_exports.number().int().nonnegative(),
|
|
16909
|
+
snippet: external_exports.string(),
|
|
16910
|
+
dynamic: external_exports.boolean(),
|
|
16911
|
+
vendored: external_exports.boolean(),
|
|
16912
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
16913
|
+
projectId: external_exports.string().nullable()
|
|
16914
|
+
}).meta({ id: "CallSite" });
|
|
16915
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
16916
|
+
sites: external_exports.array(CallSite)
|
|
16917
|
+
}).meta({ id: "EndpointWithSites" });
|
|
16918
|
+
var ShareDestinationSummary = external_exports.object({
|
|
16919
|
+
id: external_exports.string(),
|
|
16920
|
+
kind: DestinationKind,
|
|
16921
|
+
name: external_exports.string(),
|
|
16922
|
+
host: external_exports.string(),
|
|
16923
|
+
category: external_exports.string(),
|
|
16924
|
+
trust: ShareTrustLevel,
|
|
16925
|
+
/** Effective state (decision applied over the trust default). */
|
|
16926
|
+
status: EgressStatus,
|
|
16927
|
+
/** True when an egress decision override differs from the trust default. */
|
|
16928
|
+
isCustom: external_exports.boolean(),
|
|
16929
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16930
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
16931
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16932
|
+
transports: external_exports.array(Transport),
|
|
16933
|
+
/** Most-sensitive first. */
|
|
16934
|
+
dataClasses: external_exports.array(DataClass),
|
|
16935
|
+
review: ReviewInfo,
|
|
16936
|
+
/** Non-provider hosts only; null for providers. */
|
|
16937
|
+
network: DestinationNetwork.nullable(),
|
|
16938
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
16939
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
16940
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
16941
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
16942
|
+
endpointCount: true,
|
|
16943
|
+
callSiteCount: true,
|
|
16944
|
+
endpoints: true
|
|
16945
|
+
}).extend({
|
|
16946
|
+
/** Ownership/geo rationale; null for providers. */
|
|
16947
|
+
note: external_exports.string().nullable(),
|
|
16948
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
16949
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
16950
|
+
var ReviewDestination = external_exports.object({
|
|
16951
|
+
id: external_exports.string(),
|
|
16952
|
+
kind: DestinationKind,
|
|
16953
|
+
name: external_exports.string(),
|
|
16954
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
16955
|
+
host: external_exports.string(),
|
|
16956
|
+
trust: ShareTrustLevel,
|
|
16957
|
+
status: EgressStatus,
|
|
16958
|
+
review: ReviewInfo,
|
|
16959
|
+
topDataClass: DataClass,
|
|
16960
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16961
|
+
lastSeen: external_exports.iso.datetime()
|
|
16962
|
+
}).meta({ id: "ReviewDestination" });
|
|
16963
|
+
var ShareDestinationGroup = external_exports.object({
|
|
16964
|
+
kind: DestinationKind,
|
|
16965
|
+
total: external_exports.number().int().nonnegative(),
|
|
16966
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
16967
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
16968
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
16969
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
16970
|
+
var SharesStats = external_exports.object({
|
|
16971
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
16972
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
16973
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
16974
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
16975
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
16976
|
+
byKind: external_exports.object({
|
|
16977
|
+
provider: external_exports.number().int().nonnegative(),
|
|
16978
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16979
|
+
external: external_exports.number().int().nonnegative(),
|
|
16980
|
+
ip: external_exports.number().int().nonnegative()
|
|
16981
|
+
}),
|
|
16982
|
+
byTrust: external_exports.object({
|
|
16983
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
16984
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16985
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
16986
|
+
ip: external_exports.number().int().nonnegative()
|
|
16987
|
+
})
|
|
16988
|
+
}).meta({ id: "SharesStats" });
|
|
16989
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
16990
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
16991
|
+
decision: EgressDecision.nullable()
|
|
16992
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
16993
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
16994
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
16995
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
16996
|
+
q: external_exports.string().optional(),
|
|
16997
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
16998
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
16999
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17000
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17001
|
+
/**
|
|
17002
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17003
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17004
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17005
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17006
|
+
*/
|
|
17007
|
+
review: external_exports.stringbool().default(false)
|
|
17008
|
+
});
|
|
17009
|
+
var ExportSharesQuery = external_exports.object({
|
|
17010
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17011
|
+
q: external_exports.string().optional(),
|
|
17012
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
17013
|
+
});
|
|
17014
|
+
|
|
17015
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
17016
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
17017
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
17018
|
+
id: external_exports.string(),
|
|
17019
|
+
name: external_exports.string(),
|
|
17020
|
+
category: external_exports.string(),
|
|
17021
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
17022
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
17023
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
17024
|
+
apiBase: external_exports.string(),
|
|
17025
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
17026
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
17027
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
17028
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
17029
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
17030
|
+
var EgressCallSiteHit = external_exports.object({
|
|
17031
|
+
file: external_exports.string(),
|
|
17032
|
+
line: external_exports.number().int().positive(),
|
|
17033
|
+
snippet: external_exports.string(),
|
|
17034
|
+
dynamic: external_exports.boolean(),
|
|
17035
|
+
vendored: external_exports.boolean()
|
|
17036
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
17037
|
+
var ResolvedEgressHit = external_exports.object({
|
|
17038
|
+
host: external_exports.string(),
|
|
17039
|
+
kind: DestinationKind,
|
|
17040
|
+
name: external_exports.string(),
|
|
17041
|
+
category: external_exports.string(),
|
|
17042
|
+
trust: ShareTrustLevel,
|
|
17043
|
+
network: DestinationNetwork.nullable(),
|
|
17044
|
+
method: HttpMethod,
|
|
17045
|
+
transport: Transport,
|
|
17046
|
+
url: external_exports.string(),
|
|
17047
|
+
template: external_exports.boolean(),
|
|
17048
|
+
dataClass: DataClass,
|
|
17049
|
+
site: EgressCallSiteHit
|
|
17050
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
17051
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
17052
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
17053
|
+
external_exports.object({
|
|
17054
|
+
mode: external_exports.literal("ledger"),
|
|
17055
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
17056
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
17057
|
+
})
|
|
17058
|
+
]).meta({ id: "EgressReconcile" });
|
|
17059
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
17060
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
17061
|
+
projectKey: external_exports.string().min(1),
|
|
17062
|
+
/** Display name only — never keys reconciliation. */
|
|
17063
|
+
project: external_exports.string(),
|
|
17064
|
+
projectId: external_exports.string().nullable(),
|
|
17065
|
+
reconcile: EgressReconcile,
|
|
17066
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
17067
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
17068
|
+
var EgressWriteSummary = external_exports.object({
|
|
17069
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17070
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17071
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17072
|
+
truncated: external_exports.boolean(),
|
|
17073
|
+
/**
|
|
17074
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
17075
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17076
|
+
* again next scan.
|
|
17077
|
+
*/
|
|
17078
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17079
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17080
|
+
|
|
16799
17081
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16800
17082
|
function toApiAction(dbVal) {
|
|
16801
17083
|
const map2 = {
|
|
@@ -16943,6 +17225,15 @@ function groupActions(g) {
|
|
|
16943
17225
|
actionsCache.set(g, actions);
|
|
16944
17226
|
return actions;
|
|
16945
17227
|
}
|
|
17228
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17229
|
+
const statusSet = new Set(statuses);
|
|
17230
|
+
let sum = 0;
|
|
17231
|
+
for (const input of statusInputs) {
|
|
17232
|
+
if (input.count === void 0) return null;
|
|
17233
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17234
|
+
}
|
|
17235
|
+
return sum;
|
|
17236
|
+
}
|
|
16946
17237
|
function applyFindingFilters(groups, opts) {
|
|
16947
17238
|
let filtered = groups;
|
|
16948
17239
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16961,6 +17252,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
16961
17252
|
const subtypeSet = new Set(opts.subtype);
|
|
16962
17253
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
16963
17254
|
}
|
|
17255
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17256
|
+
const statusSet = new Set(opts.statuses);
|
|
17257
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17258
|
+
}
|
|
16964
17259
|
if (opts.q) {
|
|
16965
17260
|
const q = opts.q.toLowerCase();
|
|
16966
17261
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -16982,6 +17277,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16982
17277
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
16983
17278
|
providers: opts.providers,
|
|
16984
17279
|
actions: opts.actions,
|
|
17280
|
+
statuses: opts.statuses,
|
|
16985
17281
|
q: opts.q,
|
|
16986
17282
|
subtype: opts.subtype
|
|
16987
17283
|
});
|
|
@@ -16991,6 +17287,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16991
17287
|
}
|
|
16992
17288
|
const forProvider = applyFindingFilters(allGroups, {
|
|
16993
17289
|
actions: opts.actions,
|
|
17290
|
+
statuses: opts.statuses,
|
|
16994
17291
|
q: opts.q,
|
|
16995
17292
|
subtype: opts.subtype,
|
|
16996
17293
|
severity: opts.severity
|
|
@@ -17001,6 +17298,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17001
17298
|
}
|
|
17002
17299
|
const forAction = applyFindingFilters(allGroups, {
|
|
17003
17300
|
providers: opts.providers,
|
|
17301
|
+
statuses: opts.statuses,
|
|
17004
17302
|
q: opts.q,
|
|
17005
17303
|
subtype: opts.subtype,
|
|
17006
17304
|
severity: opts.severity
|
|
@@ -17012,17 +17310,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17012
17310
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17013
17311
|
providers: opts.providers,
|
|
17014
17312
|
actions: opts.actions,
|
|
17313
|
+
statuses: opts.statuses,
|
|
17015
17314
|
q: opts.q,
|
|
17016
17315
|
severity: opts.severity
|
|
17017
17316
|
});
|
|
17018
17317
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17019
17318
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17319
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17320
|
+
providers: opts.providers,
|
|
17321
|
+
actions: opts.actions,
|
|
17322
|
+
q: opts.q,
|
|
17323
|
+
subtype: opts.subtype,
|
|
17324
|
+
severity: opts.severity
|
|
17325
|
+
});
|
|
17326
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17327
|
+
for (const g of forStatus) {
|
|
17328
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17329
|
+
}
|
|
17020
17330
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17021
17331
|
return {
|
|
17022
17332
|
severity: toItems(severityMap),
|
|
17023
17333
|
provider: toItems(providerMap),
|
|
17024
17334
|
action: toItems(actionMap),
|
|
17025
|
-
subtype: toItems(subtypeMap)
|
|
17335
|
+
subtype: toItems(subtypeMap),
|
|
17336
|
+
status: toItems(statusMap)
|
|
17026
17337
|
};
|
|
17027
17338
|
}
|
|
17028
17339
|
|
|
@@ -17057,10 +17368,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17057
17368
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17058
17369
|
|
|
17059
17370
|
// ../../packages/schema/src/zod/local.ts
|
|
17060
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17371
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17061
17372
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17062
17373
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17063
17374
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17375
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17376
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17377
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17378
|
+
});
|
|
17064
17379
|
var WorkspaceSettings = external_exports.object({
|
|
17065
17380
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17066
17381
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17072,38 +17387,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17072
17387
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17073
17388
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17074
17389
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17390
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17391
|
+
// Shares writes.
|
|
17392
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17075
17393
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17076
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17394
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17395
|
+
// Records that the user consented to sending findings to the model API for
|
|
17396
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17397
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17398
|
+
// covers the current payload and must be re-granted.
|
|
17399
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17077
17400
|
});
|
|
17078
17401
|
function defaultWorkspaceSettings() {
|
|
17079
17402
|
return WorkspaceSettings.parse({});
|
|
17080
17403
|
}
|
|
17081
|
-
function toEventRow(event) {
|
|
17082
|
-
return {
|
|
17083
|
-
id: event.id,
|
|
17084
|
-
sourceTool: event.sourceTool,
|
|
17085
|
-
kind: event.kind,
|
|
17086
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17087
|
-
contentHash: event.contentHash,
|
|
17088
|
-
content: event.content,
|
|
17089
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17090
|
-
};
|
|
17091
|
-
}
|
|
17092
|
-
function toFindingRow(finding) {
|
|
17093
|
-
return {
|
|
17094
|
-
id: finding.id,
|
|
17095
|
-
eventId: finding.eventId,
|
|
17096
|
-
ruleId: finding.ruleId,
|
|
17097
|
-
category: finding.category,
|
|
17098
|
-
severity: finding.severity,
|
|
17099
|
-
spanStart: finding.span.start,
|
|
17100
|
-
spanEnd: finding.span.end,
|
|
17101
|
-
maskedMatch: finding.maskedMatch,
|
|
17102
|
-
actionTaken: finding.actionTaken,
|
|
17103
|
-
confidence: finding.confidence,
|
|
17104
|
-
findingKey: finding.findingKey ?? null
|
|
17105
|
-
};
|
|
17106
|
-
}
|
|
17107
17404
|
function toInventoryRow(input, id, now) {
|
|
17108
17405
|
return {
|
|
17109
17406
|
id,
|
|
@@ -17173,7 +17470,42 @@ function toInspectionFindingRow(input) {
|
|
|
17173
17470
|
spanEnd: input.span.end,
|
|
17174
17471
|
maskedMatch: input.maskedMatch,
|
|
17175
17472
|
actionTaken: input.actionTaken,
|
|
17176
|
-
confidence: input.confidence
|
|
17473
|
+
confidence: input.confidence,
|
|
17474
|
+
findingKey: input.findingKey ?? null,
|
|
17475
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17476
|
+
};
|
|
17477
|
+
}
|
|
17478
|
+
function toCaptureAttributes(event) {
|
|
17479
|
+
const metadata = event.metadata;
|
|
17480
|
+
return {
|
|
17481
|
+
source_tool: event.sourceTool,
|
|
17482
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17483
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17484
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17485
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17486
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17487
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17488
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17489
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17490
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17491
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17492
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17493
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17494
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17495
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17496
|
+
};
|
|
17497
|
+
}
|
|
17498
|
+
function captureDefinitionVersion(finding) {
|
|
17499
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17500
|
+
}
|
|
17501
|
+
function toCaptureDefinitionInput(finding) {
|
|
17502
|
+
return {
|
|
17503
|
+
ruleId: finding.ruleId,
|
|
17504
|
+
version: captureDefinitionVersion(finding),
|
|
17505
|
+
name: finding.ruleId,
|
|
17506
|
+
category: finding.category,
|
|
17507
|
+
severity: finding.severity,
|
|
17508
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17177
17509
|
};
|
|
17178
17510
|
}
|
|
17179
17511
|
|
|
@@ -17555,145 +17887,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17555
17887
|
path: ["liveKeys"]
|
|
17556
17888
|
});
|
|
17557
17889
|
|
|
17558
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17559
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17560
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17561
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17562
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17563
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17564
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17565
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17566
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17567
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17568
|
-
var ReviewInfo = external_exports.object({
|
|
17569
|
-
needsReview: external_exports.boolean(),
|
|
17570
|
-
reasons: external_exports.array(ReviewReason)
|
|
17571
|
-
}).meta({ id: "ReviewInfo" });
|
|
17572
|
-
var DestinationNetwork = external_exports.object({
|
|
17573
|
-
port: external_exports.number().int().nullable(),
|
|
17574
|
-
geo: external_exports.string().nullable(),
|
|
17575
|
-
ptr: external_exports.string().nullable()
|
|
17576
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17577
|
-
var EndpointSummary = external_exports.object({
|
|
17578
|
-
id: external_exports.string(),
|
|
17579
|
-
method: HttpMethod,
|
|
17580
|
-
transport: Transport,
|
|
17581
|
-
url: external_exports.string(),
|
|
17582
|
-
template: external_exports.boolean(),
|
|
17583
|
-
dataClass: DataClass,
|
|
17584
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17585
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17586
|
-
}).meta({ id: "EndpointSummary" });
|
|
17587
|
-
var CallSite = external_exports.object({
|
|
17588
|
-
id: external_exports.string(),
|
|
17589
|
-
project: external_exports.string(),
|
|
17590
|
-
file: external_exports.string(),
|
|
17591
|
-
line: external_exports.number().int().nonnegative(),
|
|
17592
|
-
snippet: external_exports.string(),
|
|
17593
|
-
dynamic: external_exports.boolean(),
|
|
17594
|
-
vendored: external_exports.boolean(),
|
|
17595
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17596
|
-
projectId: external_exports.string().nullable()
|
|
17597
|
-
}).meta({ id: "CallSite" });
|
|
17598
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17599
|
-
sites: external_exports.array(CallSite)
|
|
17600
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17601
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17602
|
-
id: external_exports.string(),
|
|
17603
|
-
kind: DestinationKind,
|
|
17604
|
-
name: external_exports.string(),
|
|
17605
|
-
host: external_exports.string(),
|
|
17606
|
-
category: external_exports.string(),
|
|
17607
|
-
trust: ShareTrustLevel,
|
|
17608
|
-
/** Effective state (decision applied over the trust default). */
|
|
17609
|
-
status: EgressStatus,
|
|
17610
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17611
|
-
isCustom: external_exports.boolean(),
|
|
17612
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17613
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17614
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17615
|
-
transports: external_exports.array(Transport),
|
|
17616
|
-
/** Most-sensitive first. */
|
|
17617
|
-
dataClasses: external_exports.array(DataClass),
|
|
17618
|
-
review: ReviewInfo,
|
|
17619
|
-
/** Non-provider hosts only; null for providers. */
|
|
17620
|
-
network: DestinationNetwork.nullable(),
|
|
17621
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17622
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17623
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17624
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17625
|
-
endpointCount: true,
|
|
17626
|
-
callSiteCount: true,
|
|
17627
|
-
endpoints: true
|
|
17628
|
-
}).extend({
|
|
17629
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17630
|
-
note: external_exports.string().nullable(),
|
|
17631
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17632
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17633
|
-
var ReviewDestination = external_exports.object({
|
|
17634
|
-
id: external_exports.string(),
|
|
17635
|
-
kind: DestinationKind,
|
|
17636
|
-
name: external_exports.string(),
|
|
17637
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17638
|
-
host: external_exports.string(),
|
|
17639
|
-
trust: ShareTrustLevel,
|
|
17640
|
-
status: EgressStatus,
|
|
17641
|
-
review: ReviewInfo,
|
|
17642
|
-
topDataClass: DataClass,
|
|
17643
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17644
|
-
lastSeen: external_exports.iso.datetime()
|
|
17645
|
-
}).meta({ id: "ReviewDestination" });
|
|
17646
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17647
|
-
kind: DestinationKind,
|
|
17648
|
-
total: external_exports.number().int().nonnegative(),
|
|
17649
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17650
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17651
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17652
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17653
|
-
var SharesStats = external_exports.object({
|
|
17654
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17655
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17656
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17657
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17658
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17659
|
-
byKind: external_exports.object({
|
|
17660
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17661
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17662
|
-
ip: external_exports.number().int().nonnegative()
|
|
17663
|
-
}),
|
|
17664
|
-
byTrust: external_exports.object({
|
|
17665
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17666
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17667
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17668
|
-
ip: external_exports.number().int().nonnegative()
|
|
17669
|
-
})
|
|
17670
|
-
}).meta({ id: "SharesStats" });
|
|
17671
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17672
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17673
|
-
decision: EgressDecision.nullable()
|
|
17674
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17675
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17676
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17677
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17678
|
-
q: external_exports.string().optional(),
|
|
17679
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17680
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17681
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17682
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17683
|
-
/**
|
|
17684
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17685
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17686
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17687
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17688
|
-
*/
|
|
17689
|
-
review: external_exports.stringbool().default(false)
|
|
17690
|
-
});
|
|
17691
|
-
var ExportSharesQuery = external_exports.object({
|
|
17692
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17693
|
-
q: external_exports.string().optional(),
|
|
17694
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17695
|
-
});
|
|
17696
|
-
|
|
17697
17890
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17698
17891
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17699
17892
|
function trustDefaultStatus(trust) {
|
|
@@ -17713,7 +17906,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17713
17906
|
const reasons = [];
|
|
17714
17907
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17715
17908
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17716
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17909
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17717
17910
|
return reasons;
|
|
17718
17911
|
}
|
|
17719
17912
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17740,6 +17933,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17740
17933
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17741
17934
|
}
|
|
17742
17935
|
|
|
17936
|
+
// ../../packages/persistence/src/ids.ts
|
|
17937
|
+
import { createHash } from "crypto";
|
|
17938
|
+
function sha256Hex(input) {
|
|
17939
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17940
|
+
}
|
|
17941
|
+
function inventoryId(objectType, identityKey) {
|
|
17942
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17943
|
+
}
|
|
17944
|
+
function sourceProjectId(url2) {
|
|
17945
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17946
|
+
}
|
|
17947
|
+
function classifiedDataId(cls) {
|
|
17948
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17949
|
+
}
|
|
17950
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17951
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17952
|
+
}
|
|
17953
|
+
function llmCallId(sessionId, messageId) {
|
|
17954
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17955
|
+
}
|
|
17956
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17957
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17958
|
+
}
|
|
17959
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17960
|
+
return sha256Hex(
|
|
17961
|
+
canonicalIdentity([
|
|
17962
|
+
"inspection_finding",
|
|
17963
|
+
auditEventId,
|
|
17964
|
+
ruleId,
|
|
17965
|
+
String(spanStart),
|
|
17966
|
+
String(spanEnd)
|
|
17967
|
+
])
|
|
17968
|
+
);
|
|
17969
|
+
}
|
|
17970
|
+
var NO_SESSION = "no_session";
|
|
17971
|
+
var NO_PATH = "no_path";
|
|
17972
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17973
|
+
return sha256Hex(
|
|
17974
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17975
|
+
);
|
|
17976
|
+
}
|
|
17977
|
+
|
|
17743
17978
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17744
17979
|
function escapeLikePattern(s) {
|
|
17745
17980
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17836,39 +18071,98 @@ function evidenceExists(db, object2) {
|
|
|
17836
18071
|
return schemaObjectExists(db, "table", object2.name);
|
|
17837
18072
|
}
|
|
17838
18073
|
|
|
17839
|
-
// ../../packages/persistence/src/
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
18074
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18075
|
+
function allRows(stmt, params) {
|
|
18076
|
+
if (params === void 0) return stmt.all();
|
|
18077
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18078
|
+
return stmt.all(params);
|
|
17843
18079
|
}
|
|
17844
|
-
function
|
|
17845
|
-
|
|
18080
|
+
function getRow(stmt, params) {
|
|
18081
|
+
if (params === void 0) return stmt.get();
|
|
18082
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18083
|
+
return stmt.get(params);
|
|
17846
18084
|
}
|
|
17847
|
-
function
|
|
17848
|
-
return
|
|
18085
|
+
function intToBool(raw) {
|
|
18086
|
+
return raw === 1 || raw === true;
|
|
17849
18087
|
}
|
|
17850
|
-
function
|
|
17851
|
-
return
|
|
18088
|
+
function boolToInt(b) {
|
|
18089
|
+
return b ? 1 : 0;
|
|
17852
18090
|
}
|
|
17853
|
-
function
|
|
17854
|
-
|
|
18091
|
+
function bindParams(row) {
|
|
18092
|
+
const out = {};
|
|
18093
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18094
|
+
out[key] = value === void 0 ? null : value;
|
|
18095
|
+
}
|
|
18096
|
+
return out;
|
|
17855
18097
|
}
|
|
17856
|
-
function
|
|
17857
|
-
return
|
|
18098
|
+
function countScalar(db, sql, params) {
|
|
18099
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17858
18100
|
}
|
|
17859
|
-
function
|
|
17860
|
-
|
|
18101
|
+
function countBy(db, sql, params) {
|
|
18102
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18103
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18104
|
+
map2.set(row.k, row.n);
|
|
18105
|
+
}
|
|
18106
|
+
return map2;
|
|
17861
18107
|
}
|
|
17862
|
-
function
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
18108
|
+
function mapRowsTolerant(rows, map2) {
|
|
18109
|
+
const out = [];
|
|
18110
|
+
for (const row of rows) {
|
|
18111
|
+
try {
|
|
18112
|
+
out.push(map2(row));
|
|
18113
|
+
} catch {
|
|
18114
|
+
}
|
|
18115
|
+
}
|
|
18116
|
+
return out;
|
|
18117
|
+
}
|
|
18118
|
+
|
|
18119
|
+
// ../../packages/persistence/src/paths.ts
|
|
18120
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18121
|
+
var DATA_DIR_MODE = 448;
|
|
18122
|
+
var DATA_FILE_MODE = 384;
|
|
18123
|
+
var DB_FILENAME = "aka.db";
|
|
18124
|
+
function chmodBestEffort(path, mode) {
|
|
18125
|
+
try {
|
|
18126
|
+
chmodSync(path, mode);
|
|
18127
|
+
} catch {
|
|
18128
|
+
}
|
|
18129
|
+
}
|
|
18130
|
+
function tightenDir(dir) {
|
|
18131
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18132
|
+
}
|
|
18133
|
+
function ensureDataDirSync(dir) {
|
|
18134
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18135
|
+
tightenDir(dir);
|
|
18136
|
+
}
|
|
18137
|
+
function dbSidecars(file2) {
|
|
18138
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18139
|
+
}
|
|
18140
|
+
function tightenFile(file2) {
|
|
18141
|
+
try {
|
|
18142
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18143
|
+
} catch {
|
|
18144
|
+
}
|
|
18145
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18146
|
+
}
|
|
18147
|
+
function tightenPerms(file2) {
|
|
18148
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18149
|
+
}
|
|
18150
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
18151
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
18152
|
+
try {
|
|
18153
|
+
rmSync(tmp, { force: true });
|
|
18154
|
+
} catch {
|
|
18155
|
+
}
|
|
18156
|
+
try {
|
|
18157
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18158
|
+
renameSync(tmp, file2);
|
|
18159
|
+
} finally {
|
|
18160
|
+
try {
|
|
18161
|
+
rmSync(tmp, { force: true });
|
|
18162
|
+
} catch {
|
|
18163
|
+
}
|
|
18164
|
+
}
|
|
18165
|
+
tightenFile(file2);
|
|
17872
18166
|
}
|
|
17873
18167
|
|
|
17874
18168
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17882,7 +18176,8 @@ function createdIndexName(statement) {
|
|
|
17882
18176
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17883
18177
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17884
18178
|
}
|
|
17885
|
-
|
|
18179
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18180
|
+
function applyMigrations(db, file2) {
|
|
17886
18181
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17887
18182
|
db.exec(
|
|
17888
18183
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17896,6 +18191,7 @@ function applyMigrations(db) {
|
|
|
17896
18191
|
);
|
|
17897
18192
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17898
18193
|
if (applied.has(migration.tag)) continue;
|
|
18194
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17899
18195
|
const evidence = evidenceObjects(migration.sql);
|
|
17900
18196
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17901
18197
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -17940,13 +18236,54 @@ function applyMigrations(db) {
|
|
|
17940
18236
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
17941
18237
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
17942
18238
|
}
|
|
17943
|
-
ensureSyncedAtColumn(db, "events");
|
|
17944
18239
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17945
18240
|
ensureScanLedgerTable(db);
|
|
17946
18241
|
ensureBlockedDetectionsTable(db);
|
|
18242
|
+
ensureRuleProbeCacheTable(db);
|
|
17947
18243
|
ensureWriteGateTrigger(db);
|
|
17948
18244
|
ensureTokenUsageColumns(db);
|
|
17949
18245
|
reconcileSourceProjectIds(db);
|
|
18246
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18247
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18248
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18249
|
+
}
|
|
18250
|
+
}
|
|
18251
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18252
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18253
|
+
if (!migration) return;
|
|
18254
|
+
if (file2) {
|
|
18255
|
+
try {
|
|
18256
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18257
|
+
} catch (error51) {
|
|
18258
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18259
|
+
return;
|
|
18260
|
+
}
|
|
18261
|
+
}
|
|
18262
|
+
try {
|
|
18263
|
+
withTransaction(
|
|
18264
|
+
db,
|
|
18265
|
+
() => {
|
|
18266
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18267
|
+
if (alreadyDropped) return;
|
|
18268
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18269
|
+
db.exec(statement);
|
|
18270
|
+
}
|
|
18271
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18272
|
+
migration.tag,
|
|
18273
|
+
Date.now()
|
|
18274
|
+
);
|
|
18275
|
+
},
|
|
18276
|
+
"IMMEDIATE"
|
|
18277
|
+
);
|
|
18278
|
+
} catch (error51) {
|
|
18279
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18280
|
+
}
|
|
18281
|
+
}
|
|
18282
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18283
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18284
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18285
|
+
tightenFile(backup);
|
|
18286
|
+
return backup;
|
|
17950
18287
|
}
|
|
17951
18288
|
var TOKEN_USAGE_COLUMNS = [
|
|
17952
18289
|
{
|
|
@@ -17975,6 +18312,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17975
18312
|
}
|
|
17976
18313
|
];
|
|
17977
18314
|
function ensureTokenUsageColumns(db) {
|
|
18315
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
17978
18316
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17979
18317
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17980
18318
|
if (!existing.has(column.name)) {
|
|
@@ -18037,7 +18375,182 @@ function reconcileSourceProjectIds(db) {
|
|
|
18037
18375
|
"IMMEDIATE"
|
|
18038
18376
|
);
|
|
18039
18377
|
} catch (error51) {
|
|
18040
|
-
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18378
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18379
|
+
}
|
|
18380
|
+
}
|
|
18381
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18382
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18383
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18384
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18385
|
+
return row?.lastRowid ?? 0;
|
|
18386
|
+
}
|
|
18387
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18388
|
+
db.prepare(
|
|
18389
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18390
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18391
|
+
).run(source, lastRowid);
|
|
18392
|
+
}
|
|
18393
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18394
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18395
|
+
let processed = 0;
|
|
18396
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18397
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18398
|
+
if (rows.length === 0) return true;
|
|
18399
|
+
withTransaction(
|
|
18400
|
+
db,
|
|
18401
|
+
() => {
|
|
18402
|
+
handleRows(rows);
|
|
18403
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18404
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18405
|
+
},
|
|
18406
|
+
"IMMEDIATE"
|
|
18407
|
+
);
|
|
18408
|
+
processed += rows.length;
|
|
18409
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18410
|
+
}
|
|
18411
|
+
return false;
|
|
18412
|
+
}
|
|
18413
|
+
function parseLegacyEventMetadata(raw) {
|
|
18414
|
+
if (raw === null) return void 0;
|
|
18415
|
+
try {
|
|
18416
|
+
return JSON.parse(raw);
|
|
18417
|
+
} catch {
|
|
18418
|
+
return void 0;
|
|
18419
|
+
}
|
|
18420
|
+
}
|
|
18421
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18422
|
+
return JSON.stringify(
|
|
18423
|
+
toCaptureAttributes({
|
|
18424
|
+
id: row.id,
|
|
18425
|
+
sourceTool: row.sourceTool,
|
|
18426
|
+
kind: row.kind,
|
|
18427
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18428
|
+
contentHash: row.contentHash,
|
|
18429
|
+
content: row.content,
|
|
18430
|
+
metadata: row.metadata
|
|
18431
|
+
})
|
|
18432
|
+
);
|
|
18433
|
+
}
|
|
18434
|
+
function copyLegacyEvents(db) {
|
|
18435
|
+
const selectStmt = db.prepare(
|
|
18436
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18437
|
+
content_hash AS contentHash, content, metadata
|
|
18438
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18439
|
+
);
|
|
18440
|
+
const insertStmt = db.prepare(
|
|
18441
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18442
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18443
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18444
|
+
);
|
|
18445
|
+
const stubRootStmt = db.prepare(
|
|
18446
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18447
|
+
);
|
|
18448
|
+
return drainLegacyTable(
|
|
18449
|
+
db,
|
|
18450
|
+
"events",
|
|
18451
|
+
selectStmt,
|
|
18452
|
+
(rows) => {
|
|
18453
|
+
for (const row of rows) {
|
|
18454
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18455
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18456
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18457
|
+
insertStmt.run(
|
|
18458
|
+
bindParams({
|
|
18459
|
+
id: row.id,
|
|
18460
|
+
parentId: sessionId,
|
|
18461
|
+
rootSessionId: sessionId,
|
|
18462
|
+
eventType: row.kind,
|
|
18463
|
+
startedAt: row.occurredAt,
|
|
18464
|
+
content: row.content,
|
|
18465
|
+
contentHash: row.contentHash,
|
|
18466
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18467
|
+
})
|
|
18468
|
+
);
|
|
18469
|
+
}
|
|
18470
|
+
}
|
|
18471
|
+
);
|
|
18472
|
+
}
|
|
18473
|
+
function copyLegacyFindings(db) {
|
|
18474
|
+
const selectStmt = db.prepare(
|
|
18475
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18476
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18477
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18478
|
+
first_detected_at AS firstDetectedAt
|
|
18479
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18480
|
+
);
|
|
18481
|
+
const definitionStmt = db.prepare(
|
|
18482
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18483
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18484
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18485
|
+
);
|
|
18486
|
+
const findingStmt = db.prepare(
|
|
18487
|
+
`INSERT INTO inspection_findings
|
|
18488
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18489
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18490
|
+
finding_key, first_detected_at)
|
|
18491
|
+
VALUES
|
|
18492
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18493
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18494
|
+
:findingKey, :firstDetectedAt)
|
|
18495
|
+
ON CONFLICT(id) DO NOTHING
|
|
18496
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18497
|
+
first_detected_at = CASE
|
|
18498
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18499
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18500
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18501
|
+
END`
|
|
18502
|
+
);
|
|
18503
|
+
return drainLegacyTable(
|
|
18504
|
+
db,
|
|
18505
|
+
"findings",
|
|
18506
|
+
selectStmt,
|
|
18507
|
+
(rows) => {
|
|
18508
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18509
|
+
for (const row of rows) {
|
|
18510
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18511
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18512
|
+
if (definitionId === void 0) {
|
|
18513
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18514
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18515
|
+
definitionStmt.run(
|
|
18516
|
+
bindParams({
|
|
18517
|
+
id: definitionId,
|
|
18518
|
+
ruleId: row.ruleId,
|
|
18519
|
+
name: row.ruleId,
|
|
18520
|
+
category: row.category,
|
|
18521
|
+
severity: row.severity,
|
|
18522
|
+
definition: "",
|
|
18523
|
+
version: version2
|
|
18524
|
+
})
|
|
18525
|
+
);
|
|
18526
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18527
|
+
}
|
|
18528
|
+
findingStmt.run(
|
|
18529
|
+
bindParams({
|
|
18530
|
+
id: row.id,
|
|
18531
|
+
auditEventId: row.eventId,
|
|
18532
|
+
inspectionDefinitionId: definitionId,
|
|
18533
|
+
spanStart: row.spanStart,
|
|
18534
|
+
spanEnd: row.spanEnd,
|
|
18535
|
+
maskedMatch: row.maskedMatch,
|
|
18536
|
+
actionTaken: row.actionTaken,
|
|
18537
|
+
confidence: row.confidence,
|
|
18538
|
+
findingKey: row.findingKey,
|
|
18539
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18540
|
+
})
|
|
18541
|
+
);
|
|
18542
|
+
}
|
|
18543
|
+
}
|
|
18544
|
+
);
|
|
18545
|
+
}
|
|
18546
|
+
function runLegacyHistoryBackfill(db) {
|
|
18547
|
+
try {
|
|
18548
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18549
|
+
if (!eventsCaughtUp) return false;
|
|
18550
|
+
return copyLegacyFindings(db);
|
|
18551
|
+
} catch (error51) {
|
|
18552
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18553
|
+
return false;
|
|
18041
18554
|
}
|
|
18042
18555
|
}
|
|
18043
18556
|
function isForeignSqliteLineage(db) {
|
|
@@ -18045,6 +18558,7 @@ function isForeignSqliteLineage(db) {
|
|
|
18045
18558
|
return columnNames(db, "events").includes("tenant_id");
|
|
18046
18559
|
}
|
|
18047
18560
|
function ensureSyncedAtColumn(db, table) {
|
|
18561
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
18048
18562
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
18049
18563
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18050
18564
|
}
|
|
@@ -18065,6 +18579,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18065
18579
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18066
18580
|
)`);
|
|
18067
18581
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18582
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18068
18583
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18069
18584
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18070
18585
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18083,29 +18598,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18083
18598
|
blocked_at INTEGER NOT NULL
|
|
18084
18599
|
)`);
|
|
18085
18600
|
}
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18094
|
-
try {
|
|
18095
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18096
|
-
} catch {
|
|
18097
|
-
}
|
|
18098
|
-
}
|
|
18099
|
-
function walSidecars(file2) {
|
|
18100
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18101
|
-
}
|
|
18102
|
-
function tightenPerms(file2) {
|
|
18103
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18104
|
-
try {
|
|
18105
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18106
|
-
} catch {
|
|
18107
|
-
}
|
|
18108
|
-
}
|
|
18601
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18602
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18603
|
+
rule_key TEXT PRIMARY KEY,
|
|
18604
|
+
verdict TEXT NOT NULL,
|
|
18605
|
+
worst_probe_ms REAL NOT NULL,
|
|
18606
|
+
checked_at INTEGER NOT NULL
|
|
18607
|
+
)`);
|
|
18109
18608
|
}
|
|
18110
18609
|
|
|
18111
18610
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18127,51 +18626,6 @@ function parseJsonObject(s) {
|
|
|
18127
18626
|
return void 0;
|
|
18128
18627
|
}
|
|
18129
18628
|
|
|
18130
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18131
|
-
function allRows(stmt, params) {
|
|
18132
|
-
if (params === void 0) return stmt.all();
|
|
18133
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18134
|
-
return stmt.all(params);
|
|
18135
|
-
}
|
|
18136
|
-
function getRow(stmt, params) {
|
|
18137
|
-
if (params === void 0) return stmt.get();
|
|
18138
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18139
|
-
return stmt.get(params);
|
|
18140
|
-
}
|
|
18141
|
-
function intToBool(raw) {
|
|
18142
|
-
return raw === 1 || raw === true;
|
|
18143
|
-
}
|
|
18144
|
-
function boolToInt(b) {
|
|
18145
|
-
return b ? 1 : 0;
|
|
18146
|
-
}
|
|
18147
|
-
function bindParams(row) {
|
|
18148
|
-
const out = {};
|
|
18149
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18150
|
-
out[key] = value === void 0 ? null : value;
|
|
18151
|
-
}
|
|
18152
|
-
return out;
|
|
18153
|
-
}
|
|
18154
|
-
function countScalar(db, sql, params) {
|
|
18155
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18156
|
-
}
|
|
18157
|
-
function countBy(db, sql, params) {
|
|
18158
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18159
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18160
|
-
map2.set(row.k, row.n);
|
|
18161
|
-
}
|
|
18162
|
-
return map2;
|
|
18163
|
-
}
|
|
18164
|
-
function mapRowsTolerant(rows, map2) {
|
|
18165
|
-
const out = [];
|
|
18166
|
-
for (const row of rows) {
|
|
18167
|
-
try {
|
|
18168
|
-
out.push(map2(row));
|
|
18169
|
-
} catch {
|
|
18170
|
-
}
|
|
18171
|
-
}
|
|
18172
|
-
return out;
|
|
18173
|
-
}
|
|
18174
|
-
|
|
18175
18629
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18176
18630
|
var DAY_MS = 864e5;
|
|
18177
18631
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18788,6 +19242,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18788
19242
|
})
|
|
18789
19243
|
);
|
|
18790
19244
|
}
|
|
19245
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19246
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19247
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19248
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19249
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19250
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19251
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19252
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19253
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19254
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19255
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19256
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19257
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19258
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19259
|
+
}
|
|
18791
19260
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18792
19261
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18793
19262
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19249,8 +19718,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19249
19718
|
)
|
|
19250
19719
|
);
|
|
19251
19720
|
}
|
|
19252
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19253
|
-
//
|
|
19721
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19722
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19723
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19724
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19725
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19726
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19727
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19728
|
+
// to the old findings⋈events one.
|
|
19254
19729
|
countFindingsLast30d(ruleIds) {
|
|
19255
19730
|
if (ruleIds.length === 0) return 0;
|
|
19256
19731
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19258,8 +19733,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19258
19733
|
return countScalar(
|
|
19259
19734
|
this.db,
|
|
19260
19735
|
`SELECT count(*) AS n
|
|
19261
|
-
FROM
|
|
19262
|
-
|
|
19736
|
+
FROM inspection_findings f
|
|
19737
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19738
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19739
|
+
WHERE e.started_at >= ?
|
|
19740
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19741
|
+
AND d.rule_id IN (${inClause})`,
|
|
19263
19742
|
[since, ...ruleIds]
|
|
19264
19743
|
);
|
|
19265
19744
|
}
|
|
@@ -19269,35 +19748,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19269
19748
|
var SqliteEventsRepository = class {
|
|
19270
19749
|
constructor(db) {
|
|
19271
19750
|
this.db = db;
|
|
19272
|
-
this.insertStmt = db.prepare(
|
|
19273
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19274
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19275
|
-
);
|
|
19276
19751
|
}
|
|
19277
19752
|
db;
|
|
19278
|
-
|
|
19279
|
-
|
|
19280
|
-
|
|
19281
|
-
this.insertStmt.run(
|
|
19282
|
-
bindParams({
|
|
19283
|
-
id: row.id,
|
|
19284
|
-
sourceTool: row.sourceTool,
|
|
19285
|
-
kind: row.kind,
|
|
19286
|
-
occurredAt: row.occurredAt,
|
|
19287
|
-
contentHash: row.contentHash,
|
|
19288
|
-
content: row.content,
|
|
19289
|
-
metadata: row.metadata
|
|
19290
|
-
})
|
|
19291
|
-
);
|
|
19292
|
-
}
|
|
19293
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19294
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19295
|
-
// never duplicates findings.
|
|
19753
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19754
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19755
|
+
// scan never duplicates findings.
|
|
19296
19756
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19297
19757
|
// async EventsReadPort contract.
|
|
19758
|
+
//
|
|
19759
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19760
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19761
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19762
|
+
// index-friendly rather than walking rows that can never match.
|
|
19298
19763
|
contentHashes() {
|
|
19299
19764
|
const rows = allRows(
|
|
19300
|
-
this.db.prepare(
|
|
19765
|
+
this.db.prepare(
|
|
19766
|
+
`SELECT content_hash FROM audit_events
|
|
19767
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19768
|
+
)
|
|
19301
19769
|
);
|
|
19302
19770
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19303
19771
|
}
|
|
@@ -19633,17 +20101,20 @@ function parseExceptionRow(row) {
|
|
|
19633
20101
|
}
|
|
19634
20102
|
|
|
19635
20103
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19636
|
-
function
|
|
20104
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19637
20105
|
return `(
|
|
19638
|
-
SELECT fr
|
|
20106
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19639
20107
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19640
20108
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19641
20109
|
LIMIT 1
|
|
19642
20110
|
)`;
|
|
19643
20111
|
}
|
|
20112
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20113
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20114
|
+
}
|
|
19644
20115
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19645
|
-
SELECT finding_key, status FROM (
|
|
19646
|
-
SELECT fr.finding_key, fr.status,
|
|
20116
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20117
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19647
20118
|
ROW_NUMBER() OVER (
|
|
19648
20119
|
PARTITION BY fr.finding_key
|
|
19649
20120
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19670,68 +20141,21 @@ var DAY_MS3 = 864e5;
|
|
|
19670
20141
|
var SqliteFindingsRepository = class {
|
|
19671
20142
|
constructor(db) {
|
|
19672
20143
|
this.db = db;
|
|
19673
|
-
this.insertStmt = db.prepare(
|
|
19674
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19675
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19676
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19677
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19678
|
-
event_id = excluded.event_id,
|
|
19679
|
-
category = excluded.category,
|
|
19680
|
-
severity = excluded.severity,
|
|
19681
|
-
span_start = excluded.span_start,
|
|
19682
|
-
span_end = excluded.span_end,
|
|
19683
|
-
masked_match = excluded.masked_match,
|
|
19684
|
-
action_taken = excluded.action_taken,
|
|
19685
|
-
confidence = excluded.confidence`
|
|
19686
|
-
);
|
|
19687
|
-
this.sessionDupStmt = db.prepare(
|
|
19688
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19689
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19690
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19691
|
-
LIMIT 1`
|
|
19692
|
-
);
|
|
19693
20144
|
}
|
|
19694
20145
|
db;
|
|
19695
|
-
insertStmt;
|
|
19696
|
-
sessionDupStmt;
|
|
19697
|
-
insertFindings(findings, scope = {}) {
|
|
19698
|
-
for (const finding of findings) {
|
|
19699
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19700
|
-
const row = toFindingRow(finding);
|
|
19701
|
-
this.insertStmt.run({
|
|
19702
|
-
id: row.id,
|
|
19703
|
-
eventId: row.eventId,
|
|
19704
|
-
ruleId: row.ruleId,
|
|
19705
|
-
category: row.category,
|
|
19706
|
-
severity: row.severity,
|
|
19707
|
-
spanStart: row.spanStart,
|
|
19708
|
-
spanEnd: row.spanEnd,
|
|
19709
|
-
maskedMatch: row.maskedMatch,
|
|
19710
|
-
actionTaken: row.actionTaken,
|
|
19711
|
-
confidence: row.confidence,
|
|
19712
|
-
findingKey: row.findingKey ?? null
|
|
19713
|
-
});
|
|
19714
|
-
}
|
|
19715
|
-
}
|
|
19716
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19717
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19718
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19719
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19720
|
-
const hit = this.sessionDupStmt.get({
|
|
19721
|
-
ruleId: finding.ruleId,
|
|
19722
|
-
maskedMatch: finding.maskedMatch,
|
|
19723
|
-
sessionId
|
|
19724
|
-
});
|
|
19725
|
-
return hit !== void 0;
|
|
19726
|
-
}
|
|
19727
20146
|
recentFindings(opts) {
|
|
19728
20147
|
const limit = opts?.limit ?? 50;
|
|
19729
20148
|
const rows = allRows(
|
|
19730
20149
|
this.db.prepare(
|
|
19731
|
-
`SELECT f.id, f.event_id,
|
|
19732
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19733
|
-
|
|
19734
|
-
|
|
20150
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20151
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20152
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20153
|
+
e.event_type AS kind
|
|
20154
|
+
FROM inspection_findings f
|
|
20155
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20156
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20157
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20158
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19735
20159
|
LIMIT :limit`
|
|
19736
20160
|
),
|
|
19737
20161
|
{ limit }
|
|
@@ -19753,25 +20177,34 @@ var SqliteFindingsRepository = class {
|
|
|
19753
20177
|
);
|
|
19754
20178
|
}
|
|
19755
20179
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19756
|
-
* session-stamped
|
|
20180
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19757
20181
|
* page can label its findings link without the grouped pipeline. */
|
|
19758
20182
|
sessionFindingsCount(sessionId) {
|
|
19759
20183
|
if (!sessionId) return Promise.resolve(0);
|
|
19760
20184
|
return Promise.resolve(
|
|
19761
20185
|
countScalar(
|
|
19762
20186
|
this.db,
|
|
19763
|
-
`SELECT count(*) AS n FROM
|
|
19764
|
-
JOIN
|
|
19765
|
-
WHERE
|
|
20187
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20188
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20189
|
+
WHERE e.root_session_id = :sessionId
|
|
20190
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19766
20191
|
{ sessionId }
|
|
19767
20192
|
)
|
|
19768
20193
|
);
|
|
19769
20194
|
}
|
|
19770
|
-
/** Per-rule transcript firing tally for one session —
|
|
19771
|
-
*
|
|
19772
|
-
*
|
|
19773
|
-
*
|
|
19774
|
-
*
|
|
20195
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20196
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20197
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20198
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20199
|
+
* tally with the deduped groups it lists.
|
|
20200
|
+
*
|
|
20201
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20202
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20203
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20204
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20205
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20206
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20207
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19775
20208
|
sessionFirings(sessionId) {
|
|
19776
20209
|
return Object.fromEntries(
|
|
19777
20210
|
countBy(
|
|
@@ -19781,18 +20214,25 @@ var SqliteFindingsRepository = class {
|
|
|
19781
20214
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19782
20215
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19783
20216
|
WHERE e.root_session_id = :sessionId
|
|
20217
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19784
20218
|
GROUP BY d.rule_id`,
|
|
19785
20219
|
{ sessionId }
|
|
19786
20220
|
)
|
|
19787
20221
|
);
|
|
19788
20222
|
}
|
|
19789
20223
|
/**
|
|
19790
|
-
* Grouped findings for the dashboard — joins
|
|
19791
|
-
* toolName from
|
|
19792
|
-
*
|
|
20224
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20225
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20226
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20227
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20228
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20229
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20230
|
+
* severity then recency. Filtering
|
|
19793
20231
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19794
20232
|
* reflect the full filtered set; `items` is the requested
|
|
19795
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20233
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20234
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20235
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19796
20236
|
*
|
|
19797
20237
|
* Two reads, neither of which materializes a row per finding:
|
|
19798
20238
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19805,10 +20245,11 @@ var SqliteFindingsRepository = class {
|
|
|
19805
20245
|
* rule is ever restated in SQL.
|
|
19806
20246
|
*/
|
|
19807
20247
|
listGroupedFindings(query) {
|
|
19808
|
-
const sessionPredicate = query.sessionId ? `
|
|
20248
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20249
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19809
20250
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19810
20251
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19811
|
-
predicate
|
|
20252
|
+
predicate,
|
|
19812
20253
|
params: sessionParams
|
|
19813
20254
|
});
|
|
19814
20255
|
const rows = allRows(
|
|
@@ -19816,24 +20257,26 @@ var SqliteFindingsRepository = class {
|
|
|
19816
20257
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19817
20258
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19818
20259
|
FROM (
|
|
19819
|
-
SELECT f.id AS id,
|
|
19820
|
-
|
|
20260
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20261
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19821
20262
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19822
|
-
e.
|
|
19823
|
-
json_extract(e.
|
|
19824
|
-
json_extract(e.
|
|
19825
|
-
json_extract(e.
|
|
19826
|
-
e.
|
|
20263
|
+
e.started_at AS occurred_at,
|
|
20264
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20265
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20266
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20267
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20268
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19827
20269
|
latest.status AS latest_status,
|
|
19828
20270
|
ROW_NUMBER() OVER (
|
|
19829
|
-
PARTITION BY
|
|
19830
|
-
ORDER BY e.
|
|
20271
|
+
PARTITION BY d.rule_id
|
|
20272
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19831
20273
|
) AS rn
|
|
19832
|
-
FROM
|
|
19833
|
-
JOIN
|
|
20274
|
+
FROM inspection_findings f
|
|
20275
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20276
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19834
20277
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19835
20278
|
ON latest.finding_key = f.finding_key
|
|
19836
|
-
${
|
|
20279
|
+
${predicate}
|
|
19837
20280
|
)
|
|
19838
20281
|
WHERE rn <= :cap
|
|
19839
20282
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19860,17 +20303,29 @@ var SqliteFindingsRepository = class {
|
|
|
19860
20303
|
severity: query.severity,
|
|
19861
20304
|
providers: query.provider,
|
|
19862
20305
|
actions: query.action,
|
|
20306
|
+
statuses: query.status,
|
|
19863
20307
|
subtype: query.subtype,
|
|
19864
20308
|
q: query.q
|
|
19865
20309
|
};
|
|
19866
20310
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19867
20311
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20312
|
+
const statusFilter = query.status ?? [];
|
|
19868
20313
|
const totals = {
|
|
19869
|
-
findings: sorted.reduce((acc, g) =>
|
|
20314
|
+
findings: sorted.reduce((acc, g) => {
|
|
20315
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20316
|
+
const agg = aggregates.get(g.id);
|
|
20317
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20318
|
+
}, 0),
|
|
19870
20319
|
groups: sorted.length
|
|
19871
20320
|
};
|
|
19872
20321
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19873
|
-
const
|
|
20322
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20323
|
+
const items = sorted.slice(0, limit).map(
|
|
20324
|
+
(g) => statusSet ? {
|
|
20325
|
+
...g,
|
|
20326
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20327
|
+
} : g
|
|
20328
|
+
);
|
|
19874
20329
|
return Promise.resolve({
|
|
19875
20330
|
totals,
|
|
19876
20331
|
facets,
|
|
@@ -19884,45 +20339,62 @@ var SqliteFindingsRepository = class {
|
|
|
19884
20339
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19885
20340
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19886
20341
|
*
|
|
19887
|
-
*
|
|
19888
|
-
*
|
|
19889
|
-
*
|
|
19890
|
-
* status
|
|
19891
|
-
*
|
|
19892
|
-
*
|
|
20342
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20343
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20344
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20345
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20346
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20347
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20348
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20349
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20350
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20351
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20352
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19893
20353
|
* a group's row stays small however many findings it holds.
|
|
19894
20354
|
*
|
|
19895
20355
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19896
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19897
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19898
|
-
* that is a string proportional to the store (~8MB over
|
|
19899
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19900
|
-
* match an instance outside the preview, which searching
|
|
19901
|
-
* would silently lose, so it is fetched only when the
|
|
19902
|
-
* carries a `q`.
|
|
20356
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20357
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20358
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20359
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20360
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20361
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20362
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20363
|
+
* path repeating across tuples.)
|
|
19903
20364
|
*/
|
|
19904
20365
|
groupAggregates(withSearchText, scope) {
|
|
19905
|
-
const
|
|
19906
|
-
group_concat(DISTINCT json_extract(e.
|
|
19907
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20366
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20367
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20368
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19908
20369
|
const rows = this.db.prepare(
|
|
19909
|
-
`SELECT
|
|
19910
|
-
|
|
19911
|
-
max(
|
|
19912
|
-
group_concat(
|
|
19913
|
-
group_concat(
|
|
19914
|
-
group_concat(
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
20370
|
+
`SELECT rule_id,
|
|
20371
|
+
sum(tuple_count) AS instance_count,
|
|
20372
|
+
max(latest_at) AS latest_at,
|
|
20373
|
+
group_concat(source_tools) AS source_tools,
|
|
20374
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20375
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20376
|
+
group_concat(repos) AS repos,
|
|
20377
|
+
group_concat(files) AS files,
|
|
20378
|
+
group_concat(tool_names) AS tool_names
|
|
20379
|
+
FROM (
|
|
20380
|
+
SELECT d.rule_id AS rule_id,
|
|
20381
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20382
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20383
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20384
|
+
count(*) AS tuple_count,
|
|
20385
|
+
max(e.started_at) AS latest_at,
|
|
20386
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20387
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20388
|
+
${innerSearchColumns}
|
|
20389
|
+
FROM inspection_findings f
|
|
20390
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20391
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20392
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20393
|
+
ON latest.finding_key = f.finding_key
|
|
20394
|
+
${scope.predicate}
|
|
20395
|
+
GROUP BY d.rule_id, status_tuple
|
|
20396
|
+
)
|
|
20397
|
+
GROUP BY rule_id`
|
|
19926
20398
|
).all(scope.params);
|
|
19927
20399
|
return new Map(
|
|
19928
20400
|
rows.map((r) => [
|
|
@@ -19932,13 +20404,14 @@ var SqliteFindingsRepository = class {
|
|
|
19932
20404
|
sourceTools: splitConcat(r.source_tools),
|
|
19933
20405
|
actionsTaken: splitConcat(r.actions_taken),
|
|
19934
20406
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19935
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20407
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
19936
20408
|
return {
|
|
19937
20409
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19938
20410
|
// so the marker stands in for the key itself (never rendered).
|
|
19939
20411
|
kind,
|
|
19940
20412
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19941
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20413
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20414
|
+
count: Number(count)
|
|
19942
20415
|
};
|
|
19943
20416
|
}),
|
|
19944
20417
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -19955,10 +20428,21 @@ var SqliteFindingsRepository = class {
|
|
|
19955
20428
|
);
|
|
19956
20429
|
}
|
|
19957
20430
|
healthSummary() {
|
|
19958
|
-
const total = countScalar(
|
|
20431
|
+
const total = countScalar(
|
|
20432
|
+
this.db,
|
|
20433
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20434
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20435
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20436
|
+
);
|
|
19959
20437
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19960
20438
|
const grouped = allRows(
|
|
19961
|
-
this.db.prepare(
|
|
20439
|
+
this.db.prepare(
|
|
20440
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20441
|
+
FROM inspection_findings f
|
|
20442
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20443
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20444
|
+
GROUP BY f.action_taken`
|
|
20445
|
+
)
|
|
19962
20446
|
);
|
|
19963
20447
|
for (const row of grouped) {
|
|
19964
20448
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -19966,12 +20450,15 @@ var SqliteFindingsRepository = class {
|
|
|
19966
20450
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19967
20451
|
const sevRows = allRows(
|
|
19968
20452
|
this.db.prepare(
|
|
19969
|
-
`SELECT
|
|
19970
|
-
FROM
|
|
20453
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20454
|
+
FROM inspection_findings f
|
|
20455
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20456
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19971
20457
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19972
20458
|
ON latest.finding_key = f.finding_key
|
|
19973
|
-
WHERE
|
|
19974
|
-
|
|
20459
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20460
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20461
|
+
GROUP BY d.severity`
|
|
19975
20462
|
)
|
|
19976
20463
|
);
|
|
19977
20464
|
for (const row of sevRows) {
|
|
@@ -19992,9 +20479,11 @@ var SqliteFindingsRepository = class {
|
|
|
19992
20479
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19993
20480
|
const rows = allRows(
|
|
19994
20481
|
this.db.prepare(
|
|
19995
|
-
`SELECT date(e.
|
|
19996
|
-
FROM
|
|
19997
|
-
|
|
20482
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20483
|
+
FROM inspection_findings f
|
|
20484
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20485
|
+
WHERE e.started_at >= :since
|
|
20486
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19998
20487
|
GROUP BY day, f.action_taken`
|
|
19999
20488
|
),
|
|
20000
20489
|
{ since }
|
|
@@ -20059,15 +20548,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20059
20548
|
this.insertStmt = db.prepare(
|
|
20060
20549
|
`INSERT INTO inspection_findings
|
|
20061
20550
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20062
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20551
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20552
|
+
finding_key, first_detected_at)
|
|
20063
20553
|
VALUES
|
|
20064
20554
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20065
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20066
|
-
|
|
20555
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20556
|
+
:findingKey,
|
|
20557
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20558
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20559
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20560
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20561
|
+
audit_event_id = excluded.audit_event_id,
|
|
20562
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20563
|
+
classified_data_id = excluded.classified_data_id,
|
|
20564
|
+
span_start = excluded.span_start,
|
|
20565
|
+
span_end = excluded.span_end,
|
|
20566
|
+
masked_match = excluded.masked_match,
|
|
20567
|
+
action_taken = excluded.action_taken,
|
|
20568
|
+
confidence = excluded.confidence`
|
|
20569
|
+
);
|
|
20570
|
+
this.sessionDupStmt = db.prepare(
|
|
20571
|
+
`SELECT 1 FROM inspection_findings f
|
|
20572
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20573
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20574
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20575
|
+
AND e.root_session_id = :sessionId
|
|
20576
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20577
|
+
LIMIT 1`
|
|
20578
|
+
);
|
|
20579
|
+
this.eventDupStmt = db.prepare(
|
|
20580
|
+
`SELECT 1 FROM inspection_findings f
|
|
20581
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20582
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20583
|
+
AND f.masked_match = :maskedMatch
|
|
20584
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20585
|
+
LIMIT 1`
|
|
20067
20586
|
);
|
|
20068
20587
|
}
|
|
20069
20588
|
db;
|
|
20070
20589
|
insertStmt;
|
|
20590
|
+
sessionDupStmt;
|
|
20591
|
+
eventDupStmt;
|
|
20592
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20593
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20594
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20595
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20596
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20597
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20598
|
+
}
|
|
20599
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20600
|
+
// recorded against the given audit event.
|
|
20601
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20602
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20603
|
+
}
|
|
20071
20604
|
insertFinding(input) {
|
|
20072
20605
|
const row = toInspectionFindingRow(input);
|
|
20073
20606
|
this.insertStmt.run(
|
|
@@ -20080,7 +20613,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20080
20613
|
spanEnd: row.spanEnd,
|
|
20081
20614
|
maskedMatch: row.maskedMatch,
|
|
20082
20615
|
actionTaken: row.actionTaken,
|
|
20083
|
-
confidence: row.confidence
|
|
20616
|
+
confidence: row.confidence,
|
|
20617
|
+
findingKey: row.findingKey,
|
|
20618
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20084
20619
|
})
|
|
20085
20620
|
);
|
|
20086
20621
|
}
|
|
@@ -20352,7 +20887,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20352
20887
|
installedRuleset() {
|
|
20353
20888
|
const rows = allRows(
|
|
20354
20889
|
this.db.prepare(
|
|
20355
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20890
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20356
20891
|
)
|
|
20357
20892
|
);
|
|
20358
20893
|
const out = {
|
|
@@ -20360,7 +20895,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20360
20895
|
enabledPacks: 0,
|
|
20361
20896
|
rules: [],
|
|
20362
20897
|
invalidRules: 0,
|
|
20363
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20898
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20899
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20364
20900
|
};
|
|
20365
20901
|
for (const row of rows) {
|
|
20366
20902
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20382,6 +20918,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20382
20918
|
if (parsed.success) {
|
|
20383
20919
|
out.rules.push(parsed.data);
|
|
20384
20920
|
out.ruleActions.set(parsed.data.id, action);
|
|
20921
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20385
20922
|
} else out.invalidRules += 1;
|
|
20386
20923
|
}
|
|
20387
20924
|
}
|
|
@@ -21556,19 +22093,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21556
22093
|
);
|
|
21557
22094
|
this.openAtRestStmt = db.prepare(
|
|
21558
22095
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21559
|
-
FROM
|
|
21560
|
-
JOIN
|
|
21561
|
-
WHERE e.
|
|
21562
|
-
AND json_extract(e.
|
|
22096
|
+
FROM inspection_findings f
|
|
22097
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22098
|
+
WHERE e.event_type = 'code_change'
|
|
22099
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21563
22100
|
AND f.finding_key IS NOT NULL
|
|
21564
22101
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21565
22102
|
);
|
|
21566
22103
|
this.resolvedAtRestStmt = db.prepare(
|
|
21567
22104
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21568
|
-
FROM
|
|
21569
|
-
JOIN
|
|
21570
|
-
WHERE e.
|
|
21571
|
-
AND json_extract(e.
|
|
22105
|
+
FROM inspection_findings f
|
|
22106
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22107
|
+
WHERE e.event_type = 'code_change'
|
|
22108
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21572
22109
|
AND f.finding_key IS NOT NULL
|
|
21573
22110
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21574
22111
|
);
|
|
@@ -21636,6 +22173,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21636
22173
|
}
|
|
21637
22174
|
};
|
|
21638
22175
|
|
|
22176
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22177
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22178
|
+
constructor(db) {
|
|
22179
|
+
this.db = db;
|
|
22180
|
+
this.upsertStmt = db.prepare(
|
|
22181
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22182
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22183
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22184
|
+
verdict = excluded.verdict,
|
|
22185
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22186
|
+
checked_at = excluded.checked_at`
|
|
22187
|
+
);
|
|
22188
|
+
this.readStmt = db.prepare(
|
|
22189
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22190
|
+
);
|
|
22191
|
+
}
|
|
22192
|
+
db;
|
|
22193
|
+
upsertStmt;
|
|
22194
|
+
readStmt;
|
|
22195
|
+
getVerdict(ruleKey) {
|
|
22196
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22197
|
+
}
|
|
22198
|
+
setVerdict(ruleKey, verdict, worstProbeMs2) {
|
|
22199
|
+
failOpenTransaction(this.db, () => {
|
|
22200
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
|
|
22201
|
+
});
|
|
22202
|
+
}
|
|
22203
|
+
};
|
|
22204
|
+
|
|
21639
22205
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21640
22206
|
var SqliteScanLedgerRepository = class {
|
|
21641
22207
|
constructor(db) {
|
|
@@ -21762,25 +22328,27 @@ var SqliteSecurityRepository = class {
|
|
|
21762
22328
|
severitySummary() {
|
|
21763
22329
|
const rows = allRows(
|
|
21764
22330
|
this.db.prepare(
|
|
21765
|
-
`SELECT
|
|
22331
|
+
`SELECT d.severity AS severity,
|
|
21766
22332
|
COUNT(*) AS count,
|
|
21767
22333
|
SUM(CASE
|
|
21768
|
-
WHEN e.
|
|
22334
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21769
22335
|
WHEN f.finding_key IS NULL THEN 0
|
|
21770
22336
|
WHEN latest.status = 'resolved' THEN 1
|
|
21771
22337
|
ELSE 0
|
|
21772
22338
|
END) AS caught,
|
|
21773
22339
|
SUM(CASE
|
|
21774
|
-
WHEN e.
|
|
22340
|
+
WHEN e.event_type = 'code_change'
|
|
21775
22341
|
AND f.finding_key IS NOT NULL
|
|
21776
22342
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21777
22343
|
ELSE 0
|
|
21778
22344
|
END) AS open_at_rest
|
|
21779
|
-
FROM
|
|
21780
|
-
JOIN
|
|
22345
|
+
FROM inspection_findings f
|
|
22346
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22347
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21781
22348
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21782
22349
|
ON latest.finding_key = f.finding_key
|
|
21783
|
-
|
|
22350
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22351
|
+
GROUP BY d.severity`
|
|
21784
22352
|
)
|
|
21785
22353
|
);
|
|
21786
22354
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21846,7 +22414,7 @@ var SqliteSecurityRepository = class {
|
|
|
21846
22414
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21847
22415
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21848
22416
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21849
|
-
// (
|
|
22417
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21850
22418
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21851
22419
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21852
22420
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21871,30 +22439,20 @@ var SqliteSecurityRepository = class {
|
|
|
21871
22439
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21872
22440
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21873
22441
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21874
|
-
//
|
|
21875
|
-
// the parent event's
|
|
21876
|
-
// backfill left null.
|
|
21877
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21878
|
-
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
WHERE fr.finding_key = f.finding_key
|
|
21887
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21888
|
-
LIMIT 1
|
|
21889
|
-
) AS latest_method,
|
|
21890
|
-
(
|
|
21891
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21892
|
-
WHERE fr.finding_key = f.finding_key
|
|
21893
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21894
|
-
LIMIT 1
|
|
21895
|
-
) AS latest_resolved_at
|
|
21896
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22442
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22443
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22444
|
+
// legacy/edge row the backfill left null.
|
|
22445
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22446
|
+
latest.status AS latest_status,
|
|
22447
|
+
latest.method AS latest_method,
|
|
22448
|
+
latest.resolved_at AS latest_resolved_at
|
|
22449
|
+
FROM inspection_findings f
|
|
22450
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22451
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22452
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22453
|
+
ON latest.finding_key = f.finding_key
|
|
21897
22454
|
WHERE f.finding_key IS NOT NULL
|
|
22455
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21898
22456
|
AND EXISTS (
|
|
21899
22457
|
SELECT 1 FROM finding_resolution fr
|
|
21900
22458
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21941,11 +22499,13 @@ var SqliteSecurityRepository = class {
|
|
|
21941
22499
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21942
22500
|
const rows = allRows(
|
|
21943
22501
|
this.db.prepare(
|
|
21944
|
-
`SELECT json_extract(e.
|
|
21945
|
-
FROM
|
|
21946
|
-
|
|
21947
|
-
|
|
21948
|
-
AND
|
|
22502
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22503
|
+
FROM inspection_findings f
|
|
22504
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22505
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22506
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22507
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22508
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
21949
22509
|
GROUP BY repo
|
|
21950
22510
|
ORDER BY c DESC, repo
|
|
21951
22511
|
LIMIT :limit`
|
|
@@ -21969,44 +22529,28 @@ var SqliteSecurityRepository = class {
|
|
|
21969
22529
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
21970
22530
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
21971
22531
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
21972
|
-
// finding's parent event (
|
|
21973
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
21974
|
-
// capped at `limit`.
|
|
22532
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22533
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22534
|
+
// DESC, capped at `limit`.
|
|
21975
22535
|
recentlyResolved(limit = 20) {
|
|
21976
22536
|
const rows = allRows(
|
|
21977
22537
|
this.db.prepare(
|
|
21978
22538
|
`SELECT f.finding_key AS finding_key,
|
|
21979
|
-
|
|
21980
|
-
|
|
21981
|
-
json_extract(e.
|
|
21982
|
-
COALESCE(f.first_detected_at, e.
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
WHERE e.kind = 'code_change'
|
|
22539
|
+
d.rule_id AS rule_id,
|
|
22540
|
+
d.severity AS severity,
|
|
22541
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22542
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22543
|
+
latest.resolved_at AS latest_resolved_at
|
|
22544
|
+
FROM inspection_findings f
|
|
22545
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22546
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22547
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22548
|
+
ON latest.finding_key = f.finding_key
|
|
22549
|
+
WHERE e.event_type = 'code_change'
|
|
21991
22550
|
AND f.finding_key IS NOT NULL
|
|
21992
|
-
AND
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21996
|
-
LIMIT 1
|
|
21997
|
-
) = 'resolved'
|
|
21998
|
-
AND (
|
|
21999
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22000
|
-
WHERE fr.finding_key = f.finding_key
|
|
22001
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22002
|
-
LIMIT 1
|
|
22003
|
-
) = 'fixed-at-source'
|
|
22004
|
-
AND (
|
|
22005
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22006
|
-
WHERE fr.finding_key = f.finding_key
|
|
22007
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22008
|
-
LIMIT 1
|
|
22009
|
-
) IS NOT NULL
|
|
22551
|
+
AND latest.status = 'resolved'
|
|
22552
|
+
AND latest.method = 'fixed-at-source'
|
|
22553
|
+
AND latest.resolved_at IS NOT NULL
|
|
22010
22554
|
ORDER BY latest_resolved_at DESC
|
|
22011
22555
|
LIMIT :limit`
|
|
22012
22556
|
),
|
|
@@ -22025,15 +22569,18 @@ var SqliteSecurityRepository = class {
|
|
|
22025
22569
|
return Promise.resolve({ items });
|
|
22026
22570
|
}
|
|
22027
22571
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22028
|
-
// epoch-millis timestamp.
|
|
22572
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22029
22573
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22030
22574
|
findingsInRange(fromMs, toMs) {
|
|
22031
22575
|
const rows = allRows(
|
|
22032
22576
|
this.db.prepare(
|
|
22033
|
-
`SELECT e.
|
|
22034
|
-
FROM
|
|
22035
|
-
|
|
22036
|
-
|
|
22577
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22578
|
+
FROM inspection_findings f
|
|
22579
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22580
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22581
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22582
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22583
|
+
ORDER BY e.started_at`
|
|
22037
22584
|
),
|
|
22038
22585
|
{ from: fromMs, to: toMs }
|
|
22039
22586
|
);
|
|
@@ -22047,11 +22594,50 @@ var SqliteSecurityRepository = class {
|
|
|
22047
22594
|
|
|
22048
22595
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22049
22596
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22050
|
-
var
|
|
22597
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22598
|
+
var IN_CHUNK = 500;
|
|
22599
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22600
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22601
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22602
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22051
22603
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22052
22604
|
function parseNetwork(networkJson) {
|
|
22053
22605
|
return safeJson(networkJson, null);
|
|
22054
22606
|
}
|
|
22607
|
+
function capHits(all, mode) {
|
|
22608
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22609
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22610
|
+
}
|
|
22611
|
+
if (mode === "walk") {
|
|
22612
|
+
return {
|
|
22613
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22614
|
+
droppedFiles: [],
|
|
22615
|
+
truncated: true
|
|
22616
|
+
};
|
|
22617
|
+
}
|
|
22618
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22619
|
+
for (const hit of all) {
|
|
22620
|
+
const bucket = byFile.get(hit.site.file);
|
|
22621
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22622
|
+
else bucket.push(hit);
|
|
22623
|
+
}
|
|
22624
|
+
const hits = [];
|
|
22625
|
+
const droppedFiles = [];
|
|
22626
|
+
for (const [file2, bucket] of byFile) {
|
|
22627
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22628
|
+
else hits.push(...bucket);
|
|
22629
|
+
}
|
|
22630
|
+
return { hits, droppedFiles, truncated: true };
|
|
22631
|
+
}
|
|
22632
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22633
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22634
|
+
const dropped = new Set(droppedFiles);
|
|
22635
|
+
return {
|
|
22636
|
+
mode: "ledger",
|
|
22637
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22638
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22639
|
+
};
|
|
22640
|
+
}
|
|
22055
22641
|
function toEndpointSummary(row) {
|
|
22056
22642
|
return {
|
|
22057
22643
|
id: row.id,
|
|
@@ -22142,13 +22728,15 @@ var SqliteSharesRepository = class {
|
|
|
22142
22728
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22143
22729
|
const insecure = countScalar(
|
|
22144
22730
|
this.db,
|
|
22145
|
-
|
|
22731
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22732
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22146
22733
|
);
|
|
22147
22734
|
const needsReview = countScalar(
|
|
22148
22735
|
this.db,
|
|
22149
22736
|
`SELECT count(DISTINCT d.id) AS n
|
|
22150
22737
|
FROM share_destination d
|
|
22151
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22738
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22739
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22152
22740
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22153
22741
|
);
|
|
22154
22742
|
const kindCounts = countBy(
|
|
@@ -22158,6 +22746,7 @@ var SqliteSharesRepository = class {
|
|
|
22158
22746
|
const byKind = {
|
|
22159
22747
|
provider: kindCounts.get("provider") ?? 0,
|
|
22160
22748
|
internal: kindCounts.get("internal") ?? 0,
|
|
22749
|
+
external: kindCounts.get("external") ?? 0,
|
|
22161
22750
|
ip: kindCounts.get("ip") ?? 0
|
|
22162
22751
|
};
|
|
22163
22752
|
const trustCounts = countBy(
|
|
@@ -22220,36 +22809,329 @@ var SqliteSharesRepository = class {
|
|
|
22220
22809
|
});
|
|
22221
22810
|
return Promise.resolve({ items });
|
|
22222
22811
|
}
|
|
22223
|
-
getDestination(destinationId) {
|
|
22224
|
-
const dest = this.fetchDestinationById(destinationId);
|
|
22225
|
-
if (!dest) return Promise.resolve(null);
|
|
22226
|
-
const endpoints = this.fetchEndpoints([dest.id]);
|
|
22227
|
-
const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
|
|
22228
|
-
return Promise.resolve(buildDetail(dest, endpoints, callSites));
|
|
22812
|
+
getDestination(destinationId) {
|
|
22813
|
+
const dest = this.fetchDestinationById(destinationId);
|
|
22814
|
+
if (!dest) return Promise.resolve(null);
|
|
22815
|
+
const endpoints = this.fetchEndpoints([dest.id]);
|
|
22816
|
+
const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
|
|
22817
|
+
return Promise.resolve(buildDetail(dest, endpoints, callSites));
|
|
22818
|
+
}
|
|
22819
|
+
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
22820
|
+
// Driven by a web-ui Server Action, not the hook path — errors surface to the
|
|
22821
|
+
// caller. Returns whether the destination existed, so the caller can tell a
|
|
22822
|
+
// real edit from a no-such-destination.
|
|
22823
|
+
/**
|
|
22824
|
+
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22825
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22826
|
+
*
|
|
22827
|
+
* The written row carries both the destination id and its host, so the
|
|
22828
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22829
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22830
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22831
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22832
|
+
* would otherwise race a concurrent prune.
|
|
22833
|
+
*/
|
|
22834
|
+
setEgressDecision(destinationId, decision) {
|
|
22835
|
+
let existed = false;
|
|
22836
|
+
withTransaction(
|
|
22837
|
+
this.db,
|
|
22838
|
+
() => {
|
|
22839
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22840
|
+
if (dest === void 0) return;
|
|
22841
|
+
existed = true;
|
|
22842
|
+
this.db.prepare(
|
|
22843
|
+
`DELETE FROM egress_decision_override
|
|
22844
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22845
|
+
).run({ host: dest.host, destinationId });
|
|
22846
|
+
if (decision === null) return;
|
|
22847
|
+
this.db.prepare(
|
|
22848
|
+
`INSERT INTO egress_decision_override
|
|
22849
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22850
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22851
|
+
).run({
|
|
22852
|
+
id: randomUUID7(),
|
|
22853
|
+
destinationId,
|
|
22854
|
+
host: dest.host,
|
|
22855
|
+
decision,
|
|
22856
|
+
now: Date.now()
|
|
22857
|
+
});
|
|
22858
|
+
},
|
|
22859
|
+
"IMMEDIATE"
|
|
22860
|
+
);
|
|
22861
|
+
return existed;
|
|
22862
|
+
}
|
|
22863
|
+
/**
|
|
22864
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22865
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22866
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22867
|
+
* references, and drop what no longer has evidence.
|
|
22868
|
+
*
|
|
22869
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22870
|
+
* display payload and never scope a delete. The whole write is one
|
|
22871
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22872
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22873
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22874
|
+
* ledger commit so the next scan retries.
|
|
22875
|
+
*
|
|
22876
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22877
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22878
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22879
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22880
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22881
|
+
*/
|
|
22882
|
+
recordProjectEgress(input) {
|
|
22883
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22884
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22885
|
+
const now = Date.now();
|
|
22886
|
+
let summary = {
|
|
22887
|
+
destinations: 0,
|
|
22888
|
+
endpoints: 0,
|
|
22889
|
+
callSites: 0,
|
|
22890
|
+
truncated,
|
|
22891
|
+
droppedFiles
|
|
22892
|
+
};
|
|
22893
|
+
withTransaction(
|
|
22894
|
+
this.db,
|
|
22895
|
+
() => {
|
|
22896
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22897
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22898
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22899
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22900
|
+
this.pruneOrphans();
|
|
22901
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22902
|
+
},
|
|
22903
|
+
"IMMEDIATE"
|
|
22904
|
+
);
|
|
22905
|
+
return summary;
|
|
22906
|
+
}
|
|
22907
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22908
|
+
/**
|
|
22909
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22910
|
+
*
|
|
22911
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22912
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22913
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22914
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22915
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22916
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22917
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22918
|
+
*/
|
|
22919
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22920
|
+
if (reconcile.mode === "walk") {
|
|
22921
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22922
|
+
this.db.prepare(
|
|
22923
|
+
`DELETE FROM share_call_site
|
|
22924
|
+
WHERE project_key = :key
|
|
22925
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22926
|
+
AND file NOT LIKE '.%'
|
|
22927
|
+
AND file NOT LIKE '%/.%'`
|
|
22928
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22929
|
+
return;
|
|
22930
|
+
}
|
|
22931
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22932
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22933
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22934
|
+
this.db.prepare(
|
|
22935
|
+
`DELETE FROM share_call_site
|
|
22936
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22937
|
+
).run(projectKey, ...chunk);
|
|
22938
|
+
}
|
|
22939
|
+
}
|
|
22940
|
+
/**
|
|
22941
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22942
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22943
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22944
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22945
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22946
|
+
* classification for this batch.
|
|
22947
|
+
*/
|
|
22948
|
+
upsertHits(input, hits, projectId, now) {
|
|
22949
|
+
if (hits.length === 0) return;
|
|
22950
|
+
const destStmt = this.db.prepare(
|
|
22951
|
+
`INSERT INTO share_destination
|
|
22952
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22953
|
+
created_at, updated_at)
|
|
22954
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22955
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22956
|
+
kind = excluded.kind,
|
|
22957
|
+
name = excluded.name,
|
|
22958
|
+
category = excluded.category,
|
|
22959
|
+
trust = excluded.trust,
|
|
22960
|
+
network_json = excluded.network_json,
|
|
22961
|
+
last_seen = excluded.last_seen,
|
|
22962
|
+
updated_at = excluded.updated_at`
|
|
22963
|
+
);
|
|
22964
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22965
|
+
const endpointStmt = this.db.prepare(
|
|
22966
|
+
`INSERT INTO share_endpoint
|
|
22967
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
22968
|
+
created_at, updated_at)
|
|
22969
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
22970
|
+
:now, :now)
|
|
22971
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
22972
|
+
transport = excluded.transport,
|
|
22973
|
+
template = excluded.template,
|
|
22974
|
+
data_class = excluded.data_class,
|
|
22975
|
+
last_seen = excluded.last_seen,
|
|
22976
|
+
updated_at = excluded.updated_at`
|
|
22977
|
+
);
|
|
22978
|
+
const endpointIdStmt = this.db.prepare(
|
|
22979
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
22980
|
+
);
|
|
22981
|
+
const siteStmt = this.db.prepare(
|
|
22982
|
+
`INSERT INTO share_call_site
|
|
22983
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
22984
|
+
project_id, created_at, updated_at)
|
|
22985
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
22986
|
+
:vendored, :projectId, :now, :now)
|
|
22987
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
22988
|
+
snippet = excluded.snippet,
|
|
22989
|
+
dynamic = excluded.dynamic,
|
|
22990
|
+
vendored = excluded.vendored,
|
|
22991
|
+
project = excluded.project,
|
|
22992
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
22993
|
+
updated_at = excluded.updated_at`
|
|
22994
|
+
);
|
|
22995
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
22996
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
22997
|
+
for (const hit of hits) {
|
|
22998
|
+
let destinationId = destIds.get(hit.host);
|
|
22999
|
+
if (destinationId === void 0) {
|
|
23000
|
+
destStmt.run({
|
|
23001
|
+
id: randomUUID7(),
|
|
23002
|
+
kind: hit.kind,
|
|
23003
|
+
name: hit.name,
|
|
23004
|
+
host: hit.host,
|
|
23005
|
+
category: hit.category,
|
|
23006
|
+
trust: hit.trust,
|
|
23007
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
23008
|
+
now
|
|
23009
|
+
});
|
|
23010
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
23011
|
+
destIds.set(hit.host, destinationId);
|
|
23012
|
+
}
|
|
23013
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
23014
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
23015
|
+
if (endpointId === void 0) {
|
|
23016
|
+
endpointStmt.run({
|
|
23017
|
+
id: randomUUID7(),
|
|
23018
|
+
destinationId,
|
|
23019
|
+
method: hit.method,
|
|
23020
|
+
transport: hit.transport,
|
|
23021
|
+
url: hit.url,
|
|
23022
|
+
template: boolToInt(hit.template),
|
|
23023
|
+
dataClass: hit.dataClass,
|
|
23024
|
+
now
|
|
23025
|
+
});
|
|
23026
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23027
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23028
|
+
}
|
|
23029
|
+
siteStmt.run({
|
|
23030
|
+
id: randomUUID7(),
|
|
23031
|
+
endpointId,
|
|
23032
|
+
project: input.project,
|
|
23033
|
+
projectKey: input.projectKey,
|
|
23034
|
+
file: hit.site.file,
|
|
23035
|
+
line: hit.site.line,
|
|
23036
|
+
snippet: hit.site.snippet,
|
|
23037
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23038
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23039
|
+
projectId,
|
|
23040
|
+
now
|
|
23041
|
+
});
|
|
23042
|
+
}
|
|
23043
|
+
}
|
|
23044
|
+
/**
|
|
23045
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23046
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23047
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23048
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23049
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23050
|
+
* representative.
|
|
23051
|
+
*/
|
|
23052
|
+
knownProjectId(projectKey) {
|
|
23053
|
+
return getRow(
|
|
23054
|
+
this.db.prepare(
|
|
23055
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23056
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23057
|
+
),
|
|
23058
|
+
[projectKey]
|
|
23059
|
+
)?.projectId ?? null;
|
|
22229
23060
|
}
|
|
22230
|
-
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
22231
|
-
// Driven by a web-ui Server Action, not the hook path — errors surface to the
|
|
22232
|
-
// caller. Returns whether the destination existed, so the caller can tell a
|
|
22233
|
-
// real edit from a no-such-destination.
|
|
22234
23061
|
/**
|
|
22235
|
-
*
|
|
22236
|
-
*
|
|
23062
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23063
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23064
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
22237
23065
|
*/
|
|
22238
|
-
|
|
22239
|
-
const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
|
|
22240
|
-
if (exists === void 0) return false;
|
|
22241
|
-
if (decision === null) {
|
|
22242
|
-
this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
|
|
22243
|
-
return true;
|
|
22244
|
-
}
|
|
23066
|
+
confirmLastSeen(projectKey, now) {
|
|
22245
23067
|
this.db.prepare(
|
|
22246
|
-
`
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
|
|
22252
|
-
|
|
23068
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23069
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23070
|
+
).run({ now, key: projectKey });
|
|
23071
|
+
this.db.prepare(
|
|
23072
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23073
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23074
|
+
FROM share_endpoint e
|
|
23075
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23076
|
+
WHERE c.project_key = :key)`
|
|
23077
|
+
).run({ now, key: projectKey });
|
|
23078
|
+
}
|
|
23079
|
+
/**
|
|
23080
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23081
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23082
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23083
|
+
*
|
|
23084
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23085
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23086
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23087
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23088
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23089
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23090
|
+
*/
|
|
23091
|
+
pruneOrphans() {
|
|
23092
|
+
this.db.exec(
|
|
23093
|
+
`DELETE FROM share_endpoint
|
|
23094
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23095
|
+
);
|
|
23096
|
+
this.db.exec(
|
|
23097
|
+
`DELETE FROM egress_decision_override
|
|
23098
|
+
WHERE host IS NULL
|
|
23099
|
+
AND destination_id IN (
|
|
23100
|
+
SELECT d.id FROM share_destination d
|
|
23101
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23102
|
+
);
|
|
23103
|
+
this.db.exec(
|
|
23104
|
+
`DELETE FROM share_destination
|
|
23105
|
+
WHERE NOT EXISTS (
|
|
23106
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23107
|
+
);
|
|
23108
|
+
}
|
|
23109
|
+
/**
|
|
23110
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23111
|
+
* projects and carry no project column, so both are counted through the call
|
|
23112
|
+
* sites that reference them.
|
|
23113
|
+
*/
|
|
23114
|
+
projectTotals(projectKey) {
|
|
23115
|
+
return {
|
|
23116
|
+
destinations: countScalar(
|
|
23117
|
+
this.db,
|
|
23118
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23119
|
+
FROM share_endpoint e
|
|
23120
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23121
|
+
WHERE c.project_key = ?`,
|
|
23122
|
+
[projectKey]
|
|
23123
|
+
),
|
|
23124
|
+
endpoints: countScalar(
|
|
23125
|
+
this.db,
|
|
23126
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23127
|
+
[projectKey]
|
|
23128
|
+
),
|
|
23129
|
+
callSites: countScalar(
|
|
23130
|
+
this.db,
|
|
23131
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23132
|
+
[projectKey]
|
|
23133
|
+
)
|
|
23134
|
+
};
|
|
22253
23135
|
}
|
|
22254
23136
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22255
23137
|
mapDestRow(r) {
|
|
@@ -22269,7 +23151,8 @@ var SqliteSharesRepository = class {
|
|
|
22269
23151
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22270
23152
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22271
23153
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22272
|
-
d.created_at AS createdAt,
|
|
23154
|
+
d.created_at AS createdAt,
|
|
23155
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22273
23156
|
const conditions = [];
|
|
22274
23157
|
const params = [];
|
|
22275
23158
|
if (kinds && kinds.length > 0) {
|
|
@@ -22280,7 +23163,8 @@ var SqliteSharesRepository = class {
|
|
|
22280
23163
|
conditions.push(
|
|
22281
23164
|
`(d.trust IN ('unverified', 'ip')
|
|
22282
23165
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22283
|
-
WHERE re.destination_id = d.id
|
|
23166
|
+
WHERE re.destination_id = d.id
|
|
23167
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22284
23168
|
);
|
|
22285
23169
|
}
|
|
22286
23170
|
let sql;
|
|
@@ -22293,7 +23177,7 @@ var SqliteSharesRepository = class {
|
|
|
22293
23177
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22294
23178
|
sql = `SELECT DISTINCT ${cols}
|
|
22295
23179
|
FROM share_destination d
|
|
22296
|
-
|
|
23180
|
+
${OVERRIDE_JOIN}
|
|
22297
23181
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22298
23182
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22299
23183
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22301,7 +23185,7 @@ var SqliteSharesRepository = class {
|
|
|
22301
23185
|
} else {
|
|
22302
23186
|
sql = `SELECT ${cols}
|
|
22303
23187
|
FROM share_destination d
|
|
22304
|
-
|
|
23188
|
+
${OVERRIDE_JOIN}
|
|
22305
23189
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22306
23190
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22307
23191
|
}
|
|
@@ -22316,9 +23200,9 @@ var SqliteSharesRepository = class {
|
|
|
22316
23200
|
this.db.prepare(
|
|
22317
23201
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22318
23202
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22319
|
-
|
|
23203
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22320
23204
|
FROM share_destination d
|
|
22321
|
-
|
|
23205
|
+
${OVERRIDE_JOIN}
|
|
22322
23206
|
WHERE d.id = ?`
|
|
22323
23207
|
),
|
|
22324
23208
|
[destinationId]
|
|
@@ -22520,9 +23404,10 @@ function openWithPragmas(file2) {
|
|
|
22520
23404
|
}
|
|
22521
23405
|
function backupLegacyStore(file2) {
|
|
22522
23406
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22523
|
-
|
|
22524
|
-
|
|
22525
|
-
|
|
23407
|
+
renameSync2(file2, backup);
|
|
23408
|
+
tightenFile(backup);
|
|
23409
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23410
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22526
23411
|
}
|
|
22527
23412
|
return backup;
|
|
22528
23413
|
}
|
|
@@ -22538,7 +23423,7 @@ function openLocalDatabase(dir) {
|
|
|
22538
23423
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22539
23424
|
);
|
|
22540
23425
|
}
|
|
22541
|
-
applyMigrations(db);
|
|
23426
|
+
applyMigrations(db, file2);
|
|
22542
23427
|
tightenPerms(file2);
|
|
22543
23428
|
const events = new SqliteEventsRepository(db);
|
|
22544
23429
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22547,6 +23432,7 @@ function openLocalDatabase(dir) {
|
|
|
22547
23432
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22548
23433
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22549
23434
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23435
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22550
23436
|
const security = new SqliteSecurityRepository(db);
|
|
22551
23437
|
const detections = new SqliteDetectionsRepository(db);
|
|
22552
23438
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22564,9 +23450,56 @@ function openLocalDatabase(dir) {
|
|
|
22564
23450
|
policies.seedDefaults();
|
|
22565
23451
|
function recordCapture(event, detected) {
|
|
22566
23452
|
failOpenTransaction(db, () => {
|
|
22567
|
-
events.insertEvent(event);
|
|
22568
23453
|
const sessionId = event.metadata?.sessionId;
|
|
22569
|
-
|
|
23454
|
+
if (sessionId) {
|
|
23455
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23456
|
+
}
|
|
23457
|
+
const auditEventId = captureId(
|
|
23458
|
+
sessionId ?? null,
|
|
23459
|
+
event.contentHash,
|
|
23460
|
+
event.metadata?.filePath ?? null
|
|
23461
|
+
);
|
|
23462
|
+
auditEvents.insertAuditEvent({
|
|
23463
|
+
id: auditEventId,
|
|
23464
|
+
eventType: event.kind,
|
|
23465
|
+
startedAt: event.occurredAt,
|
|
23466
|
+
parentId: sessionId,
|
|
23467
|
+
rootSessionId: sessionId,
|
|
23468
|
+
content: event.content,
|
|
23469
|
+
contentHash: event.contentHash,
|
|
23470
|
+
attributes: toCaptureAttributes(event)
|
|
23471
|
+
});
|
|
23472
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23473
|
+
for (const finding of detected) {
|
|
23474
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23475
|
+
continue;
|
|
23476
|
+
}
|
|
23477
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23478
|
+
auditEventId,
|
|
23479
|
+
finding.ruleId,
|
|
23480
|
+
finding.maskedMatch,
|
|
23481
|
+
finding.span.start,
|
|
23482
|
+
finding.span.end
|
|
23483
|
+
)) {
|
|
23484
|
+
continue;
|
|
23485
|
+
}
|
|
23486
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23487
|
+
let definitionId = definitionIds.get(key);
|
|
23488
|
+
if (!definitionId) {
|
|
23489
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23490
|
+
definitionIds.set(key, definitionId);
|
|
23491
|
+
}
|
|
23492
|
+
inspectionFindings.insertFinding({
|
|
23493
|
+
id: finding.id,
|
|
23494
|
+
auditEventId,
|
|
23495
|
+
inspectionDefinitionId: definitionId,
|
|
23496
|
+
span: finding.span,
|
|
23497
|
+
maskedMatch: finding.maskedMatch,
|
|
23498
|
+
actionTaken: finding.actionTaken,
|
|
23499
|
+
confidence: finding.confidence,
|
|
23500
|
+
findingKey: finding.findingKey ?? void 0
|
|
23501
|
+
});
|
|
23502
|
+
}
|
|
22570
23503
|
});
|
|
22571
23504
|
}
|
|
22572
23505
|
function ensureInventory(ctx) {
|
|
@@ -22684,6 +23617,7 @@ function openLocalDatabase(dir) {
|
|
|
22684
23617
|
scanLedger,
|
|
22685
23618
|
exceptions,
|
|
22686
23619
|
resolutions,
|
|
23620
|
+
ruleProbeCache,
|
|
22687
23621
|
security,
|
|
22688
23622
|
detections,
|
|
22689
23623
|
shares,
|
|
@@ -22713,9 +23647,19 @@ function openLocalDatabase(dir) {
|
|
|
22713
23647
|
};
|
|
22714
23648
|
}
|
|
22715
23649
|
|
|
23650
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23651
|
+
import { createHash as createHash3 } from "crypto";
|
|
23652
|
+
function normalizeFilePath(filePath) {
|
|
23653
|
+
return filePath.replaceAll("\\", "/");
|
|
23654
|
+
}
|
|
23655
|
+
function computeFindingKey(input) {
|
|
23656
|
+
const normalizedPath = normalizeFilePath(input.filePath);
|
|
23657
|
+
return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
23658
|
+
}
|
|
23659
|
+
|
|
22716
23660
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22717
23661
|
import { createHmac, randomBytes } from "crypto";
|
|
22718
|
-
import {
|
|
23662
|
+
import { readFileSync } from "fs";
|
|
22719
23663
|
import { join as join2 } from "path";
|
|
22720
23664
|
var KEY_FILENAME = "exception.key";
|
|
22721
23665
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -22743,15 +23687,9 @@ function parseKeyFile(raw) {
|
|
|
22743
23687
|
function writeKeyFile(dataDir2, key) {
|
|
22744
23688
|
ensureDataDirSync(dataDir2);
|
|
22745
23689
|
const file2 = keyFilePath(dataDir2);
|
|
22746
|
-
const tmp = `${file2}.tmp`;
|
|
22747
23690
|
const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
|
|
22748
|
-
|
|
22749
|
-
|
|
22750
|
-
renameSync2(tmp, file2);
|
|
22751
|
-
try {
|
|
22752
|
-
chmodSync2(file2, DATA_FILE_MODE);
|
|
22753
|
-
} catch {
|
|
22754
|
-
}
|
|
23691
|
+
writeOwnerOnlyFileSync(file2, `${body}
|
|
23692
|
+
`);
|
|
22755
23693
|
return key;
|
|
22756
23694
|
}
|
|
22757
23695
|
function readFingerprintKey(dataDir2) {
|
|
@@ -22767,10 +23705,7 @@ function readFingerprintKey(dataDir2) {
|
|
|
22767
23705
|
function loadOrCreateFingerprintKey(dataDir2) {
|
|
22768
23706
|
const existing = readFingerprintKey(dataDir2);
|
|
22769
23707
|
if (existing) {
|
|
22770
|
-
|
|
22771
|
-
chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
|
|
22772
|
-
} catch {
|
|
22773
|
-
}
|
|
23708
|
+
tightenFile(keyFilePath(dataDir2));
|
|
22774
23709
|
return existing;
|
|
22775
23710
|
}
|
|
22776
23711
|
return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
|
|
@@ -22780,8 +23715,8 @@ function fingerprintValue(key, raw) {
|
|
|
22780
23715
|
}
|
|
22781
23716
|
|
|
22782
23717
|
// ../../packages/persistence/src/local-layout.ts
|
|
22783
|
-
import {
|
|
22784
|
-
import {
|
|
23718
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23719
|
+
import { mkdir } from "fs/promises";
|
|
22785
23720
|
import { homedir } from "os";
|
|
22786
23721
|
import { join as join3 } from "path";
|
|
22787
23722
|
function defaultDataDir() {
|
|
@@ -22796,6 +23731,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22796
23731
|
function dbPath(base = defaultDataDir()) {
|
|
22797
23732
|
return join3(dataDir(base), "aka.db");
|
|
22798
23733
|
}
|
|
23734
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23735
|
+
ensureDataDirSync(dir);
|
|
23736
|
+
}
|
|
22799
23737
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22800
23738
|
const moves = [
|
|
22801
23739
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22803,19 +23741,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22803
23741
|
];
|
|
22804
23742
|
for (const { name, dest } of moves) {
|
|
22805
23743
|
try {
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
}
|
|
22811
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23744
|
+
ensureDataDirSync(dest);
|
|
23745
|
+
const moved = join3(dest, name);
|
|
23746
|
+
renameSync3(join3(base, name), moved);
|
|
23747
|
+
tightenFile(moved);
|
|
22812
23748
|
} catch {
|
|
22813
23749
|
}
|
|
22814
23750
|
}
|
|
22815
23751
|
}
|
|
22816
23752
|
|
|
22817
23753
|
// ../../packages/persistence/src/settings.ts
|
|
22818
|
-
import { readFileSync as readFileSync2
|
|
23754
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22819
23755
|
import { join as join4 } from "path";
|
|
22820
23756
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22821
23757
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22837,7 +23773,7 @@ function readJson(file2) {
|
|
|
22837
23773
|
}
|
|
22838
23774
|
|
|
22839
23775
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22840
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23776
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22841
23777
|
import { join as join5 } from "path";
|
|
22842
23778
|
var MARKER = "warn-era-capped";
|
|
22843
23779
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22845,7 +23781,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22845
23781
|
const marker = join5(dataDir2, MARKER);
|
|
22846
23782
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22847
23783
|
const capped = db.policies.capCategoryActions();
|
|
22848
|
-
|
|
23784
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22849
23785
|
`, { mode: DATA_FILE_MODE });
|
|
22850
23786
|
return { capped };
|
|
22851
23787
|
}
|
|
@@ -22910,6 +23846,12 @@ function providerFromModelId(modelId) {
|
|
|
22910
23846
|
|
|
22911
23847
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22912
23848
|
function loadConfig(base = defaultDataDir()) {
|
|
23849
|
+
try {
|
|
23850
|
+
ensureLayoutDirSync(base);
|
|
23851
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23852
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23853
|
+
} catch {
|
|
23854
|
+
}
|
|
22913
23855
|
migrateLegacyLayout(base);
|
|
22914
23856
|
const settings = readWorkspaceSettings(base);
|
|
22915
23857
|
return {
|
|
@@ -22932,15 +23874,583 @@ function resolveProviderSafe() {
|
|
|
22932
23874
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22933
23875
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22934
23876
|
import { homedir as homedir2 } from "os";
|
|
22935
|
-
import { basename as basename2, join as
|
|
23877
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23878
|
+
|
|
23879
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23880
|
+
var EXTRACTOR_VERSION = "1";
|
|
23881
|
+
var PROVIDER_REGISTRY = [
|
|
23882
|
+
{
|
|
23883
|
+
id: "stripe",
|
|
23884
|
+
name: "Stripe",
|
|
23885
|
+
category: "Payments",
|
|
23886
|
+
hostSuffixes: ["stripe.com"],
|
|
23887
|
+
apiBase: "https://api.stripe.com",
|
|
23888
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23889
|
+
sdks: {
|
|
23890
|
+
npm: ["stripe"],
|
|
23891
|
+
pypi: ["stripe"],
|
|
23892
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23893
|
+
maven: ["com.stripe"],
|
|
23894
|
+
rubygems: ["stripe"],
|
|
23895
|
+
composer: ["stripe/stripe-php"],
|
|
23896
|
+
nuget: ["Stripe.net"]
|
|
23897
|
+
}
|
|
23898
|
+
},
|
|
23899
|
+
{
|
|
23900
|
+
id: "datadog",
|
|
23901
|
+
name: "Datadog",
|
|
23902
|
+
category: "Observability",
|
|
23903
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23904
|
+
apiBase: "https://api.datadoghq.com",
|
|
23905
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23906
|
+
sdks: {
|
|
23907
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23908
|
+
pypi: ["datadog", "ddtrace"],
|
|
23909
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23910
|
+
maven: ["com.datadoghq"],
|
|
23911
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23912
|
+
nuget: ["Datadog.Trace"]
|
|
23913
|
+
}
|
|
23914
|
+
},
|
|
23915
|
+
{
|
|
23916
|
+
id: "newrelic",
|
|
23917
|
+
name: "New Relic",
|
|
23918
|
+
category: "Observability",
|
|
23919
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23920
|
+
apiBase: "https://api.newrelic.com",
|
|
23921
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23922
|
+
sdks: {
|
|
23923
|
+
npm: ["newrelic"],
|
|
23924
|
+
pypi: ["newrelic"],
|
|
23925
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23926
|
+
maven: ["com.newrelic.agent.java"],
|
|
23927
|
+
rubygems: ["newrelic_rpm"],
|
|
23928
|
+
nuget: ["NewRelic.Agent"]
|
|
23929
|
+
}
|
|
23930
|
+
},
|
|
23931
|
+
{
|
|
23932
|
+
id: "sentry",
|
|
23933
|
+
name: "Sentry",
|
|
23934
|
+
category: "Error tracking",
|
|
23935
|
+
hostSuffixes: ["sentry.io"],
|
|
23936
|
+
apiBase: "https://sentry.io",
|
|
23937
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23938
|
+
sdks: {
|
|
23939
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23940
|
+
pypi: ["sentry-sdk"],
|
|
23941
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23942
|
+
maven: ["io.sentry"],
|
|
23943
|
+
rubygems: ["sentry-ruby"],
|
|
23944
|
+
cargo: ["sentry"],
|
|
23945
|
+
composer: ["sentry/sentry"],
|
|
23946
|
+
nuget: ["Sentry"]
|
|
23947
|
+
}
|
|
23948
|
+
},
|
|
23949
|
+
{
|
|
23950
|
+
id: "openai",
|
|
23951
|
+
name: "OpenAI",
|
|
23952
|
+
category: "LLM provider",
|
|
23953
|
+
hostSuffixes: ["openai.com"],
|
|
23954
|
+
apiBase: "https://api.openai.com",
|
|
23955
|
+
defaultDataClasses: ["pii", "source"],
|
|
23956
|
+
sdks: {
|
|
23957
|
+
npm: ["openai"],
|
|
23958
|
+
pypi: ["openai"],
|
|
23959
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23960
|
+
maven: ["com.openai"],
|
|
23961
|
+
rubygems: ["ruby-openai"],
|
|
23962
|
+
cargo: ["async-openai"],
|
|
23963
|
+
composer: ["openai-php/client"],
|
|
23964
|
+
nuget: ["OpenAI"]
|
|
23965
|
+
}
|
|
23966
|
+
},
|
|
23967
|
+
{
|
|
23968
|
+
id: "anthropic",
|
|
23969
|
+
name: "Anthropic",
|
|
23970
|
+
category: "LLM provider",
|
|
23971
|
+
hostSuffixes: ["anthropic.com"],
|
|
23972
|
+
apiBase: "https://api.anthropic.com",
|
|
23973
|
+
defaultDataClasses: ["pii", "source"],
|
|
23974
|
+
sdks: {
|
|
23975
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23976
|
+
pypi: ["anthropic"],
|
|
23977
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23978
|
+
nuget: ["Anthropic.SDK"]
|
|
23979
|
+
}
|
|
23980
|
+
},
|
|
23981
|
+
{
|
|
23982
|
+
id: "aws",
|
|
23983
|
+
name: "Amazon Web Services",
|
|
23984
|
+
category: "Cloud platform",
|
|
23985
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23986
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23987
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23988
|
+
sdks: {
|
|
23989
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23990
|
+
pypi: ["boto3"],
|
|
23991
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23992
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23993
|
+
rubygems: ["aws-sdk-s3"],
|
|
23994
|
+
cargo: ["aws-sdk-s3"],
|
|
23995
|
+
nuget: ["AWSSDK.S3"]
|
|
23996
|
+
}
|
|
23997
|
+
},
|
|
23998
|
+
{
|
|
23999
|
+
id: "gcp",
|
|
24000
|
+
name: "Google Cloud",
|
|
24001
|
+
category: "Cloud platform",
|
|
24002
|
+
hostSuffixes: ["googleapis.com"],
|
|
24003
|
+
apiBase: "https://storage.googleapis.com",
|
|
24004
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24005
|
+
sdks: {
|
|
24006
|
+
npm: ["@google-cloud/storage"],
|
|
24007
|
+
pypi: ["google-cloud-storage"],
|
|
24008
|
+
go: ["cloud.google.com/go"],
|
|
24009
|
+
maven: ["com.google.cloud"],
|
|
24010
|
+
rubygems: ["google-cloud-storage"],
|
|
24011
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
24012
|
+
}
|
|
24013
|
+
},
|
|
24014
|
+
{
|
|
24015
|
+
id: "azure",
|
|
24016
|
+
name: "Microsoft Azure",
|
|
24017
|
+
category: "Cloud platform",
|
|
24018
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
24019
|
+
apiBase: "https://management.azure.com",
|
|
24020
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24021
|
+
sdks: {
|
|
24022
|
+
npm: ["@azure/storage-blob"],
|
|
24023
|
+
pypi: ["azure-storage-blob"],
|
|
24024
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
24025
|
+
maven: ["com.azure"],
|
|
24026
|
+
rubygems: ["azure-storage-blob"],
|
|
24027
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
24028
|
+
}
|
|
24029
|
+
},
|
|
24030
|
+
{
|
|
24031
|
+
id: "slack",
|
|
24032
|
+
name: "Slack",
|
|
24033
|
+
category: "Notifications",
|
|
24034
|
+
hostSuffixes: ["slack.com"],
|
|
24035
|
+
apiBase: "https://slack.com/api",
|
|
24036
|
+
defaultDataClasses: ["logs"],
|
|
24037
|
+
sdks: {
|
|
24038
|
+
npm: ["@slack/web-api"],
|
|
24039
|
+
pypi: ["slack-sdk"],
|
|
24040
|
+
go: ["github.com/slack-go/slack"],
|
|
24041
|
+
maven: ["com.slack.api"],
|
|
24042
|
+
rubygems: ["slack-ruby-client"],
|
|
24043
|
+
composer: ["slack-php/slack-api"],
|
|
24044
|
+
nuget: ["SlackNet"]
|
|
24045
|
+
}
|
|
24046
|
+
},
|
|
24047
|
+
{
|
|
24048
|
+
id: "segment",
|
|
24049
|
+
name: "Segment",
|
|
24050
|
+
category: "Analytics",
|
|
24051
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
24052
|
+
apiBase: "https://api.segment.io",
|
|
24053
|
+
defaultDataClasses: ["customer"],
|
|
24054
|
+
sdks: {
|
|
24055
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
24056
|
+
pypi: ["segment-analytics-python"],
|
|
24057
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
24058
|
+
maven: ["com.segment.analytics.java"],
|
|
24059
|
+
rubygems: ["analytics-ruby"],
|
|
24060
|
+
nuget: ["Analytics"]
|
|
24061
|
+
}
|
|
24062
|
+
},
|
|
24063
|
+
{
|
|
24064
|
+
id: "twilio",
|
|
24065
|
+
name: "Twilio",
|
|
24066
|
+
category: "Communications",
|
|
24067
|
+
hostSuffixes: ["twilio.com"],
|
|
24068
|
+
apiBase: "https://api.twilio.com",
|
|
24069
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24070
|
+
sdks: {
|
|
24071
|
+
npm: ["twilio"],
|
|
24072
|
+
pypi: ["twilio"],
|
|
24073
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24074
|
+
maven: ["com.twilio.sdk"],
|
|
24075
|
+
rubygems: ["twilio-ruby"],
|
|
24076
|
+
composer: ["twilio/sdk"],
|
|
24077
|
+
nuget: ["Twilio"]
|
|
24078
|
+
}
|
|
24079
|
+
},
|
|
24080
|
+
{
|
|
24081
|
+
id: "sendgrid",
|
|
24082
|
+
name: "SendGrid",
|
|
24083
|
+
category: "Email",
|
|
24084
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24085
|
+
apiBase: "https://api.sendgrid.com",
|
|
24086
|
+
defaultDataClasses: ["pii"],
|
|
24087
|
+
sdks: {
|
|
24088
|
+
npm: ["@sendgrid/mail"],
|
|
24089
|
+
pypi: ["sendgrid"],
|
|
24090
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24091
|
+
maven: ["com.sendgrid"],
|
|
24092
|
+
rubygems: ["sendgrid-ruby"],
|
|
24093
|
+
composer: ["sendgrid/sendgrid"],
|
|
24094
|
+
nuget: ["SendGrid"]
|
|
24095
|
+
}
|
|
24096
|
+
},
|
|
24097
|
+
{
|
|
24098
|
+
id: "mailgun",
|
|
24099
|
+
name: "Mailgun",
|
|
24100
|
+
category: "Email",
|
|
24101
|
+
hostSuffixes: ["mailgun.net"],
|
|
24102
|
+
apiBase: "https://api.mailgun.net",
|
|
24103
|
+
defaultDataClasses: ["pii"],
|
|
24104
|
+
sdks: {
|
|
24105
|
+
npm: ["mailgun.js"],
|
|
24106
|
+
pypi: ["mailgun"],
|
|
24107
|
+
rubygems: ["mailgun-ruby"],
|
|
24108
|
+
composer: ["mailgun/mailgun-php"],
|
|
24109
|
+
nuget: ["Mailgun"]
|
|
24110
|
+
}
|
|
24111
|
+
},
|
|
24112
|
+
{
|
|
24113
|
+
id: "mixpanel",
|
|
24114
|
+
name: "Mixpanel",
|
|
24115
|
+
category: "Analytics",
|
|
24116
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24117
|
+
apiBase: "https://api.mixpanel.com",
|
|
24118
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24119
|
+
sdks: {
|
|
24120
|
+
npm: ["mixpanel"],
|
|
24121
|
+
pypi: ["mixpanel"],
|
|
24122
|
+
rubygems: ["mixpanel-ruby"],
|
|
24123
|
+
nuget: ["Mixpanel"]
|
|
24124
|
+
}
|
|
24125
|
+
},
|
|
24126
|
+
{
|
|
24127
|
+
id: "amplitude",
|
|
24128
|
+
name: "Amplitude",
|
|
24129
|
+
category: "Analytics",
|
|
24130
|
+
hostSuffixes: ["amplitude.com"],
|
|
24131
|
+
apiBase: "https://api2.amplitude.com",
|
|
24132
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24133
|
+
sdks: {
|
|
24134
|
+
npm: ["@amplitude/analytics-node"],
|
|
24135
|
+
pypi: ["amplitude-analytics"],
|
|
24136
|
+
nuget: ["Amplitude"]
|
|
24137
|
+
}
|
|
24138
|
+
},
|
|
24139
|
+
{
|
|
24140
|
+
id: "posthog",
|
|
24141
|
+
name: "PostHog",
|
|
24142
|
+
category: "Analytics",
|
|
24143
|
+
hostSuffixes: ["posthog.com"],
|
|
24144
|
+
apiBase: "https://us.i.posthog.com",
|
|
24145
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24146
|
+
sdks: {
|
|
24147
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24148
|
+
pypi: ["posthog"],
|
|
24149
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24150
|
+
rubygems: ["posthog-ruby"],
|
|
24151
|
+
composer: ["posthog/posthog-php"],
|
|
24152
|
+
nuget: ["PostHog"]
|
|
24153
|
+
}
|
|
24154
|
+
},
|
|
24155
|
+
{
|
|
24156
|
+
id: "honeycomb",
|
|
24157
|
+
name: "Honeycomb",
|
|
24158
|
+
category: "Observability",
|
|
24159
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24160
|
+
apiBase: "https://api.honeycomb.io",
|
|
24161
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24162
|
+
sdks: {
|
|
24163
|
+
npm: ["libhoney"],
|
|
24164
|
+
pypi: ["libhoney"],
|
|
24165
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24166
|
+
rubygems: ["libhoney"]
|
|
24167
|
+
}
|
|
24168
|
+
},
|
|
24169
|
+
{
|
|
24170
|
+
id: "grafana",
|
|
24171
|
+
name: "Grafana Cloud",
|
|
24172
|
+
category: "Observability",
|
|
24173
|
+
hostSuffixes: ["grafana.net"],
|
|
24174
|
+
apiBase: "https://grafana.net",
|
|
24175
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24176
|
+
sdks: {
|
|
24177
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24178
|
+
}
|
|
24179
|
+
},
|
|
24180
|
+
{
|
|
24181
|
+
id: "splunk",
|
|
24182
|
+
name: "Splunk",
|
|
24183
|
+
category: "Observability",
|
|
24184
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24185
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24186
|
+
defaultDataClasses: ["logs"],
|
|
24187
|
+
sdks: {
|
|
24188
|
+
npm: ["splunk-logging"],
|
|
24189
|
+
pypi: ["splunk-sdk"],
|
|
24190
|
+
maven: ["com.splunk"],
|
|
24191
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24192
|
+
}
|
|
24193
|
+
},
|
|
24194
|
+
{
|
|
24195
|
+
id: "pagerduty",
|
|
24196
|
+
name: "PagerDuty",
|
|
24197
|
+
category: "Incident response",
|
|
24198
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24199
|
+
apiBase: "https://api.pagerduty.com",
|
|
24200
|
+
defaultDataClasses: ["logs"],
|
|
24201
|
+
sdks: {
|
|
24202
|
+
npm: ["@pagerduty/pdjs"],
|
|
24203
|
+
pypi: ["pdpyras"],
|
|
24204
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24205
|
+
rubygems: ["pagerduty"]
|
|
24206
|
+
}
|
|
24207
|
+
},
|
|
24208
|
+
{
|
|
24209
|
+
id: "github",
|
|
24210
|
+
name: "GitHub",
|
|
24211
|
+
category: "Developer platform",
|
|
24212
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24213
|
+
apiBase: "https://api.github.com",
|
|
24214
|
+
defaultDataClasses: ["source"],
|
|
24215
|
+
sdks: {
|
|
24216
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24217
|
+
pypi: ["pygithub"],
|
|
24218
|
+
go: ["github.com/google/go-github"],
|
|
24219
|
+
maven: ["org.kohsuke.github-api"],
|
|
24220
|
+
rubygems: ["octokit"],
|
|
24221
|
+
cargo: ["octocrab"],
|
|
24222
|
+
composer: ["knplabs/github-api"],
|
|
24223
|
+
nuget: ["Octokit"]
|
|
24224
|
+
}
|
|
24225
|
+
},
|
|
24226
|
+
{
|
|
24227
|
+
id: "gitlab",
|
|
24228
|
+
name: "GitLab",
|
|
24229
|
+
category: "Developer platform",
|
|
24230
|
+
hostSuffixes: ["gitlab.com"],
|
|
24231
|
+
apiBase: "https://gitlab.com/api",
|
|
24232
|
+
defaultDataClasses: ["source"],
|
|
24233
|
+
sdks: {
|
|
24234
|
+
npm: ["@gitbeaker/rest"],
|
|
24235
|
+
pypi: ["python-gitlab"],
|
|
24236
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24237
|
+
rubygems: ["gitlab"],
|
|
24238
|
+
nuget: ["GitLabApiClient"]
|
|
24239
|
+
}
|
|
24240
|
+
},
|
|
24241
|
+
{
|
|
24242
|
+
id: "auth0",
|
|
24243
|
+
name: "Auth0",
|
|
24244
|
+
category: "Identity",
|
|
24245
|
+
hostSuffixes: ["auth0.com"],
|
|
24246
|
+
apiBase: "https://login.auth0.com",
|
|
24247
|
+
defaultDataClasses: ["pii"],
|
|
24248
|
+
sdks: {
|
|
24249
|
+
npm: ["auth0"],
|
|
24250
|
+
pypi: ["auth0-python"],
|
|
24251
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24252
|
+
maven: ["com.auth0"],
|
|
24253
|
+
rubygems: ["auth0"],
|
|
24254
|
+
composer: ["auth0/auth0-php"],
|
|
24255
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24256
|
+
}
|
|
24257
|
+
},
|
|
24258
|
+
{
|
|
24259
|
+
id: "okta",
|
|
24260
|
+
name: "Okta",
|
|
24261
|
+
category: "Identity",
|
|
24262
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24263
|
+
apiBase: "https://login.okta.com",
|
|
24264
|
+
defaultDataClasses: ["pii"],
|
|
24265
|
+
sdks: {
|
|
24266
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24267
|
+
pypi: ["okta"],
|
|
24268
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24269
|
+
maven: ["com.okta.sdk"],
|
|
24270
|
+
nuget: ["Okta.Sdk"]
|
|
24271
|
+
}
|
|
24272
|
+
},
|
|
24273
|
+
{
|
|
24274
|
+
id: "clerk",
|
|
24275
|
+
name: "Clerk",
|
|
24276
|
+
category: "Identity",
|
|
24277
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24278
|
+
apiBase: "https://api.clerk.com",
|
|
24279
|
+
defaultDataClasses: ["pii"],
|
|
24280
|
+
sdks: {
|
|
24281
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24282
|
+
pypi: ["clerk-backend-api"],
|
|
24283
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24284
|
+
}
|
|
24285
|
+
},
|
|
24286
|
+
{
|
|
24287
|
+
id: "supabase",
|
|
24288
|
+
name: "Supabase",
|
|
24289
|
+
category: "Backend platform",
|
|
24290
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24291
|
+
apiBase: "https://api.supabase.com",
|
|
24292
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24293
|
+
sdks: {
|
|
24294
|
+
npm: ["@supabase/supabase-js"],
|
|
24295
|
+
pypi: ["supabase"],
|
|
24296
|
+
cargo: ["postgrest"]
|
|
24297
|
+
}
|
|
24298
|
+
},
|
|
24299
|
+
{
|
|
24300
|
+
id: "firebase",
|
|
24301
|
+
name: "Firebase",
|
|
24302
|
+
category: "Backend platform",
|
|
24303
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24304
|
+
apiBase: "https://firebaseio.com",
|
|
24305
|
+
defaultDataClasses: ["customer"],
|
|
24306
|
+
sdks: {
|
|
24307
|
+
npm: ["firebase", "firebase-admin"],
|
|
24308
|
+
pypi: ["firebase-admin"],
|
|
24309
|
+
go: ["firebase.google.com/go"],
|
|
24310
|
+
maven: ["com.google.firebase"]
|
|
24311
|
+
}
|
|
24312
|
+
},
|
|
24313
|
+
{
|
|
24314
|
+
id: "mongodb-atlas",
|
|
24315
|
+
name: "MongoDB Atlas",
|
|
24316
|
+
category: "Database SaaS",
|
|
24317
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24318
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24319
|
+
defaultDataClasses: ["customer"],
|
|
24320
|
+
sdks: {
|
|
24321
|
+
npm: ["mongodb"],
|
|
24322
|
+
pypi: ["pymongo"],
|
|
24323
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24324
|
+
maven: ["org.mongodb"],
|
|
24325
|
+
rubygems: ["mongo"],
|
|
24326
|
+
cargo: ["mongodb"],
|
|
24327
|
+
nuget: ["MongoDB.Driver"]
|
|
24328
|
+
}
|
|
24329
|
+
},
|
|
24330
|
+
{
|
|
24331
|
+
id: "planetscale",
|
|
24332
|
+
name: "PlanetScale",
|
|
24333
|
+
category: "Database SaaS",
|
|
24334
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24335
|
+
apiBase: "https://api.planetscale.com",
|
|
24336
|
+
defaultDataClasses: ["customer"],
|
|
24337
|
+
sdks: {
|
|
24338
|
+
npm: ["@planetscale/database"],
|
|
24339
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24340
|
+
}
|
|
24341
|
+
},
|
|
24342
|
+
{
|
|
24343
|
+
id: "algolia",
|
|
24344
|
+
name: "Algolia",
|
|
24345
|
+
category: "Search SaaS",
|
|
24346
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24347
|
+
apiBase: "https://algolia.net",
|
|
24348
|
+
defaultDataClasses: ["customer"],
|
|
24349
|
+
sdks: {
|
|
24350
|
+
npm: ["algoliasearch"],
|
|
24351
|
+
pypi: ["algoliasearch"],
|
|
24352
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24353
|
+
maven: ["com.algolia"],
|
|
24354
|
+
rubygems: ["algolia"],
|
|
24355
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24356
|
+
nuget: ["Algolia.Search"]
|
|
24357
|
+
}
|
|
24358
|
+
},
|
|
24359
|
+
{
|
|
24360
|
+
id: "cloudflare",
|
|
24361
|
+
name: "Cloudflare",
|
|
24362
|
+
category: "CDN / edge",
|
|
24363
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24364
|
+
apiBase: "https://api.cloudflare.com",
|
|
24365
|
+
defaultDataClasses: ["logs"],
|
|
24366
|
+
sdks: {
|
|
24367
|
+
npm: ["cloudflare"],
|
|
24368
|
+
pypi: ["cloudflare"],
|
|
24369
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24370
|
+
nuget: ["CloudFlare.Client"]
|
|
24371
|
+
}
|
|
24372
|
+
},
|
|
24373
|
+
{
|
|
24374
|
+
id: "huggingface",
|
|
24375
|
+
name: "Hugging Face",
|
|
24376
|
+
category: "LLM provider",
|
|
24377
|
+
hostSuffixes: ["huggingface.co"],
|
|
24378
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24379
|
+
defaultDataClasses: ["source"],
|
|
24380
|
+
sdks: {
|
|
24381
|
+
npm: ["@huggingface/inference"],
|
|
24382
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24383
|
+
rubygems: ["hugging-face"]
|
|
24384
|
+
}
|
|
24385
|
+
},
|
|
24386
|
+
{
|
|
24387
|
+
id: "cohere",
|
|
24388
|
+
name: "Cohere",
|
|
24389
|
+
category: "LLM provider",
|
|
24390
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24391
|
+
apiBase: "https://api.cohere.com",
|
|
24392
|
+
defaultDataClasses: ["pii", "source"],
|
|
24393
|
+
sdks: {
|
|
24394
|
+
npm: ["cohere-ai"],
|
|
24395
|
+
pypi: ["cohere"],
|
|
24396
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24397
|
+
}
|
|
24398
|
+
},
|
|
24399
|
+
{
|
|
24400
|
+
id: "mistral",
|
|
24401
|
+
name: "Mistral AI",
|
|
24402
|
+
category: "LLM provider",
|
|
24403
|
+
hostSuffixes: ["mistral.ai"],
|
|
24404
|
+
apiBase: "https://api.mistral.ai",
|
|
24405
|
+
defaultDataClasses: ["pii", "source"],
|
|
24406
|
+
sdks: {
|
|
24407
|
+
npm: ["@mistralai/mistralai"],
|
|
24408
|
+
pypi: ["mistralai"],
|
|
24409
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24410
|
+
}
|
|
24411
|
+
}
|
|
24412
|
+
];
|
|
24413
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24414
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24415
|
+
|
|
24416
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24417
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24418
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24419
|
+
var SECRET_VALUE = new RegExp(
|
|
24420
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24421
|
+
"gi"
|
|
24422
|
+
);
|
|
24423
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24424
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24425
|
+
"gi"
|
|
24426
|
+
);
|
|
24427
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24428
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24429
|
+
{
|
|
24430
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24431
|
+
prefix: "/api/webhooks/"
|
|
24432
|
+
},
|
|
24433
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24434
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24435
|
+
];
|
|
24436
|
+
function escapeRegExp(literal2) {
|
|
24437
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24438
|
+
}
|
|
24439
|
+
var WEBHOOK_URL = new RegExp(
|
|
24440
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24441
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24442
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24443
|
+
"gi"
|
|
24444
|
+
);
|
|
22936
24445
|
|
|
22937
24446
|
// ../../packages/detections/src/escape-regexp.ts
|
|
22938
|
-
function
|
|
24447
|
+
function escapeRegExp2(value) {
|
|
22939
24448
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22940
24449
|
}
|
|
22941
24450
|
|
|
22942
24451
|
// ../../packages/detections/src/matchers/limits.ts
|
|
22943
24452
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24453
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
22944
24454
|
|
|
22945
24455
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22946
24456
|
var KeywordMatcher2 = class {
|
|
@@ -22951,7 +24461,7 @@ var KeywordMatcher2 = class {
|
|
|
22951
24461
|
for (const kw of keywords) {
|
|
22952
24462
|
if (kw.length === 0) continue;
|
|
22953
24463
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22954
|
-
const re = new RegExp(
|
|
24464
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
22955
24465
|
let m;
|
|
22956
24466
|
while ((m = re.exec(text)) !== null) {
|
|
22957
24467
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -22968,9 +24478,13 @@ var RegexMatcher2 = class {
|
|
|
22968
24478
|
if (rule.matcher.type !== "regex") return [];
|
|
22969
24479
|
const { pattern, flags, captureGroup } = rule.matcher;
|
|
22970
24480
|
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24481
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
22971
24482
|
const spans = [];
|
|
22972
24483
|
let m;
|
|
22973
|
-
|
|
24484
|
+
const maxIterations = scanText2.length + 1;
|
|
24485
|
+
let iterations = 0;
|
|
24486
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24487
|
+
if (++iterations > maxIterations) break;
|
|
22974
24488
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
22975
24489
|
if (m[0].length === 0) re.lastIndex++;
|
|
22976
24490
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -23074,7 +24588,7 @@ function isCorroborated(candidate, candidates, text) {
|
|
|
23074
24588
|
for (const label of labels) {
|
|
23075
24589
|
const trimmed = label.trim();
|
|
23076
24590
|
if (trimmed.length === 0) continue;
|
|
23077
|
-
const re = new RegExp(`(?<![A-Za-z0-9])${
|
|
24591
|
+
const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
|
|
23078
24592
|
if (re.test(haystack)) return true;
|
|
23079
24593
|
}
|
|
23080
24594
|
}
|
|
@@ -23239,6 +24753,112 @@ var CONFIG_POSTURE_RULES = [
|
|
|
23239
24753
|
}
|
|
23240
24754
|
];
|
|
23241
24755
|
|
|
24756
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24757
|
+
var BUDGET_MS = 100;
|
|
24758
|
+
var EXPONENTIAL_UNITS = [
|
|
24759
|
+
"a",
|
|
24760
|
+
"0",
|
|
24761
|
+
" ",
|
|
24762
|
+
"x",
|
|
24763
|
+
"ab",
|
|
24764
|
+
"a.",
|
|
24765
|
+
"a-",
|
|
24766
|
+
"a_",
|
|
24767
|
+
"a@",
|
|
24768
|
+
"a/",
|
|
24769
|
+
"a:",
|
|
24770
|
+
"a=",
|
|
24771
|
+
"a;",
|
|
24772
|
+
"aA0",
|
|
24773
|
+
" "
|
|
24774
|
+
];
|
|
24775
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24776
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24777
|
+
);
|
|
24778
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24779
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24780
|
+
);
|
|
24781
|
+
function literalPrefix(pattern) {
|
|
24782
|
+
let prefix = "";
|
|
24783
|
+
let i = 0;
|
|
24784
|
+
if (pattern[i] === "^") i++;
|
|
24785
|
+
while (i < pattern.length) {
|
|
24786
|
+
const c = pattern[i];
|
|
24787
|
+
if (c === void 0) break;
|
|
24788
|
+
if (c === "\\") {
|
|
24789
|
+
const next = pattern[i + 1];
|
|
24790
|
+
if (next === "b" || next === "B") {
|
|
24791
|
+
i += 2;
|
|
24792
|
+
continue;
|
|
24793
|
+
}
|
|
24794
|
+
if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
|
|
24795
|
+
prefix += next;
|
|
24796
|
+
i += 2;
|
|
24797
|
+
continue;
|
|
24798
|
+
}
|
|
24799
|
+
if ("([{.*+?|)]}^$".includes(c)) break;
|
|
24800
|
+
prefix += c;
|
|
24801
|
+
i++;
|
|
24802
|
+
}
|
|
24803
|
+
return prefix;
|
|
24804
|
+
}
|
|
24805
|
+
function fuelChars(pattern) {
|
|
24806
|
+
const fuel = /* @__PURE__ */ new Set();
|
|
24807
|
+
for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
|
|
24808
|
+
const body = m[1];
|
|
24809
|
+
if (body === void 0) continue;
|
|
24810
|
+
const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
|
|
24811
|
+
const rangeStart = range?.[1];
|
|
24812
|
+
if (rangeStart !== void 0) fuel.add(rangeStart);
|
|
24813
|
+
else {
|
|
24814
|
+
const literal2 = body.replace(/\\/g, "")[0];
|
|
24815
|
+
if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
|
|
24816
|
+
}
|
|
24817
|
+
}
|
|
24818
|
+
if (pattern.includes("\\w")) fuel.add("a");
|
|
24819
|
+
if (pattern.includes("\\d")) fuel.add("0");
|
|
24820
|
+
if (pattern.includes("\\s")) fuel.add(" ");
|
|
24821
|
+
if (/(?<!\\)\./.test(pattern)) fuel.add("a");
|
|
24822
|
+
if (fuel.size === 0) fuel.add("a");
|
|
24823
|
+
return [...fuel];
|
|
24824
|
+
}
|
|
24825
|
+
function derivedProbes(pattern) {
|
|
24826
|
+
const prefix = literalPrefix(pattern);
|
|
24827
|
+
const fuel = fuelChars(pattern);
|
|
24828
|
+
const terminators = ["!", "#", "~", "\n"];
|
|
24829
|
+
const probes = [];
|
|
24830
|
+
for (const f of fuel) {
|
|
24831
|
+
for (const term of terminators) {
|
|
24832
|
+
if (term === f) continue;
|
|
24833
|
+
for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
|
|
24834
|
+
}
|
|
24835
|
+
}
|
|
24836
|
+
return probes;
|
|
24837
|
+
}
|
|
24838
|
+
function probesFor(rule) {
|
|
24839
|
+
const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
|
|
24840
|
+
return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
|
|
24841
|
+
}
|
|
24842
|
+
function worstProbeMs(rule) {
|
|
24843
|
+
let ms = 0;
|
|
24844
|
+
let probe = "";
|
|
24845
|
+
for (const text of probesFor(rule)) {
|
|
24846
|
+
const start = performance.now();
|
|
24847
|
+
scan(text, [rule]);
|
|
24848
|
+
const elapsed = performance.now() - start;
|
|
24849
|
+
if (elapsed > ms) {
|
|
24850
|
+
ms = elapsed;
|
|
24851
|
+
probe = text;
|
|
24852
|
+
}
|
|
24853
|
+
if (ms >= BUDGET_MS) break;
|
|
24854
|
+
}
|
|
24855
|
+
return { ms, probe };
|
|
24856
|
+
}
|
|
24857
|
+
function checkRuleTiming(rule) {
|
|
24858
|
+
const { ms, probe } = worstProbeMs(rule);
|
|
24859
|
+
return { safe: ms < BUDGET_MS, worstMs: ms, probe };
|
|
24860
|
+
}
|
|
24861
|
+
|
|
23242
24862
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23243
24863
|
var auth_jwt_no_verify_default = {
|
|
23244
24864
|
specVersion: 1,
|
|
@@ -25286,7 +26906,7 @@ function ensureBundledPacks() {
|
|
|
25286
26906
|
return false;
|
|
25287
26907
|
}
|
|
25288
26908
|
}
|
|
25289
|
-
function scanText(text) {
|
|
26909
|
+
function scanText(text, ruleVersions) {
|
|
25290
26910
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
25291
26911
|
try {
|
|
25292
26912
|
const rules = getLoadedRules();
|
|
@@ -25298,7 +26918,7 @@ function scanText(text) {
|
|
|
25298
26918
|
return {
|
|
25299
26919
|
ruleId: m.ruleId,
|
|
25300
26920
|
ruleName: rule?.name ?? m.ruleId,
|
|
25301
|
-
ruleVersion: String(rule?.specVersion ?? 1),
|
|
26921
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
25302
26922
|
category: m.category,
|
|
25303
26923
|
severity: m.severity,
|
|
25304
26924
|
span: m.span,
|
|
@@ -25313,8 +26933,8 @@ function scanText(text) {
|
|
|
25313
26933
|
}
|
|
25314
26934
|
|
|
25315
26935
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25316
|
-
import { existsSync as
|
|
25317
|
-
import { basename, dirname, isAbsolute, join as
|
|
26936
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26937
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
25318
26938
|
function resolveRepoIdentity(cwd) {
|
|
25319
26939
|
try {
|
|
25320
26940
|
const root = findGitRoot(cwd);
|
|
@@ -25347,32 +26967,32 @@ function resolveRepoNwo(cwd) {
|
|
|
25347
26967
|
function findGitRoot(start) {
|
|
25348
26968
|
let dir = start;
|
|
25349
26969
|
for (; ; ) {
|
|
25350
|
-
if (
|
|
26970
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
25351
26971
|
const parent = dirname(dir);
|
|
25352
26972
|
if (parent === dir) return void 0;
|
|
25353
26973
|
dir = parent;
|
|
25354
26974
|
}
|
|
25355
26975
|
}
|
|
25356
26976
|
function resolveGitContext(root) {
|
|
25357
|
-
const dotGit =
|
|
26977
|
+
const dotGit = join7(root, ".git");
|
|
25358
26978
|
try {
|
|
25359
26979
|
if (statSync(dotGit).isDirectory()) {
|
|
25360
|
-
return { configPath:
|
|
26980
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
25361
26981
|
}
|
|
25362
26982
|
} catch {
|
|
25363
26983
|
return void 0;
|
|
25364
26984
|
}
|
|
25365
26985
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
25366
26986
|
if (!target) return void 0;
|
|
25367
|
-
const gitdir = isAbsolute(target) ? target :
|
|
25368
|
-
if (
|
|
25369
|
-
return { configPath:
|
|
26987
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
26988
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
26989
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
25370
26990
|
}
|
|
25371
|
-
const commonRaw = safeRead(
|
|
26991
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
25372
26992
|
if (!commonRaw) return void 0;
|
|
25373
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
26993
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
25374
26994
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
25375
|
-
return { configPath:
|
|
26995
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
25376
26996
|
}
|
|
25377
26997
|
function safeRead(path) {
|
|
25378
26998
|
try {
|
|
@@ -25427,9 +27047,9 @@ function nwoFromUrl(url2) {
|
|
|
25427
27047
|
}
|
|
25428
27048
|
|
|
25429
27049
|
// ../../packages/plugin-sdk/src/events.ts
|
|
25430
|
-
import { createHash as
|
|
27050
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
25431
27051
|
function contentHashOf(text) {
|
|
25432
|
-
return
|
|
27052
|
+
return createHash4("sha256").update(text).digest("hex");
|
|
25433
27053
|
}
|
|
25434
27054
|
function buildIngestEvent(input) {
|
|
25435
27055
|
return {
|
|
@@ -25449,16 +27069,6 @@ function buildIngestEvent(input) {
|
|
|
25449
27069
|
};
|
|
25450
27070
|
}
|
|
25451
27071
|
|
|
25452
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
25453
|
-
import { createHash as createHash4 } from "crypto";
|
|
25454
|
-
function normalizeFilePath(filePath) {
|
|
25455
|
-
return filePath.replaceAll("\\", "/");
|
|
25456
|
-
}
|
|
25457
|
-
function computeFindingKey(input) {
|
|
25458
|
-
const normalizedPath = normalizeFilePath(input.filePath);
|
|
25459
|
-
return createHash4("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
25460
|
-
}
|
|
25461
|
-
|
|
25462
27072
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25463
27073
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
25464
27074
|
function resolveInventoryContext(input) {
|
|
@@ -25489,13 +27099,17 @@ function resolveInventoryContext(input) {
|
|
|
25489
27099
|
}
|
|
25490
27100
|
|
|
25491
27101
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
25492
|
-
import { mkdirSync as
|
|
25493
|
-
import { join as
|
|
27102
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
27103
|
+
import { join as join9 } from "path";
|
|
27104
|
+
|
|
27105
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
27106
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
27107
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
25494
27108
|
|
|
25495
27109
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25496
27110
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25497
|
-
import { existsSync as
|
|
25498
|
-
import { basename as
|
|
27111
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27112
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
25499
27113
|
|
|
25500
27114
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
25501
27115
|
var RawEgressError = class extends Error {
|
|
@@ -25537,6 +27151,59 @@ function safeMaskedMatch(rawMatch) {
|
|
|
25537
27151
|
return masked;
|
|
25538
27152
|
}
|
|
25539
27153
|
|
|
27154
|
+
// ../../packages/plugin-sdk/src/rule-quarantine.ts
|
|
27155
|
+
var PASS_BUDGET_MS = 2e3;
|
|
27156
|
+
function ruleProbeKey(rule) {
|
|
27157
|
+
if (rule.matcher.type !== "regex") return void 0;
|
|
27158
|
+
return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
|
|
27159
|
+
}
|
|
27160
|
+
function warnQuarantined(rule, worstMs) {
|
|
27161
|
+
const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
|
|
27162
|
+
process.stderr.write(
|
|
27163
|
+
`[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
|
|
27164
|
+
`
|
|
27165
|
+
);
|
|
27166
|
+
}
|
|
27167
|
+
async function filterUnsafeRules(rules, gateway, opts) {
|
|
27168
|
+
const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
|
|
27169
|
+
const passStart = performance.now();
|
|
27170
|
+
const safe = [];
|
|
27171
|
+
for (const rule of rules) {
|
|
27172
|
+
const key = ruleProbeKey(rule);
|
|
27173
|
+
if (key === void 0) {
|
|
27174
|
+
safe.push(rule);
|
|
27175
|
+
continue;
|
|
27176
|
+
}
|
|
27177
|
+
let cached2;
|
|
27178
|
+
try {
|
|
27179
|
+
cached2 = await gateway.getRuleProbeVerdict(key);
|
|
27180
|
+
} catch {
|
|
27181
|
+
cached2 = void 0;
|
|
27182
|
+
}
|
|
27183
|
+
if (cached2) {
|
|
27184
|
+
if (cached2.verdict === "safe") safe.push(rule);
|
|
27185
|
+
else warnQuarantined(rule, cached2.worstProbeMs);
|
|
27186
|
+
continue;
|
|
27187
|
+
}
|
|
27188
|
+
if (performance.now() - passStart >= passBudgetMs) {
|
|
27189
|
+
warnQuarantined(rule, void 0);
|
|
27190
|
+
continue;
|
|
27191
|
+
}
|
|
27192
|
+
let isSafe;
|
|
27193
|
+
let worstMs;
|
|
27194
|
+
try {
|
|
27195
|
+
({ safe: isSafe, worstMs } = checkRuleTiming(rule));
|
|
27196
|
+
} catch {
|
|
27197
|
+
isSafe = false;
|
|
27198
|
+
worstMs = Number.POSITIVE_INFINITY;
|
|
27199
|
+
}
|
|
27200
|
+
await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
|
|
27201
|
+
if (isSafe) safe.push(rule);
|
|
27202
|
+
else warnQuarantined(rule, worstMs);
|
|
27203
|
+
}
|
|
27204
|
+
return safe;
|
|
27205
|
+
}
|
|
27206
|
+
|
|
25540
27207
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25541
27208
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
25542
27209
|
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
@@ -25579,7 +27246,17 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25579
27246
|
categoryActionIndex.set(p.target.category, p.action);
|
|
25580
27247
|
}
|
|
25581
27248
|
}
|
|
25582
|
-
|
|
27249
|
+
const bundledProbeKeys = new Set(
|
|
27250
|
+
getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
|
|
27251
|
+
);
|
|
27252
|
+
const incoming = bundle.rules ?? [];
|
|
27253
|
+
const ciVerified = incoming.filter((rule) => {
|
|
27254
|
+
const key = ruleProbeKey(rule);
|
|
27255
|
+
return key !== void 0 && bundledProbeKeys.has(key);
|
|
27256
|
+
});
|
|
27257
|
+
const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
|
|
27258
|
+
const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
|
|
27259
|
+
rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
|
|
25583
27260
|
bundleExceptions = bundle.exceptions ?? [];
|
|
25584
27261
|
initialized = true;
|
|
25585
27262
|
}
|
|
@@ -25822,8 +27499,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25822
27499
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25823
27500
|
|
|
25824
27501
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25825
|
-
import { mkdirSync as
|
|
25826
|
-
import { join as
|
|
27502
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
27503
|
+
import { join as join11 } from "path";
|
|
25827
27504
|
|
|
25828
27505
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25829
27506
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -25854,7 +27531,8 @@ var StandaloneDataGateway = class {
|
|
|
25854
27531
|
}
|
|
25855
27532
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
25856
27533
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
25857
|
-
// hands the natural key across.
|
|
27534
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27535
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
25858
27536
|
recordLlmCall(input) {
|
|
25859
27537
|
this.db.auditEvents.insertLlmCall(input);
|
|
25860
27538
|
return Promise.resolve();
|
|
@@ -25896,7 +27574,9 @@ var StandaloneDataGateway = class {
|
|
|
25896
27574
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
25897
27575
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
25898
27576
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
25899
|
-
// content-addressed
|
|
27577
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27578
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27579
|
+
// than no-opping.
|
|
25900
27580
|
writeToolCall(input) {
|
|
25901
27581
|
this.db.auditEvents.insertToolCall(input);
|
|
25902
27582
|
if (input.inspections.length === 0) return;
|
|
@@ -25916,7 +27596,7 @@ var StandaloneDataGateway = class {
|
|
|
25916
27596
|
});
|
|
25917
27597
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
25918
27598
|
this.db.inspectionFindings.insertFinding({
|
|
25919
|
-
id: inspectionFindingId(auditEventId,
|
|
27599
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
25920
27600
|
auditEventId,
|
|
25921
27601
|
inspectionDefinitionId: definitionId,
|
|
25922
27602
|
classifiedDataId: classifiedDataId2,
|
|
@@ -25965,10 +27645,17 @@ var StandaloneDataGateway = class {
|
|
|
25965
27645
|
try {
|
|
25966
27646
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
25967
27647
|
if (snapshot.installedPacks === 0) return void 0;
|
|
25968
|
-
if (snapshot.enabledPacks === 0)
|
|
27648
|
+
if (snapshot.enabledPacks === 0) {
|
|
27649
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27650
|
+
}
|
|
25969
27651
|
if (snapshot.invalidRules > 0) return void 0;
|
|
25970
27652
|
if (snapshot.rules.length === 0) return void 0;
|
|
25971
|
-
return {
|
|
27653
|
+
return {
|
|
27654
|
+
rules: snapshot.rules,
|
|
27655
|
+
ruleActions: snapshot.ruleActions,
|
|
27656
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27657
|
+
complete: true
|
|
27658
|
+
};
|
|
25972
27659
|
} catch {
|
|
25973
27660
|
return void 0;
|
|
25974
27661
|
}
|
|
@@ -25996,6 +27683,7 @@ var StandaloneDataGateway = class {
|
|
|
25996
27683
|
policies: [...policies, ...rulePolicies],
|
|
25997
27684
|
rules: installed ? installed.rules : [],
|
|
25998
27685
|
...installed ? { rulesComplete: true } : {},
|
|
27686
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
25999
27687
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
26000
27688
|
customKeywords,
|
|
26001
27689
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -26094,6 +27782,13 @@ var StandaloneDataGateway = class {
|
|
|
26094
27782
|
this.db.scanLedger.upsertEntries(entries);
|
|
26095
27783
|
return Promise.resolve();
|
|
26096
27784
|
}
|
|
27785
|
+
getRuleProbeVerdict(ruleKey) {
|
|
27786
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
27787
|
+
}
|
|
27788
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
|
|
27789
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
|
|
27790
|
+
return Promise.resolve();
|
|
27791
|
+
}
|
|
26097
27792
|
openAtRestKeysForPath(path) {
|
|
26098
27793
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
26099
27794
|
}
|
|
@@ -26104,6 +27799,12 @@ var StandaloneDataGateway = class {
|
|
|
26104
27799
|
this.db.resolutions.insertResolution(input);
|
|
26105
27800
|
return Promise.resolve();
|
|
26106
27801
|
}
|
|
27802
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
27803
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
27804
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
27805
|
+
recordProjectEgress(input) {
|
|
27806
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
27807
|
+
}
|
|
26107
27808
|
close() {
|
|
26108
27809
|
this.db.close();
|
|
26109
27810
|
return Promise.resolve();
|
|
@@ -26121,11 +27822,11 @@ import { randomUUID as randomUUID12 } from "crypto";
|
|
|
26121
27822
|
var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
26122
27823
|
|
|
26123
27824
|
// src/history/transcripts.ts
|
|
26124
|
-
import { readdirSync as
|
|
27825
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
|
|
26125
27826
|
import { homedir as homedir3 } from "os";
|
|
26126
|
-
import { join as
|
|
27827
|
+
import { join as join12 } from "path";
|
|
26127
27828
|
function transcriptsDir(home) {
|
|
26128
|
-
return
|
|
27829
|
+
return join12(home ?? homedir3(), ".claude", "projects");
|
|
26129
27830
|
}
|
|
26130
27831
|
function isRecord(value) {
|
|
26131
27832
|
return typeof value === "object" && value !== null;
|
|
@@ -26193,9 +27894,9 @@ function parseTranscriptUsage(jsonl, sinceMs = 0) {
|
|
|
26193
27894
|
if (sinceMs > 0 && Date.parse(optString(rec.timestamp) ?? "") < sinceMs) continue;
|
|
26194
27895
|
if (rec.type === "user") {
|
|
26195
27896
|
const uuid5 = optString(rec.uuid);
|
|
26196
|
-
const
|
|
26197
|
-
if (uuid5 === void 0 ||
|
|
26198
|
-
out.push({ kind: "user", uuid: uuid5, promptId });
|
|
27897
|
+
const promptId2 = optString(rec.promptId);
|
|
27898
|
+
if (uuid5 === void 0 || promptId2 === void 0) continue;
|
|
27899
|
+
out.push({ kind: "user", uuid: uuid5, promptId: promptId2 });
|
|
26199
27900
|
continue;
|
|
26200
27901
|
}
|
|
26201
27902
|
if (rec.type !== "assistant") continue;
|
|
@@ -26368,22 +28069,22 @@ var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
|
26368
28069
|
function* iterateFileContents(dir, excludeSessionId) {
|
|
26369
28070
|
let projects;
|
|
26370
28071
|
try {
|
|
26371
|
-
projects =
|
|
28072
|
+
projects = readdirSync4(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
26372
28073
|
} catch {
|
|
26373
28074
|
return;
|
|
26374
28075
|
}
|
|
26375
28076
|
for (const project of projects) {
|
|
26376
|
-
const projectDir =
|
|
28077
|
+
const projectDir = join12(dir, project);
|
|
26377
28078
|
let files;
|
|
26378
28079
|
try {
|
|
26379
|
-
files =
|
|
28080
|
+
files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
|
|
26380
28081
|
} catch {
|
|
26381
28082
|
continue;
|
|
26382
28083
|
}
|
|
26383
28084
|
for (const file2 of files) {
|
|
26384
28085
|
if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
|
|
26385
28086
|
continue;
|
|
26386
|
-
const filePath =
|
|
28087
|
+
const filePath = join12(projectDir, file2);
|
|
26387
28088
|
let content;
|
|
26388
28089
|
try {
|
|
26389
28090
|
content = readFileSync7(filePath, "utf8");
|
|
@@ -26504,13 +28205,13 @@ import { createHash as createHash5 } from "crypto";
|
|
|
26504
28205
|
import {
|
|
26505
28206
|
closeSync,
|
|
26506
28207
|
fstatSync,
|
|
26507
|
-
mkdirSync as
|
|
28208
|
+
mkdirSync as mkdirSync4,
|
|
26508
28209
|
openSync,
|
|
26509
28210
|
readFileSync as readFileSync8,
|
|
26510
28211
|
readSync,
|
|
26511
|
-
writeFileSync as
|
|
28212
|
+
writeFileSync as writeFileSync5
|
|
26512
28213
|
} from "fs";
|
|
26513
|
-
import { join as
|
|
28214
|
+
import { join as join13 } from "path";
|
|
26514
28215
|
|
|
26515
28216
|
// src/history/usage.ts
|
|
26516
28217
|
var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
|
|
@@ -26561,12 +28262,18 @@ async function reconcileSessionToolCalls(gateway, sessionId, toolCalls, usageRec
|
|
|
26561
28262
|
if (toolCalls.length === 0) return 0;
|
|
26562
28263
|
const promptIdByUuid = /* @__PURE__ */ new Map();
|
|
26563
28264
|
for (const r of usageRecords) if (r.kind === "user") promptIdByUuid.set(r.uuid, r.promptId);
|
|
28265
|
+
let ruleVersions;
|
|
28266
|
+
try {
|
|
28267
|
+
ruleVersions = (await gateway.getPolicyBundle()).ruleVersions;
|
|
28268
|
+
} catch {
|
|
28269
|
+
ruleVersions = void 0;
|
|
28270
|
+
}
|
|
26564
28271
|
const inputs = toolCalls.map((tc) => {
|
|
26565
28272
|
const runKey = (tc.parentUuid !== void 0 ? promptIdByUuid.get(tc.parentUuid) : void 0) ?? opts.seedPromptId;
|
|
26566
28273
|
const attributes = { tool_name: tc.toolName, tool_use_id: tc.toolUseId };
|
|
26567
28274
|
let inspections = [];
|
|
26568
28275
|
if (tc.target !== void 0) {
|
|
26569
|
-
const { masked, findings } = scanText(tc.target);
|
|
28276
|
+
const { masked, findings } = scanText(tc.target, ruleVersions);
|
|
26570
28277
|
if (masked !== "") attributes.target = truncateTarget(masked);
|
|
26571
28278
|
inspections = findings.map((f) => ({
|
|
26572
28279
|
ruleId: f.ruleId,
|