@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/onboard.js
CHANGED
|
@@ -493,7 +493,7 @@ var require_ignore = __commonJS({
|
|
|
493
493
|
|
|
494
494
|
// ../../packages/persistence/src/database.ts
|
|
495
495
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
496
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
497
497
|
import { join, sep } from "path";
|
|
498
498
|
import { DatabaseSync } from "node:sqlite";
|
|
499
499
|
|
|
@@ -542,6 +542,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
542
542
|
{
|
|
543
543
|
tag: "0010_events_session_expression_index",
|
|
544
544
|
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"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
tag: "0011_egress_writer",
|
|
548
|
+
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tag: "0012_handy_the_captain",
|
|
552
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0013_legacy_history_backfill_support",
|
|
556
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0014_drop_legacy_events_findings",
|
|
560
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
545
561
|
}
|
|
546
562
|
];
|
|
547
563
|
|
|
@@ -15372,7 +15388,12 @@ var FindingFacets = external_exports.object({
|
|
|
15372
15388
|
severity: external_exports.array(FindingFacetItem),
|
|
15373
15389
|
subtype: external_exports.array(FindingFacetItem),
|
|
15374
15390
|
provider: external_exports.array(FindingFacetItem),
|
|
15375
|
-
action: external_exports.array(FindingFacetItem)
|
|
15391
|
+
action: external_exports.array(FindingFacetItem),
|
|
15392
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15393
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15394
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15395
|
+
// counted under no value.
|
|
15396
|
+
status: external_exports.array(FindingFacetItem)
|
|
15376
15397
|
}).meta({ id: "FindingFacets" });
|
|
15377
15398
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15378
15399
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15382,6 +15403,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15382
15403
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15383
15404
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15384
15405
|
action: external_exports.array(FindingAction).optional(),
|
|
15406
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15407
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15408
|
+
// one of the requested values.
|
|
15409
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15385
15410
|
q: external_exports.string().optional(),
|
|
15386
15411
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15387
15412
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15569,6 +15594,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15569
15594
|
parent_uuid: external_exports.string().optional(),
|
|
15570
15595
|
run_key: external_exports.string().optional()
|
|
15571
15596
|
}).catchall(external_exports.unknown());
|
|
15597
|
+
var CaptureAttributes = external_exports.object({
|
|
15598
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15599
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15600
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15601
|
+
source_tool: external_exports.string().optional(),
|
|
15602
|
+
file_path: external_exports.string().optional(),
|
|
15603
|
+
repo: external_exports.string().optional(),
|
|
15604
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15605
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15606
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15607
|
+
tool_name: external_exports.string().optional(),
|
|
15608
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15609
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15610
|
+
gitignored: external_exports.boolean().optional(),
|
|
15611
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15612
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15613
|
+
whole_file: external_exports.boolean().optional(),
|
|
15614
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15615
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15616
|
+
// trace id when telemetry is enabled.
|
|
15617
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15618
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15619
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15620
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15621
|
+
// authorized the bypass.
|
|
15622
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15623
|
+
}).catchall(external_exports.unknown());
|
|
15572
15624
|
var ToolCallInspection = external_exports.object({
|
|
15573
15625
|
ruleId: external_exports.string().min(1),
|
|
15574
15626
|
ruleName: external_exports.string(),
|
|
@@ -15655,7 +15707,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15655
15707
|
span: Span,
|
|
15656
15708
|
maskedMatch: external_exports.string(),
|
|
15657
15709
|
actionTaken: ActionTaken,
|
|
15658
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15710
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15711
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15712
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15713
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15714
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15715
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15716
|
+
findingKey: external_exports.string().optional(),
|
|
15717
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15718
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15719
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15720
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15721
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15659
15722
|
});
|
|
15660
15723
|
var InventoryContext = external_exports.object({
|
|
15661
15724
|
host: InventoryInput.optional(),
|
|
@@ -15857,6 +15920,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15857
15920
|
|
|
15858
15921
|
// ../../packages/schema/src/zod/event.ts
|
|
15859
15922
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15923
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15860
15924
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15861
15925
|
var EventMetadata = external_exports.object({
|
|
15862
15926
|
sessionId: external_exports.string().optional(),
|
|
@@ -16197,6 +16261,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16197
16261
|
|
|
16198
16262
|
// ../../packages/schema/src/zod/rule.ts
|
|
16199
16263
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16264
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16200
16265
|
var KeywordMatcher = external_exports.object({
|
|
16201
16266
|
type: external_exports.literal("keyword"),
|
|
16202
16267
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16221,9 +16286,10 @@ function matchesEmptyString(pattern, flags2) {
|
|
|
16221
16286
|
return false;
|
|
16222
16287
|
}
|
|
16223
16288
|
}
|
|
16289
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16224
16290
|
var RegexMatcher = external_exports.object({
|
|
16225
16291
|
type: external_exports.literal("regex"),
|
|
16226
|
-
pattern: external_exports.string(),
|
|
16292
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16227
16293
|
flags: external_exports.string().default("gi"),
|
|
16228
16294
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16229
16295
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16344,6 +16410,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16344
16410
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16345
16411
|
// `bundle.exceptions ?? []`.
|
|
16346
16412
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16413
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16414
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16415
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16416
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16417
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16418
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16347
16419
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16348
16420
|
fetchedAt: external_exports.iso.datetime()
|
|
16349
16421
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16795,6 +16867,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16795
16867
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16796
16868
|
}
|
|
16797
16869
|
|
|
16870
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16871
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16872
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16873
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16874
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16875
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16876
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16877
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16878
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16879
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16880
|
+
var ReviewInfo = external_exports.object({
|
|
16881
|
+
needsReview: external_exports.boolean(),
|
|
16882
|
+
reasons: external_exports.array(ReviewReason)
|
|
16883
|
+
}).meta({ id: "ReviewInfo" });
|
|
16884
|
+
var DestinationNetwork = external_exports.object({
|
|
16885
|
+
port: external_exports.number().int().nullable(),
|
|
16886
|
+
geo: external_exports.string().nullable(),
|
|
16887
|
+
ptr: external_exports.string().nullable()
|
|
16888
|
+
}).meta({ id: "DestinationNetwork" });
|
|
16889
|
+
var EndpointSummary = external_exports.object({
|
|
16890
|
+
id: external_exports.string(),
|
|
16891
|
+
method: HttpMethod,
|
|
16892
|
+
transport: Transport,
|
|
16893
|
+
url: external_exports.string(),
|
|
16894
|
+
template: external_exports.boolean(),
|
|
16895
|
+
dataClass: DataClass,
|
|
16896
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16897
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
16898
|
+
}).meta({ id: "EndpointSummary" });
|
|
16899
|
+
var CallSite = external_exports.object({
|
|
16900
|
+
id: external_exports.string(),
|
|
16901
|
+
project: external_exports.string(),
|
|
16902
|
+
file: external_exports.string(),
|
|
16903
|
+
line: external_exports.number().int().nonnegative(),
|
|
16904
|
+
snippet: external_exports.string(),
|
|
16905
|
+
dynamic: external_exports.boolean(),
|
|
16906
|
+
vendored: external_exports.boolean(),
|
|
16907
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
16908
|
+
projectId: external_exports.string().nullable()
|
|
16909
|
+
}).meta({ id: "CallSite" });
|
|
16910
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
16911
|
+
sites: external_exports.array(CallSite)
|
|
16912
|
+
}).meta({ id: "EndpointWithSites" });
|
|
16913
|
+
var ShareDestinationSummary = external_exports.object({
|
|
16914
|
+
id: external_exports.string(),
|
|
16915
|
+
kind: DestinationKind,
|
|
16916
|
+
name: external_exports.string(),
|
|
16917
|
+
host: external_exports.string(),
|
|
16918
|
+
category: external_exports.string(),
|
|
16919
|
+
trust: ShareTrustLevel,
|
|
16920
|
+
/** Effective state (decision applied over the trust default). */
|
|
16921
|
+
status: EgressStatus,
|
|
16922
|
+
/** True when an egress decision override differs from the trust default. */
|
|
16923
|
+
isCustom: external_exports.boolean(),
|
|
16924
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16925
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
16926
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16927
|
+
transports: external_exports.array(Transport),
|
|
16928
|
+
/** Most-sensitive first. */
|
|
16929
|
+
dataClasses: external_exports.array(DataClass),
|
|
16930
|
+
review: ReviewInfo,
|
|
16931
|
+
/** Non-provider hosts only; null for providers. */
|
|
16932
|
+
network: DestinationNetwork.nullable(),
|
|
16933
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
16934
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
16935
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
16936
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
16937
|
+
endpointCount: true,
|
|
16938
|
+
callSiteCount: true,
|
|
16939
|
+
endpoints: true
|
|
16940
|
+
}).extend({
|
|
16941
|
+
/** Ownership/geo rationale; null for providers. */
|
|
16942
|
+
note: external_exports.string().nullable(),
|
|
16943
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
16944
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
16945
|
+
var ReviewDestination = external_exports.object({
|
|
16946
|
+
id: external_exports.string(),
|
|
16947
|
+
kind: DestinationKind,
|
|
16948
|
+
name: external_exports.string(),
|
|
16949
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
16950
|
+
host: external_exports.string(),
|
|
16951
|
+
trust: ShareTrustLevel,
|
|
16952
|
+
status: EgressStatus,
|
|
16953
|
+
review: ReviewInfo,
|
|
16954
|
+
topDataClass: DataClass,
|
|
16955
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16956
|
+
lastSeen: external_exports.iso.datetime()
|
|
16957
|
+
}).meta({ id: "ReviewDestination" });
|
|
16958
|
+
var ShareDestinationGroup = external_exports.object({
|
|
16959
|
+
kind: DestinationKind,
|
|
16960
|
+
total: external_exports.number().int().nonnegative(),
|
|
16961
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
16962
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
16963
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
16964
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
16965
|
+
var SharesStats = external_exports.object({
|
|
16966
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
16967
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
16968
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
16969
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
16970
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
16971
|
+
byKind: external_exports.object({
|
|
16972
|
+
provider: external_exports.number().int().nonnegative(),
|
|
16973
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16974
|
+
external: external_exports.number().int().nonnegative(),
|
|
16975
|
+
ip: external_exports.number().int().nonnegative()
|
|
16976
|
+
}),
|
|
16977
|
+
byTrust: external_exports.object({
|
|
16978
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
16979
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16980
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
16981
|
+
ip: external_exports.number().int().nonnegative()
|
|
16982
|
+
})
|
|
16983
|
+
}).meta({ id: "SharesStats" });
|
|
16984
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
16985
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
16986
|
+
decision: EgressDecision.nullable()
|
|
16987
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
16988
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
16989
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
16990
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
16991
|
+
q: external_exports.string().optional(),
|
|
16992
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
16993
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
16994
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
16995
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
16996
|
+
/**
|
|
16997
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
16998
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
16999
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17000
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17001
|
+
*/
|
|
17002
|
+
review: external_exports.stringbool().default(false)
|
|
17003
|
+
});
|
|
17004
|
+
var ExportSharesQuery = external_exports.object({
|
|
17005
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17006
|
+
q: external_exports.string().optional(),
|
|
17007
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
17008
|
+
});
|
|
17009
|
+
|
|
17010
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
17011
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
17012
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
17013
|
+
id: external_exports.string(),
|
|
17014
|
+
name: external_exports.string(),
|
|
17015
|
+
category: external_exports.string(),
|
|
17016
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
17017
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
17018
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
17019
|
+
apiBase: external_exports.string(),
|
|
17020
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
17021
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
17022
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
17023
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
17024
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
17025
|
+
var EgressCallSiteHit = external_exports.object({
|
|
17026
|
+
file: external_exports.string(),
|
|
17027
|
+
line: external_exports.number().int().positive(),
|
|
17028
|
+
snippet: external_exports.string(),
|
|
17029
|
+
dynamic: external_exports.boolean(),
|
|
17030
|
+
vendored: external_exports.boolean()
|
|
17031
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
17032
|
+
var ResolvedEgressHit = external_exports.object({
|
|
17033
|
+
host: external_exports.string(),
|
|
17034
|
+
kind: DestinationKind,
|
|
17035
|
+
name: external_exports.string(),
|
|
17036
|
+
category: external_exports.string(),
|
|
17037
|
+
trust: ShareTrustLevel,
|
|
17038
|
+
network: DestinationNetwork.nullable(),
|
|
17039
|
+
method: HttpMethod,
|
|
17040
|
+
transport: Transport,
|
|
17041
|
+
url: external_exports.string(),
|
|
17042
|
+
template: external_exports.boolean(),
|
|
17043
|
+
dataClass: DataClass,
|
|
17044
|
+
site: EgressCallSiteHit
|
|
17045
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
17046
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
17047
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
17048
|
+
external_exports.object({
|
|
17049
|
+
mode: external_exports.literal("ledger"),
|
|
17050
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
17051
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
17052
|
+
})
|
|
17053
|
+
]).meta({ id: "EgressReconcile" });
|
|
17054
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
17055
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
17056
|
+
projectKey: external_exports.string().min(1),
|
|
17057
|
+
/** Display name only — never keys reconciliation. */
|
|
17058
|
+
project: external_exports.string(),
|
|
17059
|
+
projectId: external_exports.string().nullable(),
|
|
17060
|
+
reconcile: EgressReconcile,
|
|
17061
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
17062
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
17063
|
+
var EgressWriteSummary = external_exports.object({
|
|
17064
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17065
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17066
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17067
|
+
truncated: external_exports.boolean(),
|
|
17068
|
+
/**
|
|
17069
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
17070
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17071
|
+
* again next scan.
|
|
17072
|
+
*/
|
|
17073
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17074
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17075
|
+
|
|
16798
17076
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16799
17077
|
function toApiAction(dbVal) {
|
|
16800
17078
|
const map2 = {
|
|
@@ -16942,6 +17220,15 @@ function groupActions(g) {
|
|
|
16942
17220
|
actionsCache.set(g, actions);
|
|
16943
17221
|
return actions;
|
|
16944
17222
|
}
|
|
17223
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17224
|
+
const statusSet = new Set(statuses);
|
|
17225
|
+
let sum = 0;
|
|
17226
|
+
for (const input of statusInputs) {
|
|
17227
|
+
if (input.count === void 0) return null;
|
|
17228
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17229
|
+
}
|
|
17230
|
+
return sum;
|
|
17231
|
+
}
|
|
16945
17232
|
function applyFindingFilters(groups, opts) {
|
|
16946
17233
|
let filtered = groups;
|
|
16947
17234
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16960,6 +17247,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
16960
17247
|
const subtypeSet = new Set(opts.subtype);
|
|
16961
17248
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
16962
17249
|
}
|
|
17250
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17251
|
+
const statusSet = new Set(opts.statuses);
|
|
17252
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17253
|
+
}
|
|
16963
17254
|
if (opts.q) {
|
|
16964
17255
|
const q = opts.q.toLowerCase();
|
|
16965
17256
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -16981,6 +17272,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16981
17272
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
16982
17273
|
providers: opts.providers,
|
|
16983
17274
|
actions: opts.actions,
|
|
17275
|
+
statuses: opts.statuses,
|
|
16984
17276
|
q: opts.q,
|
|
16985
17277
|
subtype: opts.subtype
|
|
16986
17278
|
});
|
|
@@ -16990,6 +17282,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16990
17282
|
}
|
|
16991
17283
|
const forProvider = applyFindingFilters(allGroups, {
|
|
16992
17284
|
actions: opts.actions,
|
|
17285
|
+
statuses: opts.statuses,
|
|
16993
17286
|
q: opts.q,
|
|
16994
17287
|
subtype: opts.subtype,
|
|
16995
17288
|
severity: opts.severity
|
|
@@ -17000,6 +17293,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17000
17293
|
}
|
|
17001
17294
|
const forAction = applyFindingFilters(allGroups, {
|
|
17002
17295
|
providers: opts.providers,
|
|
17296
|
+
statuses: opts.statuses,
|
|
17003
17297
|
q: opts.q,
|
|
17004
17298
|
subtype: opts.subtype,
|
|
17005
17299
|
severity: opts.severity
|
|
@@ -17011,17 +17305,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17011
17305
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17012
17306
|
providers: opts.providers,
|
|
17013
17307
|
actions: opts.actions,
|
|
17308
|
+
statuses: opts.statuses,
|
|
17014
17309
|
q: opts.q,
|
|
17015
17310
|
severity: opts.severity
|
|
17016
17311
|
});
|
|
17017
17312
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17018
17313
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17314
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17315
|
+
providers: opts.providers,
|
|
17316
|
+
actions: opts.actions,
|
|
17317
|
+
q: opts.q,
|
|
17318
|
+
subtype: opts.subtype,
|
|
17319
|
+
severity: opts.severity
|
|
17320
|
+
});
|
|
17321
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17322
|
+
for (const g of forStatus) {
|
|
17323
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17324
|
+
}
|
|
17019
17325
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17020
17326
|
return {
|
|
17021
17327
|
severity: toItems(severityMap),
|
|
17022
17328
|
provider: toItems(providerMap),
|
|
17023
17329
|
action: toItems(actionMap),
|
|
17024
|
-
subtype: toItems(subtypeMap)
|
|
17330
|
+
subtype: toItems(subtypeMap),
|
|
17331
|
+
status: toItems(statusMap)
|
|
17025
17332
|
};
|
|
17026
17333
|
}
|
|
17027
17334
|
|
|
@@ -17056,10 +17363,15 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17056
17363
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17057
17364
|
|
|
17058
17365
|
// ../../packages/schema/src/zod/local.ts
|
|
17059
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17366
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17367
|
+
var MODEL_JUDGE_PAYLOAD_VERSION = 1;
|
|
17060
17368
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17061
17369
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17062
17370
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17371
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17372
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17373
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17374
|
+
});
|
|
17063
17375
|
var WorkspaceSettings = external_exports.object({
|
|
17064
17376
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17065
17377
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17071,38 +17383,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17071
17383
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17072
17384
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17073
17385
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17386
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17387
|
+
// Shares writes.
|
|
17388
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17074
17389
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17075
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17390
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17391
|
+
// Records that the user consented to sending findings to the model API for
|
|
17392
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17393
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17394
|
+
// covers the current payload and must be re-granted.
|
|
17395
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17076
17396
|
});
|
|
17077
17397
|
function defaultWorkspaceSettings() {
|
|
17078
17398
|
return WorkspaceSettings.parse({});
|
|
17079
17399
|
}
|
|
17080
|
-
function toEventRow(event) {
|
|
17081
|
-
return {
|
|
17082
|
-
id: event.id,
|
|
17083
|
-
sourceTool: event.sourceTool,
|
|
17084
|
-
kind: event.kind,
|
|
17085
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17086
|
-
contentHash: event.contentHash,
|
|
17087
|
-
content: event.content,
|
|
17088
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17089
|
-
};
|
|
17090
|
-
}
|
|
17091
|
-
function toFindingRow(finding) {
|
|
17092
|
-
return {
|
|
17093
|
-
id: finding.id,
|
|
17094
|
-
eventId: finding.eventId,
|
|
17095
|
-
ruleId: finding.ruleId,
|
|
17096
|
-
category: finding.category,
|
|
17097
|
-
severity: finding.severity,
|
|
17098
|
-
spanStart: finding.span.start,
|
|
17099
|
-
spanEnd: finding.span.end,
|
|
17100
|
-
maskedMatch: finding.maskedMatch,
|
|
17101
|
-
actionTaken: finding.actionTaken,
|
|
17102
|
-
confidence: finding.confidence,
|
|
17103
|
-
findingKey: finding.findingKey ?? null
|
|
17104
|
-
};
|
|
17105
|
-
}
|
|
17106
17400
|
function toInventoryRow(input, id, now) {
|
|
17107
17401
|
return {
|
|
17108
17402
|
id,
|
|
@@ -17172,7 +17466,42 @@ function toInspectionFindingRow(input) {
|
|
|
17172
17466
|
spanEnd: input.span.end,
|
|
17173
17467
|
maskedMatch: input.maskedMatch,
|
|
17174
17468
|
actionTaken: input.actionTaken,
|
|
17175
|
-
confidence: input.confidence
|
|
17469
|
+
confidence: input.confidence,
|
|
17470
|
+
findingKey: input.findingKey ?? null,
|
|
17471
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17472
|
+
};
|
|
17473
|
+
}
|
|
17474
|
+
function toCaptureAttributes(event) {
|
|
17475
|
+
const metadata = event.metadata;
|
|
17476
|
+
return {
|
|
17477
|
+
source_tool: event.sourceTool,
|
|
17478
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17479
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17480
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17481
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17482
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17483
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17484
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17485
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17486
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17487
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17488
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17489
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17490
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17491
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17492
|
+
};
|
|
17493
|
+
}
|
|
17494
|
+
function captureDefinitionVersion(finding) {
|
|
17495
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17496
|
+
}
|
|
17497
|
+
function toCaptureDefinitionInput(finding) {
|
|
17498
|
+
return {
|
|
17499
|
+
ruleId: finding.ruleId,
|
|
17500
|
+
version: captureDefinitionVersion(finding),
|
|
17501
|
+
name: finding.ruleId,
|
|
17502
|
+
category: finding.category,
|
|
17503
|
+
severity: finding.severity,
|
|
17504
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17176
17505
|
};
|
|
17177
17506
|
}
|
|
17178
17507
|
|
|
@@ -17554,145 +17883,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17554
17883
|
path: ["liveKeys"]
|
|
17555
17884
|
});
|
|
17556
17885
|
|
|
17557
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17558
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17559
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17560
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17561
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17562
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17563
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17564
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17565
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17566
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17567
|
-
var ReviewInfo = external_exports.object({
|
|
17568
|
-
needsReview: external_exports.boolean(),
|
|
17569
|
-
reasons: external_exports.array(ReviewReason)
|
|
17570
|
-
}).meta({ id: "ReviewInfo" });
|
|
17571
|
-
var DestinationNetwork = external_exports.object({
|
|
17572
|
-
port: external_exports.number().int().nullable(),
|
|
17573
|
-
geo: external_exports.string().nullable(),
|
|
17574
|
-
ptr: external_exports.string().nullable()
|
|
17575
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17576
|
-
var EndpointSummary = external_exports.object({
|
|
17577
|
-
id: external_exports.string(),
|
|
17578
|
-
method: HttpMethod,
|
|
17579
|
-
transport: Transport,
|
|
17580
|
-
url: external_exports.string(),
|
|
17581
|
-
template: external_exports.boolean(),
|
|
17582
|
-
dataClass: DataClass,
|
|
17583
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17584
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17585
|
-
}).meta({ id: "EndpointSummary" });
|
|
17586
|
-
var CallSite = external_exports.object({
|
|
17587
|
-
id: external_exports.string(),
|
|
17588
|
-
project: external_exports.string(),
|
|
17589
|
-
file: external_exports.string(),
|
|
17590
|
-
line: external_exports.number().int().nonnegative(),
|
|
17591
|
-
snippet: external_exports.string(),
|
|
17592
|
-
dynamic: external_exports.boolean(),
|
|
17593
|
-
vendored: external_exports.boolean(),
|
|
17594
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17595
|
-
projectId: external_exports.string().nullable()
|
|
17596
|
-
}).meta({ id: "CallSite" });
|
|
17597
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17598
|
-
sites: external_exports.array(CallSite)
|
|
17599
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17600
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17601
|
-
id: external_exports.string(),
|
|
17602
|
-
kind: DestinationKind,
|
|
17603
|
-
name: external_exports.string(),
|
|
17604
|
-
host: external_exports.string(),
|
|
17605
|
-
category: external_exports.string(),
|
|
17606
|
-
trust: ShareTrustLevel,
|
|
17607
|
-
/** Effective state (decision applied over the trust default). */
|
|
17608
|
-
status: EgressStatus,
|
|
17609
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17610
|
-
isCustom: external_exports.boolean(),
|
|
17611
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17612
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17613
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17614
|
-
transports: external_exports.array(Transport),
|
|
17615
|
-
/** Most-sensitive first. */
|
|
17616
|
-
dataClasses: external_exports.array(DataClass),
|
|
17617
|
-
review: ReviewInfo,
|
|
17618
|
-
/** Non-provider hosts only; null for providers. */
|
|
17619
|
-
network: DestinationNetwork.nullable(),
|
|
17620
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17621
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17622
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17623
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17624
|
-
endpointCount: true,
|
|
17625
|
-
callSiteCount: true,
|
|
17626
|
-
endpoints: true
|
|
17627
|
-
}).extend({
|
|
17628
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17629
|
-
note: external_exports.string().nullable(),
|
|
17630
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17631
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17632
|
-
var ReviewDestination = external_exports.object({
|
|
17633
|
-
id: external_exports.string(),
|
|
17634
|
-
kind: DestinationKind,
|
|
17635
|
-
name: external_exports.string(),
|
|
17636
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17637
|
-
host: external_exports.string(),
|
|
17638
|
-
trust: ShareTrustLevel,
|
|
17639
|
-
status: EgressStatus,
|
|
17640
|
-
review: ReviewInfo,
|
|
17641
|
-
topDataClass: DataClass,
|
|
17642
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17643
|
-
lastSeen: external_exports.iso.datetime()
|
|
17644
|
-
}).meta({ id: "ReviewDestination" });
|
|
17645
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17646
|
-
kind: DestinationKind,
|
|
17647
|
-
total: external_exports.number().int().nonnegative(),
|
|
17648
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17649
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17650
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17651
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17652
|
-
var SharesStats = external_exports.object({
|
|
17653
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17654
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17655
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17656
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17657
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17658
|
-
byKind: external_exports.object({
|
|
17659
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17660
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17661
|
-
ip: external_exports.number().int().nonnegative()
|
|
17662
|
-
}),
|
|
17663
|
-
byTrust: external_exports.object({
|
|
17664
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17665
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17666
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17667
|
-
ip: external_exports.number().int().nonnegative()
|
|
17668
|
-
})
|
|
17669
|
-
}).meta({ id: "SharesStats" });
|
|
17670
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17671
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17672
|
-
decision: EgressDecision.nullable()
|
|
17673
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17674
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17675
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17676
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17677
|
-
q: external_exports.string().optional(),
|
|
17678
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17679
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17680
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17681
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17682
|
-
/**
|
|
17683
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17684
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17685
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17686
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17687
|
-
*/
|
|
17688
|
-
review: external_exports.stringbool().default(false)
|
|
17689
|
-
});
|
|
17690
|
-
var ExportSharesQuery = external_exports.object({
|
|
17691
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17692
|
-
q: external_exports.string().optional(),
|
|
17693
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17694
|
-
});
|
|
17695
|
-
|
|
17696
17886
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17697
17887
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17698
17888
|
function trustDefaultStatus(trust) {
|
|
@@ -17712,7 +17902,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17712
17902
|
const reasons = [];
|
|
17713
17903
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17714
17904
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17715
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17905
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17716
17906
|
return reasons;
|
|
17717
17907
|
}
|
|
17718
17908
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17739,6 +17929,37 @@ function reviewSeverityRank(reasons) {
|
|
|
17739
17929
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17740
17930
|
}
|
|
17741
17931
|
|
|
17932
|
+
// ../../packages/persistence/src/ids.ts
|
|
17933
|
+
import { createHash } from "crypto";
|
|
17934
|
+
function sha256Hex(input) {
|
|
17935
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17936
|
+
}
|
|
17937
|
+
function inventoryId(objectType, identityKey) {
|
|
17938
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17939
|
+
}
|
|
17940
|
+
function sourceProjectId(url2) {
|
|
17941
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17942
|
+
}
|
|
17943
|
+
function classifiedDataId(cls) {
|
|
17944
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17945
|
+
}
|
|
17946
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17947
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17948
|
+
}
|
|
17949
|
+
function llmCallId(sessionId, messageId) {
|
|
17950
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17951
|
+
}
|
|
17952
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17953
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17954
|
+
}
|
|
17955
|
+
var NO_SESSION = "no_session";
|
|
17956
|
+
var NO_PATH = "no_path";
|
|
17957
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17958
|
+
return sha256Hex(
|
|
17959
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17960
|
+
);
|
|
17961
|
+
}
|
|
17962
|
+
|
|
17742
17963
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17743
17964
|
function escapeLikePattern(s) {
|
|
17744
17965
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17835,28 +18056,98 @@ function evidenceExists(db, object2) {
|
|
|
17835
18056
|
return schemaObjectExists(db, "table", object2.name);
|
|
17836
18057
|
}
|
|
17837
18058
|
|
|
17838
|
-
// ../../packages/persistence/src/
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
18059
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18060
|
+
function allRows(stmt, params) {
|
|
18061
|
+
if (params === void 0) return stmt.all();
|
|
18062
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18063
|
+
return stmt.all(params);
|
|
17842
18064
|
}
|
|
17843
|
-
function
|
|
17844
|
-
|
|
18065
|
+
function getRow(stmt, params) {
|
|
18066
|
+
if (params === void 0) return stmt.get();
|
|
18067
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18068
|
+
return stmt.get(params);
|
|
17845
18069
|
}
|
|
17846
|
-
function
|
|
17847
|
-
return
|
|
18070
|
+
function intToBool(raw) {
|
|
18071
|
+
return raw === 1 || raw === true;
|
|
17848
18072
|
}
|
|
17849
|
-
function
|
|
17850
|
-
return
|
|
18073
|
+
function boolToInt(b) {
|
|
18074
|
+
return b ? 1 : 0;
|
|
17851
18075
|
}
|
|
17852
|
-
function
|
|
17853
|
-
|
|
18076
|
+
function bindParams(row) {
|
|
18077
|
+
const out = {};
|
|
18078
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18079
|
+
out[key] = value === void 0 ? null : value;
|
|
18080
|
+
}
|
|
18081
|
+
return out;
|
|
17854
18082
|
}
|
|
17855
|
-
function
|
|
17856
|
-
return
|
|
18083
|
+
function countScalar(db, sql, params) {
|
|
18084
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17857
18085
|
}
|
|
17858
|
-
function
|
|
17859
|
-
|
|
18086
|
+
function countBy(db, sql, params) {
|
|
18087
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18088
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18089
|
+
map2.set(row.k, row.n);
|
|
18090
|
+
}
|
|
18091
|
+
return map2;
|
|
18092
|
+
}
|
|
18093
|
+
function mapRowsTolerant(rows, map2) {
|
|
18094
|
+
const out = [];
|
|
18095
|
+
for (const row of rows) {
|
|
18096
|
+
try {
|
|
18097
|
+
out.push(map2(row));
|
|
18098
|
+
} catch {
|
|
18099
|
+
}
|
|
18100
|
+
}
|
|
18101
|
+
return out;
|
|
18102
|
+
}
|
|
18103
|
+
|
|
18104
|
+
// ../../packages/persistence/src/paths.ts
|
|
18105
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18106
|
+
var DATA_DIR_MODE = 448;
|
|
18107
|
+
var DATA_FILE_MODE = 384;
|
|
18108
|
+
var DB_FILENAME = "aka.db";
|
|
18109
|
+
function chmodBestEffort(path, mode) {
|
|
18110
|
+
try {
|
|
18111
|
+
chmodSync(path, mode);
|
|
18112
|
+
} catch {
|
|
18113
|
+
}
|
|
18114
|
+
}
|
|
18115
|
+
function tightenDir(dir) {
|
|
18116
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18117
|
+
}
|
|
18118
|
+
function ensureDataDirSync(dir) {
|
|
18119
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18120
|
+
tightenDir(dir);
|
|
18121
|
+
}
|
|
18122
|
+
function dbSidecars(file2) {
|
|
18123
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18124
|
+
}
|
|
18125
|
+
function tightenFile(file2) {
|
|
18126
|
+
try {
|
|
18127
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18128
|
+
} catch {
|
|
18129
|
+
}
|
|
18130
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18131
|
+
}
|
|
18132
|
+
function tightenPerms(file2) {
|
|
18133
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18134
|
+
}
|
|
18135
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
18136
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
18137
|
+
try {
|
|
18138
|
+
rmSync(tmp, { force: true });
|
|
18139
|
+
} catch {
|
|
18140
|
+
}
|
|
18141
|
+
try {
|
|
18142
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18143
|
+
renameSync(tmp, file2);
|
|
18144
|
+
} finally {
|
|
18145
|
+
try {
|
|
18146
|
+
rmSync(tmp, { force: true });
|
|
18147
|
+
} catch {
|
|
18148
|
+
}
|
|
18149
|
+
}
|
|
18150
|
+
tightenFile(file2);
|
|
17860
18151
|
}
|
|
17861
18152
|
|
|
17862
18153
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17870,7 +18161,8 @@ function createdIndexName(statement) {
|
|
|
17870
18161
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17871
18162
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17872
18163
|
}
|
|
17873
|
-
|
|
18164
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18165
|
+
function applyMigrations(db, file2) {
|
|
17874
18166
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17875
18167
|
db.exec(
|
|
17876
18168
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17884,6 +18176,7 @@ function applyMigrations(db) {
|
|
|
17884
18176
|
);
|
|
17885
18177
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17886
18178
|
if (applied.has(migration.tag)) continue;
|
|
18179
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17887
18180
|
const evidence = evidenceObjects(migration.sql);
|
|
17888
18181
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17889
18182
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -17928,13 +18221,54 @@ function applyMigrations(db) {
|
|
|
17928
18221
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
17929
18222
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
17930
18223
|
}
|
|
17931
|
-
ensureSyncedAtColumn(db, "events");
|
|
17932
18224
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17933
18225
|
ensureScanLedgerTable(db);
|
|
17934
18226
|
ensureBlockedDetectionsTable(db);
|
|
18227
|
+
ensureRuleProbeCacheTable(db);
|
|
17935
18228
|
ensureWriteGateTrigger(db);
|
|
17936
18229
|
ensureTokenUsageColumns(db);
|
|
17937
18230
|
reconcileSourceProjectIds(db);
|
|
18231
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18232
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18233
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18234
|
+
}
|
|
18235
|
+
}
|
|
18236
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18237
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18238
|
+
if (!migration) return;
|
|
18239
|
+
if (file2) {
|
|
18240
|
+
try {
|
|
18241
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18242
|
+
} catch (error51) {
|
|
18243
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18244
|
+
return;
|
|
18245
|
+
}
|
|
18246
|
+
}
|
|
18247
|
+
try {
|
|
18248
|
+
withTransaction(
|
|
18249
|
+
db,
|
|
18250
|
+
() => {
|
|
18251
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18252
|
+
if (alreadyDropped) return;
|
|
18253
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18254
|
+
db.exec(statement);
|
|
18255
|
+
}
|
|
18256
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18257
|
+
migration.tag,
|
|
18258
|
+
Date.now()
|
|
18259
|
+
);
|
|
18260
|
+
},
|
|
18261
|
+
"IMMEDIATE"
|
|
18262
|
+
);
|
|
18263
|
+
} catch (error51) {
|
|
18264
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18265
|
+
}
|
|
18266
|
+
}
|
|
18267
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18268
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18269
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18270
|
+
tightenFile(backup);
|
|
18271
|
+
return backup;
|
|
17938
18272
|
}
|
|
17939
18273
|
var TOKEN_USAGE_COLUMNS = [
|
|
17940
18274
|
{
|
|
@@ -17963,6 +18297,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17963
18297
|
}
|
|
17964
18298
|
];
|
|
17965
18299
|
function ensureTokenUsageColumns(db) {
|
|
18300
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
17966
18301
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17967
18302
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17968
18303
|
if (!existing.has(column.name)) {
|
|
@@ -18028,11 +18363,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18028
18363
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18029
18364
|
}
|
|
18030
18365
|
}
|
|
18366
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18367
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18368
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18369
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18370
|
+
return row?.lastRowid ?? 0;
|
|
18371
|
+
}
|
|
18372
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18373
|
+
db.prepare(
|
|
18374
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18375
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18376
|
+
).run(source, lastRowid);
|
|
18377
|
+
}
|
|
18378
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18379
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18380
|
+
let processed = 0;
|
|
18381
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18382
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18383
|
+
if (rows.length === 0) return true;
|
|
18384
|
+
withTransaction(
|
|
18385
|
+
db,
|
|
18386
|
+
() => {
|
|
18387
|
+
handleRows(rows);
|
|
18388
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18389
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18390
|
+
},
|
|
18391
|
+
"IMMEDIATE"
|
|
18392
|
+
);
|
|
18393
|
+
processed += rows.length;
|
|
18394
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18395
|
+
}
|
|
18396
|
+
return false;
|
|
18397
|
+
}
|
|
18398
|
+
function parseLegacyEventMetadata(raw) {
|
|
18399
|
+
if (raw === null) return void 0;
|
|
18400
|
+
try {
|
|
18401
|
+
return JSON.parse(raw);
|
|
18402
|
+
} catch {
|
|
18403
|
+
return void 0;
|
|
18404
|
+
}
|
|
18405
|
+
}
|
|
18406
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18407
|
+
return JSON.stringify(
|
|
18408
|
+
toCaptureAttributes({
|
|
18409
|
+
id: row.id,
|
|
18410
|
+
sourceTool: row.sourceTool,
|
|
18411
|
+
kind: row.kind,
|
|
18412
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18413
|
+
contentHash: row.contentHash,
|
|
18414
|
+
content: row.content,
|
|
18415
|
+
metadata: row.metadata
|
|
18416
|
+
})
|
|
18417
|
+
);
|
|
18418
|
+
}
|
|
18419
|
+
function copyLegacyEvents(db) {
|
|
18420
|
+
const selectStmt = db.prepare(
|
|
18421
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18422
|
+
content_hash AS contentHash, content, metadata
|
|
18423
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18424
|
+
);
|
|
18425
|
+
const insertStmt = db.prepare(
|
|
18426
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18427
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18428
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18429
|
+
);
|
|
18430
|
+
const stubRootStmt = db.prepare(
|
|
18431
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18432
|
+
);
|
|
18433
|
+
return drainLegacyTable(
|
|
18434
|
+
db,
|
|
18435
|
+
"events",
|
|
18436
|
+
selectStmt,
|
|
18437
|
+
(rows) => {
|
|
18438
|
+
for (const row of rows) {
|
|
18439
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18440
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18441
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18442
|
+
insertStmt.run(
|
|
18443
|
+
bindParams({
|
|
18444
|
+
id: row.id,
|
|
18445
|
+
parentId: sessionId,
|
|
18446
|
+
rootSessionId: sessionId,
|
|
18447
|
+
eventType: row.kind,
|
|
18448
|
+
startedAt: row.occurredAt,
|
|
18449
|
+
content: row.content,
|
|
18450
|
+
contentHash: row.contentHash,
|
|
18451
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18452
|
+
})
|
|
18453
|
+
);
|
|
18454
|
+
}
|
|
18455
|
+
}
|
|
18456
|
+
);
|
|
18457
|
+
}
|
|
18458
|
+
function copyLegacyFindings(db) {
|
|
18459
|
+
const selectStmt = db.prepare(
|
|
18460
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18461
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18462
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18463
|
+
first_detected_at AS firstDetectedAt
|
|
18464
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18465
|
+
);
|
|
18466
|
+
const definitionStmt = db.prepare(
|
|
18467
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18468
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18469
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18470
|
+
);
|
|
18471
|
+
const findingStmt = db.prepare(
|
|
18472
|
+
`INSERT INTO inspection_findings
|
|
18473
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18474
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18475
|
+
finding_key, first_detected_at)
|
|
18476
|
+
VALUES
|
|
18477
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18478
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18479
|
+
:findingKey, :firstDetectedAt)
|
|
18480
|
+
ON CONFLICT(id) DO NOTHING
|
|
18481
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18482
|
+
first_detected_at = CASE
|
|
18483
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18484
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18485
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18486
|
+
END`
|
|
18487
|
+
);
|
|
18488
|
+
return drainLegacyTable(
|
|
18489
|
+
db,
|
|
18490
|
+
"findings",
|
|
18491
|
+
selectStmt,
|
|
18492
|
+
(rows) => {
|
|
18493
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18494
|
+
for (const row of rows) {
|
|
18495
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18496
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18497
|
+
if (definitionId === void 0) {
|
|
18498
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18499
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18500
|
+
definitionStmt.run(
|
|
18501
|
+
bindParams({
|
|
18502
|
+
id: definitionId,
|
|
18503
|
+
ruleId: row.ruleId,
|
|
18504
|
+
name: row.ruleId,
|
|
18505
|
+
category: row.category,
|
|
18506
|
+
severity: row.severity,
|
|
18507
|
+
definition: "",
|
|
18508
|
+
version: version2
|
|
18509
|
+
})
|
|
18510
|
+
);
|
|
18511
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18512
|
+
}
|
|
18513
|
+
findingStmt.run(
|
|
18514
|
+
bindParams({
|
|
18515
|
+
id: row.id,
|
|
18516
|
+
auditEventId: row.eventId,
|
|
18517
|
+
inspectionDefinitionId: definitionId,
|
|
18518
|
+
spanStart: row.spanStart,
|
|
18519
|
+
spanEnd: row.spanEnd,
|
|
18520
|
+
maskedMatch: row.maskedMatch,
|
|
18521
|
+
actionTaken: row.actionTaken,
|
|
18522
|
+
confidence: row.confidence,
|
|
18523
|
+
findingKey: row.findingKey,
|
|
18524
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18525
|
+
})
|
|
18526
|
+
);
|
|
18527
|
+
}
|
|
18528
|
+
}
|
|
18529
|
+
);
|
|
18530
|
+
}
|
|
18531
|
+
function runLegacyHistoryBackfill(db) {
|
|
18532
|
+
try {
|
|
18533
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18534
|
+
if (!eventsCaughtUp) return false;
|
|
18535
|
+
return copyLegacyFindings(db);
|
|
18536
|
+
} catch (error51) {
|
|
18537
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18538
|
+
return false;
|
|
18539
|
+
}
|
|
18540
|
+
}
|
|
18031
18541
|
function isForeignSqliteLineage(db) {
|
|
18032
18542
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18033
18543
|
return columnNames(db, "events").includes("tenant_id");
|
|
18034
18544
|
}
|
|
18035
18545
|
function ensureSyncedAtColumn(db, table2) {
|
|
18546
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18036
18547
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18037
18548
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18038
18549
|
}
|
|
@@ -18053,6 +18564,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18053
18564
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18054
18565
|
)`);
|
|
18055
18566
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18567
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18056
18568
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18057
18569
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18058
18570
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18071,29 +18583,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18071
18583
|
blocked_at INTEGER NOT NULL
|
|
18072
18584
|
)`);
|
|
18073
18585
|
}
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
|
|
18081
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18082
|
-
try {
|
|
18083
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18084
|
-
} catch {
|
|
18085
|
-
}
|
|
18086
|
-
}
|
|
18087
|
-
function walSidecars(file2) {
|
|
18088
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18089
|
-
}
|
|
18090
|
-
function tightenPerms(file2) {
|
|
18091
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18092
|
-
try {
|
|
18093
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18094
|
-
} catch {
|
|
18095
|
-
}
|
|
18096
|
-
}
|
|
18586
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18587
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18588
|
+
rule_key TEXT PRIMARY KEY,
|
|
18589
|
+
verdict TEXT NOT NULL,
|
|
18590
|
+
worst_probe_ms REAL NOT NULL,
|
|
18591
|
+
checked_at INTEGER NOT NULL
|
|
18592
|
+
)`);
|
|
18097
18593
|
}
|
|
18098
18594
|
|
|
18099
18595
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18115,51 +18611,6 @@ function parseJsonObject(s) {
|
|
|
18115
18611
|
return void 0;
|
|
18116
18612
|
}
|
|
18117
18613
|
|
|
18118
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18119
|
-
function allRows(stmt, params) {
|
|
18120
|
-
if (params === void 0) return stmt.all();
|
|
18121
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18122
|
-
return stmt.all(params);
|
|
18123
|
-
}
|
|
18124
|
-
function getRow(stmt, params) {
|
|
18125
|
-
if (params === void 0) return stmt.get();
|
|
18126
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18127
|
-
return stmt.get(params);
|
|
18128
|
-
}
|
|
18129
|
-
function intToBool(raw) {
|
|
18130
|
-
return raw === 1 || raw === true;
|
|
18131
|
-
}
|
|
18132
|
-
function boolToInt(b) {
|
|
18133
|
-
return b ? 1 : 0;
|
|
18134
|
-
}
|
|
18135
|
-
function bindParams(row) {
|
|
18136
|
-
const out = {};
|
|
18137
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18138
|
-
out[key] = value === void 0 ? null : value;
|
|
18139
|
-
}
|
|
18140
|
-
return out;
|
|
18141
|
-
}
|
|
18142
|
-
function countScalar(db, sql, params) {
|
|
18143
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18144
|
-
}
|
|
18145
|
-
function countBy(db, sql, params) {
|
|
18146
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18147
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18148
|
-
map2.set(row.k, row.n);
|
|
18149
|
-
}
|
|
18150
|
-
return map2;
|
|
18151
|
-
}
|
|
18152
|
-
function mapRowsTolerant(rows, map2) {
|
|
18153
|
-
const out = [];
|
|
18154
|
-
for (const row of rows) {
|
|
18155
|
-
try {
|
|
18156
|
-
out.push(map2(row));
|
|
18157
|
-
} catch {
|
|
18158
|
-
}
|
|
18159
|
-
}
|
|
18160
|
-
return out;
|
|
18161
|
-
}
|
|
18162
|
-
|
|
18163
18614
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18164
18615
|
var DAY_MS = 864e5;
|
|
18165
18616
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18776,6 +19227,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18776
19227
|
})
|
|
18777
19228
|
);
|
|
18778
19229
|
}
|
|
19230
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19231
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19232
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19233
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19234
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19235
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19236
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19237
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19238
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19239
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19240
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19241
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19242
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19243
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19244
|
+
}
|
|
18779
19245
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18780
19246
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18781
19247
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19237,8 +19703,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19237
19703
|
)
|
|
19238
19704
|
);
|
|
19239
19705
|
}
|
|
19240
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19241
|
-
//
|
|
19706
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19707
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19708
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19709
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19710
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19711
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19712
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19713
|
+
// to the old findings⋈events one.
|
|
19242
19714
|
countFindingsLast30d(ruleIds) {
|
|
19243
19715
|
if (ruleIds.length === 0) return 0;
|
|
19244
19716
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19246,8 +19718,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19246
19718
|
return countScalar(
|
|
19247
19719
|
this.db,
|
|
19248
19720
|
`SELECT count(*) AS n
|
|
19249
|
-
FROM
|
|
19250
|
-
|
|
19721
|
+
FROM inspection_findings f
|
|
19722
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19723
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19724
|
+
WHERE e.started_at >= ?
|
|
19725
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19726
|
+
AND d.rule_id IN (${inClause})`,
|
|
19251
19727
|
[since, ...ruleIds]
|
|
19252
19728
|
);
|
|
19253
19729
|
}
|
|
@@ -19257,35 +19733,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19257
19733
|
var SqliteEventsRepository = class {
|
|
19258
19734
|
constructor(db) {
|
|
19259
19735
|
this.db = db;
|
|
19260
|
-
this.insertStmt = db.prepare(
|
|
19261
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19262
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19263
|
-
);
|
|
19264
19736
|
}
|
|
19265
19737
|
db;
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
this.insertStmt.run(
|
|
19270
|
-
bindParams({
|
|
19271
|
-
id: row.id,
|
|
19272
|
-
sourceTool: row.sourceTool,
|
|
19273
|
-
kind: row.kind,
|
|
19274
|
-
occurredAt: row.occurredAt,
|
|
19275
|
-
contentHash: row.contentHash,
|
|
19276
|
-
content: row.content,
|
|
19277
|
-
metadata: row.metadata
|
|
19278
|
-
})
|
|
19279
|
-
);
|
|
19280
|
-
}
|
|
19281
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19282
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19283
|
-
// never duplicates findings.
|
|
19738
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19739
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19740
|
+
// scan never duplicates findings.
|
|
19284
19741
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19285
19742
|
// async EventsReadPort contract.
|
|
19743
|
+
//
|
|
19744
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19745
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19746
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19747
|
+
// index-friendly rather than walking rows that can never match.
|
|
19286
19748
|
contentHashes() {
|
|
19287
19749
|
const rows = allRows(
|
|
19288
|
-
this.db.prepare(
|
|
19750
|
+
this.db.prepare(
|
|
19751
|
+
`SELECT content_hash FROM audit_events
|
|
19752
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19753
|
+
)
|
|
19289
19754
|
);
|
|
19290
19755
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19291
19756
|
}
|
|
@@ -19621,17 +20086,20 @@ function parseExceptionRow(row) {
|
|
|
19621
20086
|
}
|
|
19622
20087
|
|
|
19623
20088
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19624
|
-
function
|
|
20089
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19625
20090
|
return `(
|
|
19626
|
-
SELECT fr
|
|
20091
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19627
20092
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19628
20093
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19629
20094
|
LIMIT 1
|
|
19630
20095
|
)`;
|
|
19631
20096
|
}
|
|
20097
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20098
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20099
|
+
}
|
|
19632
20100
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19633
|
-
SELECT finding_key, status FROM (
|
|
19634
|
-
SELECT fr.finding_key, fr.status,
|
|
20101
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20102
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19635
20103
|
ROW_NUMBER() OVER (
|
|
19636
20104
|
PARTITION BY fr.finding_key
|
|
19637
20105
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19658,68 +20126,21 @@ var DAY_MS3 = 864e5;
|
|
|
19658
20126
|
var SqliteFindingsRepository = class {
|
|
19659
20127
|
constructor(db) {
|
|
19660
20128
|
this.db = db;
|
|
19661
|
-
this.insertStmt = db.prepare(
|
|
19662
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19663
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19664
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19665
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19666
|
-
event_id = excluded.event_id,
|
|
19667
|
-
category = excluded.category,
|
|
19668
|
-
severity = excluded.severity,
|
|
19669
|
-
span_start = excluded.span_start,
|
|
19670
|
-
span_end = excluded.span_end,
|
|
19671
|
-
masked_match = excluded.masked_match,
|
|
19672
|
-
action_taken = excluded.action_taken,
|
|
19673
|
-
confidence = excluded.confidence`
|
|
19674
|
-
);
|
|
19675
|
-
this.sessionDupStmt = db.prepare(
|
|
19676
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19677
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19678
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19679
|
-
LIMIT 1`
|
|
19680
|
-
);
|
|
19681
20129
|
}
|
|
19682
20130
|
db;
|
|
19683
|
-
insertStmt;
|
|
19684
|
-
sessionDupStmt;
|
|
19685
|
-
insertFindings(findings, scope = {}) {
|
|
19686
|
-
for (const finding of findings) {
|
|
19687
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19688
|
-
const row = toFindingRow(finding);
|
|
19689
|
-
this.insertStmt.run({
|
|
19690
|
-
id: row.id,
|
|
19691
|
-
eventId: row.eventId,
|
|
19692
|
-
ruleId: row.ruleId,
|
|
19693
|
-
category: row.category,
|
|
19694
|
-
severity: row.severity,
|
|
19695
|
-
spanStart: row.spanStart,
|
|
19696
|
-
spanEnd: row.spanEnd,
|
|
19697
|
-
maskedMatch: row.maskedMatch,
|
|
19698
|
-
actionTaken: row.actionTaken,
|
|
19699
|
-
confidence: row.confidence,
|
|
19700
|
-
findingKey: row.findingKey ?? null
|
|
19701
|
-
});
|
|
19702
|
-
}
|
|
19703
|
-
}
|
|
19704
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19705
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19706
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19707
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19708
|
-
const hit = this.sessionDupStmt.get({
|
|
19709
|
-
ruleId: finding.ruleId,
|
|
19710
|
-
maskedMatch: finding.maskedMatch,
|
|
19711
|
-
sessionId
|
|
19712
|
-
});
|
|
19713
|
-
return hit !== void 0;
|
|
19714
|
-
}
|
|
19715
20131
|
recentFindings(opts) {
|
|
19716
20132
|
const limit = opts?.limit ?? 50;
|
|
19717
20133
|
const rows = allRows(
|
|
19718
20134
|
this.db.prepare(
|
|
19719
|
-
`SELECT f.id, f.event_id,
|
|
19720
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19721
|
-
|
|
19722
|
-
|
|
20135
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20136
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20137
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20138
|
+
e.event_type AS kind
|
|
20139
|
+
FROM inspection_findings f
|
|
20140
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20141
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20142
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20143
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19723
20144
|
LIMIT :limit`
|
|
19724
20145
|
),
|
|
19725
20146
|
{ limit }
|
|
@@ -19741,25 +20162,34 @@ var SqliteFindingsRepository = class {
|
|
|
19741
20162
|
);
|
|
19742
20163
|
}
|
|
19743
20164
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19744
|
-
* session-stamped
|
|
20165
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19745
20166
|
* page can label its findings link without the grouped pipeline. */
|
|
19746
20167
|
sessionFindingsCount(sessionId) {
|
|
19747
20168
|
if (!sessionId) return Promise.resolve(0);
|
|
19748
20169
|
return Promise.resolve(
|
|
19749
20170
|
countScalar(
|
|
19750
20171
|
this.db,
|
|
19751
|
-
`SELECT count(*) AS n FROM
|
|
19752
|
-
JOIN
|
|
19753
|
-
WHERE
|
|
20172
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20173
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20174
|
+
WHERE e.root_session_id = :sessionId
|
|
20175
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19754
20176
|
{ sessionId }
|
|
19755
20177
|
)
|
|
19756
20178
|
);
|
|
19757
20179
|
}
|
|
19758
|
-
/** Per-rule transcript firing tally for one session —
|
|
19759
|
-
*
|
|
19760
|
-
*
|
|
19761
|
-
*
|
|
19762
|
-
*
|
|
20180
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20181
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20182
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20183
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20184
|
+
* tally with the deduped groups it lists.
|
|
20185
|
+
*
|
|
20186
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20187
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20188
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20189
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20190
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20191
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20192
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19763
20193
|
sessionFirings(sessionId) {
|
|
19764
20194
|
return Object.fromEntries(
|
|
19765
20195
|
countBy(
|
|
@@ -19769,18 +20199,25 @@ var SqliteFindingsRepository = class {
|
|
|
19769
20199
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19770
20200
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19771
20201
|
WHERE e.root_session_id = :sessionId
|
|
20202
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19772
20203
|
GROUP BY d.rule_id`,
|
|
19773
20204
|
{ sessionId }
|
|
19774
20205
|
)
|
|
19775
20206
|
);
|
|
19776
20207
|
}
|
|
19777
20208
|
/**
|
|
19778
|
-
* Grouped findings for the dashboard — joins
|
|
19779
|
-
* toolName from
|
|
19780
|
-
*
|
|
20209
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20210
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20211
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20212
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20213
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20214
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20215
|
+
* severity then recency. Filtering
|
|
19781
20216
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19782
20217
|
* reflect the full filtered set; `items` is the requested
|
|
19783
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20218
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20219
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20220
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19784
20221
|
*
|
|
19785
20222
|
* Two reads, neither of which materializes a row per finding:
|
|
19786
20223
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19793,10 +20230,11 @@ var SqliteFindingsRepository = class {
|
|
|
19793
20230
|
* rule is ever restated in SQL.
|
|
19794
20231
|
*/
|
|
19795
20232
|
listGroupedFindings(query) {
|
|
19796
|
-
const sessionPredicate = query.sessionId ? `
|
|
20233
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20234
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19797
20235
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19798
20236
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19799
|
-
predicate
|
|
20237
|
+
predicate,
|
|
19800
20238
|
params: sessionParams
|
|
19801
20239
|
});
|
|
19802
20240
|
const rows = allRows(
|
|
@@ -19804,24 +20242,26 @@ var SqliteFindingsRepository = class {
|
|
|
19804
20242
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19805
20243
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19806
20244
|
FROM (
|
|
19807
|
-
SELECT f.id AS id,
|
|
19808
|
-
|
|
20245
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20246
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19809
20247
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19810
|
-
e.
|
|
19811
|
-
json_extract(e.
|
|
19812
|
-
json_extract(e.
|
|
19813
|
-
json_extract(e.
|
|
19814
|
-
e.
|
|
20248
|
+
e.started_at AS occurred_at,
|
|
20249
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20250
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20251
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20252
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20253
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19815
20254
|
latest.status AS latest_status,
|
|
19816
20255
|
ROW_NUMBER() OVER (
|
|
19817
|
-
PARTITION BY
|
|
19818
|
-
ORDER BY e.
|
|
20256
|
+
PARTITION BY d.rule_id
|
|
20257
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19819
20258
|
) AS rn
|
|
19820
|
-
FROM
|
|
19821
|
-
JOIN
|
|
20259
|
+
FROM inspection_findings f
|
|
20260
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20261
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19822
20262
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19823
20263
|
ON latest.finding_key = f.finding_key
|
|
19824
|
-
${
|
|
20264
|
+
${predicate}
|
|
19825
20265
|
)
|
|
19826
20266
|
WHERE rn <= :cap
|
|
19827
20267
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19848,17 +20288,29 @@ var SqliteFindingsRepository = class {
|
|
|
19848
20288
|
severity: query.severity,
|
|
19849
20289
|
providers: query.provider,
|
|
19850
20290
|
actions: query.action,
|
|
20291
|
+
statuses: query.status,
|
|
19851
20292
|
subtype: query.subtype,
|
|
19852
20293
|
q: query.q
|
|
19853
20294
|
};
|
|
19854
20295
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19855
20296
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20297
|
+
const statusFilter = query.status ?? [];
|
|
19856
20298
|
const totals = {
|
|
19857
|
-
findings: sorted.reduce((acc, g) =>
|
|
20299
|
+
findings: sorted.reduce((acc, g) => {
|
|
20300
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20301
|
+
const agg = aggregates.get(g.id);
|
|
20302
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20303
|
+
}, 0),
|
|
19858
20304
|
groups: sorted.length
|
|
19859
20305
|
};
|
|
19860
20306
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19861
|
-
const
|
|
20307
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20308
|
+
const items = sorted.slice(0, limit).map(
|
|
20309
|
+
(g) => statusSet ? {
|
|
20310
|
+
...g,
|
|
20311
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20312
|
+
} : g
|
|
20313
|
+
);
|
|
19862
20314
|
return Promise.resolve({
|
|
19863
20315
|
totals,
|
|
19864
20316
|
facets,
|
|
@@ -19872,45 +20324,62 @@ var SqliteFindingsRepository = class {
|
|
|
19872
20324
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19873
20325
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19874
20326
|
*
|
|
19875
|
-
*
|
|
19876
|
-
*
|
|
19877
|
-
*
|
|
19878
|
-
* status
|
|
19879
|
-
*
|
|
19880
|
-
*
|
|
20327
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20328
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20329
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20330
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20331
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20332
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20333
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20334
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20335
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20336
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20337
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19881
20338
|
* a group's row stays small however many findings it holds.
|
|
19882
20339
|
*
|
|
19883
20340
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19884
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19885
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19886
|
-
* that is a string proportional to the store (~8MB over
|
|
19887
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19888
|
-
* match an instance outside the preview, which searching
|
|
19889
|
-
* would silently lose, so it is fetched only when the
|
|
19890
|
-
* carries a `q`.
|
|
20341
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20342
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20343
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20344
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20345
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20346
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20347
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20348
|
+
* path repeating across tuples.)
|
|
19891
20349
|
*/
|
|
19892
20350
|
groupAggregates(withSearchText, scope) {
|
|
19893
|
-
const
|
|
19894
|
-
group_concat(DISTINCT json_extract(e.
|
|
19895
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20351
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20352
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20353
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19896
20354
|
const rows = this.db.prepare(
|
|
19897
|
-
`SELECT
|
|
19898
|
-
|
|
19899
|
-
max(
|
|
19900
|
-
group_concat(
|
|
19901
|
-
group_concat(
|
|
19902
|
-
group_concat(
|
|
19903
|
-
|
|
19904
|
-
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
20355
|
+
`SELECT rule_id,
|
|
20356
|
+
sum(tuple_count) AS instance_count,
|
|
20357
|
+
max(latest_at) AS latest_at,
|
|
20358
|
+
group_concat(source_tools) AS source_tools,
|
|
20359
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20360
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20361
|
+
group_concat(repos) AS repos,
|
|
20362
|
+
group_concat(files) AS files,
|
|
20363
|
+
group_concat(tool_names) AS tool_names
|
|
20364
|
+
FROM (
|
|
20365
|
+
SELECT d.rule_id AS rule_id,
|
|
20366
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20367
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20368
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20369
|
+
count(*) AS tuple_count,
|
|
20370
|
+
max(e.started_at) AS latest_at,
|
|
20371
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20372
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20373
|
+
${innerSearchColumns}
|
|
20374
|
+
FROM inspection_findings f
|
|
20375
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20376
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20377
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20378
|
+
ON latest.finding_key = f.finding_key
|
|
20379
|
+
${scope.predicate}
|
|
20380
|
+
GROUP BY d.rule_id, status_tuple
|
|
20381
|
+
)
|
|
20382
|
+
GROUP BY rule_id`
|
|
19914
20383
|
).all(scope.params);
|
|
19915
20384
|
return new Map(
|
|
19916
20385
|
rows.map((r) => [
|
|
@@ -19920,13 +20389,14 @@ var SqliteFindingsRepository = class {
|
|
|
19920
20389
|
sourceTools: splitConcat(r.source_tools),
|
|
19921
20390
|
actionsTaken: splitConcat(r.actions_taken),
|
|
19922
20391
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19923
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20392
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
19924
20393
|
return {
|
|
19925
20394
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19926
20395
|
// so the marker stands in for the key itself (never rendered).
|
|
19927
20396
|
kind,
|
|
19928
20397
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19929
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20398
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20399
|
+
count: Number(count)
|
|
19930
20400
|
};
|
|
19931
20401
|
}),
|
|
19932
20402
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -19943,10 +20413,21 @@ var SqliteFindingsRepository = class {
|
|
|
19943
20413
|
);
|
|
19944
20414
|
}
|
|
19945
20415
|
healthSummary() {
|
|
19946
|
-
const total = countScalar(
|
|
20416
|
+
const total = countScalar(
|
|
20417
|
+
this.db,
|
|
20418
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20419
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20420
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20421
|
+
);
|
|
19947
20422
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19948
20423
|
const grouped = allRows(
|
|
19949
|
-
this.db.prepare(
|
|
20424
|
+
this.db.prepare(
|
|
20425
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20426
|
+
FROM inspection_findings f
|
|
20427
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20428
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20429
|
+
GROUP BY f.action_taken`
|
|
20430
|
+
)
|
|
19950
20431
|
);
|
|
19951
20432
|
for (const row of grouped) {
|
|
19952
20433
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -19954,12 +20435,15 @@ var SqliteFindingsRepository = class {
|
|
|
19954
20435
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19955
20436
|
const sevRows = allRows(
|
|
19956
20437
|
this.db.prepare(
|
|
19957
|
-
`SELECT
|
|
19958
|
-
FROM
|
|
20438
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20439
|
+
FROM inspection_findings f
|
|
20440
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20441
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19959
20442
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19960
20443
|
ON latest.finding_key = f.finding_key
|
|
19961
|
-
WHERE
|
|
19962
|
-
|
|
20444
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20445
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20446
|
+
GROUP BY d.severity`
|
|
19963
20447
|
)
|
|
19964
20448
|
);
|
|
19965
20449
|
for (const row of sevRows) {
|
|
@@ -19980,9 +20464,11 @@ var SqliteFindingsRepository = class {
|
|
|
19980
20464
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19981
20465
|
const rows = allRows(
|
|
19982
20466
|
this.db.prepare(
|
|
19983
|
-
`SELECT date(e.
|
|
19984
|
-
FROM
|
|
19985
|
-
|
|
20467
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20468
|
+
FROM inspection_findings f
|
|
20469
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20470
|
+
WHERE e.started_at >= :since
|
|
20471
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19986
20472
|
GROUP BY day, f.action_taken`
|
|
19987
20473
|
),
|
|
19988
20474
|
{ since }
|
|
@@ -20047,15 +20533,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20047
20533
|
this.insertStmt = db.prepare(
|
|
20048
20534
|
`INSERT INTO inspection_findings
|
|
20049
20535
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20050
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20536
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20537
|
+
finding_key, first_detected_at)
|
|
20051
20538
|
VALUES
|
|
20052
20539
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20053
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20054
|
-
|
|
20540
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20541
|
+
:findingKey,
|
|
20542
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20543
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20544
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20545
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20546
|
+
audit_event_id = excluded.audit_event_id,
|
|
20547
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20548
|
+
classified_data_id = excluded.classified_data_id,
|
|
20549
|
+
span_start = excluded.span_start,
|
|
20550
|
+
span_end = excluded.span_end,
|
|
20551
|
+
masked_match = excluded.masked_match,
|
|
20552
|
+
action_taken = excluded.action_taken,
|
|
20553
|
+
confidence = excluded.confidence`
|
|
20554
|
+
);
|
|
20555
|
+
this.sessionDupStmt = db.prepare(
|
|
20556
|
+
`SELECT 1 FROM inspection_findings f
|
|
20557
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20558
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20559
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20560
|
+
AND e.root_session_id = :sessionId
|
|
20561
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20562
|
+
LIMIT 1`
|
|
20563
|
+
);
|
|
20564
|
+
this.eventDupStmt = db.prepare(
|
|
20565
|
+
`SELECT 1 FROM inspection_findings f
|
|
20566
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20567
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20568
|
+
AND f.masked_match = :maskedMatch
|
|
20569
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20570
|
+
LIMIT 1`
|
|
20055
20571
|
);
|
|
20056
20572
|
}
|
|
20057
20573
|
db;
|
|
20058
20574
|
insertStmt;
|
|
20575
|
+
sessionDupStmt;
|
|
20576
|
+
eventDupStmt;
|
|
20577
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20578
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20579
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20580
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20581
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20582
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20583
|
+
}
|
|
20584
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20585
|
+
// recorded against the given audit event.
|
|
20586
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20587
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20588
|
+
}
|
|
20059
20589
|
insertFinding(input) {
|
|
20060
20590
|
const row = toInspectionFindingRow(input);
|
|
20061
20591
|
this.insertStmt.run(
|
|
@@ -20068,7 +20598,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20068
20598
|
spanEnd: row.spanEnd,
|
|
20069
20599
|
maskedMatch: row.maskedMatch,
|
|
20070
20600
|
actionTaken: row.actionTaken,
|
|
20071
|
-
confidence: row.confidence
|
|
20601
|
+
confidence: row.confidence,
|
|
20602
|
+
findingKey: row.findingKey,
|
|
20603
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20072
20604
|
})
|
|
20073
20605
|
);
|
|
20074
20606
|
}
|
|
@@ -20340,7 +20872,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20340
20872
|
installedRuleset() {
|
|
20341
20873
|
const rows = allRows(
|
|
20342
20874
|
this.db.prepare(
|
|
20343
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20875
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20344
20876
|
)
|
|
20345
20877
|
);
|
|
20346
20878
|
const out = {
|
|
@@ -20348,7 +20880,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20348
20880
|
enabledPacks: 0,
|
|
20349
20881
|
rules: [],
|
|
20350
20882
|
invalidRules: 0,
|
|
20351
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20883
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20884
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20352
20885
|
};
|
|
20353
20886
|
for (const row of rows) {
|
|
20354
20887
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20370,6 +20903,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20370
20903
|
if (parsed.success) {
|
|
20371
20904
|
out.rules.push(parsed.data);
|
|
20372
20905
|
out.ruleActions.set(parsed.data.id, action);
|
|
20906
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20373
20907
|
} else out.invalidRules += 1;
|
|
20374
20908
|
}
|
|
20375
20909
|
}
|
|
@@ -21544,19 +22078,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21544
22078
|
);
|
|
21545
22079
|
this.openAtRestStmt = db.prepare(
|
|
21546
22080
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21547
|
-
FROM
|
|
21548
|
-
JOIN
|
|
21549
|
-
WHERE e.
|
|
21550
|
-
AND json_extract(e.
|
|
22081
|
+
FROM inspection_findings f
|
|
22082
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22083
|
+
WHERE e.event_type = 'code_change'
|
|
22084
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21551
22085
|
AND f.finding_key IS NOT NULL
|
|
21552
22086
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21553
22087
|
);
|
|
21554
22088
|
this.resolvedAtRestStmt = db.prepare(
|
|
21555
22089
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21556
|
-
FROM
|
|
21557
|
-
JOIN
|
|
21558
|
-
WHERE e.
|
|
21559
|
-
AND json_extract(e.
|
|
22090
|
+
FROM inspection_findings f
|
|
22091
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22092
|
+
WHERE e.event_type = 'code_change'
|
|
22093
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21560
22094
|
AND f.finding_key IS NOT NULL
|
|
21561
22095
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21562
22096
|
);
|
|
@@ -21624,6 +22158,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21624
22158
|
}
|
|
21625
22159
|
};
|
|
21626
22160
|
|
|
22161
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22162
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22163
|
+
constructor(db) {
|
|
22164
|
+
this.db = db;
|
|
22165
|
+
this.upsertStmt = db.prepare(
|
|
22166
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22167
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22168
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22169
|
+
verdict = excluded.verdict,
|
|
22170
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22171
|
+
checked_at = excluded.checked_at`
|
|
22172
|
+
);
|
|
22173
|
+
this.readStmt = db.prepare(
|
|
22174
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22175
|
+
);
|
|
22176
|
+
}
|
|
22177
|
+
db;
|
|
22178
|
+
upsertStmt;
|
|
22179
|
+
readStmt;
|
|
22180
|
+
getVerdict(ruleKey) {
|
|
22181
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22182
|
+
}
|
|
22183
|
+
setVerdict(ruleKey, verdict, worstProbeMs) {
|
|
22184
|
+
failOpenTransaction(this.db, () => {
|
|
22185
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
|
|
22186
|
+
});
|
|
22187
|
+
}
|
|
22188
|
+
};
|
|
22189
|
+
|
|
21627
22190
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21628
22191
|
var SqliteScanLedgerRepository = class {
|
|
21629
22192
|
constructor(db) {
|
|
@@ -21750,25 +22313,27 @@ var SqliteSecurityRepository = class {
|
|
|
21750
22313
|
severitySummary() {
|
|
21751
22314
|
const rows = allRows(
|
|
21752
22315
|
this.db.prepare(
|
|
21753
|
-
`SELECT
|
|
22316
|
+
`SELECT d.severity AS severity,
|
|
21754
22317
|
COUNT(*) AS count,
|
|
21755
22318
|
SUM(CASE
|
|
21756
|
-
WHEN e.
|
|
22319
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21757
22320
|
WHEN f.finding_key IS NULL THEN 0
|
|
21758
22321
|
WHEN latest.status = 'resolved' THEN 1
|
|
21759
22322
|
ELSE 0
|
|
21760
22323
|
END) AS caught,
|
|
21761
22324
|
SUM(CASE
|
|
21762
|
-
WHEN e.
|
|
22325
|
+
WHEN e.event_type = 'code_change'
|
|
21763
22326
|
AND f.finding_key IS NOT NULL
|
|
21764
22327
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21765
22328
|
ELSE 0
|
|
21766
22329
|
END) AS open_at_rest
|
|
21767
|
-
FROM
|
|
21768
|
-
JOIN
|
|
22330
|
+
FROM inspection_findings f
|
|
22331
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22332
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21769
22333
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21770
22334
|
ON latest.finding_key = f.finding_key
|
|
21771
|
-
|
|
22335
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22336
|
+
GROUP BY d.severity`
|
|
21772
22337
|
)
|
|
21773
22338
|
);
|
|
21774
22339
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21834,7 +22399,7 @@ var SqliteSecurityRepository = class {
|
|
|
21834
22399
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21835
22400
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21836
22401
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21837
|
-
// (
|
|
22402
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21838
22403
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21839
22404
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21840
22405
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21859,30 +22424,20 @@ var SqliteSecurityRepository = class {
|
|
|
21859
22424
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21860
22425
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21861
22426
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21862
|
-
//
|
|
21863
|
-
// the parent event's
|
|
21864
|
-
// backfill left null.
|
|
21865
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
WHERE fr.finding_key = f.finding_key
|
|
21875
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21876
|
-
LIMIT 1
|
|
21877
|
-
) AS latest_method,
|
|
21878
|
-
(
|
|
21879
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21880
|
-
WHERE fr.finding_key = f.finding_key
|
|
21881
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21882
|
-
LIMIT 1
|
|
21883
|
-
) AS latest_resolved_at
|
|
21884
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22427
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22428
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22429
|
+
// legacy/edge row the backfill left null.
|
|
22430
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22431
|
+
latest.status AS latest_status,
|
|
22432
|
+
latest.method AS latest_method,
|
|
22433
|
+
latest.resolved_at AS latest_resolved_at
|
|
22434
|
+
FROM inspection_findings f
|
|
22435
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22436
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22437
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22438
|
+
ON latest.finding_key = f.finding_key
|
|
21885
22439
|
WHERE f.finding_key IS NOT NULL
|
|
22440
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21886
22441
|
AND EXISTS (
|
|
21887
22442
|
SELECT 1 FROM finding_resolution fr
|
|
21888
22443
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21929,11 +22484,13 @@ var SqliteSecurityRepository = class {
|
|
|
21929
22484
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21930
22485
|
const rows = allRows(
|
|
21931
22486
|
this.db.prepare(
|
|
21932
|
-
`SELECT json_extract(e.
|
|
21933
|
-
FROM
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
AND
|
|
22487
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22488
|
+
FROM inspection_findings f
|
|
22489
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22490
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22491
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22492
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22493
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
21937
22494
|
GROUP BY repo
|
|
21938
22495
|
ORDER BY c DESC, repo
|
|
21939
22496
|
LIMIT :limit`
|
|
@@ -21957,44 +22514,28 @@ var SqliteSecurityRepository = class {
|
|
|
21957
22514
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
21958
22515
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
21959
22516
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
21960
|
-
// finding's parent event (
|
|
21961
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
21962
|
-
// capped at `limit`.
|
|
22517
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22518
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22519
|
+
// DESC, capped at `limit`.
|
|
21963
22520
|
recentlyResolved(limit = 20) {
|
|
21964
22521
|
const rows = allRows(
|
|
21965
22522
|
this.db.prepare(
|
|
21966
22523
|
`SELECT f.finding_key AS finding_key,
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
json_extract(e.
|
|
21970
|
-
COALESCE(f.first_detected_at, e.
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
|
|
21977
|
-
|
|
21978
|
-
WHERE e.kind = 'code_change'
|
|
22524
|
+
d.rule_id AS rule_id,
|
|
22525
|
+
d.severity AS severity,
|
|
22526
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22527
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22528
|
+
latest.resolved_at AS latest_resolved_at
|
|
22529
|
+
FROM inspection_findings f
|
|
22530
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22531
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22532
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22533
|
+
ON latest.finding_key = f.finding_key
|
|
22534
|
+
WHERE e.event_type = 'code_change'
|
|
21979
22535
|
AND f.finding_key IS NOT NULL
|
|
21980
|
-
AND
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21984
|
-
LIMIT 1
|
|
21985
|
-
) = 'resolved'
|
|
21986
|
-
AND (
|
|
21987
|
-
SELECT fr.method FROM finding_resolution fr
|
|
21988
|
-
WHERE fr.finding_key = f.finding_key
|
|
21989
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21990
|
-
LIMIT 1
|
|
21991
|
-
) = 'fixed-at-source'
|
|
21992
|
-
AND (
|
|
21993
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21994
|
-
WHERE fr.finding_key = f.finding_key
|
|
21995
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21996
|
-
LIMIT 1
|
|
21997
|
-
) IS NOT NULL
|
|
22536
|
+
AND latest.status = 'resolved'
|
|
22537
|
+
AND latest.method = 'fixed-at-source'
|
|
22538
|
+
AND latest.resolved_at IS NOT NULL
|
|
21998
22539
|
ORDER BY latest_resolved_at DESC
|
|
21999
22540
|
LIMIT :limit`
|
|
22000
22541
|
),
|
|
@@ -22013,15 +22554,18 @@ var SqliteSecurityRepository = class {
|
|
|
22013
22554
|
return Promise.resolve({ items });
|
|
22014
22555
|
}
|
|
22015
22556
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22016
|
-
// epoch-millis timestamp.
|
|
22557
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22017
22558
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22018
22559
|
findingsInRange(fromMs, toMs) {
|
|
22019
22560
|
const rows = allRows(
|
|
22020
22561
|
this.db.prepare(
|
|
22021
|
-
`SELECT e.
|
|
22022
|
-
FROM
|
|
22023
|
-
|
|
22024
|
-
|
|
22562
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22563
|
+
FROM inspection_findings f
|
|
22564
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22565
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22566
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22567
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22568
|
+
ORDER BY e.started_at`
|
|
22025
22569
|
),
|
|
22026
22570
|
{ from: fromMs, to: toMs }
|
|
22027
22571
|
);
|
|
@@ -22035,11 +22579,50 @@ var SqliteSecurityRepository = class {
|
|
|
22035
22579
|
|
|
22036
22580
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22037
22581
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22038
|
-
var
|
|
22582
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22583
|
+
var IN_CHUNK = 500;
|
|
22584
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22585
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22586
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22587
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22039
22588
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22040
22589
|
function parseNetwork(networkJson) {
|
|
22041
22590
|
return safeJson(networkJson, null);
|
|
22042
22591
|
}
|
|
22592
|
+
function capHits(all, mode) {
|
|
22593
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22594
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22595
|
+
}
|
|
22596
|
+
if (mode === "walk") {
|
|
22597
|
+
return {
|
|
22598
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22599
|
+
droppedFiles: [],
|
|
22600
|
+
truncated: true
|
|
22601
|
+
};
|
|
22602
|
+
}
|
|
22603
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22604
|
+
for (const hit of all) {
|
|
22605
|
+
const bucket = byFile.get(hit.site.file);
|
|
22606
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22607
|
+
else bucket.push(hit);
|
|
22608
|
+
}
|
|
22609
|
+
const hits = [];
|
|
22610
|
+
const droppedFiles = [];
|
|
22611
|
+
for (const [file2, bucket] of byFile) {
|
|
22612
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22613
|
+
else hits.push(...bucket);
|
|
22614
|
+
}
|
|
22615
|
+
return { hits, droppedFiles, truncated: true };
|
|
22616
|
+
}
|
|
22617
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22618
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22619
|
+
const dropped = new Set(droppedFiles);
|
|
22620
|
+
return {
|
|
22621
|
+
mode: "ledger",
|
|
22622
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22623
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22624
|
+
};
|
|
22625
|
+
}
|
|
22043
22626
|
function toEndpointSummary(row) {
|
|
22044
22627
|
return {
|
|
22045
22628
|
id: row.id,
|
|
@@ -22130,13 +22713,15 @@ var SqliteSharesRepository = class {
|
|
|
22130
22713
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22131
22714
|
const insecure = countScalar(
|
|
22132
22715
|
this.db,
|
|
22133
|
-
|
|
22716
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22717
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22134
22718
|
);
|
|
22135
22719
|
const needsReview = countScalar(
|
|
22136
22720
|
this.db,
|
|
22137
22721
|
`SELECT count(DISTINCT d.id) AS n
|
|
22138
22722
|
FROM share_destination d
|
|
22139
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22723
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22724
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22140
22725
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22141
22726
|
);
|
|
22142
22727
|
const kindCounts = countBy(
|
|
@@ -22146,6 +22731,7 @@ var SqliteSharesRepository = class {
|
|
|
22146
22731
|
const byKind = {
|
|
22147
22732
|
provider: kindCounts.get("provider") ?? 0,
|
|
22148
22733
|
internal: kindCounts.get("internal") ?? 0,
|
|
22734
|
+
external: kindCounts.get("external") ?? 0,
|
|
22149
22735
|
ip: kindCounts.get("ip") ?? 0
|
|
22150
22736
|
};
|
|
22151
22737
|
const trustCounts = countBy(
|
|
@@ -22221,23 +22807,316 @@ var SqliteSharesRepository = class {
|
|
|
22221
22807
|
// real edit from a no-such-destination.
|
|
22222
22808
|
/**
|
|
22223
22809
|
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22224
|
-
* `null` deletes the override
|
|
22810
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22811
|
+
*
|
|
22812
|
+
* The written row carries both the destination id and its host, so the
|
|
22813
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22814
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22815
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22816
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22817
|
+
* would otherwise race a concurrent prune.
|
|
22225
22818
|
*/
|
|
22226
22819
|
setEgressDecision(destinationId, decision) {
|
|
22227
|
-
|
|
22228
|
-
|
|
22229
|
-
|
|
22230
|
-
|
|
22231
|
-
|
|
22820
|
+
let existed = false;
|
|
22821
|
+
withTransaction(
|
|
22822
|
+
this.db,
|
|
22823
|
+
() => {
|
|
22824
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22825
|
+
if (dest === void 0) return;
|
|
22826
|
+
existed = true;
|
|
22827
|
+
this.db.prepare(
|
|
22828
|
+
`DELETE FROM egress_decision_override
|
|
22829
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22830
|
+
).run({ host: dest.host, destinationId });
|
|
22831
|
+
if (decision === null) return;
|
|
22832
|
+
this.db.prepare(
|
|
22833
|
+
`INSERT INTO egress_decision_override
|
|
22834
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22835
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22836
|
+
).run({
|
|
22837
|
+
id: randomUUID7(),
|
|
22838
|
+
destinationId,
|
|
22839
|
+
host: dest.host,
|
|
22840
|
+
decision,
|
|
22841
|
+
now: Date.now()
|
|
22842
|
+
});
|
|
22843
|
+
},
|
|
22844
|
+
"IMMEDIATE"
|
|
22845
|
+
);
|
|
22846
|
+
return existed;
|
|
22847
|
+
}
|
|
22848
|
+
/**
|
|
22849
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22850
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22851
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22852
|
+
* references, and drop what no longer has evidence.
|
|
22853
|
+
*
|
|
22854
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22855
|
+
* display payload and never scope a delete. The whole write is one
|
|
22856
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22857
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22858
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22859
|
+
* ledger commit so the next scan retries.
|
|
22860
|
+
*
|
|
22861
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22862
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22863
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22864
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22865
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22866
|
+
*/
|
|
22867
|
+
recordProjectEgress(input) {
|
|
22868
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22869
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22870
|
+
const now = Date.now();
|
|
22871
|
+
let summary = {
|
|
22872
|
+
destinations: 0,
|
|
22873
|
+
endpoints: 0,
|
|
22874
|
+
callSites: 0,
|
|
22875
|
+
truncated,
|
|
22876
|
+
droppedFiles
|
|
22877
|
+
};
|
|
22878
|
+
withTransaction(
|
|
22879
|
+
this.db,
|
|
22880
|
+
() => {
|
|
22881
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22882
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22883
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22884
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22885
|
+
this.pruneOrphans();
|
|
22886
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22887
|
+
},
|
|
22888
|
+
"IMMEDIATE"
|
|
22889
|
+
);
|
|
22890
|
+
return summary;
|
|
22891
|
+
}
|
|
22892
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22893
|
+
/**
|
|
22894
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22895
|
+
*
|
|
22896
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22897
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22898
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22899
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22900
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22901
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22902
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22903
|
+
*/
|
|
22904
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22905
|
+
if (reconcile.mode === "walk") {
|
|
22906
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22907
|
+
this.db.prepare(
|
|
22908
|
+
`DELETE FROM share_call_site
|
|
22909
|
+
WHERE project_key = :key
|
|
22910
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22911
|
+
AND file NOT LIKE '.%'
|
|
22912
|
+
AND file NOT LIKE '%/.%'`
|
|
22913
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22914
|
+
return;
|
|
22915
|
+
}
|
|
22916
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22917
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22918
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22919
|
+
this.db.prepare(
|
|
22920
|
+
`DELETE FROM share_call_site
|
|
22921
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22922
|
+
).run(projectKey, ...chunk);
|
|
22923
|
+
}
|
|
22924
|
+
}
|
|
22925
|
+
/**
|
|
22926
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22927
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22928
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22929
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22930
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22931
|
+
* classification for this batch.
|
|
22932
|
+
*/
|
|
22933
|
+
upsertHits(input, hits, projectId, now) {
|
|
22934
|
+
if (hits.length === 0) return;
|
|
22935
|
+
const destStmt = this.db.prepare(
|
|
22936
|
+
`INSERT INTO share_destination
|
|
22937
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22938
|
+
created_at, updated_at)
|
|
22939
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22940
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22941
|
+
kind = excluded.kind,
|
|
22942
|
+
name = excluded.name,
|
|
22943
|
+
category = excluded.category,
|
|
22944
|
+
trust = excluded.trust,
|
|
22945
|
+
network_json = excluded.network_json,
|
|
22946
|
+
last_seen = excluded.last_seen,
|
|
22947
|
+
updated_at = excluded.updated_at`
|
|
22948
|
+
);
|
|
22949
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22950
|
+
const endpointStmt = this.db.prepare(
|
|
22951
|
+
`INSERT INTO share_endpoint
|
|
22952
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
22953
|
+
created_at, updated_at)
|
|
22954
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
22955
|
+
:now, :now)
|
|
22956
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
22957
|
+
transport = excluded.transport,
|
|
22958
|
+
template = excluded.template,
|
|
22959
|
+
data_class = excluded.data_class,
|
|
22960
|
+
last_seen = excluded.last_seen,
|
|
22961
|
+
updated_at = excluded.updated_at`
|
|
22962
|
+
);
|
|
22963
|
+
const endpointIdStmt = this.db.prepare(
|
|
22964
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
22965
|
+
);
|
|
22966
|
+
const siteStmt = this.db.prepare(
|
|
22967
|
+
`INSERT INTO share_call_site
|
|
22968
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
22969
|
+
project_id, created_at, updated_at)
|
|
22970
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
22971
|
+
:vendored, :projectId, :now, :now)
|
|
22972
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
22973
|
+
snippet = excluded.snippet,
|
|
22974
|
+
dynamic = excluded.dynamic,
|
|
22975
|
+
vendored = excluded.vendored,
|
|
22976
|
+
project = excluded.project,
|
|
22977
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
22978
|
+
updated_at = excluded.updated_at`
|
|
22979
|
+
);
|
|
22980
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
22981
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
22982
|
+
for (const hit of hits) {
|
|
22983
|
+
let destinationId = destIds.get(hit.host);
|
|
22984
|
+
if (destinationId === void 0) {
|
|
22985
|
+
destStmt.run({
|
|
22986
|
+
id: randomUUID7(),
|
|
22987
|
+
kind: hit.kind,
|
|
22988
|
+
name: hit.name,
|
|
22989
|
+
host: hit.host,
|
|
22990
|
+
category: hit.category,
|
|
22991
|
+
trust: hit.trust,
|
|
22992
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
22993
|
+
now
|
|
22994
|
+
});
|
|
22995
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
22996
|
+
destIds.set(hit.host, destinationId);
|
|
22997
|
+
}
|
|
22998
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
22999
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
23000
|
+
if (endpointId === void 0) {
|
|
23001
|
+
endpointStmt.run({
|
|
23002
|
+
id: randomUUID7(),
|
|
23003
|
+
destinationId,
|
|
23004
|
+
method: hit.method,
|
|
23005
|
+
transport: hit.transport,
|
|
23006
|
+
url: hit.url,
|
|
23007
|
+
template: boolToInt(hit.template),
|
|
23008
|
+
dataClass: hit.dataClass,
|
|
23009
|
+
now
|
|
23010
|
+
});
|
|
23011
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23012
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23013
|
+
}
|
|
23014
|
+
siteStmt.run({
|
|
23015
|
+
id: randomUUID7(),
|
|
23016
|
+
endpointId,
|
|
23017
|
+
project: input.project,
|
|
23018
|
+
projectKey: input.projectKey,
|
|
23019
|
+
file: hit.site.file,
|
|
23020
|
+
line: hit.site.line,
|
|
23021
|
+
snippet: hit.site.snippet,
|
|
23022
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23023
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23024
|
+
projectId,
|
|
23025
|
+
now
|
|
23026
|
+
});
|
|
22232
23027
|
}
|
|
23028
|
+
}
|
|
23029
|
+
/**
|
|
23030
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23031
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23032
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23033
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23034
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23035
|
+
* representative.
|
|
23036
|
+
*/
|
|
23037
|
+
knownProjectId(projectKey) {
|
|
23038
|
+
return getRow(
|
|
23039
|
+
this.db.prepare(
|
|
23040
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23041
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23042
|
+
),
|
|
23043
|
+
[projectKey]
|
|
23044
|
+
)?.projectId ?? null;
|
|
23045
|
+
}
|
|
23046
|
+
/**
|
|
23047
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23048
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23049
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
23050
|
+
*/
|
|
23051
|
+
confirmLastSeen(projectKey, now) {
|
|
22233
23052
|
this.db.prepare(
|
|
22234
|
-
`
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
23053
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23054
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23055
|
+
).run({ now, key: projectKey });
|
|
23056
|
+
this.db.prepare(
|
|
23057
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23058
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23059
|
+
FROM share_endpoint e
|
|
23060
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23061
|
+
WHERE c.project_key = :key)`
|
|
23062
|
+
).run({ now, key: projectKey });
|
|
23063
|
+
}
|
|
23064
|
+
/**
|
|
23065
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23066
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23067
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23068
|
+
*
|
|
23069
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23070
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23071
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23072
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23073
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23074
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23075
|
+
*/
|
|
23076
|
+
pruneOrphans() {
|
|
23077
|
+
this.db.exec(
|
|
23078
|
+
`DELETE FROM share_endpoint
|
|
23079
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23080
|
+
);
|
|
23081
|
+
this.db.exec(
|
|
23082
|
+
`DELETE FROM egress_decision_override
|
|
23083
|
+
WHERE host IS NULL
|
|
23084
|
+
AND destination_id IN (
|
|
23085
|
+
SELECT d.id FROM share_destination d
|
|
23086
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23087
|
+
);
|
|
23088
|
+
this.db.exec(
|
|
23089
|
+
`DELETE FROM share_destination
|
|
23090
|
+
WHERE NOT EXISTS (
|
|
23091
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23092
|
+
);
|
|
23093
|
+
}
|
|
23094
|
+
/**
|
|
23095
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23096
|
+
* projects and carry no project column, so both are counted through the call
|
|
23097
|
+
* sites that reference them.
|
|
23098
|
+
*/
|
|
23099
|
+
projectTotals(projectKey) {
|
|
23100
|
+
return {
|
|
23101
|
+
destinations: countScalar(
|
|
23102
|
+
this.db,
|
|
23103
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23104
|
+
FROM share_endpoint e
|
|
23105
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23106
|
+
WHERE c.project_key = ?`,
|
|
23107
|
+
[projectKey]
|
|
23108
|
+
),
|
|
23109
|
+
endpoints: countScalar(
|
|
23110
|
+
this.db,
|
|
23111
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23112
|
+
[projectKey]
|
|
23113
|
+
),
|
|
23114
|
+
callSites: countScalar(
|
|
23115
|
+
this.db,
|
|
23116
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23117
|
+
[projectKey]
|
|
23118
|
+
)
|
|
23119
|
+
};
|
|
22241
23120
|
}
|
|
22242
23121
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22243
23122
|
mapDestRow(r) {
|
|
@@ -22257,7 +23136,8 @@ var SqliteSharesRepository = class {
|
|
|
22257
23136
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22258
23137
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22259
23138
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22260
|
-
d.created_at AS createdAt,
|
|
23139
|
+
d.created_at AS createdAt,
|
|
23140
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22261
23141
|
const conditions = [];
|
|
22262
23142
|
const params = [];
|
|
22263
23143
|
if (kinds && kinds.length > 0) {
|
|
@@ -22268,7 +23148,8 @@ var SqliteSharesRepository = class {
|
|
|
22268
23148
|
conditions.push(
|
|
22269
23149
|
`(d.trust IN ('unverified', 'ip')
|
|
22270
23150
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22271
|
-
WHERE re.destination_id = d.id
|
|
23151
|
+
WHERE re.destination_id = d.id
|
|
23152
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22272
23153
|
);
|
|
22273
23154
|
}
|
|
22274
23155
|
let sql;
|
|
@@ -22281,7 +23162,7 @@ var SqliteSharesRepository = class {
|
|
|
22281
23162
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22282
23163
|
sql = `SELECT DISTINCT ${cols}
|
|
22283
23164
|
FROM share_destination d
|
|
22284
|
-
|
|
23165
|
+
${OVERRIDE_JOIN}
|
|
22285
23166
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22286
23167
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22287
23168
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22289,7 +23170,7 @@ var SqliteSharesRepository = class {
|
|
|
22289
23170
|
} else {
|
|
22290
23171
|
sql = `SELECT ${cols}
|
|
22291
23172
|
FROM share_destination d
|
|
22292
|
-
|
|
23173
|
+
${OVERRIDE_JOIN}
|
|
22293
23174
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22294
23175
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22295
23176
|
}
|
|
@@ -22304,9 +23185,9 @@ var SqliteSharesRepository = class {
|
|
|
22304
23185
|
this.db.prepare(
|
|
22305
23186
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22306
23187
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22307
|
-
|
|
23188
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22308
23189
|
FROM share_destination d
|
|
22309
|
-
|
|
23190
|
+
${OVERRIDE_JOIN}
|
|
22310
23191
|
WHERE d.id = ?`
|
|
22311
23192
|
),
|
|
22312
23193
|
[destinationId]
|
|
@@ -22508,9 +23389,10 @@ function openWithPragmas(file2) {
|
|
|
22508
23389
|
}
|
|
22509
23390
|
function backupLegacyStore(file2) {
|
|
22510
23391
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22511
|
-
|
|
22512
|
-
|
|
22513
|
-
|
|
23392
|
+
renameSync2(file2, backup);
|
|
23393
|
+
tightenFile(backup);
|
|
23394
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23395
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22514
23396
|
}
|
|
22515
23397
|
return backup;
|
|
22516
23398
|
}
|
|
@@ -22526,7 +23408,7 @@ function openLocalDatabase(dir) {
|
|
|
22526
23408
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22527
23409
|
);
|
|
22528
23410
|
}
|
|
22529
|
-
applyMigrations(db);
|
|
23411
|
+
applyMigrations(db, file2);
|
|
22530
23412
|
tightenPerms(file2);
|
|
22531
23413
|
const events = new SqliteEventsRepository(db);
|
|
22532
23414
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22535,6 +23417,7 @@ function openLocalDatabase(dir) {
|
|
|
22535
23417
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22536
23418
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22537
23419
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23420
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22538
23421
|
const security = new SqliteSecurityRepository(db);
|
|
22539
23422
|
const detections = new SqliteDetectionsRepository(db);
|
|
22540
23423
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22552,9 +23435,56 @@ function openLocalDatabase(dir) {
|
|
|
22552
23435
|
policies.seedDefaults();
|
|
22553
23436
|
function recordCapture(event, detected) {
|
|
22554
23437
|
failOpenTransaction(db, () => {
|
|
22555
|
-
events.insertEvent(event);
|
|
22556
23438
|
const sessionId = event.metadata?.sessionId;
|
|
22557
|
-
|
|
23439
|
+
if (sessionId) {
|
|
23440
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23441
|
+
}
|
|
23442
|
+
const auditEventId = captureId(
|
|
23443
|
+
sessionId ?? null,
|
|
23444
|
+
event.contentHash,
|
|
23445
|
+
event.metadata?.filePath ?? null
|
|
23446
|
+
);
|
|
23447
|
+
auditEvents.insertAuditEvent({
|
|
23448
|
+
id: auditEventId,
|
|
23449
|
+
eventType: event.kind,
|
|
23450
|
+
startedAt: event.occurredAt,
|
|
23451
|
+
parentId: sessionId,
|
|
23452
|
+
rootSessionId: sessionId,
|
|
23453
|
+
content: event.content,
|
|
23454
|
+
contentHash: event.contentHash,
|
|
23455
|
+
attributes: toCaptureAttributes(event)
|
|
23456
|
+
});
|
|
23457
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23458
|
+
for (const finding of detected) {
|
|
23459
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23460
|
+
continue;
|
|
23461
|
+
}
|
|
23462
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23463
|
+
auditEventId,
|
|
23464
|
+
finding.ruleId,
|
|
23465
|
+
finding.maskedMatch,
|
|
23466
|
+
finding.span.start,
|
|
23467
|
+
finding.span.end
|
|
23468
|
+
)) {
|
|
23469
|
+
continue;
|
|
23470
|
+
}
|
|
23471
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23472
|
+
let definitionId = definitionIds.get(key);
|
|
23473
|
+
if (!definitionId) {
|
|
23474
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23475
|
+
definitionIds.set(key, definitionId);
|
|
23476
|
+
}
|
|
23477
|
+
inspectionFindings.insertFinding({
|
|
23478
|
+
id: finding.id,
|
|
23479
|
+
auditEventId,
|
|
23480
|
+
inspectionDefinitionId: definitionId,
|
|
23481
|
+
span: finding.span,
|
|
23482
|
+
maskedMatch: finding.maskedMatch,
|
|
23483
|
+
actionTaken: finding.actionTaken,
|
|
23484
|
+
confidence: finding.confidence,
|
|
23485
|
+
findingKey: finding.findingKey ?? void 0
|
|
23486
|
+
});
|
|
23487
|
+
}
|
|
22558
23488
|
});
|
|
22559
23489
|
}
|
|
22560
23490
|
function ensureInventory(ctx) {
|
|
@@ -22672,6 +23602,7 @@ function openLocalDatabase(dir) {
|
|
|
22672
23602
|
scanLedger,
|
|
22673
23603
|
exceptions,
|
|
22674
23604
|
resolutions,
|
|
23605
|
+
ruleProbeCache,
|
|
22675
23606
|
security,
|
|
22676
23607
|
detections,
|
|
22677
23608
|
shares,
|
|
@@ -22701,14 +23632,17 @@ function openLocalDatabase(dir) {
|
|
|
22701
23632
|
};
|
|
22702
23633
|
}
|
|
22703
23634
|
|
|
23635
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23636
|
+
import { createHash as createHash3 } from "crypto";
|
|
23637
|
+
|
|
22704
23638
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22705
23639
|
import { createHmac, randomBytes } from "crypto";
|
|
22706
|
-
import {
|
|
23640
|
+
import { readFileSync } from "fs";
|
|
22707
23641
|
import { join as join2 } from "path";
|
|
22708
23642
|
|
|
22709
23643
|
// ../../packages/persistence/src/local-layout.ts
|
|
22710
|
-
import {
|
|
22711
|
-
import {
|
|
23644
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23645
|
+
import { mkdir } from "fs/promises";
|
|
22712
23646
|
import { homedir } from "os";
|
|
22713
23647
|
import { join as join3 } from "path";
|
|
22714
23648
|
function defaultDataDir() {
|
|
@@ -22723,6 +23657,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22723
23657
|
function dbPath(base = defaultDataDir()) {
|
|
22724
23658
|
return join3(dataDir(base), "aka.db");
|
|
22725
23659
|
}
|
|
23660
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23661
|
+
ensureDataDirSync(dir);
|
|
23662
|
+
}
|
|
22726
23663
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22727
23664
|
const moves = [
|
|
22728
23665
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22730,19 +23667,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22730
23667
|
];
|
|
22731
23668
|
for (const { name, dest } of moves) {
|
|
22732
23669
|
try {
|
|
22733
|
-
|
|
22734
|
-
|
|
22735
|
-
|
|
22736
|
-
|
|
22737
|
-
}
|
|
22738
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23670
|
+
ensureDataDirSync(dest);
|
|
23671
|
+
const moved = join3(dest, name);
|
|
23672
|
+
renameSync3(join3(base, name), moved);
|
|
23673
|
+
tightenFile(moved);
|
|
22739
23674
|
} catch {
|
|
22740
23675
|
}
|
|
22741
23676
|
}
|
|
22742
23677
|
}
|
|
22743
23678
|
|
|
22744
23679
|
// ../../packages/persistence/src/settings.ts
|
|
22745
|
-
import { readFileSync as readFileSync2
|
|
23680
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22746
23681
|
import { join as join4 } from "path";
|
|
22747
23682
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22748
23683
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22764,10 +23699,8 @@ function applyOnboarding(answers2, base = defaultDataDir()) {
|
|
|
22764
23699
|
});
|
|
22765
23700
|
ensureDataDirSync(dir);
|
|
22766
23701
|
const file2 = join4(dir, "settings.json");
|
|
22767
|
-
|
|
22768
|
-
|
|
22769
|
-
`, { mode: DATA_FILE_MODE });
|
|
22770
|
-
renameSync4(tmp, file2);
|
|
23702
|
+
writeOwnerOnlyFileSync(file2, `${JSON.stringify(merged, null, 2)}
|
|
23703
|
+
`);
|
|
22771
23704
|
return merged;
|
|
22772
23705
|
}
|
|
22773
23706
|
function readJson(file2) {
|
|
@@ -22781,7 +23714,7 @@ function readJson(file2) {
|
|
|
22781
23714
|
}
|
|
22782
23715
|
|
|
22783
23716
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22784
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23717
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22785
23718
|
import { join as join5 } from "path";
|
|
22786
23719
|
var MARKER = "warn-era-capped";
|
|
22787
23720
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22789,11 +23722,15 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22789
23722
|
const marker = join5(dataDir2, MARKER);
|
|
22790
23723
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22791
23724
|
const capped = db.policies.capCategoryActions();
|
|
22792
|
-
|
|
23725
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22793
23726
|
`, { mode: DATA_FILE_MODE });
|
|
22794
23727
|
return { capped };
|
|
22795
23728
|
}
|
|
22796
23729
|
|
|
23730
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23731
|
+
import { existsSync as existsSync3 } from "fs";
|
|
23732
|
+
import { join as join6 } from "path";
|
|
23733
|
+
|
|
22797
23734
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
22798
23735
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
22799
23736
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -22844,6 +23781,12 @@ function resolveProvider() {
|
|
|
22844
23781
|
|
|
22845
23782
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22846
23783
|
function loadConfig(base = defaultDataDir()) {
|
|
23784
|
+
try {
|
|
23785
|
+
ensureLayoutDirSync(base);
|
|
23786
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23787
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23788
|
+
} catch {
|
|
23789
|
+
}
|
|
22847
23790
|
migrateLegacyLayout(base);
|
|
22848
23791
|
const settings = readWorkspaceSettings(base);
|
|
22849
23792
|
return {
|
|
@@ -22866,15 +23809,583 @@ function resolveProviderSafe() {
|
|
|
22866
23809
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22867
23810
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22868
23811
|
import { homedir as homedir2 } from "os";
|
|
22869
|
-
import { basename as basename2, join as
|
|
23812
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23813
|
+
|
|
23814
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23815
|
+
var EXTRACTOR_VERSION = "1";
|
|
23816
|
+
var PROVIDER_REGISTRY = [
|
|
23817
|
+
{
|
|
23818
|
+
id: "stripe",
|
|
23819
|
+
name: "Stripe",
|
|
23820
|
+
category: "Payments",
|
|
23821
|
+
hostSuffixes: ["stripe.com"],
|
|
23822
|
+
apiBase: "https://api.stripe.com",
|
|
23823
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23824
|
+
sdks: {
|
|
23825
|
+
npm: ["stripe"],
|
|
23826
|
+
pypi: ["stripe"],
|
|
23827
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23828
|
+
maven: ["com.stripe"],
|
|
23829
|
+
rubygems: ["stripe"],
|
|
23830
|
+
composer: ["stripe/stripe-php"],
|
|
23831
|
+
nuget: ["Stripe.net"]
|
|
23832
|
+
}
|
|
23833
|
+
},
|
|
23834
|
+
{
|
|
23835
|
+
id: "datadog",
|
|
23836
|
+
name: "Datadog",
|
|
23837
|
+
category: "Observability",
|
|
23838
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23839
|
+
apiBase: "https://api.datadoghq.com",
|
|
23840
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23841
|
+
sdks: {
|
|
23842
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23843
|
+
pypi: ["datadog", "ddtrace"],
|
|
23844
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23845
|
+
maven: ["com.datadoghq"],
|
|
23846
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23847
|
+
nuget: ["Datadog.Trace"]
|
|
23848
|
+
}
|
|
23849
|
+
},
|
|
23850
|
+
{
|
|
23851
|
+
id: "newrelic",
|
|
23852
|
+
name: "New Relic",
|
|
23853
|
+
category: "Observability",
|
|
23854
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23855
|
+
apiBase: "https://api.newrelic.com",
|
|
23856
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23857
|
+
sdks: {
|
|
23858
|
+
npm: ["newrelic"],
|
|
23859
|
+
pypi: ["newrelic"],
|
|
23860
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23861
|
+
maven: ["com.newrelic.agent.java"],
|
|
23862
|
+
rubygems: ["newrelic_rpm"],
|
|
23863
|
+
nuget: ["NewRelic.Agent"]
|
|
23864
|
+
}
|
|
23865
|
+
},
|
|
23866
|
+
{
|
|
23867
|
+
id: "sentry",
|
|
23868
|
+
name: "Sentry",
|
|
23869
|
+
category: "Error tracking",
|
|
23870
|
+
hostSuffixes: ["sentry.io"],
|
|
23871
|
+
apiBase: "https://sentry.io",
|
|
23872
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23873
|
+
sdks: {
|
|
23874
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23875
|
+
pypi: ["sentry-sdk"],
|
|
23876
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23877
|
+
maven: ["io.sentry"],
|
|
23878
|
+
rubygems: ["sentry-ruby"],
|
|
23879
|
+
cargo: ["sentry"],
|
|
23880
|
+
composer: ["sentry/sentry"],
|
|
23881
|
+
nuget: ["Sentry"]
|
|
23882
|
+
}
|
|
23883
|
+
},
|
|
23884
|
+
{
|
|
23885
|
+
id: "openai",
|
|
23886
|
+
name: "OpenAI",
|
|
23887
|
+
category: "LLM provider",
|
|
23888
|
+
hostSuffixes: ["openai.com"],
|
|
23889
|
+
apiBase: "https://api.openai.com",
|
|
23890
|
+
defaultDataClasses: ["pii", "source"],
|
|
23891
|
+
sdks: {
|
|
23892
|
+
npm: ["openai"],
|
|
23893
|
+
pypi: ["openai"],
|
|
23894
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23895
|
+
maven: ["com.openai"],
|
|
23896
|
+
rubygems: ["ruby-openai"],
|
|
23897
|
+
cargo: ["async-openai"],
|
|
23898
|
+
composer: ["openai-php/client"],
|
|
23899
|
+
nuget: ["OpenAI"]
|
|
23900
|
+
}
|
|
23901
|
+
},
|
|
23902
|
+
{
|
|
23903
|
+
id: "anthropic",
|
|
23904
|
+
name: "Anthropic",
|
|
23905
|
+
category: "LLM provider",
|
|
23906
|
+
hostSuffixes: ["anthropic.com"],
|
|
23907
|
+
apiBase: "https://api.anthropic.com",
|
|
23908
|
+
defaultDataClasses: ["pii", "source"],
|
|
23909
|
+
sdks: {
|
|
23910
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23911
|
+
pypi: ["anthropic"],
|
|
23912
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23913
|
+
nuget: ["Anthropic.SDK"]
|
|
23914
|
+
}
|
|
23915
|
+
},
|
|
23916
|
+
{
|
|
23917
|
+
id: "aws",
|
|
23918
|
+
name: "Amazon Web Services",
|
|
23919
|
+
category: "Cloud platform",
|
|
23920
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23921
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23922
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23923
|
+
sdks: {
|
|
23924
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23925
|
+
pypi: ["boto3"],
|
|
23926
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23927
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23928
|
+
rubygems: ["aws-sdk-s3"],
|
|
23929
|
+
cargo: ["aws-sdk-s3"],
|
|
23930
|
+
nuget: ["AWSSDK.S3"]
|
|
23931
|
+
}
|
|
23932
|
+
},
|
|
23933
|
+
{
|
|
23934
|
+
id: "gcp",
|
|
23935
|
+
name: "Google Cloud",
|
|
23936
|
+
category: "Cloud platform",
|
|
23937
|
+
hostSuffixes: ["googleapis.com"],
|
|
23938
|
+
apiBase: "https://storage.googleapis.com",
|
|
23939
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23940
|
+
sdks: {
|
|
23941
|
+
npm: ["@google-cloud/storage"],
|
|
23942
|
+
pypi: ["google-cloud-storage"],
|
|
23943
|
+
go: ["cloud.google.com/go"],
|
|
23944
|
+
maven: ["com.google.cloud"],
|
|
23945
|
+
rubygems: ["google-cloud-storage"],
|
|
23946
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
23947
|
+
}
|
|
23948
|
+
},
|
|
23949
|
+
{
|
|
23950
|
+
id: "azure",
|
|
23951
|
+
name: "Microsoft Azure",
|
|
23952
|
+
category: "Cloud platform",
|
|
23953
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
23954
|
+
apiBase: "https://management.azure.com",
|
|
23955
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23956
|
+
sdks: {
|
|
23957
|
+
npm: ["@azure/storage-blob"],
|
|
23958
|
+
pypi: ["azure-storage-blob"],
|
|
23959
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
23960
|
+
maven: ["com.azure"],
|
|
23961
|
+
rubygems: ["azure-storage-blob"],
|
|
23962
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
23963
|
+
}
|
|
23964
|
+
},
|
|
23965
|
+
{
|
|
23966
|
+
id: "slack",
|
|
23967
|
+
name: "Slack",
|
|
23968
|
+
category: "Notifications",
|
|
23969
|
+
hostSuffixes: ["slack.com"],
|
|
23970
|
+
apiBase: "https://slack.com/api",
|
|
23971
|
+
defaultDataClasses: ["logs"],
|
|
23972
|
+
sdks: {
|
|
23973
|
+
npm: ["@slack/web-api"],
|
|
23974
|
+
pypi: ["slack-sdk"],
|
|
23975
|
+
go: ["github.com/slack-go/slack"],
|
|
23976
|
+
maven: ["com.slack.api"],
|
|
23977
|
+
rubygems: ["slack-ruby-client"],
|
|
23978
|
+
composer: ["slack-php/slack-api"],
|
|
23979
|
+
nuget: ["SlackNet"]
|
|
23980
|
+
}
|
|
23981
|
+
},
|
|
23982
|
+
{
|
|
23983
|
+
id: "segment",
|
|
23984
|
+
name: "Segment",
|
|
23985
|
+
category: "Analytics",
|
|
23986
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
23987
|
+
apiBase: "https://api.segment.io",
|
|
23988
|
+
defaultDataClasses: ["customer"],
|
|
23989
|
+
sdks: {
|
|
23990
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
23991
|
+
pypi: ["segment-analytics-python"],
|
|
23992
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
23993
|
+
maven: ["com.segment.analytics.java"],
|
|
23994
|
+
rubygems: ["analytics-ruby"],
|
|
23995
|
+
nuget: ["Analytics"]
|
|
23996
|
+
}
|
|
23997
|
+
},
|
|
23998
|
+
{
|
|
23999
|
+
id: "twilio",
|
|
24000
|
+
name: "Twilio",
|
|
24001
|
+
category: "Communications",
|
|
24002
|
+
hostSuffixes: ["twilio.com"],
|
|
24003
|
+
apiBase: "https://api.twilio.com",
|
|
24004
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24005
|
+
sdks: {
|
|
24006
|
+
npm: ["twilio"],
|
|
24007
|
+
pypi: ["twilio"],
|
|
24008
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24009
|
+
maven: ["com.twilio.sdk"],
|
|
24010
|
+
rubygems: ["twilio-ruby"],
|
|
24011
|
+
composer: ["twilio/sdk"],
|
|
24012
|
+
nuget: ["Twilio"]
|
|
24013
|
+
}
|
|
24014
|
+
},
|
|
24015
|
+
{
|
|
24016
|
+
id: "sendgrid",
|
|
24017
|
+
name: "SendGrid",
|
|
24018
|
+
category: "Email",
|
|
24019
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24020
|
+
apiBase: "https://api.sendgrid.com",
|
|
24021
|
+
defaultDataClasses: ["pii"],
|
|
24022
|
+
sdks: {
|
|
24023
|
+
npm: ["@sendgrid/mail"],
|
|
24024
|
+
pypi: ["sendgrid"],
|
|
24025
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24026
|
+
maven: ["com.sendgrid"],
|
|
24027
|
+
rubygems: ["sendgrid-ruby"],
|
|
24028
|
+
composer: ["sendgrid/sendgrid"],
|
|
24029
|
+
nuget: ["SendGrid"]
|
|
24030
|
+
}
|
|
24031
|
+
},
|
|
24032
|
+
{
|
|
24033
|
+
id: "mailgun",
|
|
24034
|
+
name: "Mailgun",
|
|
24035
|
+
category: "Email",
|
|
24036
|
+
hostSuffixes: ["mailgun.net"],
|
|
24037
|
+
apiBase: "https://api.mailgun.net",
|
|
24038
|
+
defaultDataClasses: ["pii"],
|
|
24039
|
+
sdks: {
|
|
24040
|
+
npm: ["mailgun.js"],
|
|
24041
|
+
pypi: ["mailgun"],
|
|
24042
|
+
rubygems: ["mailgun-ruby"],
|
|
24043
|
+
composer: ["mailgun/mailgun-php"],
|
|
24044
|
+
nuget: ["Mailgun"]
|
|
24045
|
+
}
|
|
24046
|
+
},
|
|
24047
|
+
{
|
|
24048
|
+
id: "mixpanel",
|
|
24049
|
+
name: "Mixpanel",
|
|
24050
|
+
category: "Analytics",
|
|
24051
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24052
|
+
apiBase: "https://api.mixpanel.com",
|
|
24053
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24054
|
+
sdks: {
|
|
24055
|
+
npm: ["mixpanel"],
|
|
24056
|
+
pypi: ["mixpanel"],
|
|
24057
|
+
rubygems: ["mixpanel-ruby"],
|
|
24058
|
+
nuget: ["Mixpanel"]
|
|
24059
|
+
}
|
|
24060
|
+
},
|
|
24061
|
+
{
|
|
24062
|
+
id: "amplitude",
|
|
24063
|
+
name: "Amplitude",
|
|
24064
|
+
category: "Analytics",
|
|
24065
|
+
hostSuffixes: ["amplitude.com"],
|
|
24066
|
+
apiBase: "https://api2.amplitude.com",
|
|
24067
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24068
|
+
sdks: {
|
|
24069
|
+
npm: ["@amplitude/analytics-node"],
|
|
24070
|
+
pypi: ["amplitude-analytics"],
|
|
24071
|
+
nuget: ["Amplitude"]
|
|
24072
|
+
}
|
|
24073
|
+
},
|
|
24074
|
+
{
|
|
24075
|
+
id: "posthog",
|
|
24076
|
+
name: "PostHog",
|
|
24077
|
+
category: "Analytics",
|
|
24078
|
+
hostSuffixes: ["posthog.com"],
|
|
24079
|
+
apiBase: "https://us.i.posthog.com",
|
|
24080
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24081
|
+
sdks: {
|
|
24082
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24083
|
+
pypi: ["posthog"],
|
|
24084
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24085
|
+
rubygems: ["posthog-ruby"],
|
|
24086
|
+
composer: ["posthog/posthog-php"],
|
|
24087
|
+
nuget: ["PostHog"]
|
|
24088
|
+
}
|
|
24089
|
+
},
|
|
24090
|
+
{
|
|
24091
|
+
id: "honeycomb",
|
|
24092
|
+
name: "Honeycomb",
|
|
24093
|
+
category: "Observability",
|
|
24094
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24095
|
+
apiBase: "https://api.honeycomb.io",
|
|
24096
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24097
|
+
sdks: {
|
|
24098
|
+
npm: ["libhoney"],
|
|
24099
|
+
pypi: ["libhoney"],
|
|
24100
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24101
|
+
rubygems: ["libhoney"]
|
|
24102
|
+
}
|
|
24103
|
+
},
|
|
24104
|
+
{
|
|
24105
|
+
id: "grafana",
|
|
24106
|
+
name: "Grafana Cloud",
|
|
24107
|
+
category: "Observability",
|
|
24108
|
+
hostSuffixes: ["grafana.net"],
|
|
24109
|
+
apiBase: "https://grafana.net",
|
|
24110
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24111
|
+
sdks: {
|
|
24112
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24113
|
+
}
|
|
24114
|
+
},
|
|
24115
|
+
{
|
|
24116
|
+
id: "splunk",
|
|
24117
|
+
name: "Splunk",
|
|
24118
|
+
category: "Observability",
|
|
24119
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24120
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24121
|
+
defaultDataClasses: ["logs"],
|
|
24122
|
+
sdks: {
|
|
24123
|
+
npm: ["splunk-logging"],
|
|
24124
|
+
pypi: ["splunk-sdk"],
|
|
24125
|
+
maven: ["com.splunk"],
|
|
24126
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24127
|
+
}
|
|
24128
|
+
},
|
|
24129
|
+
{
|
|
24130
|
+
id: "pagerduty",
|
|
24131
|
+
name: "PagerDuty",
|
|
24132
|
+
category: "Incident response",
|
|
24133
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24134
|
+
apiBase: "https://api.pagerduty.com",
|
|
24135
|
+
defaultDataClasses: ["logs"],
|
|
24136
|
+
sdks: {
|
|
24137
|
+
npm: ["@pagerduty/pdjs"],
|
|
24138
|
+
pypi: ["pdpyras"],
|
|
24139
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24140
|
+
rubygems: ["pagerduty"]
|
|
24141
|
+
}
|
|
24142
|
+
},
|
|
24143
|
+
{
|
|
24144
|
+
id: "github",
|
|
24145
|
+
name: "GitHub",
|
|
24146
|
+
category: "Developer platform",
|
|
24147
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24148
|
+
apiBase: "https://api.github.com",
|
|
24149
|
+
defaultDataClasses: ["source"],
|
|
24150
|
+
sdks: {
|
|
24151
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24152
|
+
pypi: ["pygithub"],
|
|
24153
|
+
go: ["github.com/google/go-github"],
|
|
24154
|
+
maven: ["org.kohsuke.github-api"],
|
|
24155
|
+
rubygems: ["octokit"],
|
|
24156
|
+
cargo: ["octocrab"],
|
|
24157
|
+
composer: ["knplabs/github-api"],
|
|
24158
|
+
nuget: ["Octokit"]
|
|
24159
|
+
}
|
|
24160
|
+
},
|
|
24161
|
+
{
|
|
24162
|
+
id: "gitlab",
|
|
24163
|
+
name: "GitLab",
|
|
24164
|
+
category: "Developer platform",
|
|
24165
|
+
hostSuffixes: ["gitlab.com"],
|
|
24166
|
+
apiBase: "https://gitlab.com/api",
|
|
24167
|
+
defaultDataClasses: ["source"],
|
|
24168
|
+
sdks: {
|
|
24169
|
+
npm: ["@gitbeaker/rest"],
|
|
24170
|
+
pypi: ["python-gitlab"],
|
|
24171
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24172
|
+
rubygems: ["gitlab"],
|
|
24173
|
+
nuget: ["GitLabApiClient"]
|
|
24174
|
+
}
|
|
24175
|
+
},
|
|
24176
|
+
{
|
|
24177
|
+
id: "auth0",
|
|
24178
|
+
name: "Auth0",
|
|
24179
|
+
category: "Identity",
|
|
24180
|
+
hostSuffixes: ["auth0.com"],
|
|
24181
|
+
apiBase: "https://login.auth0.com",
|
|
24182
|
+
defaultDataClasses: ["pii"],
|
|
24183
|
+
sdks: {
|
|
24184
|
+
npm: ["auth0"],
|
|
24185
|
+
pypi: ["auth0-python"],
|
|
24186
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24187
|
+
maven: ["com.auth0"],
|
|
24188
|
+
rubygems: ["auth0"],
|
|
24189
|
+
composer: ["auth0/auth0-php"],
|
|
24190
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24191
|
+
}
|
|
24192
|
+
},
|
|
24193
|
+
{
|
|
24194
|
+
id: "okta",
|
|
24195
|
+
name: "Okta",
|
|
24196
|
+
category: "Identity",
|
|
24197
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24198
|
+
apiBase: "https://login.okta.com",
|
|
24199
|
+
defaultDataClasses: ["pii"],
|
|
24200
|
+
sdks: {
|
|
24201
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24202
|
+
pypi: ["okta"],
|
|
24203
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24204
|
+
maven: ["com.okta.sdk"],
|
|
24205
|
+
nuget: ["Okta.Sdk"]
|
|
24206
|
+
}
|
|
24207
|
+
},
|
|
24208
|
+
{
|
|
24209
|
+
id: "clerk",
|
|
24210
|
+
name: "Clerk",
|
|
24211
|
+
category: "Identity",
|
|
24212
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24213
|
+
apiBase: "https://api.clerk.com",
|
|
24214
|
+
defaultDataClasses: ["pii"],
|
|
24215
|
+
sdks: {
|
|
24216
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24217
|
+
pypi: ["clerk-backend-api"],
|
|
24218
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24219
|
+
}
|
|
24220
|
+
},
|
|
24221
|
+
{
|
|
24222
|
+
id: "supabase",
|
|
24223
|
+
name: "Supabase",
|
|
24224
|
+
category: "Backend platform",
|
|
24225
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24226
|
+
apiBase: "https://api.supabase.com",
|
|
24227
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24228
|
+
sdks: {
|
|
24229
|
+
npm: ["@supabase/supabase-js"],
|
|
24230
|
+
pypi: ["supabase"],
|
|
24231
|
+
cargo: ["postgrest"]
|
|
24232
|
+
}
|
|
24233
|
+
},
|
|
24234
|
+
{
|
|
24235
|
+
id: "firebase",
|
|
24236
|
+
name: "Firebase",
|
|
24237
|
+
category: "Backend platform",
|
|
24238
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24239
|
+
apiBase: "https://firebaseio.com",
|
|
24240
|
+
defaultDataClasses: ["customer"],
|
|
24241
|
+
sdks: {
|
|
24242
|
+
npm: ["firebase", "firebase-admin"],
|
|
24243
|
+
pypi: ["firebase-admin"],
|
|
24244
|
+
go: ["firebase.google.com/go"],
|
|
24245
|
+
maven: ["com.google.firebase"]
|
|
24246
|
+
}
|
|
24247
|
+
},
|
|
24248
|
+
{
|
|
24249
|
+
id: "mongodb-atlas",
|
|
24250
|
+
name: "MongoDB Atlas",
|
|
24251
|
+
category: "Database SaaS",
|
|
24252
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24253
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24254
|
+
defaultDataClasses: ["customer"],
|
|
24255
|
+
sdks: {
|
|
24256
|
+
npm: ["mongodb"],
|
|
24257
|
+
pypi: ["pymongo"],
|
|
24258
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24259
|
+
maven: ["org.mongodb"],
|
|
24260
|
+
rubygems: ["mongo"],
|
|
24261
|
+
cargo: ["mongodb"],
|
|
24262
|
+
nuget: ["MongoDB.Driver"]
|
|
24263
|
+
}
|
|
24264
|
+
},
|
|
24265
|
+
{
|
|
24266
|
+
id: "planetscale",
|
|
24267
|
+
name: "PlanetScale",
|
|
24268
|
+
category: "Database SaaS",
|
|
24269
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24270
|
+
apiBase: "https://api.planetscale.com",
|
|
24271
|
+
defaultDataClasses: ["customer"],
|
|
24272
|
+
sdks: {
|
|
24273
|
+
npm: ["@planetscale/database"],
|
|
24274
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24275
|
+
}
|
|
24276
|
+
},
|
|
24277
|
+
{
|
|
24278
|
+
id: "algolia",
|
|
24279
|
+
name: "Algolia",
|
|
24280
|
+
category: "Search SaaS",
|
|
24281
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24282
|
+
apiBase: "https://algolia.net",
|
|
24283
|
+
defaultDataClasses: ["customer"],
|
|
24284
|
+
sdks: {
|
|
24285
|
+
npm: ["algoliasearch"],
|
|
24286
|
+
pypi: ["algoliasearch"],
|
|
24287
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24288
|
+
maven: ["com.algolia"],
|
|
24289
|
+
rubygems: ["algolia"],
|
|
24290
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24291
|
+
nuget: ["Algolia.Search"]
|
|
24292
|
+
}
|
|
24293
|
+
},
|
|
24294
|
+
{
|
|
24295
|
+
id: "cloudflare",
|
|
24296
|
+
name: "Cloudflare",
|
|
24297
|
+
category: "CDN / edge",
|
|
24298
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24299
|
+
apiBase: "https://api.cloudflare.com",
|
|
24300
|
+
defaultDataClasses: ["logs"],
|
|
24301
|
+
sdks: {
|
|
24302
|
+
npm: ["cloudflare"],
|
|
24303
|
+
pypi: ["cloudflare"],
|
|
24304
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24305
|
+
nuget: ["CloudFlare.Client"]
|
|
24306
|
+
}
|
|
24307
|
+
},
|
|
24308
|
+
{
|
|
24309
|
+
id: "huggingface",
|
|
24310
|
+
name: "Hugging Face",
|
|
24311
|
+
category: "LLM provider",
|
|
24312
|
+
hostSuffixes: ["huggingface.co"],
|
|
24313
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24314
|
+
defaultDataClasses: ["source"],
|
|
24315
|
+
sdks: {
|
|
24316
|
+
npm: ["@huggingface/inference"],
|
|
24317
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24318
|
+
rubygems: ["hugging-face"]
|
|
24319
|
+
}
|
|
24320
|
+
},
|
|
24321
|
+
{
|
|
24322
|
+
id: "cohere",
|
|
24323
|
+
name: "Cohere",
|
|
24324
|
+
category: "LLM provider",
|
|
24325
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24326
|
+
apiBase: "https://api.cohere.com",
|
|
24327
|
+
defaultDataClasses: ["pii", "source"],
|
|
24328
|
+
sdks: {
|
|
24329
|
+
npm: ["cohere-ai"],
|
|
24330
|
+
pypi: ["cohere"],
|
|
24331
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24332
|
+
}
|
|
24333
|
+
},
|
|
24334
|
+
{
|
|
24335
|
+
id: "mistral",
|
|
24336
|
+
name: "Mistral AI",
|
|
24337
|
+
category: "LLM provider",
|
|
24338
|
+
hostSuffixes: ["mistral.ai"],
|
|
24339
|
+
apiBase: "https://api.mistral.ai",
|
|
24340
|
+
defaultDataClasses: ["pii", "source"],
|
|
24341
|
+
sdks: {
|
|
24342
|
+
npm: ["@mistralai/mistralai"],
|
|
24343
|
+
pypi: ["mistralai"],
|
|
24344
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24345
|
+
}
|
|
24346
|
+
}
|
|
24347
|
+
];
|
|
24348
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24349
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24350
|
+
|
|
24351
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24352
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24353
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24354
|
+
var SECRET_VALUE = new RegExp(
|
|
24355
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24356
|
+
"gi"
|
|
24357
|
+
);
|
|
24358
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24359
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24360
|
+
"gi"
|
|
24361
|
+
);
|
|
24362
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24363
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24364
|
+
{
|
|
24365
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24366
|
+
prefix: "/api/webhooks/"
|
|
24367
|
+
},
|
|
24368
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24369
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24370
|
+
];
|
|
24371
|
+
function escapeRegExp(literal2) {
|
|
24372
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24373
|
+
}
|
|
24374
|
+
var WEBHOOK_URL = new RegExp(
|
|
24375
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24376
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24377
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24378
|
+
"gi"
|
|
24379
|
+
);
|
|
22870
24380
|
|
|
22871
24381
|
// ../../packages/detections/src/escape-regexp.ts
|
|
22872
|
-
function
|
|
24382
|
+
function escapeRegExp2(value) {
|
|
22873
24383
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22874
24384
|
}
|
|
22875
24385
|
|
|
22876
24386
|
// ../../packages/detections/src/matchers/limits.ts
|
|
22877
24387
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24388
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
22878
24389
|
|
|
22879
24390
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22880
24391
|
var KeywordMatcher2 = class {
|
|
@@ -22885,7 +24396,7 @@ var KeywordMatcher2 = class {
|
|
|
22885
24396
|
for (const kw of keywords) {
|
|
22886
24397
|
if (kw.length === 0) continue;
|
|
22887
24398
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22888
|
-
const re = new RegExp(
|
|
24399
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
22889
24400
|
let m;
|
|
22890
24401
|
while ((m = re.exec(text)) !== null) {
|
|
22891
24402
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -22902,9 +24413,13 @@ var RegexMatcher2 = class {
|
|
|
22902
24413
|
if (rule.matcher.type !== "regex") return [];
|
|
22903
24414
|
const { pattern, flags: flags2, captureGroup } = rule.matcher;
|
|
22904
24415
|
const re = new RegExp(pattern, flags2.includes("d") ? flags2 : `${flags2}d`);
|
|
24416
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
22905
24417
|
const spans = [];
|
|
22906
24418
|
let m;
|
|
22907
|
-
|
|
24419
|
+
const maxIterations = scanText2.length + 1;
|
|
24420
|
+
let iterations = 0;
|
|
24421
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24422
|
+
if (++iterations > maxIterations) break;
|
|
22908
24423
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
22909
24424
|
if (m[0].length === 0) re.lastIndex++;
|
|
22910
24425
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -22979,22 +24494,48 @@ var CONFIG_POSTURE_RULES = [
|
|
|
22979
24494
|
}
|
|
22980
24495
|
];
|
|
22981
24496
|
|
|
24497
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24498
|
+
var EXPONENTIAL_UNITS = [
|
|
24499
|
+
"a",
|
|
24500
|
+
"0",
|
|
24501
|
+
" ",
|
|
24502
|
+
"x",
|
|
24503
|
+
"ab",
|
|
24504
|
+
"a.",
|
|
24505
|
+
"a-",
|
|
24506
|
+
"a_",
|
|
24507
|
+
"a@",
|
|
24508
|
+
"a/",
|
|
24509
|
+
"a:",
|
|
24510
|
+
"a=",
|
|
24511
|
+
"a;",
|
|
24512
|
+
"aA0",
|
|
24513
|
+
" "
|
|
24514
|
+
];
|
|
24515
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24516
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24517
|
+
);
|
|
24518
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24519
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24520
|
+
);
|
|
24521
|
+
|
|
22982
24522
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
22983
|
-
import { existsSync as
|
|
22984
|
-
import { basename, dirname, isAbsolute, join as
|
|
24523
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
24524
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
22985
24525
|
|
|
22986
24526
|
// ../../packages/plugin-sdk/src/events.ts
|
|
22987
|
-
import { createHash as
|
|
22988
|
-
|
|
22989
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
22990
|
-
import { createHash as createHash4 } from "crypto";
|
|
24527
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
22991
24528
|
|
|
22992
24529
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
22993
24530
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
22994
24531
|
|
|
22995
24532
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
22996
|
-
import { mkdirSync as
|
|
22997
|
-
import { join as
|
|
24533
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
24534
|
+
import { join as join9 } from "path";
|
|
24535
|
+
|
|
24536
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
24537
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
24538
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
22998
24539
|
|
|
22999
24540
|
// ../../packages/plugin-sdk/src/posture.ts
|
|
23000
24541
|
function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
@@ -23008,8 +24549,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
23008
24549
|
|
|
23009
24550
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
23010
24551
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
23011
|
-
import { existsSync as
|
|
23012
|
-
import { basename as
|
|
24552
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
24553
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
23013
24554
|
|
|
23014
24555
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
23015
24556
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -23018,8 +24559,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
23018
24559
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
23019
24560
|
|
|
23020
24561
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
23021
|
-
import { mkdirSync as
|
|
23022
|
-
import { join as
|
|
24562
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
24563
|
+
import { join as join11 } from "path";
|
|
23023
24564
|
|
|
23024
24565
|
// src/onboard-posture.ts
|
|
23025
24566
|
function parsePosture(json2) {
|
|
@@ -23086,7 +24627,7 @@ function show(body) {
|
|
|
23086
24627
|
}
|
|
23087
24628
|
|
|
23088
24629
|
// src/command-registry.ts
|
|
23089
|
-
import { readdirSync as
|
|
24630
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
23090
24631
|
import { fileURLToPath } from "url";
|
|
23091
24632
|
var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
|
|
23092
24633
|
|
|
@@ -23141,6 +24682,12 @@ if (rawHistorical !== void 0) {
|
|
|
23141
24682
|
fail(`invalid --historical "${rawHistorical}" (expected full or session-only)`);
|
|
23142
24683
|
else answers.historicalAccess = parsed.data;
|
|
23143
24684
|
}
|
|
24685
|
+
if (process.argv.includes("--model-judge-consent")) {
|
|
24686
|
+
answers.modelJudgeConsent = {
|
|
24687
|
+
acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24688
|
+
payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
|
|
24689
|
+
};
|
|
24690
|
+
}
|
|
23144
24691
|
var rawPosture = flags.get("posture");
|
|
23145
24692
|
var useFloor = process.argv.includes("--floor");
|
|
23146
24693
|
var recalibrate = process.argv.includes("--recalibrate");
|
|
@@ -23148,26 +24695,37 @@ if (useFloor && rawPosture !== void 0) fail("--floor and --posture are mutually
|
|
|
23148
24695
|
if (Object.keys(answers).length === 0 && rawPosture === void 0 && !useFloor) {
|
|
23149
24696
|
fail("nothing to save \u2014 pass --policy, --historical, --posture and/or --floor");
|
|
23150
24697
|
}
|
|
24698
|
+
var wroteConsent = answers.modelJudgeConsent !== void 0;
|
|
24699
|
+
var wrotePosture = answers.policy !== void 0 || answers.historicalAccess !== void 0;
|
|
23151
24700
|
if (Object.keys(answers).length > 0) {
|
|
23152
24701
|
try {
|
|
23153
24702
|
const settings = applyOnboarding(answers);
|
|
23154
|
-
|
|
23155
|
-
|
|
23156
|
-
|
|
23157
|
-
|
|
24703
|
+
if (wrotePosture) {
|
|
24704
|
+
process.stdout.write(show("Got it \u2014 I'll look over Claude's recent work to tune things."));
|
|
24705
|
+
}
|
|
24706
|
+
if (wroteConsent) {
|
|
24707
|
+
process.stdout.write(
|
|
24708
|
+
show("Noted \u2014 I'll send findings to the model to rate them. You can revoke that anytime.")
|
|
24709
|
+
);
|
|
24710
|
+
}
|
|
24711
|
+
if (wrotePosture) {
|
|
23158
24712
|
try {
|
|
23159
|
-
const
|
|
23160
|
-
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23165
|
-
|
|
24713
|
+
const dataDir2 = loadConfig().dataDir;
|
|
24714
|
+
const db = openLocalDatabase(dataDir2);
|
|
24715
|
+
try {
|
|
24716
|
+
const { capped } = capWarnEraEnforcementOnce(db, settings.policy, dataDir2);
|
|
24717
|
+
if (capped > 0) {
|
|
24718
|
+
process.stdout.write(
|
|
24719
|
+
show(
|
|
24720
|
+
`I eased ${String(capped)} detection level${capped === 1 ? "" : "s"} back to "warn" to match the new defaults \u2014 you can raise any of them again in this setup.`
|
|
24721
|
+
)
|
|
24722
|
+
);
|
|
24723
|
+
}
|
|
24724
|
+
} finally {
|
|
24725
|
+
db.close();
|
|
23166
24726
|
}
|
|
23167
|
-
}
|
|
23168
|
-
db.close();
|
|
24727
|
+
} catch {
|
|
23169
24728
|
}
|
|
23170
|
-
} catch {
|
|
23171
24729
|
}
|
|
23172
24730
|
} catch (err) {
|
|
23173
24731
|
fail(err instanceof Error ? err.message : "could not save your settings");
|