@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/reconcile.js
CHANGED
|
@@ -491,9 +491,13 @@ var require_ignore = __commonJS({
|
|
|
491
491
|
}
|
|
492
492
|
});
|
|
493
493
|
|
|
494
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
495
|
+
import { existsSync as existsSync3 } from "fs";
|
|
496
|
+
import { join as join6 } from "path";
|
|
497
|
+
|
|
494
498
|
// ../../packages/persistence/src/database.ts
|
|
495
499
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
500
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
497
501
|
import { join, sep } from "path";
|
|
498
502
|
import { DatabaseSync } from "node:sqlite";
|
|
499
503
|
|
|
@@ -542,6 +546,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
542
546
|
{
|
|
543
547
|
tag: "0010_events_session_expression_index",
|
|
544
548
|
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"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tag: "0011_egress_writer",
|
|
552
|
+
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0012_handy_the_captain",
|
|
556
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0013_legacy_history_backfill_support",
|
|
560
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
tag: "0014_drop_legacy_events_findings",
|
|
564
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
545
565
|
}
|
|
546
566
|
];
|
|
547
567
|
|
|
@@ -15372,7 +15392,12 @@ var FindingFacets = external_exports.object({
|
|
|
15372
15392
|
severity: external_exports.array(FindingFacetItem),
|
|
15373
15393
|
subtype: external_exports.array(FindingFacetItem),
|
|
15374
15394
|
provider: external_exports.array(FindingFacetItem),
|
|
15375
|
-
action: external_exports.array(FindingFacetItem)
|
|
15395
|
+
action: external_exports.array(FindingFacetItem),
|
|
15396
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15397
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15398
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15399
|
+
// counted under no value.
|
|
15400
|
+
status: external_exports.array(FindingFacetItem)
|
|
15376
15401
|
}).meta({ id: "FindingFacets" });
|
|
15377
15402
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15378
15403
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15382,6 +15407,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15382
15407
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15383
15408
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15384
15409
|
action: external_exports.array(FindingAction).optional(),
|
|
15410
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15411
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15412
|
+
// one of the requested values.
|
|
15413
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15385
15414
|
q: external_exports.string().optional(),
|
|
15386
15415
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15387
15416
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15572,6 +15601,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15572
15601
|
parent_uuid: external_exports.string().optional(),
|
|
15573
15602
|
run_key: external_exports.string().optional()
|
|
15574
15603
|
}).catchall(external_exports.unknown());
|
|
15604
|
+
var CaptureAttributes = external_exports.object({
|
|
15605
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15606
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15607
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15608
|
+
source_tool: external_exports.string().optional(),
|
|
15609
|
+
file_path: external_exports.string().optional(),
|
|
15610
|
+
repo: external_exports.string().optional(),
|
|
15611
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15612
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15613
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15614
|
+
tool_name: external_exports.string().optional(),
|
|
15615
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15616
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15617
|
+
gitignored: external_exports.boolean().optional(),
|
|
15618
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15619
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15620
|
+
whole_file: external_exports.boolean().optional(),
|
|
15621
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15622
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15623
|
+
// trace id when telemetry is enabled.
|
|
15624
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15625
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15626
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15627
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15628
|
+
// authorized the bypass.
|
|
15629
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15630
|
+
}).catchall(external_exports.unknown());
|
|
15575
15631
|
var ToolCallInspection = external_exports.object({
|
|
15576
15632
|
ruleId: external_exports.string().min(1),
|
|
15577
15633
|
ruleName: external_exports.string(),
|
|
@@ -15658,7 +15714,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15658
15714
|
span: Span,
|
|
15659
15715
|
maskedMatch: external_exports.string(),
|
|
15660
15716
|
actionTaken: ActionTaken,
|
|
15661
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15717
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15718
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15719
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15720
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15721
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15722
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15723
|
+
findingKey: external_exports.string().optional(),
|
|
15724
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15725
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15726
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15727
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15728
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15662
15729
|
});
|
|
15663
15730
|
var InventoryContext = external_exports.object({
|
|
15664
15731
|
host: InventoryInput.optional(),
|
|
@@ -15860,6 +15927,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15860
15927
|
|
|
15861
15928
|
// ../../packages/schema/src/zod/event.ts
|
|
15862
15929
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15930
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15863
15931
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15864
15932
|
var EventMetadata = external_exports.object({
|
|
15865
15933
|
sessionId: external_exports.string().optional(),
|
|
@@ -16200,6 +16268,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16200
16268
|
|
|
16201
16269
|
// ../../packages/schema/src/zod/rule.ts
|
|
16202
16270
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16271
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16203
16272
|
var KeywordMatcher = external_exports.object({
|
|
16204
16273
|
type: external_exports.literal("keyword"),
|
|
16205
16274
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16224,9 +16293,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16224
16293
|
return false;
|
|
16225
16294
|
}
|
|
16226
16295
|
}
|
|
16296
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16227
16297
|
var RegexMatcher = external_exports.object({
|
|
16228
16298
|
type: external_exports.literal("regex"),
|
|
16229
|
-
pattern: external_exports.string(),
|
|
16299
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16230
16300
|
flags: external_exports.string().default("gi"),
|
|
16231
16301
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16232
16302
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16347,6 +16417,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16347
16417
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16348
16418
|
// `bundle.exceptions ?? []`.
|
|
16349
16419
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16420
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16421
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16422
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16423
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16424
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16425
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16350
16426
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16351
16427
|
fetchedAt: external_exports.iso.datetime()
|
|
16352
16428
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16793,6 +16869,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16793
16869
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16794
16870
|
}
|
|
16795
16871
|
|
|
16872
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16873
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16874
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16875
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16876
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16877
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16878
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16879
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16880
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16881
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16882
|
+
var ReviewInfo = external_exports.object({
|
|
16883
|
+
needsReview: external_exports.boolean(),
|
|
16884
|
+
reasons: external_exports.array(ReviewReason)
|
|
16885
|
+
}).meta({ id: "ReviewInfo" });
|
|
16886
|
+
var DestinationNetwork = external_exports.object({
|
|
16887
|
+
port: external_exports.number().int().nullable(),
|
|
16888
|
+
geo: external_exports.string().nullable(),
|
|
16889
|
+
ptr: external_exports.string().nullable()
|
|
16890
|
+
}).meta({ id: "DestinationNetwork" });
|
|
16891
|
+
var EndpointSummary = external_exports.object({
|
|
16892
|
+
id: external_exports.string(),
|
|
16893
|
+
method: HttpMethod,
|
|
16894
|
+
transport: Transport,
|
|
16895
|
+
url: external_exports.string(),
|
|
16896
|
+
template: external_exports.boolean(),
|
|
16897
|
+
dataClass: DataClass,
|
|
16898
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16899
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
16900
|
+
}).meta({ id: "EndpointSummary" });
|
|
16901
|
+
var CallSite = external_exports.object({
|
|
16902
|
+
id: external_exports.string(),
|
|
16903
|
+
project: external_exports.string(),
|
|
16904
|
+
file: external_exports.string(),
|
|
16905
|
+
line: external_exports.number().int().nonnegative(),
|
|
16906
|
+
snippet: external_exports.string(),
|
|
16907
|
+
dynamic: external_exports.boolean(),
|
|
16908
|
+
vendored: external_exports.boolean(),
|
|
16909
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
16910
|
+
projectId: external_exports.string().nullable()
|
|
16911
|
+
}).meta({ id: "CallSite" });
|
|
16912
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
16913
|
+
sites: external_exports.array(CallSite)
|
|
16914
|
+
}).meta({ id: "EndpointWithSites" });
|
|
16915
|
+
var ShareDestinationSummary = external_exports.object({
|
|
16916
|
+
id: external_exports.string(),
|
|
16917
|
+
kind: DestinationKind,
|
|
16918
|
+
name: external_exports.string(),
|
|
16919
|
+
host: external_exports.string(),
|
|
16920
|
+
category: external_exports.string(),
|
|
16921
|
+
trust: ShareTrustLevel,
|
|
16922
|
+
/** Effective state (decision applied over the trust default). */
|
|
16923
|
+
status: EgressStatus,
|
|
16924
|
+
/** True when an egress decision override differs from the trust default. */
|
|
16925
|
+
isCustom: external_exports.boolean(),
|
|
16926
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16927
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
16928
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16929
|
+
transports: external_exports.array(Transport),
|
|
16930
|
+
/** Most-sensitive first. */
|
|
16931
|
+
dataClasses: external_exports.array(DataClass),
|
|
16932
|
+
review: ReviewInfo,
|
|
16933
|
+
/** Non-provider hosts only; null for providers. */
|
|
16934
|
+
network: DestinationNetwork.nullable(),
|
|
16935
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
16936
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
16937
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
16938
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
16939
|
+
endpointCount: true,
|
|
16940
|
+
callSiteCount: true,
|
|
16941
|
+
endpoints: true
|
|
16942
|
+
}).extend({
|
|
16943
|
+
/** Ownership/geo rationale; null for providers. */
|
|
16944
|
+
note: external_exports.string().nullable(),
|
|
16945
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
16946
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
16947
|
+
var ReviewDestination = external_exports.object({
|
|
16948
|
+
id: external_exports.string(),
|
|
16949
|
+
kind: DestinationKind,
|
|
16950
|
+
name: external_exports.string(),
|
|
16951
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
16952
|
+
host: external_exports.string(),
|
|
16953
|
+
trust: ShareTrustLevel,
|
|
16954
|
+
status: EgressStatus,
|
|
16955
|
+
review: ReviewInfo,
|
|
16956
|
+
topDataClass: DataClass,
|
|
16957
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16958
|
+
lastSeen: external_exports.iso.datetime()
|
|
16959
|
+
}).meta({ id: "ReviewDestination" });
|
|
16960
|
+
var ShareDestinationGroup = external_exports.object({
|
|
16961
|
+
kind: DestinationKind,
|
|
16962
|
+
total: external_exports.number().int().nonnegative(),
|
|
16963
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
16964
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
16965
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
16966
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
16967
|
+
var SharesStats = external_exports.object({
|
|
16968
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
16969
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
16970
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
16971
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
16972
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
16973
|
+
byKind: external_exports.object({
|
|
16974
|
+
provider: external_exports.number().int().nonnegative(),
|
|
16975
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16976
|
+
external: external_exports.number().int().nonnegative(),
|
|
16977
|
+
ip: external_exports.number().int().nonnegative()
|
|
16978
|
+
}),
|
|
16979
|
+
byTrust: external_exports.object({
|
|
16980
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
16981
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16982
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
16983
|
+
ip: external_exports.number().int().nonnegative()
|
|
16984
|
+
})
|
|
16985
|
+
}).meta({ id: "SharesStats" });
|
|
16986
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
16987
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
16988
|
+
decision: EgressDecision.nullable()
|
|
16989
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
16990
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
16991
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
16992
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
16993
|
+
q: external_exports.string().optional(),
|
|
16994
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
16995
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
16996
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
16997
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
16998
|
+
/**
|
|
16999
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17000
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17001
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17002
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17003
|
+
*/
|
|
17004
|
+
review: external_exports.stringbool().default(false)
|
|
17005
|
+
});
|
|
17006
|
+
var ExportSharesQuery = external_exports.object({
|
|
17007
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17008
|
+
q: external_exports.string().optional(),
|
|
17009
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
17010
|
+
});
|
|
17011
|
+
|
|
17012
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
17013
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
17014
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
17015
|
+
id: external_exports.string(),
|
|
17016
|
+
name: external_exports.string(),
|
|
17017
|
+
category: external_exports.string(),
|
|
17018
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
17019
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
17020
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
17021
|
+
apiBase: external_exports.string(),
|
|
17022
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
17023
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
17024
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
17025
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
17026
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
17027
|
+
var EgressCallSiteHit = external_exports.object({
|
|
17028
|
+
file: external_exports.string(),
|
|
17029
|
+
line: external_exports.number().int().positive(),
|
|
17030
|
+
snippet: external_exports.string(),
|
|
17031
|
+
dynamic: external_exports.boolean(),
|
|
17032
|
+
vendored: external_exports.boolean()
|
|
17033
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
17034
|
+
var ResolvedEgressHit = external_exports.object({
|
|
17035
|
+
host: external_exports.string(),
|
|
17036
|
+
kind: DestinationKind,
|
|
17037
|
+
name: external_exports.string(),
|
|
17038
|
+
category: external_exports.string(),
|
|
17039
|
+
trust: ShareTrustLevel,
|
|
17040
|
+
network: DestinationNetwork.nullable(),
|
|
17041
|
+
method: HttpMethod,
|
|
17042
|
+
transport: Transport,
|
|
17043
|
+
url: external_exports.string(),
|
|
17044
|
+
template: external_exports.boolean(),
|
|
17045
|
+
dataClass: DataClass,
|
|
17046
|
+
site: EgressCallSiteHit
|
|
17047
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
17048
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
17049
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
17050
|
+
external_exports.object({
|
|
17051
|
+
mode: external_exports.literal("ledger"),
|
|
17052
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
17053
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
17054
|
+
})
|
|
17055
|
+
]).meta({ id: "EgressReconcile" });
|
|
17056
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
17057
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
17058
|
+
projectKey: external_exports.string().min(1),
|
|
17059
|
+
/** Display name only — never keys reconciliation. */
|
|
17060
|
+
project: external_exports.string(),
|
|
17061
|
+
projectId: external_exports.string().nullable(),
|
|
17062
|
+
reconcile: EgressReconcile,
|
|
17063
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
17064
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
17065
|
+
var EgressWriteSummary = external_exports.object({
|
|
17066
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17067
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17068
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17069
|
+
truncated: external_exports.boolean(),
|
|
17070
|
+
/**
|
|
17071
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
17072
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17073
|
+
* again next scan.
|
|
17074
|
+
*/
|
|
17075
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17076
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17077
|
+
|
|
16796
17078
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16797
17079
|
function toApiAction(dbVal) {
|
|
16798
17080
|
const map2 = {
|
|
@@ -16940,6 +17222,15 @@ function groupActions(g) {
|
|
|
16940
17222
|
actionsCache.set(g, actions);
|
|
16941
17223
|
return actions;
|
|
16942
17224
|
}
|
|
17225
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17226
|
+
const statusSet = new Set(statuses);
|
|
17227
|
+
let sum = 0;
|
|
17228
|
+
for (const input of statusInputs) {
|
|
17229
|
+
if (input.count === void 0) return null;
|
|
17230
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17231
|
+
}
|
|
17232
|
+
return sum;
|
|
17233
|
+
}
|
|
16943
17234
|
function applyFindingFilters(groups, opts) {
|
|
16944
17235
|
let filtered = groups;
|
|
16945
17236
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16958,6 +17249,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
16958
17249
|
const subtypeSet = new Set(opts.subtype);
|
|
16959
17250
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
16960
17251
|
}
|
|
17252
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17253
|
+
const statusSet = new Set(opts.statuses);
|
|
17254
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17255
|
+
}
|
|
16961
17256
|
if (opts.q) {
|
|
16962
17257
|
const q = opts.q.toLowerCase();
|
|
16963
17258
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -16979,6 +17274,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16979
17274
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
16980
17275
|
providers: opts.providers,
|
|
16981
17276
|
actions: opts.actions,
|
|
17277
|
+
statuses: opts.statuses,
|
|
16982
17278
|
q: opts.q,
|
|
16983
17279
|
subtype: opts.subtype
|
|
16984
17280
|
});
|
|
@@ -16988,6 +17284,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16988
17284
|
}
|
|
16989
17285
|
const forProvider = applyFindingFilters(allGroups, {
|
|
16990
17286
|
actions: opts.actions,
|
|
17287
|
+
statuses: opts.statuses,
|
|
16991
17288
|
q: opts.q,
|
|
16992
17289
|
subtype: opts.subtype,
|
|
16993
17290
|
severity: opts.severity
|
|
@@ -16998,6 +17295,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16998
17295
|
}
|
|
16999
17296
|
const forAction = applyFindingFilters(allGroups, {
|
|
17000
17297
|
providers: opts.providers,
|
|
17298
|
+
statuses: opts.statuses,
|
|
17001
17299
|
q: opts.q,
|
|
17002
17300
|
subtype: opts.subtype,
|
|
17003
17301
|
severity: opts.severity
|
|
@@ -17009,17 +17307,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17009
17307
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17010
17308
|
providers: opts.providers,
|
|
17011
17309
|
actions: opts.actions,
|
|
17310
|
+
statuses: opts.statuses,
|
|
17012
17311
|
q: opts.q,
|
|
17013
17312
|
severity: opts.severity
|
|
17014
17313
|
});
|
|
17015
17314
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17016
17315
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17316
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17317
|
+
providers: opts.providers,
|
|
17318
|
+
actions: opts.actions,
|
|
17319
|
+
q: opts.q,
|
|
17320
|
+
subtype: opts.subtype,
|
|
17321
|
+
severity: opts.severity
|
|
17322
|
+
});
|
|
17323
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17324
|
+
for (const g of forStatus) {
|
|
17325
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17326
|
+
}
|
|
17017
17327
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17018
17328
|
return {
|
|
17019
17329
|
severity: toItems(severityMap),
|
|
17020
17330
|
provider: toItems(providerMap),
|
|
17021
17331
|
action: toItems(actionMap),
|
|
17022
|
-
subtype: toItems(subtypeMap)
|
|
17332
|
+
subtype: toItems(subtypeMap),
|
|
17333
|
+
status: toItems(statusMap)
|
|
17023
17334
|
};
|
|
17024
17335
|
}
|
|
17025
17336
|
|
|
@@ -17054,10 +17365,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17054
17365
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17055
17366
|
|
|
17056
17367
|
// ../../packages/schema/src/zod/local.ts
|
|
17057
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17368
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17058
17369
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17059
17370
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17060
17371
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17372
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17373
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17374
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17375
|
+
});
|
|
17061
17376
|
var WorkspaceSettings = external_exports.object({
|
|
17062
17377
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17063
17378
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17069,38 +17384,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17069
17384
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17070
17385
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17071
17386
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17387
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17388
|
+
// Shares writes.
|
|
17389
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17072
17390
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17073
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17391
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17392
|
+
// Records that the user consented to sending findings to the model API for
|
|
17393
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17394
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17395
|
+
// covers the current payload and must be re-granted.
|
|
17396
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17074
17397
|
});
|
|
17075
17398
|
function defaultWorkspaceSettings() {
|
|
17076
17399
|
return WorkspaceSettings.parse({});
|
|
17077
17400
|
}
|
|
17078
|
-
function toEventRow(event) {
|
|
17079
|
-
return {
|
|
17080
|
-
id: event.id,
|
|
17081
|
-
sourceTool: event.sourceTool,
|
|
17082
|
-
kind: event.kind,
|
|
17083
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17084
|
-
contentHash: event.contentHash,
|
|
17085
|
-
content: event.content,
|
|
17086
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17087
|
-
};
|
|
17088
|
-
}
|
|
17089
|
-
function toFindingRow(finding) {
|
|
17090
|
-
return {
|
|
17091
|
-
id: finding.id,
|
|
17092
|
-
eventId: finding.eventId,
|
|
17093
|
-
ruleId: finding.ruleId,
|
|
17094
|
-
category: finding.category,
|
|
17095
|
-
severity: finding.severity,
|
|
17096
|
-
spanStart: finding.span.start,
|
|
17097
|
-
spanEnd: finding.span.end,
|
|
17098
|
-
maskedMatch: finding.maskedMatch,
|
|
17099
|
-
actionTaken: finding.actionTaken,
|
|
17100
|
-
confidence: finding.confidence,
|
|
17101
|
-
findingKey: finding.findingKey ?? null
|
|
17102
|
-
};
|
|
17103
|
-
}
|
|
17104
17401
|
function toInventoryRow(input, id, now) {
|
|
17105
17402
|
return {
|
|
17106
17403
|
id,
|
|
@@ -17170,7 +17467,42 @@ function toInspectionFindingRow(input) {
|
|
|
17170
17467
|
spanEnd: input.span.end,
|
|
17171
17468
|
maskedMatch: input.maskedMatch,
|
|
17172
17469
|
actionTaken: input.actionTaken,
|
|
17173
|
-
confidence: input.confidence
|
|
17470
|
+
confidence: input.confidence,
|
|
17471
|
+
findingKey: input.findingKey ?? null,
|
|
17472
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17473
|
+
};
|
|
17474
|
+
}
|
|
17475
|
+
function toCaptureAttributes(event) {
|
|
17476
|
+
const metadata = event.metadata;
|
|
17477
|
+
return {
|
|
17478
|
+
source_tool: event.sourceTool,
|
|
17479
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17480
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17481
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17482
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17483
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17484
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17485
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17486
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17487
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17488
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17489
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17490
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17491
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17492
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17495
|
+
function captureDefinitionVersion(finding) {
|
|
17496
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17497
|
+
}
|
|
17498
|
+
function toCaptureDefinitionInput(finding) {
|
|
17499
|
+
return {
|
|
17500
|
+
ruleId: finding.ruleId,
|
|
17501
|
+
version: captureDefinitionVersion(finding),
|
|
17502
|
+
name: finding.ruleId,
|
|
17503
|
+
category: finding.category,
|
|
17504
|
+
severity: finding.severity,
|
|
17505
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17174
17506
|
};
|
|
17175
17507
|
}
|
|
17176
17508
|
|
|
@@ -17552,145 +17884,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17552
17884
|
path: ["liveKeys"]
|
|
17553
17885
|
});
|
|
17554
17886
|
|
|
17555
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17556
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17557
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17558
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17559
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17560
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17561
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17562
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17563
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17564
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17565
|
-
var ReviewInfo = external_exports.object({
|
|
17566
|
-
needsReview: external_exports.boolean(),
|
|
17567
|
-
reasons: external_exports.array(ReviewReason)
|
|
17568
|
-
}).meta({ id: "ReviewInfo" });
|
|
17569
|
-
var DestinationNetwork = external_exports.object({
|
|
17570
|
-
port: external_exports.number().int().nullable(),
|
|
17571
|
-
geo: external_exports.string().nullable(),
|
|
17572
|
-
ptr: external_exports.string().nullable()
|
|
17573
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17574
|
-
var EndpointSummary = external_exports.object({
|
|
17575
|
-
id: external_exports.string(),
|
|
17576
|
-
method: HttpMethod,
|
|
17577
|
-
transport: Transport,
|
|
17578
|
-
url: external_exports.string(),
|
|
17579
|
-
template: external_exports.boolean(),
|
|
17580
|
-
dataClass: DataClass,
|
|
17581
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17582
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17583
|
-
}).meta({ id: "EndpointSummary" });
|
|
17584
|
-
var CallSite = external_exports.object({
|
|
17585
|
-
id: external_exports.string(),
|
|
17586
|
-
project: external_exports.string(),
|
|
17587
|
-
file: external_exports.string(),
|
|
17588
|
-
line: external_exports.number().int().nonnegative(),
|
|
17589
|
-
snippet: external_exports.string(),
|
|
17590
|
-
dynamic: external_exports.boolean(),
|
|
17591
|
-
vendored: external_exports.boolean(),
|
|
17592
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17593
|
-
projectId: external_exports.string().nullable()
|
|
17594
|
-
}).meta({ id: "CallSite" });
|
|
17595
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17596
|
-
sites: external_exports.array(CallSite)
|
|
17597
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17598
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17599
|
-
id: external_exports.string(),
|
|
17600
|
-
kind: DestinationKind,
|
|
17601
|
-
name: external_exports.string(),
|
|
17602
|
-
host: external_exports.string(),
|
|
17603
|
-
category: external_exports.string(),
|
|
17604
|
-
trust: ShareTrustLevel,
|
|
17605
|
-
/** Effective state (decision applied over the trust default). */
|
|
17606
|
-
status: EgressStatus,
|
|
17607
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17608
|
-
isCustom: external_exports.boolean(),
|
|
17609
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17610
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17611
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17612
|
-
transports: external_exports.array(Transport),
|
|
17613
|
-
/** Most-sensitive first. */
|
|
17614
|
-
dataClasses: external_exports.array(DataClass),
|
|
17615
|
-
review: ReviewInfo,
|
|
17616
|
-
/** Non-provider hosts only; null for providers. */
|
|
17617
|
-
network: DestinationNetwork.nullable(),
|
|
17618
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17619
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17620
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17621
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17622
|
-
endpointCount: true,
|
|
17623
|
-
callSiteCount: true,
|
|
17624
|
-
endpoints: true
|
|
17625
|
-
}).extend({
|
|
17626
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17627
|
-
note: external_exports.string().nullable(),
|
|
17628
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17629
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17630
|
-
var ReviewDestination = external_exports.object({
|
|
17631
|
-
id: external_exports.string(),
|
|
17632
|
-
kind: DestinationKind,
|
|
17633
|
-
name: external_exports.string(),
|
|
17634
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17635
|
-
host: external_exports.string(),
|
|
17636
|
-
trust: ShareTrustLevel,
|
|
17637
|
-
status: EgressStatus,
|
|
17638
|
-
review: ReviewInfo,
|
|
17639
|
-
topDataClass: DataClass,
|
|
17640
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17641
|
-
lastSeen: external_exports.iso.datetime()
|
|
17642
|
-
}).meta({ id: "ReviewDestination" });
|
|
17643
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17644
|
-
kind: DestinationKind,
|
|
17645
|
-
total: external_exports.number().int().nonnegative(),
|
|
17646
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17647
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17648
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17649
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17650
|
-
var SharesStats = external_exports.object({
|
|
17651
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17652
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17653
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17654
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17655
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17656
|
-
byKind: external_exports.object({
|
|
17657
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17658
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17659
|
-
ip: external_exports.number().int().nonnegative()
|
|
17660
|
-
}),
|
|
17661
|
-
byTrust: external_exports.object({
|
|
17662
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17663
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17664
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17665
|
-
ip: external_exports.number().int().nonnegative()
|
|
17666
|
-
})
|
|
17667
|
-
}).meta({ id: "SharesStats" });
|
|
17668
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17669
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17670
|
-
decision: EgressDecision.nullable()
|
|
17671
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17672
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17673
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17674
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17675
|
-
q: external_exports.string().optional(),
|
|
17676
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17677
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17678
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17679
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17680
|
-
/**
|
|
17681
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17682
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17683
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17684
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17685
|
-
*/
|
|
17686
|
-
review: external_exports.stringbool().default(false)
|
|
17687
|
-
});
|
|
17688
|
-
var ExportSharesQuery = external_exports.object({
|
|
17689
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17690
|
-
q: external_exports.string().optional(),
|
|
17691
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17692
|
-
});
|
|
17693
|
-
|
|
17694
17887
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17695
17888
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17696
17889
|
function trustDefaultStatus(trust) {
|
|
@@ -17710,7 +17903,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17710
17903
|
const reasons = [];
|
|
17711
17904
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17712
17905
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17713
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17906
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17714
17907
|
return reasons;
|
|
17715
17908
|
}
|
|
17716
17909
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17737,6 +17930,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17737
17930
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17738
17931
|
}
|
|
17739
17932
|
|
|
17933
|
+
// ../../packages/persistence/src/ids.ts
|
|
17934
|
+
import { createHash } from "crypto";
|
|
17935
|
+
function sha256Hex(input) {
|
|
17936
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17937
|
+
}
|
|
17938
|
+
function inventoryId(objectType, identityKey) {
|
|
17939
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17940
|
+
}
|
|
17941
|
+
function sourceProjectId(url2) {
|
|
17942
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17943
|
+
}
|
|
17944
|
+
function classifiedDataId(cls) {
|
|
17945
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17946
|
+
}
|
|
17947
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17948
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17949
|
+
}
|
|
17950
|
+
function llmCallId(sessionId, messageId) {
|
|
17951
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17952
|
+
}
|
|
17953
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17954
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17955
|
+
}
|
|
17956
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17957
|
+
return sha256Hex(
|
|
17958
|
+
canonicalIdentity([
|
|
17959
|
+
"inspection_finding",
|
|
17960
|
+
auditEventId,
|
|
17961
|
+
ruleId,
|
|
17962
|
+
String(spanStart),
|
|
17963
|
+
String(spanEnd)
|
|
17964
|
+
])
|
|
17965
|
+
);
|
|
17966
|
+
}
|
|
17967
|
+
var NO_SESSION = "no_session";
|
|
17968
|
+
var NO_PATH = "no_path";
|
|
17969
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17970
|
+
return sha256Hex(
|
|
17971
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17972
|
+
);
|
|
17973
|
+
}
|
|
17974
|
+
|
|
17740
17975
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17741
17976
|
function escapeLikePattern(s) {
|
|
17742
17977
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17833,39 +18068,81 @@ function evidenceExists(db, object2) {
|
|
|
17833
18068
|
return schemaObjectExists(db, "table", object2.name);
|
|
17834
18069
|
}
|
|
17835
18070
|
|
|
17836
|
-
// ../../packages/persistence/src/
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
18071
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18072
|
+
function allRows(stmt, params) {
|
|
18073
|
+
if (params === void 0) return stmt.all();
|
|
18074
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18075
|
+
return stmt.all(params);
|
|
17840
18076
|
}
|
|
17841
|
-
function
|
|
17842
|
-
|
|
18077
|
+
function getRow(stmt, params) {
|
|
18078
|
+
if (params === void 0) return stmt.get();
|
|
18079
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18080
|
+
return stmt.get(params);
|
|
17843
18081
|
}
|
|
17844
|
-
function
|
|
17845
|
-
return
|
|
18082
|
+
function intToBool(raw) {
|
|
18083
|
+
return raw === 1 || raw === true;
|
|
17846
18084
|
}
|
|
17847
|
-
function
|
|
17848
|
-
return
|
|
18085
|
+
function boolToInt(b) {
|
|
18086
|
+
return b ? 1 : 0;
|
|
17849
18087
|
}
|
|
17850
|
-
function
|
|
17851
|
-
|
|
18088
|
+
function bindParams(row) {
|
|
18089
|
+
const out = {};
|
|
18090
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18091
|
+
out[key] = value === void 0 ? null : value;
|
|
18092
|
+
}
|
|
18093
|
+
return out;
|
|
17852
18094
|
}
|
|
17853
|
-
function
|
|
17854
|
-
return
|
|
18095
|
+
function countScalar(db, sql, params) {
|
|
18096
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17855
18097
|
}
|
|
17856
|
-
function
|
|
17857
|
-
|
|
18098
|
+
function countBy(db, sql, params) {
|
|
18099
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18100
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18101
|
+
map2.set(row.k, row.n);
|
|
18102
|
+
}
|
|
18103
|
+
return map2;
|
|
17858
18104
|
}
|
|
17859
|
-
function
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
18105
|
+
function mapRowsTolerant(rows, map2) {
|
|
18106
|
+
const out = [];
|
|
18107
|
+
for (const row of rows) {
|
|
18108
|
+
try {
|
|
18109
|
+
out.push(map2(row));
|
|
18110
|
+
} catch {
|
|
18111
|
+
}
|
|
18112
|
+
}
|
|
18113
|
+
return out;
|
|
18114
|
+
}
|
|
18115
|
+
|
|
18116
|
+
// ../../packages/persistence/src/paths.ts
|
|
18117
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18118
|
+
var DATA_DIR_MODE = 448;
|
|
18119
|
+
var DATA_FILE_MODE = 384;
|
|
18120
|
+
var DB_FILENAME = "aka.db";
|
|
18121
|
+
function chmodBestEffort(path, mode) {
|
|
18122
|
+
try {
|
|
18123
|
+
chmodSync(path, mode);
|
|
18124
|
+
} catch {
|
|
18125
|
+
}
|
|
18126
|
+
}
|
|
18127
|
+
function tightenDir(dir) {
|
|
18128
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18129
|
+
}
|
|
18130
|
+
function ensureDataDirSync(dir) {
|
|
18131
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18132
|
+
tightenDir(dir);
|
|
18133
|
+
}
|
|
18134
|
+
function dbSidecars(file2) {
|
|
18135
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18136
|
+
}
|
|
18137
|
+
function tightenFile(file2) {
|
|
18138
|
+
try {
|
|
18139
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18140
|
+
} catch {
|
|
18141
|
+
}
|
|
18142
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18143
|
+
}
|
|
18144
|
+
function tightenPerms(file2) {
|
|
18145
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
17869
18146
|
}
|
|
17870
18147
|
|
|
17871
18148
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17879,7 +18156,8 @@ function createdIndexName(statement) {
|
|
|
17879
18156
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17880
18157
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17881
18158
|
}
|
|
17882
|
-
|
|
18159
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18160
|
+
function applyMigrations(db, file2) {
|
|
17883
18161
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17884
18162
|
db.exec(
|
|
17885
18163
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17893,6 +18171,7 @@ function applyMigrations(db) {
|
|
|
17893
18171
|
);
|
|
17894
18172
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17895
18173
|
if (applied.has(migration.tag)) continue;
|
|
18174
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17896
18175
|
const evidence = evidenceObjects(migration.sql);
|
|
17897
18176
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17898
18177
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -17937,13 +18216,54 @@ function applyMigrations(db) {
|
|
|
17937
18216
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
17938
18217
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
17939
18218
|
}
|
|
17940
|
-
ensureSyncedAtColumn(db, "events");
|
|
17941
18219
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17942
18220
|
ensureScanLedgerTable(db);
|
|
17943
18221
|
ensureBlockedDetectionsTable(db);
|
|
18222
|
+
ensureRuleProbeCacheTable(db);
|
|
17944
18223
|
ensureWriteGateTrigger(db);
|
|
17945
18224
|
ensureTokenUsageColumns(db);
|
|
17946
18225
|
reconcileSourceProjectIds(db);
|
|
18226
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18227
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18228
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18229
|
+
}
|
|
18230
|
+
}
|
|
18231
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18232
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18233
|
+
if (!migration) return;
|
|
18234
|
+
if (file2) {
|
|
18235
|
+
try {
|
|
18236
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18237
|
+
} catch (error51) {
|
|
18238
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18239
|
+
return;
|
|
18240
|
+
}
|
|
18241
|
+
}
|
|
18242
|
+
try {
|
|
18243
|
+
withTransaction(
|
|
18244
|
+
db,
|
|
18245
|
+
() => {
|
|
18246
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18247
|
+
if (alreadyDropped) return;
|
|
18248
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18249
|
+
db.exec(statement);
|
|
18250
|
+
}
|
|
18251
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18252
|
+
migration.tag,
|
|
18253
|
+
Date.now()
|
|
18254
|
+
);
|
|
18255
|
+
},
|
|
18256
|
+
"IMMEDIATE"
|
|
18257
|
+
);
|
|
18258
|
+
} catch (error51) {
|
|
18259
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18260
|
+
}
|
|
18261
|
+
}
|
|
18262
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18263
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18264
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18265
|
+
tightenFile(backup);
|
|
18266
|
+
return backup;
|
|
17947
18267
|
}
|
|
17948
18268
|
var TOKEN_USAGE_COLUMNS = [
|
|
17949
18269
|
{
|
|
@@ -17972,6 +18292,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17972
18292
|
}
|
|
17973
18293
|
];
|
|
17974
18294
|
function ensureTokenUsageColumns(db) {
|
|
18295
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
17975
18296
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17976
18297
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17977
18298
|
if (!existing.has(column.name)) {
|
|
@@ -18037,11 +18358,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18037
18358
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18038
18359
|
}
|
|
18039
18360
|
}
|
|
18361
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18362
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18363
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18364
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18365
|
+
return row?.lastRowid ?? 0;
|
|
18366
|
+
}
|
|
18367
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18368
|
+
db.prepare(
|
|
18369
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18370
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18371
|
+
).run(source, lastRowid);
|
|
18372
|
+
}
|
|
18373
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18374
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18375
|
+
let processed = 0;
|
|
18376
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18377
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18378
|
+
if (rows.length === 0) return true;
|
|
18379
|
+
withTransaction(
|
|
18380
|
+
db,
|
|
18381
|
+
() => {
|
|
18382
|
+
handleRows(rows);
|
|
18383
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18384
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18385
|
+
},
|
|
18386
|
+
"IMMEDIATE"
|
|
18387
|
+
);
|
|
18388
|
+
processed += rows.length;
|
|
18389
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18390
|
+
}
|
|
18391
|
+
return false;
|
|
18392
|
+
}
|
|
18393
|
+
function parseLegacyEventMetadata(raw) {
|
|
18394
|
+
if (raw === null) return void 0;
|
|
18395
|
+
try {
|
|
18396
|
+
return JSON.parse(raw);
|
|
18397
|
+
} catch {
|
|
18398
|
+
return void 0;
|
|
18399
|
+
}
|
|
18400
|
+
}
|
|
18401
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18402
|
+
return JSON.stringify(
|
|
18403
|
+
toCaptureAttributes({
|
|
18404
|
+
id: row.id,
|
|
18405
|
+
sourceTool: row.sourceTool,
|
|
18406
|
+
kind: row.kind,
|
|
18407
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18408
|
+
contentHash: row.contentHash,
|
|
18409
|
+
content: row.content,
|
|
18410
|
+
metadata: row.metadata
|
|
18411
|
+
})
|
|
18412
|
+
);
|
|
18413
|
+
}
|
|
18414
|
+
function copyLegacyEvents(db) {
|
|
18415
|
+
const selectStmt = db.prepare(
|
|
18416
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18417
|
+
content_hash AS contentHash, content, metadata
|
|
18418
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18419
|
+
);
|
|
18420
|
+
const insertStmt = db.prepare(
|
|
18421
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18422
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18423
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18424
|
+
);
|
|
18425
|
+
const stubRootStmt = db.prepare(
|
|
18426
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18427
|
+
);
|
|
18428
|
+
return drainLegacyTable(
|
|
18429
|
+
db,
|
|
18430
|
+
"events",
|
|
18431
|
+
selectStmt,
|
|
18432
|
+
(rows) => {
|
|
18433
|
+
for (const row of rows) {
|
|
18434
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18435
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18436
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18437
|
+
insertStmt.run(
|
|
18438
|
+
bindParams({
|
|
18439
|
+
id: row.id,
|
|
18440
|
+
parentId: sessionId,
|
|
18441
|
+
rootSessionId: sessionId,
|
|
18442
|
+
eventType: row.kind,
|
|
18443
|
+
startedAt: row.occurredAt,
|
|
18444
|
+
content: row.content,
|
|
18445
|
+
contentHash: row.contentHash,
|
|
18446
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18447
|
+
})
|
|
18448
|
+
);
|
|
18449
|
+
}
|
|
18450
|
+
}
|
|
18451
|
+
);
|
|
18452
|
+
}
|
|
18453
|
+
function copyLegacyFindings(db) {
|
|
18454
|
+
const selectStmt = db.prepare(
|
|
18455
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18456
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18457
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18458
|
+
first_detected_at AS firstDetectedAt
|
|
18459
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18460
|
+
);
|
|
18461
|
+
const definitionStmt = db.prepare(
|
|
18462
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18463
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18464
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18465
|
+
);
|
|
18466
|
+
const findingStmt = db.prepare(
|
|
18467
|
+
`INSERT INTO inspection_findings
|
|
18468
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18469
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18470
|
+
finding_key, first_detected_at)
|
|
18471
|
+
VALUES
|
|
18472
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18473
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18474
|
+
:findingKey, :firstDetectedAt)
|
|
18475
|
+
ON CONFLICT(id) DO NOTHING
|
|
18476
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18477
|
+
first_detected_at = CASE
|
|
18478
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18479
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18480
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18481
|
+
END`
|
|
18482
|
+
);
|
|
18483
|
+
return drainLegacyTable(
|
|
18484
|
+
db,
|
|
18485
|
+
"findings",
|
|
18486
|
+
selectStmt,
|
|
18487
|
+
(rows) => {
|
|
18488
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18489
|
+
for (const row of rows) {
|
|
18490
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18491
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18492
|
+
if (definitionId === void 0) {
|
|
18493
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18494
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18495
|
+
definitionStmt.run(
|
|
18496
|
+
bindParams({
|
|
18497
|
+
id: definitionId,
|
|
18498
|
+
ruleId: row.ruleId,
|
|
18499
|
+
name: row.ruleId,
|
|
18500
|
+
category: row.category,
|
|
18501
|
+
severity: row.severity,
|
|
18502
|
+
definition: "",
|
|
18503
|
+
version: version2
|
|
18504
|
+
})
|
|
18505
|
+
);
|
|
18506
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18507
|
+
}
|
|
18508
|
+
findingStmt.run(
|
|
18509
|
+
bindParams({
|
|
18510
|
+
id: row.id,
|
|
18511
|
+
auditEventId: row.eventId,
|
|
18512
|
+
inspectionDefinitionId: definitionId,
|
|
18513
|
+
spanStart: row.spanStart,
|
|
18514
|
+
spanEnd: row.spanEnd,
|
|
18515
|
+
maskedMatch: row.maskedMatch,
|
|
18516
|
+
actionTaken: row.actionTaken,
|
|
18517
|
+
confidence: row.confidence,
|
|
18518
|
+
findingKey: row.findingKey,
|
|
18519
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18520
|
+
})
|
|
18521
|
+
);
|
|
18522
|
+
}
|
|
18523
|
+
}
|
|
18524
|
+
);
|
|
18525
|
+
}
|
|
18526
|
+
function runLegacyHistoryBackfill(db) {
|
|
18527
|
+
try {
|
|
18528
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18529
|
+
if (!eventsCaughtUp) return false;
|
|
18530
|
+
return copyLegacyFindings(db);
|
|
18531
|
+
} catch (error51) {
|
|
18532
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18533
|
+
return false;
|
|
18534
|
+
}
|
|
18535
|
+
}
|
|
18040
18536
|
function isForeignSqliteLineage(db) {
|
|
18041
18537
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18042
18538
|
return columnNames(db, "events").includes("tenant_id");
|
|
18043
18539
|
}
|
|
18044
18540
|
function ensureSyncedAtColumn(db, table) {
|
|
18541
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
18045
18542
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
18046
18543
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18047
18544
|
}
|
|
@@ -18062,6 +18559,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18062
18559
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18063
18560
|
)`);
|
|
18064
18561
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18562
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18065
18563
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18066
18564
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18067
18565
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18080,29 +18578,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18080
18578
|
blocked_at INTEGER NOT NULL
|
|
18081
18579
|
)`);
|
|
18082
18580
|
}
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18091
|
-
try {
|
|
18092
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18093
|
-
} catch {
|
|
18094
|
-
}
|
|
18095
|
-
}
|
|
18096
|
-
function walSidecars(file2) {
|
|
18097
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18098
|
-
}
|
|
18099
|
-
function tightenPerms(file2) {
|
|
18100
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18101
|
-
try {
|
|
18102
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18103
|
-
} catch {
|
|
18104
|
-
}
|
|
18105
|
-
}
|
|
18581
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18582
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18583
|
+
rule_key TEXT PRIMARY KEY,
|
|
18584
|
+
verdict TEXT NOT NULL,
|
|
18585
|
+
worst_probe_ms REAL NOT NULL,
|
|
18586
|
+
checked_at INTEGER NOT NULL
|
|
18587
|
+
)`);
|
|
18106
18588
|
}
|
|
18107
18589
|
|
|
18108
18590
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18124,51 +18606,6 @@ function parseJsonObject(s) {
|
|
|
18124
18606
|
return void 0;
|
|
18125
18607
|
}
|
|
18126
18608
|
|
|
18127
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18128
|
-
function allRows(stmt, params) {
|
|
18129
|
-
if (params === void 0) return stmt.all();
|
|
18130
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18131
|
-
return stmt.all(params);
|
|
18132
|
-
}
|
|
18133
|
-
function getRow(stmt, params) {
|
|
18134
|
-
if (params === void 0) return stmt.get();
|
|
18135
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18136
|
-
return stmt.get(params);
|
|
18137
|
-
}
|
|
18138
|
-
function intToBool(raw) {
|
|
18139
|
-
return raw === 1 || raw === true;
|
|
18140
|
-
}
|
|
18141
|
-
function boolToInt(b) {
|
|
18142
|
-
return b ? 1 : 0;
|
|
18143
|
-
}
|
|
18144
|
-
function bindParams(row) {
|
|
18145
|
-
const out = {};
|
|
18146
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18147
|
-
out[key] = value === void 0 ? null : value;
|
|
18148
|
-
}
|
|
18149
|
-
return out;
|
|
18150
|
-
}
|
|
18151
|
-
function countScalar(db, sql, params) {
|
|
18152
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18153
|
-
}
|
|
18154
|
-
function countBy(db, sql, params) {
|
|
18155
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18156
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18157
|
-
map2.set(row.k, row.n);
|
|
18158
|
-
}
|
|
18159
|
-
return map2;
|
|
18160
|
-
}
|
|
18161
|
-
function mapRowsTolerant(rows, map2) {
|
|
18162
|
-
const out = [];
|
|
18163
|
-
for (const row of rows) {
|
|
18164
|
-
try {
|
|
18165
|
-
out.push(map2(row));
|
|
18166
|
-
} catch {
|
|
18167
|
-
}
|
|
18168
|
-
}
|
|
18169
|
-
return out;
|
|
18170
|
-
}
|
|
18171
|
-
|
|
18172
18609
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18173
18610
|
var DAY_MS = 864e5;
|
|
18174
18611
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18785,6 +19222,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18785
19222
|
})
|
|
18786
19223
|
);
|
|
18787
19224
|
}
|
|
19225
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19226
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19227
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19228
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19229
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19230
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19231
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19232
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19233
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19234
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19235
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19236
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19237
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19238
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19239
|
+
}
|
|
18788
19240
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18789
19241
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18790
19242
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19246,8 +19698,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19246
19698
|
)
|
|
19247
19699
|
);
|
|
19248
19700
|
}
|
|
19249
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19250
|
-
//
|
|
19701
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19702
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19703
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19704
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19705
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19706
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19707
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19708
|
+
// to the old findings⋈events one.
|
|
19251
19709
|
countFindingsLast30d(ruleIds) {
|
|
19252
19710
|
if (ruleIds.length === 0) return 0;
|
|
19253
19711
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19255,8 +19713,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19255
19713
|
return countScalar(
|
|
19256
19714
|
this.db,
|
|
19257
19715
|
`SELECT count(*) AS n
|
|
19258
|
-
FROM
|
|
19259
|
-
|
|
19716
|
+
FROM inspection_findings f
|
|
19717
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19718
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19719
|
+
WHERE e.started_at >= ?
|
|
19720
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19721
|
+
AND d.rule_id IN (${inClause})`,
|
|
19260
19722
|
[since, ...ruleIds]
|
|
19261
19723
|
);
|
|
19262
19724
|
}
|
|
@@ -19266,35 +19728,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19266
19728
|
var SqliteEventsRepository = class {
|
|
19267
19729
|
constructor(db) {
|
|
19268
19730
|
this.db = db;
|
|
19269
|
-
this.insertStmt = db.prepare(
|
|
19270
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19271
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19272
|
-
);
|
|
19273
19731
|
}
|
|
19274
19732
|
db;
|
|
19275
|
-
|
|
19276
|
-
|
|
19277
|
-
|
|
19278
|
-
this.insertStmt.run(
|
|
19279
|
-
bindParams({
|
|
19280
|
-
id: row.id,
|
|
19281
|
-
sourceTool: row.sourceTool,
|
|
19282
|
-
kind: row.kind,
|
|
19283
|
-
occurredAt: row.occurredAt,
|
|
19284
|
-
contentHash: row.contentHash,
|
|
19285
|
-
content: row.content,
|
|
19286
|
-
metadata: row.metadata
|
|
19287
|
-
})
|
|
19288
|
-
);
|
|
19289
|
-
}
|
|
19290
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19291
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19292
|
-
// never duplicates findings.
|
|
19733
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19734
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19735
|
+
// scan never duplicates findings.
|
|
19293
19736
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19294
19737
|
// async EventsReadPort contract.
|
|
19738
|
+
//
|
|
19739
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19740
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19741
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19742
|
+
// index-friendly rather than walking rows that can never match.
|
|
19295
19743
|
contentHashes() {
|
|
19296
19744
|
const rows = allRows(
|
|
19297
|
-
this.db.prepare(
|
|
19745
|
+
this.db.prepare(
|
|
19746
|
+
`SELECT content_hash FROM audit_events
|
|
19747
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19748
|
+
)
|
|
19298
19749
|
);
|
|
19299
19750
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19300
19751
|
}
|
|
@@ -19630,17 +20081,20 @@ function parseExceptionRow(row) {
|
|
|
19630
20081
|
}
|
|
19631
20082
|
|
|
19632
20083
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19633
|
-
function
|
|
20084
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19634
20085
|
return `(
|
|
19635
|
-
SELECT fr
|
|
20086
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19636
20087
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19637
20088
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19638
20089
|
LIMIT 1
|
|
19639
20090
|
)`;
|
|
19640
20091
|
}
|
|
20092
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20093
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20094
|
+
}
|
|
19641
20095
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19642
|
-
SELECT finding_key, status FROM (
|
|
19643
|
-
SELECT fr.finding_key, fr.status,
|
|
20096
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20097
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19644
20098
|
ROW_NUMBER() OVER (
|
|
19645
20099
|
PARTITION BY fr.finding_key
|
|
19646
20100
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19667,68 +20121,21 @@ var DAY_MS3 = 864e5;
|
|
|
19667
20121
|
var SqliteFindingsRepository = class {
|
|
19668
20122
|
constructor(db) {
|
|
19669
20123
|
this.db = db;
|
|
19670
|
-
this.insertStmt = db.prepare(
|
|
19671
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19672
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19673
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19674
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19675
|
-
event_id = excluded.event_id,
|
|
19676
|
-
category = excluded.category,
|
|
19677
|
-
severity = excluded.severity,
|
|
19678
|
-
span_start = excluded.span_start,
|
|
19679
|
-
span_end = excluded.span_end,
|
|
19680
|
-
masked_match = excluded.masked_match,
|
|
19681
|
-
action_taken = excluded.action_taken,
|
|
19682
|
-
confidence = excluded.confidence`
|
|
19683
|
-
);
|
|
19684
|
-
this.sessionDupStmt = db.prepare(
|
|
19685
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19686
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19687
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19688
|
-
LIMIT 1`
|
|
19689
|
-
);
|
|
19690
20124
|
}
|
|
19691
20125
|
db;
|
|
19692
|
-
insertStmt;
|
|
19693
|
-
sessionDupStmt;
|
|
19694
|
-
insertFindings(findings, scope = {}) {
|
|
19695
|
-
for (const finding of findings) {
|
|
19696
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19697
|
-
const row = toFindingRow(finding);
|
|
19698
|
-
this.insertStmt.run({
|
|
19699
|
-
id: row.id,
|
|
19700
|
-
eventId: row.eventId,
|
|
19701
|
-
ruleId: row.ruleId,
|
|
19702
|
-
category: row.category,
|
|
19703
|
-
severity: row.severity,
|
|
19704
|
-
spanStart: row.spanStart,
|
|
19705
|
-
spanEnd: row.spanEnd,
|
|
19706
|
-
maskedMatch: row.maskedMatch,
|
|
19707
|
-
actionTaken: row.actionTaken,
|
|
19708
|
-
confidence: row.confidence,
|
|
19709
|
-
findingKey: row.findingKey ?? null
|
|
19710
|
-
});
|
|
19711
|
-
}
|
|
19712
|
-
}
|
|
19713
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19714
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19715
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19716
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19717
|
-
const hit = this.sessionDupStmt.get({
|
|
19718
|
-
ruleId: finding.ruleId,
|
|
19719
|
-
maskedMatch: finding.maskedMatch,
|
|
19720
|
-
sessionId
|
|
19721
|
-
});
|
|
19722
|
-
return hit !== void 0;
|
|
19723
|
-
}
|
|
19724
20126
|
recentFindings(opts) {
|
|
19725
20127
|
const limit = opts?.limit ?? 50;
|
|
19726
20128
|
const rows = allRows(
|
|
19727
20129
|
this.db.prepare(
|
|
19728
|
-
`SELECT f.id, f.event_id,
|
|
19729
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19730
|
-
|
|
19731
|
-
|
|
20130
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20131
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20132
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20133
|
+
e.event_type AS kind
|
|
20134
|
+
FROM inspection_findings f
|
|
20135
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20136
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20137
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20138
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19732
20139
|
LIMIT :limit`
|
|
19733
20140
|
),
|
|
19734
20141
|
{ limit }
|
|
@@ -19750,25 +20157,34 @@ var SqliteFindingsRepository = class {
|
|
|
19750
20157
|
);
|
|
19751
20158
|
}
|
|
19752
20159
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19753
|
-
* session-stamped
|
|
20160
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19754
20161
|
* page can label its findings link without the grouped pipeline. */
|
|
19755
20162
|
sessionFindingsCount(sessionId) {
|
|
19756
20163
|
if (!sessionId) return Promise.resolve(0);
|
|
19757
20164
|
return Promise.resolve(
|
|
19758
20165
|
countScalar(
|
|
19759
20166
|
this.db,
|
|
19760
|
-
`SELECT count(*) AS n FROM
|
|
19761
|
-
JOIN
|
|
19762
|
-
WHERE
|
|
20167
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20168
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20169
|
+
WHERE e.root_session_id = :sessionId
|
|
20170
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19763
20171
|
{ sessionId }
|
|
19764
20172
|
)
|
|
19765
20173
|
);
|
|
19766
20174
|
}
|
|
19767
|
-
/** Per-rule transcript firing tally for one session —
|
|
19768
|
-
*
|
|
19769
|
-
*
|
|
19770
|
-
*
|
|
19771
|
-
*
|
|
20175
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20176
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20177
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20178
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20179
|
+
* tally with the deduped groups it lists.
|
|
20180
|
+
*
|
|
20181
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20182
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20183
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20184
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20185
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20186
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20187
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19772
20188
|
sessionFirings(sessionId) {
|
|
19773
20189
|
return Object.fromEntries(
|
|
19774
20190
|
countBy(
|
|
@@ -19778,18 +20194,25 @@ var SqliteFindingsRepository = class {
|
|
|
19778
20194
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19779
20195
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19780
20196
|
WHERE e.root_session_id = :sessionId
|
|
20197
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19781
20198
|
GROUP BY d.rule_id`,
|
|
19782
20199
|
{ sessionId }
|
|
19783
20200
|
)
|
|
19784
20201
|
);
|
|
19785
20202
|
}
|
|
19786
20203
|
/**
|
|
19787
|
-
* Grouped findings for the dashboard — joins
|
|
19788
|
-
* toolName from
|
|
19789
|
-
*
|
|
20204
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20205
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20206
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20207
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20208
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20209
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20210
|
+
* severity then recency. Filtering
|
|
19790
20211
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19791
20212
|
* reflect the full filtered set; `items` is the requested
|
|
19792
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20213
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20214
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20215
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19793
20216
|
*
|
|
19794
20217
|
* Two reads, neither of which materializes a row per finding:
|
|
19795
20218
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19802,10 +20225,11 @@ var SqliteFindingsRepository = class {
|
|
|
19802
20225
|
* rule is ever restated in SQL.
|
|
19803
20226
|
*/
|
|
19804
20227
|
listGroupedFindings(query) {
|
|
19805
|
-
const sessionPredicate = query.sessionId ? `
|
|
20228
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20229
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19806
20230
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19807
20231
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19808
|
-
predicate
|
|
20232
|
+
predicate,
|
|
19809
20233
|
params: sessionParams
|
|
19810
20234
|
});
|
|
19811
20235
|
const rows = allRows(
|
|
@@ -19813,24 +20237,26 @@ var SqliteFindingsRepository = class {
|
|
|
19813
20237
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19814
20238
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19815
20239
|
FROM (
|
|
19816
|
-
SELECT f.id AS id,
|
|
19817
|
-
|
|
20240
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20241
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19818
20242
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19819
|
-
e.
|
|
19820
|
-
json_extract(e.
|
|
19821
|
-
json_extract(e.
|
|
19822
|
-
json_extract(e.
|
|
19823
|
-
e.
|
|
20243
|
+
e.started_at AS occurred_at,
|
|
20244
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20245
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20246
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20247
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20248
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19824
20249
|
latest.status AS latest_status,
|
|
19825
20250
|
ROW_NUMBER() OVER (
|
|
19826
|
-
PARTITION BY
|
|
19827
|
-
ORDER BY e.
|
|
20251
|
+
PARTITION BY d.rule_id
|
|
20252
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19828
20253
|
) AS rn
|
|
19829
|
-
FROM
|
|
19830
|
-
JOIN
|
|
20254
|
+
FROM inspection_findings f
|
|
20255
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20256
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19831
20257
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19832
20258
|
ON latest.finding_key = f.finding_key
|
|
19833
|
-
${
|
|
20259
|
+
${predicate}
|
|
19834
20260
|
)
|
|
19835
20261
|
WHERE rn <= :cap
|
|
19836
20262
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19857,17 +20283,29 @@ var SqliteFindingsRepository = class {
|
|
|
19857
20283
|
severity: query.severity,
|
|
19858
20284
|
providers: query.provider,
|
|
19859
20285
|
actions: query.action,
|
|
20286
|
+
statuses: query.status,
|
|
19860
20287
|
subtype: query.subtype,
|
|
19861
20288
|
q: query.q
|
|
19862
20289
|
};
|
|
19863
20290
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19864
20291
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20292
|
+
const statusFilter = query.status ?? [];
|
|
19865
20293
|
const totals = {
|
|
19866
|
-
findings: sorted.reduce((acc, g) =>
|
|
20294
|
+
findings: sorted.reduce((acc, g) => {
|
|
20295
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20296
|
+
const agg = aggregates.get(g.id);
|
|
20297
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20298
|
+
}, 0),
|
|
19867
20299
|
groups: sorted.length
|
|
19868
20300
|
};
|
|
19869
20301
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19870
|
-
const
|
|
20302
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20303
|
+
const items = sorted.slice(0, limit).map(
|
|
20304
|
+
(g) => statusSet ? {
|
|
20305
|
+
...g,
|
|
20306
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20307
|
+
} : g
|
|
20308
|
+
);
|
|
19871
20309
|
return Promise.resolve({
|
|
19872
20310
|
totals,
|
|
19873
20311
|
facets,
|
|
@@ -19881,45 +20319,62 @@ var SqliteFindingsRepository = class {
|
|
|
19881
20319
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19882
20320
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19883
20321
|
*
|
|
19884
|
-
*
|
|
19885
|
-
*
|
|
19886
|
-
*
|
|
19887
|
-
* status
|
|
19888
|
-
*
|
|
19889
|
-
*
|
|
20322
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20323
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20324
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20325
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20326
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20327
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20328
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20329
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20330
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20331
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20332
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19890
20333
|
* a group's row stays small however many findings it holds.
|
|
19891
20334
|
*
|
|
19892
20335
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19893
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19894
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19895
|
-
* that is a string proportional to the store (~8MB over
|
|
19896
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19897
|
-
* match an instance outside the preview, which searching
|
|
19898
|
-
* would silently lose, so it is fetched only when the
|
|
19899
|
-
* carries a `q`.
|
|
20336
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20337
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20338
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20339
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20340
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20341
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20342
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20343
|
+
* path repeating across tuples.)
|
|
19900
20344
|
*/
|
|
19901
20345
|
groupAggregates(withSearchText, scope) {
|
|
19902
|
-
const
|
|
19903
|
-
group_concat(DISTINCT json_extract(e.
|
|
19904
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20346
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20347
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20348
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19905
20349
|
const rows = this.db.prepare(
|
|
19906
|
-
`SELECT
|
|
19907
|
-
|
|
19908
|
-
max(
|
|
19909
|
-
group_concat(
|
|
19910
|
-
group_concat(
|
|
19911
|
-
group_concat(
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
20350
|
+
`SELECT rule_id,
|
|
20351
|
+
sum(tuple_count) AS instance_count,
|
|
20352
|
+
max(latest_at) AS latest_at,
|
|
20353
|
+
group_concat(source_tools) AS source_tools,
|
|
20354
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20355
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20356
|
+
group_concat(repos) AS repos,
|
|
20357
|
+
group_concat(files) AS files,
|
|
20358
|
+
group_concat(tool_names) AS tool_names
|
|
20359
|
+
FROM (
|
|
20360
|
+
SELECT d.rule_id AS rule_id,
|
|
20361
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20362
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20363
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20364
|
+
count(*) AS tuple_count,
|
|
20365
|
+
max(e.started_at) AS latest_at,
|
|
20366
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20367
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20368
|
+
${innerSearchColumns}
|
|
20369
|
+
FROM inspection_findings f
|
|
20370
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20371
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20372
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20373
|
+
ON latest.finding_key = f.finding_key
|
|
20374
|
+
${scope.predicate}
|
|
20375
|
+
GROUP BY d.rule_id, status_tuple
|
|
20376
|
+
)
|
|
20377
|
+
GROUP BY rule_id`
|
|
19923
20378
|
).all(scope.params);
|
|
19924
20379
|
return new Map(
|
|
19925
20380
|
rows.map((r) => [
|
|
@@ -19929,13 +20384,14 @@ var SqliteFindingsRepository = class {
|
|
|
19929
20384
|
sourceTools: splitConcat(r.source_tools),
|
|
19930
20385
|
actionsTaken: splitConcat(r.actions_taken),
|
|
19931
20386
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19932
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20387
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
19933
20388
|
return {
|
|
19934
20389
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19935
20390
|
// so the marker stands in for the key itself (never rendered).
|
|
19936
20391
|
kind,
|
|
19937
20392
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19938
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20393
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20394
|
+
count: Number(count)
|
|
19939
20395
|
};
|
|
19940
20396
|
}),
|
|
19941
20397
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -19952,10 +20408,21 @@ var SqliteFindingsRepository = class {
|
|
|
19952
20408
|
);
|
|
19953
20409
|
}
|
|
19954
20410
|
healthSummary() {
|
|
19955
|
-
const total = countScalar(
|
|
20411
|
+
const total = countScalar(
|
|
20412
|
+
this.db,
|
|
20413
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20414
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20415
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20416
|
+
);
|
|
19956
20417
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19957
20418
|
const grouped = allRows(
|
|
19958
|
-
this.db.prepare(
|
|
20419
|
+
this.db.prepare(
|
|
20420
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20421
|
+
FROM inspection_findings f
|
|
20422
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20423
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20424
|
+
GROUP BY f.action_taken`
|
|
20425
|
+
)
|
|
19959
20426
|
);
|
|
19960
20427
|
for (const row of grouped) {
|
|
19961
20428
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -19963,12 +20430,15 @@ var SqliteFindingsRepository = class {
|
|
|
19963
20430
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19964
20431
|
const sevRows = allRows(
|
|
19965
20432
|
this.db.prepare(
|
|
19966
|
-
`SELECT
|
|
19967
|
-
FROM
|
|
20433
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20434
|
+
FROM inspection_findings f
|
|
20435
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20436
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19968
20437
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19969
20438
|
ON latest.finding_key = f.finding_key
|
|
19970
|
-
WHERE
|
|
19971
|
-
|
|
20439
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20440
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20441
|
+
GROUP BY d.severity`
|
|
19972
20442
|
)
|
|
19973
20443
|
);
|
|
19974
20444
|
for (const row of sevRows) {
|
|
@@ -19989,9 +20459,11 @@ var SqliteFindingsRepository = class {
|
|
|
19989
20459
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19990
20460
|
const rows = allRows(
|
|
19991
20461
|
this.db.prepare(
|
|
19992
|
-
`SELECT date(e.
|
|
19993
|
-
FROM
|
|
19994
|
-
|
|
20462
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20463
|
+
FROM inspection_findings f
|
|
20464
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20465
|
+
WHERE e.started_at >= :since
|
|
20466
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19995
20467
|
GROUP BY day, f.action_taken`
|
|
19996
20468
|
),
|
|
19997
20469
|
{ since }
|
|
@@ -20056,15 +20528,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20056
20528
|
this.insertStmt = db.prepare(
|
|
20057
20529
|
`INSERT INTO inspection_findings
|
|
20058
20530
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20059
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20531
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20532
|
+
finding_key, first_detected_at)
|
|
20060
20533
|
VALUES
|
|
20061
20534
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20062
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20063
|
-
|
|
20535
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20536
|
+
:findingKey,
|
|
20537
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20538
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20539
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20540
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20541
|
+
audit_event_id = excluded.audit_event_id,
|
|
20542
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20543
|
+
classified_data_id = excluded.classified_data_id,
|
|
20544
|
+
span_start = excluded.span_start,
|
|
20545
|
+
span_end = excluded.span_end,
|
|
20546
|
+
masked_match = excluded.masked_match,
|
|
20547
|
+
action_taken = excluded.action_taken,
|
|
20548
|
+
confidence = excluded.confidence`
|
|
20549
|
+
);
|
|
20550
|
+
this.sessionDupStmt = db.prepare(
|
|
20551
|
+
`SELECT 1 FROM inspection_findings f
|
|
20552
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20553
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20554
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20555
|
+
AND e.root_session_id = :sessionId
|
|
20556
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20557
|
+
LIMIT 1`
|
|
20558
|
+
);
|
|
20559
|
+
this.eventDupStmt = db.prepare(
|
|
20560
|
+
`SELECT 1 FROM inspection_findings f
|
|
20561
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20562
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20563
|
+
AND f.masked_match = :maskedMatch
|
|
20564
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20565
|
+
LIMIT 1`
|
|
20064
20566
|
);
|
|
20065
20567
|
}
|
|
20066
20568
|
db;
|
|
20067
20569
|
insertStmt;
|
|
20570
|
+
sessionDupStmt;
|
|
20571
|
+
eventDupStmt;
|
|
20572
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20573
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20574
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20575
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20576
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20577
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20578
|
+
}
|
|
20579
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20580
|
+
// recorded against the given audit event.
|
|
20581
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20582
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20583
|
+
}
|
|
20068
20584
|
insertFinding(input) {
|
|
20069
20585
|
const row = toInspectionFindingRow(input);
|
|
20070
20586
|
this.insertStmt.run(
|
|
@@ -20077,7 +20593,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20077
20593
|
spanEnd: row.spanEnd,
|
|
20078
20594
|
maskedMatch: row.maskedMatch,
|
|
20079
20595
|
actionTaken: row.actionTaken,
|
|
20080
|
-
confidence: row.confidence
|
|
20596
|
+
confidence: row.confidence,
|
|
20597
|
+
findingKey: row.findingKey,
|
|
20598
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20081
20599
|
})
|
|
20082
20600
|
);
|
|
20083
20601
|
}
|
|
@@ -20349,7 +20867,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20349
20867
|
installedRuleset() {
|
|
20350
20868
|
const rows = allRows(
|
|
20351
20869
|
this.db.prepare(
|
|
20352
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20870
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20353
20871
|
)
|
|
20354
20872
|
);
|
|
20355
20873
|
const out = {
|
|
@@ -20357,7 +20875,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20357
20875
|
enabledPacks: 0,
|
|
20358
20876
|
rules: [],
|
|
20359
20877
|
invalidRules: 0,
|
|
20360
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20878
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20879
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20361
20880
|
};
|
|
20362
20881
|
for (const row of rows) {
|
|
20363
20882
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20379,6 +20898,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20379
20898
|
if (parsed.success) {
|
|
20380
20899
|
out.rules.push(parsed.data);
|
|
20381
20900
|
out.ruleActions.set(parsed.data.id, action);
|
|
20901
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20382
20902
|
} else out.invalidRules += 1;
|
|
20383
20903
|
}
|
|
20384
20904
|
}
|
|
@@ -21553,19 +22073,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21553
22073
|
);
|
|
21554
22074
|
this.openAtRestStmt = db.prepare(
|
|
21555
22075
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21556
|
-
FROM
|
|
21557
|
-
JOIN
|
|
21558
|
-
WHERE e.
|
|
21559
|
-
AND json_extract(e.
|
|
22076
|
+
FROM inspection_findings f
|
|
22077
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22078
|
+
WHERE e.event_type = 'code_change'
|
|
22079
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21560
22080
|
AND f.finding_key IS NOT NULL
|
|
21561
22081
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21562
22082
|
);
|
|
21563
22083
|
this.resolvedAtRestStmt = db.prepare(
|
|
21564
22084
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21565
|
-
FROM
|
|
21566
|
-
JOIN
|
|
21567
|
-
WHERE e.
|
|
21568
|
-
AND json_extract(e.
|
|
22085
|
+
FROM inspection_findings f
|
|
22086
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22087
|
+
WHERE e.event_type = 'code_change'
|
|
22088
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21569
22089
|
AND f.finding_key IS NOT NULL
|
|
21570
22090
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21571
22091
|
);
|
|
@@ -21633,6 +22153,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21633
22153
|
}
|
|
21634
22154
|
};
|
|
21635
22155
|
|
|
22156
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22157
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22158
|
+
constructor(db) {
|
|
22159
|
+
this.db = db;
|
|
22160
|
+
this.upsertStmt = db.prepare(
|
|
22161
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22162
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22163
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22164
|
+
verdict = excluded.verdict,
|
|
22165
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22166
|
+
checked_at = excluded.checked_at`
|
|
22167
|
+
);
|
|
22168
|
+
this.readStmt = db.prepare(
|
|
22169
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22170
|
+
);
|
|
22171
|
+
}
|
|
22172
|
+
db;
|
|
22173
|
+
upsertStmt;
|
|
22174
|
+
readStmt;
|
|
22175
|
+
getVerdict(ruleKey) {
|
|
22176
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22177
|
+
}
|
|
22178
|
+
setVerdict(ruleKey, verdict, worstProbeMs) {
|
|
22179
|
+
failOpenTransaction(this.db, () => {
|
|
22180
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
|
|
22181
|
+
});
|
|
22182
|
+
}
|
|
22183
|
+
};
|
|
22184
|
+
|
|
21636
22185
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21637
22186
|
var SqliteScanLedgerRepository = class {
|
|
21638
22187
|
constructor(db) {
|
|
@@ -21759,25 +22308,27 @@ var SqliteSecurityRepository = class {
|
|
|
21759
22308
|
severitySummary() {
|
|
21760
22309
|
const rows = allRows(
|
|
21761
22310
|
this.db.prepare(
|
|
21762
|
-
`SELECT
|
|
22311
|
+
`SELECT d.severity AS severity,
|
|
21763
22312
|
COUNT(*) AS count,
|
|
21764
22313
|
SUM(CASE
|
|
21765
|
-
WHEN e.
|
|
22314
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21766
22315
|
WHEN f.finding_key IS NULL THEN 0
|
|
21767
22316
|
WHEN latest.status = 'resolved' THEN 1
|
|
21768
22317
|
ELSE 0
|
|
21769
22318
|
END) AS caught,
|
|
21770
22319
|
SUM(CASE
|
|
21771
|
-
WHEN e.
|
|
22320
|
+
WHEN e.event_type = 'code_change'
|
|
21772
22321
|
AND f.finding_key IS NOT NULL
|
|
21773
22322
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21774
22323
|
ELSE 0
|
|
21775
22324
|
END) AS open_at_rest
|
|
21776
|
-
FROM
|
|
21777
|
-
JOIN
|
|
22325
|
+
FROM inspection_findings f
|
|
22326
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22327
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21778
22328
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21779
22329
|
ON latest.finding_key = f.finding_key
|
|
21780
|
-
|
|
22330
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22331
|
+
GROUP BY d.severity`
|
|
21781
22332
|
)
|
|
21782
22333
|
);
|
|
21783
22334
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21843,7 +22394,7 @@ var SqliteSecurityRepository = class {
|
|
|
21843
22394
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21844
22395
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21845
22396
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21846
|
-
// (
|
|
22397
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21847
22398
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21848
22399
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21849
22400
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21868,30 +22419,20 @@ var SqliteSecurityRepository = class {
|
|
|
21868
22419
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21869
22420
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21870
22421
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21871
|
-
//
|
|
21872
|
-
// the parent event's
|
|
21873
|
-
// backfill left null.
|
|
21874
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21875
|
-
|
|
21876
|
-
|
|
21877
|
-
|
|
21878
|
-
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
WHERE fr.finding_key = f.finding_key
|
|
21884
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21885
|
-
LIMIT 1
|
|
21886
|
-
) AS latest_method,
|
|
21887
|
-
(
|
|
21888
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21889
|
-
WHERE fr.finding_key = f.finding_key
|
|
21890
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21891
|
-
LIMIT 1
|
|
21892
|
-
) AS latest_resolved_at
|
|
21893
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22422
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22423
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22424
|
+
// legacy/edge row the backfill left null.
|
|
22425
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22426
|
+
latest.status AS latest_status,
|
|
22427
|
+
latest.method AS latest_method,
|
|
22428
|
+
latest.resolved_at AS latest_resolved_at
|
|
22429
|
+
FROM inspection_findings f
|
|
22430
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22431
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22432
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22433
|
+
ON latest.finding_key = f.finding_key
|
|
21894
22434
|
WHERE f.finding_key IS NOT NULL
|
|
22435
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21895
22436
|
AND EXISTS (
|
|
21896
22437
|
SELECT 1 FROM finding_resolution fr
|
|
21897
22438
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21938,11 +22479,13 @@ var SqliteSecurityRepository = class {
|
|
|
21938
22479
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21939
22480
|
const rows = allRows(
|
|
21940
22481
|
this.db.prepare(
|
|
21941
|
-
`SELECT json_extract(e.
|
|
21942
|
-
FROM
|
|
21943
|
-
|
|
21944
|
-
|
|
21945
|
-
AND
|
|
22482
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22483
|
+
FROM inspection_findings f
|
|
22484
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22485
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22486
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22487
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22488
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
21946
22489
|
GROUP BY repo
|
|
21947
22490
|
ORDER BY c DESC, repo
|
|
21948
22491
|
LIMIT :limit`
|
|
@@ -21966,44 +22509,28 @@ var SqliteSecurityRepository = class {
|
|
|
21966
22509
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
21967
22510
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
21968
22511
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
21969
|
-
// finding's parent event (
|
|
21970
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
21971
|
-
// capped at `limit`.
|
|
22512
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22513
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22514
|
+
// DESC, capped at `limit`.
|
|
21972
22515
|
recentlyResolved(limit = 20) {
|
|
21973
22516
|
const rows = allRows(
|
|
21974
22517
|
this.db.prepare(
|
|
21975
22518
|
`SELECT f.finding_key AS finding_key,
|
|
21976
|
-
|
|
21977
|
-
|
|
21978
|
-
json_extract(e.
|
|
21979
|
-
COALESCE(f.first_detected_at, e.
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
WHERE e.kind = 'code_change'
|
|
22519
|
+
d.rule_id AS rule_id,
|
|
22520
|
+
d.severity AS severity,
|
|
22521
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22522
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22523
|
+
latest.resolved_at AS latest_resolved_at
|
|
22524
|
+
FROM inspection_findings f
|
|
22525
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22526
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22527
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22528
|
+
ON latest.finding_key = f.finding_key
|
|
22529
|
+
WHERE e.event_type = 'code_change'
|
|
21988
22530
|
AND f.finding_key IS NOT NULL
|
|
21989
|
-
AND
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21993
|
-
LIMIT 1
|
|
21994
|
-
) = 'resolved'
|
|
21995
|
-
AND (
|
|
21996
|
-
SELECT fr.method FROM finding_resolution fr
|
|
21997
|
-
WHERE fr.finding_key = f.finding_key
|
|
21998
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21999
|
-
LIMIT 1
|
|
22000
|
-
) = 'fixed-at-source'
|
|
22001
|
-
AND (
|
|
22002
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22003
|
-
WHERE fr.finding_key = f.finding_key
|
|
22004
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22005
|
-
LIMIT 1
|
|
22006
|
-
) IS NOT NULL
|
|
22531
|
+
AND latest.status = 'resolved'
|
|
22532
|
+
AND latest.method = 'fixed-at-source'
|
|
22533
|
+
AND latest.resolved_at IS NOT NULL
|
|
22007
22534
|
ORDER BY latest_resolved_at DESC
|
|
22008
22535
|
LIMIT :limit`
|
|
22009
22536
|
),
|
|
@@ -22022,15 +22549,18 @@ var SqliteSecurityRepository = class {
|
|
|
22022
22549
|
return Promise.resolve({ items });
|
|
22023
22550
|
}
|
|
22024
22551
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22025
|
-
// epoch-millis timestamp.
|
|
22552
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22026
22553
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22027
22554
|
findingsInRange(fromMs, toMs) {
|
|
22028
22555
|
const rows = allRows(
|
|
22029
22556
|
this.db.prepare(
|
|
22030
|
-
`SELECT e.
|
|
22031
|
-
FROM
|
|
22032
|
-
|
|
22033
|
-
|
|
22557
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22558
|
+
FROM inspection_findings f
|
|
22559
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22560
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22561
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22562
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22563
|
+
ORDER BY e.started_at`
|
|
22034
22564
|
),
|
|
22035
22565
|
{ from: fromMs, to: toMs }
|
|
22036
22566
|
);
|
|
@@ -22044,11 +22574,50 @@ var SqliteSecurityRepository = class {
|
|
|
22044
22574
|
|
|
22045
22575
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22046
22576
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22047
|
-
var
|
|
22577
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22578
|
+
var IN_CHUNK = 500;
|
|
22579
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22580
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22581
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22582
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22048
22583
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22049
22584
|
function parseNetwork(networkJson) {
|
|
22050
22585
|
return safeJson(networkJson, null);
|
|
22051
22586
|
}
|
|
22587
|
+
function capHits(all, mode) {
|
|
22588
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22589
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22590
|
+
}
|
|
22591
|
+
if (mode === "walk") {
|
|
22592
|
+
return {
|
|
22593
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22594
|
+
droppedFiles: [],
|
|
22595
|
+
truncated: true
|
|
22596
|
+
};
|
|
22597
|
+
}
|
|
22598
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22599
|
+
for (const hit of all) {
|
|
22600
|
+
const bucket = byFile.get(hit.site.file);
|
|
22601
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22602
|
+
else bucket.push(hit);
|
|
22603
|
+
}
|
|
22604
|
+
const hits = [];
|
|
22605
|
+
const droppedFiles = [];
|
|
22606
|
+
for (const [file2, bucket] of byFile) {
|
|
22607
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22608
|
+
else hits.push(...bucket);
|
|
22609
|
+
}
|
|
22610
|
+
return { hits, droppedFiles, truncated: true };
|
|
22611
|
+
}
|
|
22612
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22613
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22614
|
+
const dropped = new Set(droppedFiles);
|
|
22615
|
+
return {
|
|
22616
|
+
mode: "ledger",
|
|
22617
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22618
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22619
|
+
};
|
|
22620
|
+
}
|
|
22052
22621
|
function toEndpointSummary(row) {
|
|
22053
22622
|
return {
|
|
22054
22623
|
id: row.id,
|
|
@@ -22139,13 +22708,15 @@ var SqliteSharesRepository = class {
|
|
|
22139
22708
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22140
22709
|
const insecure = countScalar(
|
|
22141
22710
|
this.db,
|
|
22142
|
-
|
|
22711
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22712
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22143
22713
|
);
|
|
22144
22714
|
const needsReview = countScalar(
|
|
22145
22715
|
this.db,
|
|
22146
22716
|
`SELECT count(DISTINCT d.id) AS n
|
|
22147
22717
|
FROM share_destination d
|
|
22148
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22718
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22719
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22149
22720
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22150
22721
|
);
|
|
22151
22722
|
const kindCounts = countBy(
|
|
@@ -22155,6 +22726,7 @@ var SqliteSharesRepository = class {
|
|
|
22155
22726
|
const byKind = {
|
|
22156
22727
|
provider: kindCounts.get("provider") ?? 0,
|
|
22157
22728
|
internal: kindCounts.get("internal") ?? 0,
|
|
22729
|
+
external: kindCounts.get("external") ?? 0,
|
|
22158
22730
|
ip: kindCounts.get("ip") ?? 0
|
|
22159
22731
|
};
|
|
22160
22732
|
const trustCounts = countBy(
|
|
@@ -22230,23 +22802,316 @@ var SqliteSharesRepository = class {
|
|
|
22230
22802
|
// real edit from a no-such-destination.
|
|
22231
22803
|
/**
|
|
22232
22804
|
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22233
|
-
* `null` deletes the override
|
|
22805
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22806
|
+
*
|
|
22807
|
+
* The written row carries both the destination id and its host, so the
|
|
22808
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22809
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22810
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22811
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22812
|
+
* would otherwise race a concurrent prune.
|
|
22234
22813
|
*/
|
|
22235
22814
|
setEgressDecision(destinationId, decision) {
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22815
|
+
let existed = false;
|
|
22816
|
+
withTransaction(
|
|
22817
|
+
this.db,
|
|
22818
|
+
() => {
|
|
22819
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22820
|
+
if (dest === void 0) return;
|
|
22821
|
+
existed = true;
|
|
22822
|
+
this.db.prepare(
|
|
22823
|
+
`DELETE FROM egress_decision_override
|
|
22824
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22825
|
+
).run({ host: dest.host, destinationId });
|
|
22826
|
+
if (decision === null) return;
|
|
22827
|
+
this.db.prepare(
|
|
22828
|
+
`INSERT INTO egress_decision_override
|
|
22829
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22830
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22831
|
+
).run({
|
|
22832
|
+
id: randomUUID7(),
|
|
22833
|
+
destinationId,
|
|
22834
|
+
host: dest.host,
|
|
22835
|
+
decision,
|
|
22836
|
+
now: Date.now()
|
|
22837
|
+
});
|
|
22838
|
+
},
|
|
22839
|
+
"IMMEDIATE"
|
|
22840
|
+
);
|
|
22841
|
+
return existed;
|
|
22842
|
+
}
|
|
22843
|
+
/**
|
|
22844
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22845
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22846
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22847
|
+
* references, and drop what no longer has evidence.
|
|
22848
|
+
*
|
|
22849
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22850
|
+
* display payload and never scope a delete. The whole write is one
|
|
22851
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22852
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22853
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22854
|
+
* ledger commit so the next scan retries.
|
|
22855
|
+
*
|
|
22856
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22857
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22858
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22859
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22860
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22861
|
+
*/
|
|
22862
|
+
recordProjectEgress(input) {
|
|
22863
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22864
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22865
|
+
const now = Date.now();
|
|
22866
|
+
let summary = {
|
|
22867
|
+
destinations: 0,
|
|
22868
|
+
endpoints: 0,
|
|
22869
|
+
callSites: 0,
|
|
22870
|
+
truncated,
|
|
22871
|
+
droppedFiles
|
|
22872
|
+
};
|
|
22873
|
+
withTransaction(
|
|
22874
|
+
this.db,
|
|
22875
|
+
() => {
|
|
22876
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22877
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22878
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22879
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22880
|
+
this.pruneOrphans();
|
|
22881
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22882
|
+
},
|
|
22883
|
+
"IMMEDIATE"
|
|
22884
|
+
);
|
|
22885
|
+
return summary;
|
|
22886
|
+
}
|
|
22887
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22888
|
+
/**
|
|
22889
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22890
|
+
*
|
|
22891
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22892
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22893
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22894
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22895
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22896
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22897
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22898
|
+
*/
|
|
22899
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22900
|
+
if (reconcile.mode === "walk") {
|
|
22901
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22902
|
+
this.db.prepare(
|
|
22903
|
+
`DELETE FROM share_call_site
|
|
22904
|
+
WHERE project_key = :key
|
|
22905
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22906
|
+
AND file NOT LIKE '.%'
|
|
22907
|
+
AND file NOT LIKE '%/.%'`
|
|
22908
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22909
|
+
return;
|
|
22910
|
+
}
|
|
22911
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22912
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22913
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22914
|
+
this.db.prepare(
|
|
22915
|
+
`DELETE FROM share_call_site
|
|
22916
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22917
|
+
).run(projectKey, ...chunk);
|
|
22918
|
+
}
|
|
22919
|
+
}
|
|
22920
|
+
/**
|
|
22921
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22922
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22923
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22924
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22925
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22926
|
+
* classification for this batch.
|
|
22927
|
+
*/
|
|
22928
|
+
upsertHits(input, hits, projectId, now) {
|
|
22929
|
+
if (hits.length === 0) return;
|
|
22930
|
+
const destStmt = this.db.prepare(
|
|
22931
|
+
`INSERT INTO share_destination
|
|
22932
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22933
|
+
created_at, updated_at)
|
|
22934
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22935
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22936
|
+
kind = excluded.kind,
|
|
22937
|
+
name = excluded.name,
|
|
22938
|
+
category = excluded.category,
|
|
22939
|
+
trust = excluded.trust,
|
|
22940
|
+
network_json = excluded.network_json,
|
|
22941
|
+
last_seen = excluded.last_seen,
|
|
22942
|
+
updated_at = excluded.updated_at`
|
|
22943
|
+
);
|
|
22944
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22945
|
+
const endpointStmt = this.db.prepare(
|
|
22946
|
+
`INSERT INTO share_endpoint
|
|
22947
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
22948
|
+
created_at, updated_at)
|
|
22949
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
22950
|
+
:now, :now)
|
|
22951
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
22952
|
+
transport = excluded.transport,
|
|
22953
|
+
template = excluded.template,
|
|
22954
|
+
data_class = excluded.data_class,
|
|
22955
|
+
last_seen = excluded.last_seen,
|
|
22956
|
+
updated_at = excluded.updated_at`
|
|
22957
|
+
);
|
|
22958
|
+
const endpointIdStmt = this.db.prepare(
|
|
22959
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
22960
|
+
);
|
|
22961
|
+
const siteStmt = this.db.prepare(
|
|
22962
|
+
`INSERT INTO share_call_site
|
|
22963
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
22964
|
+
project_id, created_at, updated_at)
|
|
22965
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
22966
|
+
:vendored, :projectId, :now, :now)
|
|
22967
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
22968
|
+
snippet = excluded.snippet,
|
|
22969
|
+
dynamic = excluded.dynamic,
|
|
22970
|
+
vendored = excluded.vendored,
|
|
22971
|
+
project = excluded.project,
|
|
22972
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
22973
|
+
updated_at = excluded.updated_at`
|
|
22974
|
+
);
|
|
22975
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
22976
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
22977
|
+
for (const hit of hits) {
|
|
22978
|
+
let destinationId = destIds.get(hit.host);
|
|
22979
|
+
if (destinationId === void 0) {
|
|
22980
|
+
destStmt.run({
|
|
22981
|
+
id: randomUUID7(),
|
|
22982
|
+
kind: hit.kind,
|
|
22983
|
+
name: hit.name,
|
|
22984
|
+
host: hit.host,
|
|
22985
|
+
category: hit.category,
|
|
22986
|
+
trust: hit.trust,
|
|
22987
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
22988
|
+
now
|
|
22989
|
+
});
|
|
22990
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
22991
|
+
destIds.set(hit.host, destinationId);
|
|
22992
|
+
}
|
|
22993
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
22994
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
22995
|
+
if (endpointId === void 0) {
|
|
22996
|
+
endpointStmt.run({
|
|
22997
|
+
id: randomUUID7(),
|
|
22998
|
+
destinationId,
|
|
22999
|
+
method: hit.method,
|
|
23000
|
+
transport: hit.transport,
|
|
23001
|
+
url: hit.url,
|
|
23002
|
+
template: boolToInt(hit.template),
|
|
23003
|
+
dataClass: hit.dataClass,
|
|
23004
|
+
now
|
|
23005
|
+
});
|
|
23006
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23007
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23008
|
+
}
|
|
23009
|
+
siteStmt.run({
|
|
23010
|
+
id: randomUUID7(),
|
|
23011
|
+
endpointId,
|
|
23012
|
+
project: input.project,
|
|
23013
|
+
projectKey: input.projectKey,
|
|
23014
|
+
file: hit.site.file,
|
|
23015
|
+
line: hit.site.line,
|
|
23016
|
+
snippet: hit.site.snippet,
|
|
23017
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23018
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23019
|
+
projectId,
|
|
23020
|
+
now
|
|
23021
|
+
});
|
|
22241
23022
|
}
|
|
23023
|
+
}
|
|
23024
|
+
/**
|
|
23025
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23026
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23027
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23028
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23029
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23030
|
+
* representative.
|
|
23031
|
+
*/
|
|
23032
|
+
knownProjectId(projectKey) {
|
|
23033
|
+
return getRow(
|
|
23034
|
+
this.db.prepare(
|
|
23035
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23036
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23037
|
+
),
|
|
23038
|
+
[projectKey]
|
|
23039
|
+
)?.projectId ?? null;
|
|
23040
|
+
}
|
|
23041
|
+
/**
|
|
23042
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23043
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23044
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
23045
|
+
*/
|
|
23046
|
+
confirmLastSeen(projectKey, now) {
|
|
22242
23047
|
this.db.prepare(
|
|
22243
|
-
`
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
23048
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23049
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23050
|
+
).run({ now, key: projectKey });
|
|
23051
|
+
this.db.prepare(
|
|
23052
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23053
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23054
|
+
FROM share_endpoint e
|
|
23055
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23056
|
+
WHERE c.project_key = :key)`
|
|
23057
|
+
).run({ now, key: projectKey });
|
|
23058
|
+
}
|
|
23059
|
+
/**
|
|
23060
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23061
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23062
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23063
|
+
*
|
|
23064
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23065
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23066
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23067
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23068
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23069
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23070
|
+
*/
|
|
23071
|
+
pruneOrphans() {
|
|
23072
|
+
this.db.exec(
|
|
23073
|
+
`DELETE FROM share_endpoint
|
|
23074
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23075
|
+
);
|
|
23076
|
+
this.db.exec(
|
|
23077
|
+
`DELETE FROM egress_decision_override
|
|
23078
|
+
WHERE host IS NULL
|
|
23079
|
+
AND destination_id IN (
|
|
23080
|
+
SELECT d.id FROM share_destination d
|
|
23081
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23082
|
+
);
|
|
23083
|
+
this.db.exec(
|
|
23084
|
+
`DELETE FROM share_destination
|
|
23085
|
+
WHERE NOT EXISTS (
|
|
23086
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23087
|
+
);
|
|
23088
|
+
}
|
|
23089
|
+
/**
|
|
23090
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23091
|
+
* projects and carry no project column, so both are counted through the call
|
|
23092
|
+
* sites that reference them.
|
|
23093
|
+
*/
|
|
23094
|
+
projectTotals(projectKey) {
|
|
23095
|
+
return {
|
|
23096
|
+
destinations: countScalar(
|
|
23097
|
+
this.db,
|
|
23098
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23099
|
+
FROM share_endpoint e
|
|
23100
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23101
|
+
WHERE c.project_key = ?`,
|
|
23102
|
+
[projectKey]
|
|
23103
|
+
),
|
|
23104
|
+
endpoints: countScalar(
|
|
23105
|
+
this.db,
|
|
23106
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23107
|
+
[projectKey]
|
|
23108
|
+
),
|
|
23109
|
+
callSites: countScalar(
|
|
23110
|
+
this.db,
|
|
23111
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23112
|
+
[projectKey]
|
|
23113
|
+
)
|
|
23114
|
+
};
|
|
22250
23115
|
}
|
|
22251
23116
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22252
23117
|
mapDestRow(r) {
|
|
@@ -22266,7 +23131,8 @@ var SqliteSharesRepository = class {
|
|
|
22266
23131
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22267
23132
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22268
23133
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22269
|
-
d.created_at AS createdAt,
|
|
23134
|
+
d.created_at AS createdAt,
|
|
23135
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22270
23136
|
const conditions = [];
|
|
22271
23137
|
const params = [];
|
|
22272
23138
|
if (kinds && kinds.length > 0) {
|
|
@@ -22277,7 +23143,8 @@ var SqliteSharesRepository = class {
|
|
|
22277
23143
|
conditions.push(
|
|
22278
23144
|
`(d.trust IN ('unverified', 'ip')
|
|
22279
23145
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22280
|
-
WHERE re.destination_id = d.id
|
|
23146
|
+
WHERE re.destination_id = d.id
|
|
23147
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22281
23148
|
);
|
|
22282
23149
|
}
|
|
22283
23150
|
let sql;
|
|
@@ -22290,7 +23157,7 @@ var SqliteSharesRepository = class {
|
|
|
22290
23157
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22291
23158
|
sql = `SELECT DISTINCT ${cols}
|
|
22292
23159
|
FROM share_destination d
|
|
22293
|
-
|
|
23160
|
+
${OVERRIDE_JOIN}
|
|
22294
23161
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22295
23162
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22296
23163
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22298,7 +23165,7 @@ var SqliteSharesRepository = class {
|
|
|
22298
23165
|
} else {
|
|
22299
23166
|
sql = `SELECT ${cols}
|
|
22300
23167
|
FROM share_destination d
|
|
22301
|
-
|
|
23168
|
+
${OVERRIDE_JOIN}
|
|
22302
23169
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22303
23170
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22304
23171
|
}
|
|
@@ -22313,9 +23180,9 @@ var SqliteSharesRepository = class {
|
|
|
22313
23180
|
this.db.prepare(
|
|
22314
23181
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22315
23182
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22316
|
-
|
|
23183
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22317
23184
|
FROM share_destination d
|
|
22318
|
-
|
|
23185
|
+
${OVERRIDE_JOIN}
|
|
22319
23186
|
WHERE d.id = ?`
|
|
22320
23187
|
),
|
|
22321
23188
|
[destinationId]
|
|
@@ -22517,9 +23384,10 @@ function openWithPragmas(file2) {
|
|
|
22517
23384
|
}
|
|
22518
23385
|
function backupLegacyStore(file2) {
|
|
22519
23386
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22520
|
-
|
|
22521
|
-
|
|
22522
|
-
|
|
23387
|
+
renameSync2(file2, backup);
|
|
23388
|
+
tightenFile(backup);
|
|
23389
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23390
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22523
23391
|
}
|
|
22524
23392
|
return backup;
|
|
22525
23393
|
}
|
|
@@ -22535,7 +23403,7 @@ function openLocalDatabase(dir) {
|
|
|
22535
23403
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22536
23404
|
);
|
|
22537
23405
|
}
|
|
22538
|
-
applyMigrations(db);
|
|
23406
|
+
applyMigrations(db, file2);
|
|
22539
23407
|
tightenPerms(file2);
|
|
22540
23408
|
const events = new SqliteEventsRepository(db);
|
|
22541
23409
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22544,6 +23412,7 @@ function openLocalDatabase(dir) {
|
|
|
22544
23412
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22545
23413
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22546
23414
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23415
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22547
23416
|
const security = new SqliteSecurityRepository(db);
|
|
22548
23417
|
const detections = new SqliteDetectionsRepository(db);
|
|
22549
23418
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22561,9 +23430,56 @@ function openLocalDatabase(dir) {
|
|
|
22561
23430
|
policies.seedDefaults();
|
|
22562
23431
|
function recordCapture(event, detected) {
|
|
22563
23432
|
failOpenTransaction(db, () => {
|
|
22564
|
-
events.insertEvent(event);
|
|
22565
23433
|
const sessionId = event.metadata?.sessionId;
|
|
22566
|
-
|
|
23434
|
+
if (sessionId) {
|
|
23435
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23436
|
+
}
|
|
23437
|
+
const auditEventId = captureId(
|
|
23438
|
+
sessionId ?? null,
|
|
23439
|
+
event.contentHash,
|
|
23440
|
+
event.metadata?.filePath ?? null
|
|
23441
|
+
);
|
|
23442
|
+
auditEvents.insertAuditEvent({
|
|
23443
|
+
id: auditEventId,
|
|
23444
|
+
eventType: event.kind,
|
|
23445
|
+
startedAt: event.occurredAt,
|
|
23446
|
+
parentId: sessionId,
|
|
23447
|
+
rootSessionId: sessionId,
|
|
23448
|
+
content: event.content,
|
|
23449
|
+
contentHash: event.contentHash,
|
|
23450
|
+
attributes: toCaptureAttributes(event)
|
|
23451
|
+
});
|
|
23452
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23453
|
+
for (const finding of detected) {
|
|
23454
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23455
|
+
continue;
|
|
23456
|
+
}
|
|
23457
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23458
|
+
auditEventId,
|
|
23459
|
+
finding.ruleId,
|
|
23460
|
+
finding.maskedMatch,
|
|
23461
|
+
finding.span.start,
|
|
23462
|
+
finding.span.end
|
|
23463
|
+
)) {
|
|
23464
|
+
continue;
|
|
23465
|
+
}
|
|
23466
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23467
|
+
let definitionId = definitionIds.get(key);
|
|
23468
|
+
if (!definitionId) {
|
|
23469
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23470
|
+
definitionIds.set(key, definitionId);
|
|
23471
|
+
}
|
|
23472
|
+
inspectionFindings.insertFinding({
|
|
23473
|
+
id: finding.id,
|
|
23474
|
+
auditEventId,
|
|
23475
|
+
inspectionDefinitionId: definitionId,
|
|
23476
|
+
span: finding.span,
|
|
23477
|
+
maskedMatch: finding.maskedMatch,
|
|
23478
|
+
actionTaken: finding.actionTaken,
|
|
23479
|
+
confidence: finding.confidence,
|
|
23480
|
+
findingKey: finding.findingKey ?? void 0
|
|
23481
|
+
});
|
|
23482
|
+
}
|
|
22567
23483
|
});
|
|
22568
23484
|
}
|
|
22569
23485
|
function ensureInventory(ctx) {
|
|
@@ -22681,6 +23597,7 @@ function openLocalDatabase(dir) {
|
|
|
22681
23597
|
scanLedger,
|
|
22682
23598
|
exceptions,
|
|
22683
23599
|
resolutions,
|
|
23600
|
+
ruleProbeCache,
|
|
22684
23601
|
security,
|
|
22685
23602
|
detections,
|
|
22686
23603
|
shares,
|
|
@@ -22710,9 +23627,12 @@ function openLocalDatabase(dir) {
|
|
|
22710
23627
|
};
|
|
22711
23628
|
}
|
|
22712
23629
|
|
|
23630
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23631
|
+
import { createHash as createHash3 } from "crypto";
|
|
23632
|
+
|
|
22713
23633
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22714
23634
|
import { createHmac, randomBytes } from "crypto";
|
|
22715
|
-
import {
|
|
23635
|
+
import { readFileSync } from "fs";
|
|
22716
23636
|
import { join as join2 } from "path";
|
|
22717
23637
|
var KEY_FILENAME = "exception.key";
|
|
22718
23638
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -22749,8 +23669,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
22749
23669
|
}
|
|
22750
23670
|
|
|
22751
23671
|
// ../../packages/persistence/src/local-layout.ts
|
|
22752
|
-
import {
|
|
22753
|
-
import {
|
|
23672
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23673
|
+
import { mkdir } from "fs/promises";
|
|
22754
23674
|
import { homedir } from "os";
|
|
22755
23675
|
import { join as join3 } from "path";
|
|
22756
23676
|
function defaultDataDir() {
|
|
@@ -22765,6 +23685,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22765
23685
|
function dbPath(base = defaultDataDir()) {
|
|
22766
23686
|
return join3(dataDir(base), "aka.db");
|
|
22767
23687
|
}
|
|
23688
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23689
|
+
ensureDataDirSync(dir);
|
|
23690
|
+
}
|
|
22768
23691
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22769
23692
|
const moves = [
|
|
22770
23693
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22772,19 +23695,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22772
23695
|
];
|
|
22773
23696
|
for (const { name, dest } of moves) {
|
|
22774
23697
|
try {
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
}
|
|
22780
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23698
|
+
ensureDataDirSync(dest);
|
|
23699
|
+
const moved = join3(dest, name);
|
|
23700
|
+
renameSync3(join3(base, name), moved);
|
|
23701
|
+
tightenFile(moved);
|
|
22781
23702
|
} catch {
|
|
22782
23703
|
}
|
|
22783
23704
|
}
|
|
22784
23705
|
}
|
|
22785
23706
|
|
|
22786
23707
|
// ../../packages/persistence/src/settings.ts
|
|
22787
|
-
import { readFileSync as readFileSync2
|
|
23708
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22788
23709
|
import { join as join4 } from "path";
|
|
22789
23710
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22790
23711
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22806,7 +23727,7 @@ function readJson(file2) {
|
|
|
22806
23727
|
}
|
|
22807
23728
|
|
|
22808
23729
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22809
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23730
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22810
23731
|
import { join as join5 } from "path";
|
|
22811
23732
|
var MARKER = "warn-era-capped";
|
|
22812
23733
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22814,7 +23735,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22814
23735
|
const marker = join5(dataDir2, MARKER);
|
|
22815
23736
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22816
23737
|
const capped = db.policies.capCategoryActions();
|
|
22817
|
-
|
|
23738
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22818
23739
|
`, { mode: DATA_FILE_MODE });
|
|
22819
23740
|
return { capped };
|
|
22820
23741
|
}
|
|
@@ -22879,6 +23800,12 @@ function providerFromModelId(modelId) {
|
|
|
22879
23800
|
|
|
22880
23801
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22881
23802
|
function loadConfig(base = defaultDataDir()) {
|
|
23803
|
+
try {
|
|
23804
|
+
ensureLayoutDirSync(base);
|
|
23805
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23806
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23807
|
+
} catch {
|
|
23808
|
+
}
|
|
22882
23809
|
migrateLegacyLayout(base);
|
|
22883
23810
|
const settings = readWorkspaceSettings(base);
|
|
22884
23811
|
return {
|
|
@@ -22901,15 +23828,583 @@ function resolveProviderSafe() {
|
|
|
22901
23828
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22902
23829
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22903
23830
|
import { homedir as homedir2 } from "os";
|
|
22904
|
-
import { basename as basename2, join as
|
|
23831
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23832
|
+
|
|
23833
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23834
|
+
var EXTRACTOR_VERSION = "1";
|
|
23835
|
+
var PROVIDER_REGISTRY = [
|
|
23836
|
+
{
|
|
23837
|
+
id: "stripe",
|
|
23838
|
+
name: "Stripe",
|
|
23839
|
+
category: "Payments",
|
|
23840
|
+
hostSuffixes: ["stripe.com"],
|
|
23841
|
+
apiBase: "https://api.stripe.com",
|
|
23842
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23843
|
+
sdks: {
|
|
23844
|
+
npm: ["stripe"],
|
|
23845
|
+
pypi: ["stripe"],
|
|
23846
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23847
|
+
maven: ["com.stripe"],
|
|
23848
|
+
rubygems: ["stripe"],
|
|
23849
|
+
composer: ["stripe/stripe-php"],
|
|
23850
|
+
nuget: ["Stripe.net"]
|
|
23851
|
+
}
|
|
23852
|
+
},
|
|
23853
|
+
{
|
|
23854
|
+
id: "datadog",
|
|
23855
|
+
name: "Datadog",
|
|
23856
|
+
category: "Observability",
|
|
23857
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23858
|
+
apiBase: "https://api.datadoghq.com",
|
|
23859
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23860
|
+
sdks: {
|
|
23861
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23862
|
+
pypi: ["datadog", "ddtrace"],
|
|
23863
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23864
|
+
maven: ["com.datadoghq"],
|
|
23865
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23866
|
+
nuget: ["Datadog.Trace"]
|
|
23867
|
+
}
|
|
23868
|
+
},
|
|
23869
|
+
{
|
|
23870
|
+
id: "newrelic",
|
|
23871
|
+
name: "New Relic",
|
|
23872
|
+
category: "Observability",
|
|
23873
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23874
|
+
apiBase: "https://api.newrelic.com",
|
|
23875
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23876
|
+
sdks: {
|
|
23877
|
+
npm: ["newrelic"],
|
|
23878
|
+
pypi: ["newrelic"],
|
|
23879
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23880
|
+
maven: ["com.newrelic.agent.java"],
|
|
23881
|
+
rubygems: ["newrelic_rpm"],
|
|
23882
|
+
nuget: ["NewRelic.Agent"]
|
|
23883
|
+
}
|
|
23884
|
+
},
|
|
23885
|
+
{
|
|
23886
|
+
id: "sentry",
|
|
23887
|
+
name: "Sentry",
|
|
23888
|
+
category: "Error tracking",
|
|
23889
|
+
hostSuffixes: ["sentry.io"],
|
|
23890
|
+
apiBase: "https://sentry.io",
|
|
23891
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23892
|
+
sdks: {
|
|
23893
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23894
|
+
pypi: ["sentry-sdk"],
|
|
23895
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23896
|
+
maven: ["io.sentry"],
|
|
23897
|
+
rubygems: ["sentry-ruby"],
|
|
23898
|
+
cargo: ["sentry"],
|
|
23899
|
+
composer: ["sentry/sentry"],
|
|
23900
|
+
nuget: ["Sentry"]
|
|
23901
|
+
}
|
|
23902
|
+
},
|
|
23903
|
+
{
|
|
23904
|
+
id: "openai",
|
|
23905
|
+
name: "OpenAI",
|
|
23906
|
+
category: "LLM provider",
|
|
23907
|
+
hostSuffixes: ["openai.com"],
|
|
23908
|
+
apiBase: "https://api.openai.com",
|
|
23909
|
+
defaultDataClasses: ["pii", "source"],
|
|
23910
|
+
sdks: {
|
|
23911
|
+
npm: ["openai"],
|
|
23912
|
+
pypi: ["openai"],
|
|
23913
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23914
|
+
maven: ["com.openai"],
|
|
23915
|
+
rubygems: ["ruby-openai"],
|
|
23916
|
+
cargo: ["async-openai"],
|
|
23917
|
+
composer: ["openai-php/client"],
|
|
23918
|
+
nuget: ["OpenAI"]
|
|
23919
|
+
}
|
|
23920
|
+
},
|
|
23921
|
+
{
|
|
23922
|
+
id: "anthropic",
|
|
23923
|
+
name: "Anthropic",
|
|
23924
|
+
category: "LLM provider",
|
|
23925
|
+
hostSuffixes: ["anthropic.com"],
|
|
23926
|
+
apiBase: "https://api.anthropic.com",
|
|
23927
|
+
defaultDataClasses: ["pii", "source"],
|
|
23928
|
+
sdks: {
|
|
23929
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23930
|
+
pypi: ["anthropic"],
|
|
23931
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23932
|
+
nuget: ["Anthropic.SDK"]
|
|
23933
|
+
}
|
|
23934
|
+
},
|
|
23935
|
+
{
|
|
23936
|
+
id: "aws",
|
|
23937
|
+
name: "Amazon Web Services",
|
|
23938
|
+
category: "Cloud platform",
|
|
23939
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23940
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23941
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23942
|
+
sdks: {
|
|
23943
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23944
|
+
pypi: ["boto3"],
|
|
23945
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23946
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23947
|
+
rubygems: ["aws-sdk-s3"],
|
|
23948
|
+
cargo: ["aws-sdk-s3"],
|
|
23949
|
+
nuget: ["AWSSDK.S3"]
|
|
23950
|
+
}
|
|
23951
|
+
},
|
|
23952
|
+
{
|
|
23953
|
+
id: "gcp",
|
|
23954
|
+
name: "Google Cloud",
|
|
23955
|
+
category: "Cloud platform",
|
|
23956
|
+
hostSuffixes: ["googleapis.com"],
|
|
23957
|
+
apiBase: "https://storage.googleapis.com",
|
|
23958
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23959
|
+
sdks: {
|
|
23960
|
+
npm: ["@google-cloud/storage"],
|
|
23961
|
+
pypi: ["google-cloud-storage"],
|
|
23962
|
+
go: ["cloud.google.com/go"],
|
|
23963
|
+
maven: ["com.google.cloud"],
|
|
23964
|
+
rubygems: ["google-cloud-storage"],
|
|
23965
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
23966
|
+
}
|
|
23967
|
+
},
|
|
23968
|
+
{
|
|
23969
|
+
id: "azure",
|
|
23970
|
+
name: "Microsoft Azure",
|
|
23971
|
+
category: "Cloud platform",
|
|
23972
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
23973
|
+
apiBase: "https://management.azure.com",
|
|
23974
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23975
|
+
sdks: {
|
|
23976
|
+
npm: ["@azure/storage-blob"],
|
|
23977
|
+
pypi: ["azure-storage-blob"],
|
|
23978
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
23979
|
+
maven: ["com.azure"],
|
|
23980
|
+
rubygems: ["azure-storage-blob"],
|
|
23981
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
23982
|
+
}
|
|
23983
|
+
},
|
|
23984
|
+
{
|
|
23985
|
+
id: "slack",
|
|
23986
|
+
name: "Slack",
|
|
23987
|
+
category: "Notifications",
|
|
23988
|
+
hostSuffixes: ["slack.com"],
|
|
23989
|
+
apiBase: "https://slack.com/api",
|
|
23990
|
+
defaultDataClasses: ["logs"],
|
|
23991
|
+
sdks: {
|
|
23992
|
+
npm: ["@slack/web-api"],
|
|
23993
|
+
pypi: ["slack-sdk"],
|
|
23994
|
+
go: ["github.com/slack-go/slack"],
|
|
23995
|
+
maven: ["com.slack.api"],
|
|
23996
|
+
rubygems: ["slack-ruby-client"],
|
|
23997
|
+
composer: ["slack-php/slack-api"],
|
|
23998
|
+
nuget: ["SlackNet"]
|
|
23999
|
+
}
|
|
24000
|
+
},
|
|
24001
|
+
{
|
|
24002
|
+
id: "segment",
|
|
24003
|
+
name: "Segment",
|
|
24004
|
+
category: "Analytics",
|
|
24005
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
24006
|
+
apiBase: "https://api.segment.io",
|
|
24007
|
+
defaultDataClasses: ["customer"],
|
|
24008
|
+
sdks: {
|
|
24009
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
24010
|
+
pypi: ["segment-analytics-python"],
|
|
24011
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
24012
|
+
maven: ["com.segment.analytics.java"],
|
|
24013
|
+
rubygems: ["analytics-ruby"],
|
|
24014
|
+
nuget: ["Analytics"]
|
|
24015
|
+
}
|
|
24016
|
+
},
|
|
24017
|
+
{
|
|
24018
|
+
id: "twilio",
|
|
24019
|
+
name: "Twilio",
|
|
24020
|
+
category: "Communications",
|
|
24021
|
+
hostSuffixes: ["twilio.com"],
|
|
24022
|
+
apiBase: "https://api.twilio.com",
|
|
24023
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24024
|
+
sdks: {
|
|
24025
|
+
npm: ["twilio"],
|
|
24026
|
+
pypi: ["twilio"],
|
|
24027
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24028
|
+
maven: ["com.twilio.sdk"],
|
|
24029
|
+
rubygems: ["twilio-ruby"],
|
|
24030
|
+
composer: ["twilio/sdk"],
|
|
24031
|
+
nuget: ["Twilio"]
|
|
24032
|
+
}
|
|
24033
|
+
},
|
|
24034
|
+
{
|
|
24035
|
+
id: "sendgrid",
|
|
24036
|
+
name: "SendGrid",
|
|
24037
|
+
category: "Email",
|
|
24038
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24039
|
+
apiBase: "https://api.sendgrid.com",
|
|
24040
|
+
defaultDataClasses: ["pii"],
|
|
24041
|
+
sdks: {
|
|
24042
|
+
npm: ["@sendgrid/mail"],
|
|
24043
|
+
pypi: ["sendgrid"],
|
|
24044
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24045
|
+
maven: ["com.sendgrid"],
|
|
24046
|
+
rubygems: ["sendgrid-ruby"],
|
|
24047
|
+
composer: ["sendgrid/sendgrid"],
|
|
24048
|
+
nuget: ["SendGrid"]
|
|
24049
|
+
}
|
|
24050
|
+
},
|
|
24051
|
+
{
|
|
24052
|
+
id: "mailgun",
|
|
24053
|
+
name: "Mailgun",
|
|
24054
|
+
category: "Email",
|
|
24055
|
+
hostSuffixes: ["mailgun.net"],
|
|
24056
|
+
apiBase: "https://api.mailgun.net",
|
|
24057
|
+
defaultDataClasses: ["pii"],
|
|
24058
|
+
sdks: {
|
|
24059
|
+
npm: ["mailgun.js"],
|
|
24060
|
+
pypi: ["mailgun"],
|
|
24061
|
+
rubygems: ["mailgun-ruby"],
|
|
24062
|
+
composer: ["mailgun/mailgun-php"],
|
|
24063
|
+
nuget: ["Mailgun"]
|
|
24064
|
+
}
|
|
24065
|
+
},
|
|
24066
|
+
{
|
|
24067
|
+
id: "mixpanel",
|
|
24068
|
+
name: "Mixpanel",
|
|
24069
|
+
category: "Analytics",
|
|
24070
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24071
|
+
apiBase: "https://api.mixpanel.com",
|
|
24072
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24073
|
+
sdks: {
|
|
24074
|
+
npm: ["mixpanel"],
|
|
24075
|
+
pypi: ["mixpanel"],
|
|
24076
|
+
rubygems: ["mixpanel-ruby"],
|
|
24077
|
+
nuget: ["Mixpanel"]
|
|
24078
|
+
}
|
|
24079
|
+
},
|
|
24080
|
+
{
|
|
24081
|
+
id: "amplitude",
|
|
24082
|
+
name: "Amplitude",
|
|
24083
|
+
category: "Analytics",
|
|
24084
|
+
hostSuffixes: ["amplitude.com"],
|
|
24085
|
+
apiBase: "https://api2.amplitude.com",
|
|
24086
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24087
|
+
sdks: {
|
|
24088
|
+
npm: ["@amplitude/analytics-node"],
|
|
24089
|
+
pypi: ["amplitude-analytics"],
|
|
24090
|
+
nuget: ["Amplitude"]
|
|
24091
|
+
}
|
|
24092
|
+
},
|
|
24093
|
+
{
|
|
24094
|
+
id: "posthog",
|
|
24095
|
+
name: "PostHog",
|
|
24096
|
+
category: "Analytics",
|
|
24097
|
+
hostSuffixes: ["posthog.com"],
|
|
24098
|
+
apiBase: "https://us.i.posthog.com",
|
|
24099
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24100
|
+
sdks: {
|
|
24101
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24102
|
+
pypi: ["posthog"],
|
|
24103
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24104
|
+
rubygems: ["posthog-ruby"],
|
|
24105
|
+
composer: ["posthog/posthog-php"],
|
|
24106
|
+
nuget: ["PostHog"]
|
|
24107
|
+
}
|
|
24108
|
+
},
|
|
24109
|
+
{
|
|
24110
|
+
id: "honeycomb",
|
|
24111
|
+
name: "Honeycomb",
|
|
24112
|
+
category: "Observability",
|
|
24113
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24114
|
+
apiBase: "https://api.honeycomb.io",
|
|
24115
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24116
|
+
sdks: {
|
|
24117
|
+
npm: ["libhoney"],
|
|
24118
|
+
pypi: ["libhoney"],
|
|
24119
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24120
|
+
rubygems: ["libhoney"]
|
|
24121
|
+
}
|
|
24122
|
+
},
|
|
24123
|
+
{
|
|
24124
|
+
id: "grafana",
|
|
24125
|
+
name: "Grafana Cloud",
|
|
24126
|
+
category: "Observability",
|
|
24127
|
+
hostSuffixes: ["grafana.net"],
|
|
24128
|
+
apiBase: "https://grafana.net",
|
|
24129
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24130
|
+
sdks: {
|
|
24131
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24132
|
+
}
|
|
24133
|
+
},
|
|
24134
|
+
{
|
|
24135
|
+
id: "splunk",
|
|
24136
|
+
name: "Splunk",
|
|
24137
|
+
category: "Observability",
|
|
24138
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24139
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24140
|
+
defaultDataClasses: ["logs"],
|
|
24141
|
+
sdks: {
|
|
24142
|
+
npm: ["splunk-logging"],
|
|
24143
|
+
pypi: ["splunk-sdk"],
|
|
24144
|
+
maven: ["com.splunk"],
|
|
24145
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24146
|
+
}
|
|
24147
|
+
},
|
|
24148
|
+
{
|
|
24149
|
+
id: "pagerduty",
|
|
24150
|
+
name: "PagerDuty",
|
|
24151
|
+
category: "Incident response",
|
|
24152
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24153
|
+
apiBase: "https://api.pagerduty.com",
|
|
24154
|
+
defaultDataClasses: ["logs"],
|
|
24155
|
+
sdks: {
|
|
24156
|
+
npm: ["@pagerduty/pdjs"],
|
|
24157
|
+
pypi: ["pdpyras"],
|
|
24158
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24159
|
+
rubygems: ["pagerduty"]
|
|
24160
|
+
}
|
|
24161
|
+
},
|
|
24162
|
+
{
|
|
24163
|
+
id: "github",
|
|
24164
|
+
name: "GitHub",
|
|
24165
|
+
category: "Developer platform",
|
|
24166
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24167
|
+
apiBase: "https://api.github.com",
|
|
24168
|
+
defaultDataClasses: ["source"],
|
|
24169
|
+
sdks: {
|
|
24170
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24171
|
+
pypi: ["pygithub"],
|
|
24172
|
+
go: ["github.com/google/go-github"],
|
|
24173
|
+
maven: ["org.kohsuke.github-api"],
|
|
24174
|
+
rubygems: ["octokit"],
|
|
24175
|
+
cargo: ["octocrab"],
|
|
24176
|
+
composer: ["knplabs/github-api"],
|
|
24177
|
+
nuget: ["Octokit"]
|
|
24178
|
+
}
|
|
24179
|
+
},
|
|
24180
|
+
{
|
|
24181
|
+
id: "gitlab",
|
|
24182
|
+
name: "GitLab",
|
|
24183
|
+
category: "Developer platform",
|
|
24184
|
+
hostSuffixes: ["gitlab.com"],
|
|
24185
|
+
apiBase: "https://gitlab.com/api",
|
|
24186
|
+
defaultDataClasses: ["source"],
|
|
24187
|
+
sdks: {
|
|
24188
|
+
npm: ["@gitbeaker/rest"],
|
|
24189
|
+
pypi: ["python-gitlab"],
|
|
24190
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24191
|
+
rubygems: ["gitlab"],
|
|
24192
|
+
nuget: ["GitLabApiClient"]
|
|
24193
|
+
}
|
|
24194
|
+
},
|
|
24195
|
+
{
|
|
24196
|
+
id: "auth0",
|
|
24197
|
+
name: "Auth0",
|
|
24198
|
+
category: "Identity",
|
|
24199
|
+
hostSuffixes: ["auth0.com"],
|
|
24200
|
+
apiBase: "https://login.auth0.com",
|
|
24201
|
+
defaultDataClasses: ["pii"],
|
|
24202
|
+
sdks: {
|
|
24203
|
+
npm: ["auth0"],
|
|
24204
|
+
pypi: ["auth0-python"],
|
|
24205
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24206
|
+
maven: ["com.auth0"],
|
|
24207
|
+
rubygems: ["auth0"],
|
|
24208
|
+
composer: ["auth0/auth0-php"],
|
|
24209
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24210
|
+
}
|
|
24211
|
+
},
|
|
24212
|
+
{
|
|
24213
|
+
id: "okta",
|
|
24214
|
+
name: "Okta",
|
|
24215
|
+
category: "Identity",
|
|
24216
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24217
|
+
apiBase: "https://login.okta.com",
|
|
24218
|
+
defaultDataClasses: ["pii"],
|
|
24219
|
+
sdks: {
|
|
24220
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24221
|
+
pypi: ["okta"],
|
|
24222
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24223
|
+
maven: ["com.okta.sdk"],
|
|
24224
|
+
nuget: ["Okta.Sdk"]
|
|
24225
|
+
}
|
|
24226
|
+
},
|
|
24227
|
+
{
|
|
24228
|
+
id: "clerk",
|
|
24229
|
+
name: "Clerk",
|
|
24230
|
+
category: "Identity",
|
|
24231
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24232
|
+
apiBase: "https://api.clerk.com",
|
|
24233
|
+
defaultDataClasses: ["pii"],
|
|
24234
|
+
sdks: {
|
|
24235
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24236
|
+
pypi: ["clerk-backend-api"],
|
|
24237
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24238
|
+
}
|
|
24239
|
+
},
|
|
24240
|
+
{
|
|
24241
|
+
id: "supabase",
|
|
24242
|
+
name: "Supabase",
|
|
24243
|
+
category: "Backend platform",
|
|
24244
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24245
|
+
apiBase: "https://api.supabase.com",
|
|
24246
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24247
|
+
sdks: {
|
|
24248
|
+
npm: ["@supabase/supabase-js"],
|
|
24249
|
+
pypi: ["supabase"],
|
|
24250
|
+
cargo: ["postgrest"]
|
|
24251
|
+
}
|
|
24252
|
+
},
|
|
24253
|
+
{
|
|
24254
|
+
id: "firebase",
|
|
24255
|
+
name: "Firebase",
|
|
24256
|
+
category: "Backend platform",
|
|
24257
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24258
|
+
apiBase: "https://firebaseio.com",
|
|
24259
|
+
defaultDataClasses: ["customer"],
|
|
24260
|
+
sdks: {
|
|
24261
|
+
npm: ["firebase", "firebase-admin"],
|
|
24262
|
+
pypi: ["firebase-admin"],
|
|
24263
|
+
go: ["firebase.google.com/go"],
|
|
24264
|
+
maven: ["com.google.firebase"]
|
|
24265
|
+
}
|
|
24266
|
+
},
|
|
24267
|
+
{
|
|
24268
|
+
id: "mongodb-atlas",
|
|
24269
|
+
name: "MongoDB Atlas",
|
|
24270
|
+
category: "Database SaaS",
|
|
24271
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24272
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24273
|
+
defaultDataClasses: ["customer"],
|
|
24274
|
+
sdks: {
|
|
24275
|
+
npm: ["mongodb"],
|
|
24276
|
+
pypi: ["pymongo"],
|
|
24277
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24278
|
+
maven: ["org.mongodb"],
|
|
24279
|
+
rubygems: ["mongo"],
|
|
24280
|
+
cargo: ["mongodb"],
|
|
24281
|
+
nuget: ["MongoDB.Driver"]
|
|
24282
|
+
}
|
|
24283
|
+
},
|
|
24284
|
+
{
|
|
24285
|
+
id: "planetscale",
|
|
24286
|
+
name: "PlanetScale",
|
|
24287
|
+
category: "Database SaaS",
|
|
24288
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24289
|
+
apiBase: "https://api.planetscale.com",
|
|
24290
|
+
defaultDataClasses: ["customer"],
|
|
24291
|
+
sdks: {
|
|
24292
|
+
npm: ["@planetscale/database"],
|
|
24293
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24294
|
+
}
|
|
24295
|
+
},
|
|
24296
|
+
{
|
|
24297
|
+
id: "algolia",
|
|
24298
|
+
name: "Algolia",
|
|
24299
|
+
category: "Search SaaS",
|
|
24300
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24301
|
+
apiBase: "https://algolia.net",
|
|
24302
|
+
defaultDataClasses: ["customer"],
|
|
24303
|
+
sdks: {
|
|
24304
|
+
npm: ["algoliasearch"],
|
|
24305
|
+
pypi: ["algoliasearch"],
|
|
24306
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24307
|
+
maven: ["com.algolia"],
|
|
24308
|
+
rubygems: ["algolia"],
|
|
24309
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24310
|
+
nuget: ["Algolia.Search"]
|
|
24311
|
+
}
|
|
24312
|
+
},
|
|
24313
|
+
{
|
|
24314
|
+
id: "cloudflare",
|
|
24315
|
+
name: "Cloudflare",
|
|
24316
|
+
category: "CDN / edge",
|
|
24317
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24318
|
+
apiBase: "https://api.cloudflare.com",
|
|
24319
|
+
defaultDataClasses: ["logs"],
|
|
24320
|
+
sdks: {
|
|
24321
|
+
npm: ["cloudflare"],
|
|
24322
|
+
pypi: ["cloudflare"],
|
|
24323
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24324
|
+
nuget: ["CloudFlare.Client"]
|
|
24325
|
+
}
|
|
24326
|
+
},
|
|
24327
|
+
{
|
|
24328
|
+
id: "huggingface",
|
|
24329
|
+
name: "Hugging Face",
|
|
24330
|
+
category: "LLM provider",
|
|
24331
|
+
hostSuffixes: ["huggingface.co"],
|
|
24332
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24333
|
+
defaultDataClasses: ["source"],
|
|
24334
|
+
sdks: {
|
|
24335
|
+
npm: ["@huggingface/inference"],
|
|
24336
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24337
|
+
rubygems: ["hugging-face"]
|
|
24338
|
+
}
|
|
24339
|
+
},
|
|
24340
|
+
{
|
|
24341
|
+
id: "cohere",
|
|
24342
|
+
name: "Cohere",
|
|
24343
|
+
category: "LLM provider",
|
|
24344
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24345
|
+
apiBase: "https://api.cohere.com",
|
|
24346
|
+
defaultDataClasses: ["pii", "source"],
|
|
24347
|
+
sdks: {
|
|
24348
|
+
npm: ["cohere-ai"],
|
|
24349
|
+
pypi: ["cohere"],
|
|
24350
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24351
|
+
}
|
|
24352
|
+
},
|
|
24353
|
+
{
|
|
24354
|
+
id: "mistral",
|
|
24355
|
+
name: "Mistral AI",
|
|
24356
|
+
category: "LLM provider",
|
|
24357
|
+
hostSuffixes: ["mistral.ai"],
|
|
24358
|
+
apiBase: "https://api.mistral.ai",
|
|
24359
|
+
defaultDataClasses: ["pii", "source"],
|
|
24360
|
+
sdks: {
|
|
24361
|
+
npm: ["@mistralai/mistralai"],
|
|
24362
|
+
pypi: ["mistralai"],
|
|
24363
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24364
|
+
}
|
|
24365
|
+
}
|
|
24366
|
+
];
|
|
24367
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24368
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24369
|
+
|
|
24370
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24371
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24372
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24373
|
+
var SECRET_VALUE = new RegExp(
|
|
24374
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24375
|
+
"gi"
|
|
24376
|
+
);
|
|
24377
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24378
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24379
|
+
"gi"
|
|
24380
|
+
);
|
|
24381
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24382
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24383
|
+
{
|
|
24384
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24385
|
+
prefix: "/api/webhooks/"
|
|
24386
|
+
},
|
|
24387
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24388
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24389
|
+
];
|
|
24390
|
+
function escapeRegExp(literal2) {
|
|
24391
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24392
|
+
}
|
|
24393
|
+
var WEBHOOK_URL = new RegExp(
|
|
24394
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24395
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24396
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24397
|
+
"gi"
|
|
24398
|
+
);
|
|
22905
24399
|
|
|
22906
24400
|
// ../../packages/detections/src/escape-regexp.ts
|
|
22907
|
-
function
|
|
24401
|
+
function escapeRegExp2(value) {
|
|
22908
24402
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22909
24403
|
}
|
|
22910
24404
|
|
|
22911
24405
|
// ../../packages/detections/src/matchers/limits.ts
|
|
22912
24406
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24407
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
22913
24408
|
|
|
22914
24409
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22915
24410
|
var KeywordMatcher2 = class {
|
|
@@ -22920,7 +24415,7 @@ var KeywordMatcher2 = class {
|
|
|
22920
24415
|
for (const kw of keywords) {
|
|
22921
24416
|
if (kw.length === 0) continue;
|
|
22922
24417
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22923
|
-
const re = new RegExp(
|
|
24418
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
22924
24419
|
let m;
|
|
22925
24420
|
while ((m = re.exec(text)) !== null) {
|
|
22926
24421
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -22937,9 +24432,13 @@ var RegexMatcher2 = class {
|
|
|
22937
24432
|
if (rule.matcher.type !== "regex") return [];
|
|
22938
24433
|
const { pattern, flags, captureGroup } = rule.matcher;
|
|
22939
24434
|
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24435
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
22940
24436
|
const spans = [];
|
|
22941
24437
|
let m;
|
|
22942
|
-
|
|
24438
|
+
const maxIterations = scanText2.length + 1;
|
|
24439
|
+
let iterations = 0;
|
|
24440
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24441
|
+
if (++iterations > maxIterations) break;
|
|
22943
24442
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
22944
24443
|
if (m[0].length === 0) re.lastIndex++;
|
|
22945
24444
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -23043,7 +24542,7 @@ function isCorroborated(candidate, candidates, text) {
|
|
|
23043
24542
|
for (const label of labels) {
|
|
23044
24543
|
const trimmed = label.trim();
|
|
23045
24544
|
if (trimmed.length === 0) continue;
|
|
23046
|
-
const re = new RegExp(`(?<![A-Za-z0-9])${
|
|
24545
|
+
const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
|
|
23047
24546
|
if (re.test(haystack)) return true;
|
|
23048
24547
|
}
|
|
23049
24548
|
}
|
|
@@ -23208,6 +24707,31 @@ var CONFIG_POSTURE_RULES = [
|
|
|
23208
24707
|
}
|
|
23209
24708
|
];
|
|
23210
24709
|
|
|
24710
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24711
|
+
var EXPONENTIAL_UNITS = [
|
|
24712
|
+
"a",
|
|
24713
|
+
"0",
|
|
24714
|
+
" ",
|
|
24715
|
+
"x",
|
|
24716
|
+
"ab",
|
|
24717
|
+
"a.",
|
|
24718
|
+
"a-",
|
|
24719
|
+
"a_",
|
|
24720
|
+
"a@",
|
|
24721
|
+
"a/",
|
|
24722
|
+
"a:",
|
|
24723
|
+
"a=",
|
|
24724
|
+
"a;",
|
|
24725
|
+
"aA0",
|
|
24726
|
+
" "
|
|
24727
|
+
];
|
|
24728
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24729
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24730
|
+
);
|
|
24731
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24732
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24733
|
+
);
|
|
24734
|
+
|
|
23211
24735
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23212
24736
|
var auth_jwt_no_verify_default = {
|
|
23213
24737
|
specVersion: 1,
|
|
@@ -25255,7 +26779,7 @@ function ensureBundledPacks() {
|
|
|
25255
26779
|
return false;
|
|
25256
26780
|
}
|
|
25257
26781
|
}
|
|
25258
|
-
function scanText(text) {
|
|
26782
|
+
function scanText(text, ruleVersions) {
|
|
25259
26783
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
25260
26784
|
try {
|
|
25261
26785
|
const rules = getLoadedRules();
|
|
@@ -25267,7 +26791,7 @@ function scanText(text) {
|
|
|
25267
26791
|
return {
|
|
25268
26792
|
ruleId: m.ruleId,
|
|
25269
26793
|
ruleName: rule?.name ?? m.ruleId,
|
|
25270
|
-
ruleVersion: String(rule?.specVersion ?? 1),
|
|
26794
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
25271
26795
|
category: m.category,
|
|
25272
26796
|
severity: m.severity,
|
|
25273
26797
|
span: m.span,
|
|
@@ -25282,8 +26806,8 @@ function scanText(text) {
|
|
|
25282
26806
|
}
|
|
25283
26807
|
|
|
25284
26808
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25285
|
-
import { existsSync as
|
|
25286
|
-
import { basename, dirname, isAbsolute, join as
|
|
26809
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26810
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
25287
26811
|
function resolveRepoIdentity(cwd) {
|
|
25288
26812
|
try {
|
|
25289
26813
|
const root = findGitRoot(cwd);
|
|
@@ -25316,32 +26840,32 @@ function resolveRepoNwo(cwd) {
|
|
|
25316
26840
|
function findGitRoot(start) {
|
|
25317
26841
|
let dir = start;
|
|
25318
26842
|
for (; ; ) {
|
|
25319
|
-
if (
|
|
26843
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
25320
26844
|
const parent = dirname(dir);
|
|
25321
26845
|
if (parent === dir) return void 0;
|
|
25322
26846
|
dir = parent;
|
|
25323
26847
|
}
|
|
25324
26848
|
}
|
|
25325
26849
|
function resolveGitContext(root) {
|
|
25326
|
-
const dotGit =
|
|
26850
|
+
const dotGit = join7(root, ".git");
|
|
25327
26851
|
try {
|
|
25328
26852
|
if (statSync(dotGit).isDirectory()) {
|
|
25329
|
-
return { configPath:
|
|
26853
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
25330
26854
|
}
|
|
25331
26855
|
} catch {
|
|
25332
26856
|
return void 0;
|
|
25333
26857
|
}
|
|
25334
26858
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
25335
26859
|
if (!target) return void 0;
|
|
25336
|
-
const gitdir = isAbsolute(target) ? target :
|
|
25337
|
-
if (
|
|
25338
|
-
return { configPath:
|
|
26860
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
26861
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
26862
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
25339
26863
|
}
|
|
25340
|
-
const commonRaw = safeRead(
|
|
26864
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
25341
26865
|
if (!commonRaw) return void 0;
|
|
25342
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
26866
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
25343
26867
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
25344
|
-
return { configPath:
|
|
26868
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
25345
26869
|
}
|
|
25346
26870
|
function safeRead(path) {
|
|
25347
26871
|
try {
|
|
@@ -25396,10 +26920,7 @@ function nwoFromUrl(url2) {
|
|
|
25396
26920
|
}
|
|
25397
26921
|
|
|
25398
26922
|
// ../../packages/plugin-sdk/src/events.ts
|
|
25399
|
-
import { createHash as
|
|
25400
|
-
|
|
25401
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
25402
|
-
import { createHash as createHash4 } from "crypto";
|
|
26923
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
25403
26924
|
|
|
25404
26925
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25405
26926
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
@@ -25431,13 +26952,17 @@ function resolveInventoryContext(input) {
|
|
|
25431
26952
|
}
|
|
25432
26953
|
|
|
25433
26954
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
25434
|
-
import { mkdirSync as
|
|
25435
|
-
import { join as
|
|
26955
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26956
|
+
import { join as join9 } from "path";
|
|
26957
|
+
|
|
26958
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
26959
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
26960
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
25436
26961
|
|
|
25437
26962
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25438
26963
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25439
|
-
import { existsSync as
|
|
25440
|
-
import { basename as
|
|
26964
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26965
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
25441
26966
|
|
|
25442
26967
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25443
26968
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -25446,8 +26971,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
25446
26971
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25447
26972
|
|
|
25448
26973
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25449
|
-
import { mkdirSync as
|
|
25450
|
-
import { join as
|
|
26974
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26975
|
+
import { join as join11 } from "path";
|
|
25451
26976
|
|
|
25452
26977
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25453
26978
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -25478,7 +27003,8 @@ var StandaloneDataGateway = class {
|
|
|
25478
27003
|
}
|
|
25479
27004
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
25480
27005
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
25481
|
-
// hands the natural key across.
|
|
27006
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27007
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
25482
27008
|
recordLlmCall(input) {
|
|
25483
27009
|
this.db.auditEvents.insertLlmCall(input);
|
|
25484
27010
|
return Promise.resolve();
|
|
@@ -25520,7 +27046,9 @@ var StandaloneDataGateway = class {
|
|
|
25520
27046
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
25521
27047
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
25522
27048
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
25523
|
-
// content-addressed
|
|
27049
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27050
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27051
|
+
// than no-opping.
|
|
25524
27052
|
writeToolCall(input) {
|
|
25525
27053
|
this.db.auditEvents.insertToolCall(input);
|
|
25526
27054
|
if (input.inspections.length === 0) return;
|
|
@@ -25540,7 +27068,7 @@ var StandaloneDataGateway = class {
|
|
|
25540
27068
|
});
|
|
25541
27069
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
25542
27070
|
this.db.inspectionFindings.insertFinding({
|
|
25543
|
-
id: inspectionFindingId(auditEventId,
|
|
27071
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
25544
27072
|
auditEventId,
|
|
25545
27073
|
inspectionDefinitionId: definitionId,
|
|
25546
27074
|
classifiedDataId: classifiedDataId2,
|
|
@@ -25589,10 +27117,17 @@ var StandaloneDataGateway = class {
|
|
|
25589
27117
|
try {
|
|
25590
27118
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
25591
27119
|
if (snapshot.installedPacks === 0) return void 0;
|
|
25592
|
-
if (snapshot.enabledPacks === 0)
|
|
27120
|
+
if (snapshot.enabledPacks === 0) {
|
|
27121
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27122
|
+
}
|
|
25593
27123
|
if (snapshot.invalidRules > 0) return void 0;
|
|
25594
27124
|
if (snapshot.rules.length === 0) return void 0;
|
|
25595
|
-
return {
|
|
27125
|
+
return {
|
|
27126
|
+
rules: snapshot.rules,
|
|
27127
|
+
ruleActions: snapshot.ruleActions,
|
|
27128
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27129
|
+
complete: true
|
|
27130
|
+
};
|
|
25596
27131
|
} catch {
|
|
25597
27132
|
return void 0;
|
|
25598
27133
|
}
|
|
@@ -25620,6 +27155,7 @@ var StandaloneDataGateway = class {
|
|
|
25620
27155
|
policies: [...policies, ...rulePolicies],
|
|
25621
27156
|
rules: installed ? installed.rules : [],
|
|
25622
27157
|
...installed ? { rulesComplete: true } : {},
|
|
27158
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
25623
27159
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
25624
27160
|
customKeywords,
|
|
25625
27161
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -25718,6 +27254,13 @@ var StandaloneDataGateway = class {
|
|
|
25718
27254
|
this.db.scanLedger.upsertEntries(entries);
|
|
25719
27255
|
return Promise.resolve();
|
|
25720
27256
|
}
|
|
27257
|
+
getRuleProbeVerdict(ruleKey) {
|
|
27258
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
27259
|
+
}
|
|
27260
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
|
|
27261
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
|
|
27262
|
+
return Promise.resolve();
|
|
27263
|
+
}
|
|
25721
27264
|
openAtRestKeysForPath(path) {
|
|
25722
27265
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
25723
27266
|
}
|
|
@@ -25728,6 +27271,12 @@ var StandaloneDataGateway = class {
|
|
|
25728
27271
|
this.db.resolutions.insertResolution(input);
|
|
25729
27272
|
return Promise.resolve();
|
|
25730
27273
|
}
|
|
27274
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
27275
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
27276
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
27277
|
+
recordProjectEgress(input) {
|
|
27278
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
27279
|
+
}
|
|
25731
27280
|
close() {
|
|
25732
27281
|
this.db.close();
|
|
25733
27282
|
return Promise.resolve();
|
|
@@ -25749,15 +27298,15 @@ import { createHash as createHash5 } from "crypto";
|
|
|
25749
27298
|
import {
|
|
25750
27299
|
closeSync,
|
|
25751
27300
|
fstatSync,
|
|
25752
|
-
mkdirSync as
|
|
27301
|
+
mkdirSync as mkdirSync4,
|
|
25753
27302
|
openSync,
|
|
25754
27303
|
readFileSync as readFileSync7,
|
|
25755
27304
|
readSync,
|
|
25756
|
-
writeFileSync as
|
|
27305
|
+
writeFileSync as writeFileSync5
|
|
25757
27306
|
} from "fs";
|
|
25758
|
-
import { join as
|
|
27307
|
+
import { join as join12 } from "path";
|
|
25759
27308
|
function offsetsDir(dataDir2) {
|
|
25760
|
-
return
|
|
27309
|
+
return join12(dataDir2, "usage-offsets");
|
|
25761
27310
|
}
|
|
25762
27311
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
25763
27312
|
function safeSessionId(sessionId) {
|
|
@@ -25767,7 +27316,7 @@ function safeSessionId(sessionId) {
|
|
|
25767
27316
|
return createHash5("sha256").update(sessionId).digest("hex");
|
|
25768
27317
|
}
|
|
25769
27318
|
function offsetPath(dataDir2, sessionId) {
|
|
25770
|
-
return
|
|
27319
|
+
return join12(offsetsDir(dataDir2), safeSessionId(sessionId));
|
|
25771
27320
|
}
|
|
25772
27321
|
function readOffset(dataDir2, sessionId) {
|
|
25773
27322
|
try {
|
|
@@ -25785,9 +27334,9 @@ function readOffset(dataDir2, sessionId) {
|
|
|
25785
27334
|
}
|
|
25786
27335
|
function writeOffset(dataDir2, sessionId, value) {
|
|
25787
27336
|
try {
|
|
25788
|
-
|
|
27337
|
+
mkdirSync4(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
|
|
25789
27338
|
const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
|
|
25790
|
-
|
|
27339
|
+
writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
|
|
25791
27340
|
mode: DATA_FILE_MODE
|
|
25792
27341
|
});
|
|
25793
27342
|
} catch {
|
|
@@ -25829,9 +27378,9 @@ function readTail(transcriptPath, startOffset) {
|
|
|
25829
27378
|
}
|
|
25830
27379
|
|
|
25831
27380
|
// src/history/transcripts.ts
|
|
25832
|
-
import { readdirSync as
|
|
27381
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
25833
27382
|
import { homedir as homedir3 } from "os";
|
|
25834
|
-
import { join as
|
|
27383
|
+
import { join as join13 } from "path";
|
|
25835
27384
|
function isRecord(value) {
|
|
25836
27385
|
return typeof value === "object" && value !== null;
|
|
25837
27386
|
}
|
|
@@ -25860,9 +27409,9 @@ function parseTranscriptUsage(jsonl, sinceMs = 0) {
|
|
|
25860
27409
|
if (sinceMs > 0 && Date.parse(optString(rec.timestamp) ?? "") < sinceMs) continue;
|
|
25861
27410
|
if (rec.type === "user") {
|
|
25862
27411
|
const uuid5 = optString(rec.uuid);
|
|
25863
|
-
const
|
|
25864
|
-
if (uuid5 === void 0 ||
|
|
25865
|
-
out.push({ kind: "user", uuid: uuid5, promptId });
|
|
27412
|
+
const promptId2 = optString(rec.promptId);
|
|
27413
|
+
if (uuid5 === void 0 || promptId2 === void 0) continue;
|
|
27414
|
+
out.push({ kind: "user", uuid: uuid5, promptId: promptId2 });
|
|
25866
27415
|
continue;
|
|
25867
27416
|
}
|
|
25868
27417
|
if (rec.type !== "assistant") continue;
|
|
@@ -26082,12 +27631,18 @@ async function reconcileSessionToolCalls(gateway, sessionId, toolCalls, usageRec
|
|
|
26082
27631
|
if (toolCalls.length === 0) return 0;
|
|
26083
27632
|
const promptIdByUuid = /* @__PURE__ */ new Map();
|
|
26084
27633
|
for (const r of usageRecords) if (r.kind === "user") promptIdByUuid.set(r.uuid, r.promptId);
|
|
27634
|
+
let ruleVersions;
|
|
27635
|
+
try {
|
|
27636
|
+
ruleVersions = (await gateway.getPolicyBundle()).ruleVersions;
|
|
27637
|
+
} catch {
|
|
27638
|
+
ruleVersions = void 0;
|
|
27639
|
+
}
|
|
26085
27640
|
const inputs = toolCalls.map((tc) => {
|
|
26086
27641
|
const runKey = (tc.parentUuid !== void 0 ? promptIdByUuid.get(tc.parentUuid) : void 0) ?? opts.seedPromptId;
|
|
26087
27642
|
const attributes = { tool_name: tc.toolName, tool_use_id: tc.toolUseId };
|
|
26088
27643
|
let inspections = [];
|
|
26089
27644
|
if (tc.target !== void 0) {
|
|
26090
|
-
const { masked, findings } = scanText(tc.target);
|
|
27645
|
+
const { masked, findings } = scanText(tc.target, ruleVersions);
|
|
26091
27646
|
if (masked !== "") attributes.target = truncateTarget(masked);
|
|
26092
27647
|
inspections = findings.map((f) => ({
|
|
26093
27648
|
ruleId: f.ruleId,
|