@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/session-start.js
CHANGED
|
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
|
|
|
494
494
|
// src/hooks/session-start.ts
|
|
495
495
|
import { readFileSync as readFileSync8 } from "fs";
|
|
496
496
|
|
|
497
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
498
|
+
import { existsSync as existsSync3 } from "fs";
|
|
499
|
+
import { join as join6 } from "path";
|
|
500
|
+
|
|
497
501
|
// ../../packages/persistence/src/database.ts
|
|
498
502
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
499
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
503
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
500
504
|
import { join, sep } from "path";
|
|
501
505
|
import { DatabaseSync } from "node:sqlite";
|
|
502
506
|
|
|
@@ -545,6 +549,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
545
549
|
{
|
|
546
550
|
tag: "0010_events_session_expression_index",
|
|
547
551
|
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
tag: "0011_egress_writer",
|
|
555
|
+
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
tag: "0012_handy_the_captain",
|
|
559
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
tag: "0013_legacy_history_backfill_support",
|
|
563
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
tag: "0014_drop_legacy_events_findings",
|
|
567
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
548
568
|
}
|
|
549
569
|
];
|
|
550
570
|
|
|
@@ -15375,7 +15395,12 @@ var FindingFacets = external_exports.object({
|
|
|
15375
15395
|
severity: external_exports.array(FindingFacetItem),
|
|
15376
15396
|
subtype: external_exports.array(FindingFacetItem),
|
|
15377
15397
|
provider: external_exports.array(FindingFacetItem),
|
|
15378
|
-
action: external_exports.array(FindingFacetItem)
|
|
15398
|
+
action: external_exports.array(FindingFacetItem),
|
|
15399
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15400
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15401
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15402
|
+
// counted under no value.
|
|
15403
|
+
status: external_exports.array(FindingFacetItem)
|
|
15379
15404
|
}).meta({ id: "FindingFacets" });
|
|
15380
15405
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15381
15406
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15385,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15385
15410
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15386
15411
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15387
15412
|
action: external_exports.array(FindingAction).optional(),
|
|
15413
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15414
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15415
|
+
// one of the requested values.
|
|
15416
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15388
15417
|
q: external_exports.string().optional(),
|
|
15389
15418
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15390
15419
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15575,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15575
15604
|
parent_uuid: external_exports.string().optional(),
|
|
15576
15605
|
run_key: external_exports.string().optional()
|
|
15577
15606
|
}).catchall(external_exports.unknown());
|
|
15607
|
+
var CaptureAttributes = external_exports.object({
|
|
15608
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15609
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15610
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15611
|
+
source_tool: external_exports.string().optional(),
|
|
15612
|
+
file_path: external_exports.string().optional(),
|
|
15613
|
+
repo: external_exports.string().optional(),
|
|
15614
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15615
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15616
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15617
|
+
tool_name: external_exports.string().optional(),
|
|
15618
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15619
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15620
|
+
gitignored: external_exports.boolean().optional(),
|
|
15621
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15622
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15623
|
+
whole_file: external_exports.boolean().optional(),
|
|
15624
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15625
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15626
|
+
// trace id when telemetry is enabled.
|
|
15627
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15628
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15629
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15630
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15631
|
+
// authorized the bypass.
|
|
15632
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15633
|
+
}).catchall(external_exports.unknown());
|
|
15578
15634
|
var ToolCallInspection = external_exports.object({
|
|
15579
15635
|
ruleId: external_exports.string().min(1),
|
|
15580
15636
|
ruleName: external_exports.string(),
|
|
@@ -15661,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15661
15717
|
span: Span,
|
|
15662
15718
|
maskedMatch: external_exports.string(),
|
|
15663
15719
|
actionTaken: ActionTaken,
|
|
15664
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15720
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15721
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15722
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15723
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15724
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15725
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15726
|
+
findingKey: external_exports.string().optional(),
|
|
15727
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15728
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15729
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15730
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15731
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15665
15732
|
});
|
|
15666
15733
|
var InventoryContext = external_exports.object({
|
|
15667
15734
|
host: InventoryInput.optional(),
|
|
@@ -15863,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15863
15930
|
|
|
15864
15931
|
// ../../packages/schema/src/zod/event.ts
|
|
15865
15932
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15933
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15866
15934
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15867
15935
|
var EventMetadata = external_exports.object({
|
|
15868
15936
|
sessionId: external_exports.string().optional(),
|
|
@@ -16203,6 +16271,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16203
16271
|
|
|
16204
16272
|
// ../../packages/schema/src/zod/rule.ts
|
|
16205
16273
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16274
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16206
16275
|
var KeywordMatcher = external_exports.object({
|
|
16207
16276
|
type: external_exports.literal("keyword"),
|
|
16208
16277
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16227,9 +16296,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16227
16296
|
return false;
|
|
16228
16297
|
}
|
|
16229
16298
|
}
|
|
16299
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16230
16300
|
var RegexMatcher = external_exports.object({
|
|
16231
16301
|
type: external_exports.literal("regex"),
|
|
16232
|
-
pattern: external_exports.string(),
|
|
16302
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16233
16303
|
flags: external_exports.string().default("gi"),
|
|
16234
16304
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16235
16305
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16350,6 +16420,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16350
16420
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16351
16421
|
// `bundle.exceptions ?? []`.
|
|
16352
16422
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16423
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16424
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16425
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16426
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16427
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16428
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16353
16429
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16354
16430
|
fetchedAt: external_exports.iso.datetime()
|
|
16355
16431
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16910,6 +16986,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16910
16986
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16911
16987
|
}
|
|
16912
16988
|
|
|
16989
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16990
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16991
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16992
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16993
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16994
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16995
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16996
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16997
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16998
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16999
|
+
var ReviewInfo = external_exports.object({
|
|
17000
|
+
needsReview: external_exports.boolean(),
|
|
17001
|
+
reasons: external_exports.array(ReviewReason)
|
|
17002
|
+
}).meta({ id: "ReviewInfo" });
|
|
17003
|
+
var DestinationNetwork = external_exports.object({
|
|
17004
|
+
port: external_exports.number().int().nullable(),
|
|
17005
|
+
geo: external_exports.string().nullable(),
|
|
17006
|
+
ptr: external_exports.string().nullable()
|
|
17007
|
+
}).meta({ id: "DestinationNetwork" });
|
|
17008
|
+
var EndpointSummary = external_exports.object({
|
|
17009
|
+
id: external_exports.string(),
|
|
17010
|
+
method: HttpMethod,
|
|
17011
|
+
transport: Transport,
|
|
17012
|
+
url: external_exports.string(),
|
|
17013
|
+
template: external_exports.boolean(),
|
|
17014
|
+
dataClass: DataClass,
|
|
17015
|
+
lastSeen: external_exports.iso.datetime(),
|
|
17016
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
17017
|
+
}).meta({ id: "EndpointSummary" });
|
|
17018
|
+
var CallSite = external_exports.object({
|
|
17019
|
+
id: external_exports.string(),
|
|
17020
|
+
project: external_exports.string(),
|
|
17021
|
+
file: external_exports.string(),
|
|
17022
|
+
line: external_exports.number().int().nonnegative(),
|
|
17023
|
+
snippet: external_exports.string(),
|
|
17024
|
+
dynamic: external_exports.boolean(),
|
|
17025
|
+
vendored: external_exports.boolean(),
|
|
17026
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17027
|
+
projectId: external_exports.string().nullable()
|
|
17028
|
+
}).meta({ id: "CallSite" });
|
|
17029
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
17030
|
+
sites: external_exports.array(CallSite)
|
|
17031
|
+
}).meta({ id: "EndpointWithSites" });
|
|
17032
|
+
var ShareDestinationSummary = external_exports.object({
|
|
17033
|
+
id: external_exports.string(),
|
|
17034
|
+
kind: DestinationKind,
|
|
17035
|
+
name: external_exports.string(),
|
|
17036
|
+
host: external_exports.string(),
|
|
17037
|
+
category: external_exports.string(),
|
|
17038
|
+
trust: ShareTrustLevel,
|
|
17039
|
+
/** Effective state (decision applied over the trust default). */
|
|
17040
|
+
status: EgressStatus,
|
|
17041
|
+
/** True when an egress decision override differs from the trust default. */
|
|
17042
|
+
isCustom: external_exports.boolean(),
|
|
17043
|
+
lastSeen: external_exports.iso.datetime(),
|
|
17044
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
17045
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17046
|
+
transports: external_exports.array(Transport),
|
|
17047
|
+
/** Most-sensitive first. */
|
|
17048
|
+
dataClasses: external_exports.array(DataClass),
|
|
17049
|
+
review: ReviewInfo,
|
|
17050
|
+
/** Non-provider hosts only; null for providers. */
|
|
17051
|
+
network: DestinationNetwork.nullable(),
|
|
17052
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
17053
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
17054
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
17055
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17056
|
+
endpointCount: true,
|
|
17057
|
+
callSiteCount: true,
|
|
17058
|
+
endpoints: true
|
|
17059
|
+
}).extend({
|
|
17060
|
+
/** Ownership/geo rationale; null for providers. */
|
|
17061
|
+
note: external_exports.string().nullable(),
|
|
17062
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
17063
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
17064
|
+
var ReviewDestination = external_exports.object({
|
|
17065
|
+
id: external_exports.string(),
|
|
17066
|
+
kind: DestinationKind,
|
|
17067
|
+
name: external_exports.string(),
|
|
17068
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17069
|
+
host: external_exports.string(),
|
|
17070
|
+
trust: ShareTrustLevel,
|
|
17071
|
+
status: EgressStatus,
|
|
17072
|
+
review: ReviewInfo,
|
|
17073
|
+
topDataClass: DataClass,
|
|
17074
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17075
|
+
lastSeen: external_exports.iso.datetime()
|
|
17076
|
+
}).meta({ id: "ReviewDestination" });
|
|
17077
|
+
var ShareDestinationGroup = external_exports.object({
|
|
17078
|
+
kind: DestinationKind,
|
|
17079
|
+
total: external_exports.number().int().nonnegative(),
|
|
17080
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
17081
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
17082
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17083
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17084
|
+
var SharesStats = external_exports.object({
|
|
17085
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17086
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17087
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17088
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
17089
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
17090
|
+
byKind: external_exports.object({
|
|
17091
|
+
provider: external_exports.number().int().nonnegative(),
|
|
17092
|
+
internal: external_exports.number().int().nonnegative(),
|
|
17093
|
+
external: external_exports.number().int().nonnegative(),
|
|
17094
|
+
ip: external_exports.number().int().nonnegative()
|
|
17095
|
+
}),
|
|
17096
|
+
byTrust: external_exports.object({
|
|
17097
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
17098
|
+
internal: external_exports.number().int().nonnegative(),
|
|
17099
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
17100
|
+
ip: external_exports.number().int().nonnegative()
|
|
17101
|
+
})
|
|
17102
|
+
}).meta({ id: "SharesStats" });
|
|
17103
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
17104
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17105
|
+
decision: EgressDecision.nullable()
|
|
17106
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
17107
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17108
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
17109
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17110
|
+
q: external_exports.string().optional(),
|
|
17111
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17112
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
17113
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17114
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17115
|
+
/**
|
|
17116
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17117
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17118
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17119
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17120
|
+
*/
|
|
17121
|
+
review: external_exports.stringbool().default(false)
|
|
17122
|
+
});
|
|
17123
|
+
var ExportSharesQuery = external_exports.object({
|
|
17124
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17125
|
+
q: external_exports.string().optional(),
|
|
17126
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
17127
|
+
});
|
|
17128
|
+
|
|
17129
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
17130
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
17131
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
17132
|
+
id: external_exports.string(),
|
|
17133
|
+
name: external_exports.string(),
|
|
17134
|
+
category: external_exports.string(),
|
|
17135
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
17136
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
17137
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
17138
|
+
apiBase: external_exports.string(),
|
|
17139
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
17140
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
17141
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
17142
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
17143
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
17144
|
+
var EgressCallSiteHit = external_exports.object({
|
|
17145
|
+
file: external_exports.string(),
|
|
17146
|
+
line: external_exports.number().int().positive(),
|
|
17147
|
+
snippet: external_exports.string(),
|
|
17148
|
+
dynamic: external_exports.boolean(),
|
|
17149
|
+
vendored: external_exports.boolean()
|
|
17150
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
17151
|
+
var ResolvedEgressHit = external_exports.object({
|
|
17152
|
+
host: external_exports.string(),
|
|
17153
|
+
kind: DestinationKind,
|
|
17154
|
+
name: external_exports.string(),
|
|
17155
|
+
category: external_exports.string(),
|
|
17156
|
+
trust: ShareTrustLevel,
|
|
17157
|
+
network: DestinationNetwork.nullable(),
|
|
17158
|
+
method: HttpMethod,
|
|
17159
|
+
transport: Transport,
|
|
17160
|
+
url: external_exports.string(),
|
|
17161
|
+
template: external_exports.boolean(),
|
|
17162
|
+
dataClass: DataClass,
|
|
17163
|
+
site: EgressCallSiteHit
|
|
17164
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
17165
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
17166
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
17167
|
+
external_exports.object({
|
|
17168
|
+
mode: external_exports.literal("ledger"),
|
|
17169
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
17170
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
17171
|
+
})
|
|
17172
|
+
]).meta({ id: "EgressReconcile" });
|
|
17173
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
17174
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
17175
|
+
projectKey: external_exports.string().min(1),
|
|
17176
|
+
/** Display name only — never keys reconciliation. */
|
|
17177
|
+
project: external_exports.string(),
|
|
17178
|
+
projectId: external_exports.string().nullable(),
|
|
17179
|
+
reconcile: EgressReconcile,
|
|
17180
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
17181
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
17182
|
+
var EgressWriteSummary = external_exports.object({
|
|
17183
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17184
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17185
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17186
|
+
truncated: external_exports.boolean(),
|
|
17187
|
+
/**
|
|
17188
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
17189
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17190
|
+
* again next scan.
|
|
17191
|
+
*/
|
|
17192
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17193
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17194
|
+
|
|
16913
17195
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16914
17196
|
function toApiAction(dbVal) {
|
|
16915
17197
|
const map2 = {
|
|
@@ -17057,6 +17339,15 @@ function groupActions(g) {
|
|
|
17057
17339
|
actionsCache.set(g, actions);
|
|
17058
17340
|
return actions;
|
|
17059
17341
|
}
|
|
17342
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17343
|
+
const statusSet = new Set(statuses);
|
|
17344
|
+
let sum = 0;
|
|
17345
|
+
for (const input of statusInputs) {
|
|
17346
|
+
if (input.count === void 0) return null;
|
|
17347
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17348
|
+
}
|
|
17349
|
+
return sum;
|
|
17350
|
+
}
|
|
17060
17351
|
function applyFindingFilters(groups, opts) {
|
|
17061
17352
|
let filtered = groups;
|
|
17062
17353
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17075,6 +17366,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17075
17366
|
const subtypeSet = new Set(opts.subtype);
|
|
17076
17367
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17077
17368
|
}
|
|
17369
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17370
|
+
const statusSet = new Set(opts.statuses);
|
|
17371
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17372
|
+
}
|
|
17078
17373
|
if (opts.q) {
|
|
17079
17374
|
const q = opts.q.toLowerCase();
|
|
17080
17375
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17096,6 +17391,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17096
17391
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17097
17392
|
providers: opts.providers,
|
|
17098
17393
|
actions: opts.actions,
|
|
17394
|
+
statuses: opts.statuses,
|
|
17099
17395
|
q: opts.q,
|
|
17100
17396
|
subtype: opts.subtype
|
|
17101
17397
|
});
|
|
@@ -17105,6 +17401,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17105
17401
|
}
|
|
17106
17402
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17107
17403
|
actions: opts.actions,
|
|
17404
|
+
statuses: opts.statuses,
|
|
17108
17405
|
q: opts.q,
|
|
17109
17406
|
subtype: opts.subtype,
|
|
17110
17407
|
severity: opts.severity
|
|
@@ -17115,6 +17412,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17115
17412
|
}
|
|
17116
17413
|
const forAction = applyFindingFilters(allGroups, {
|
|
17117
17414
|
providers: opts.providers,
|
|
17415
|
+
statuses: opts.statuses,
|
|
17118
17416
|
q: opts.q,
|
|
17119
17417
|
subtype: opts.subtype,
|
|
17120
17418
|
severity: opts.severity
|
|
@@ -17126,17 +17424,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17126
17424
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17127
17425
|
providers: opts.providers,
|
|
17128
17426
|
actions: opts.actions,
|
|
17427
|
+
statuses: opts.statuses,
|
|
17129
17428
|
q: opts.q,
|
|
17130
17429
|
severity: opts.severity
|
|
17131
17430
|
});
|
|
17132
17431
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17133
17432
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17433
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17434
|
+
providers: opts.providers,
|
|
17435
|
+
actions: opts.actions,
|
|
17436
|
+
q: opts.q,
|
|
17437
|
+
subtype: opts.subtype,
|
|
17438
|
+
severity: opts.severity
|
|
17439
|
+
});
|
|
17440
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17441
|
+
for (const g of forStatus) {
|
|
17442
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17443
|
+
}
|
|
17134
17444
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17135
17445
|
return {
|
|
17136
17446
|
severity: toItems(severityMap),
|
|
17137
17447
|
provider: toItems(providerMap),
|
|
17138
17448
|
action: toItems(actionMap),
|
|
17139
|
-
subtype: toItems(subtypeMap)
|
|
17449
|
+
subtype: toItems(subtypeMap),
|
|
17450
|
+
status: toItems(statusMap)
|
|
17140
17451
|
};
|
|
17141
17452
|
}
|
|
17142
17453
|
|
|
@@ -17171,10 +17482,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17171
17482
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17172
17483
|
|
|
17173
17484
|
// ../../packages/schema/src/zod/local.ts
|
|
17174
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17485
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17175
17486
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17176
17487
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17177
17488
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17489
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17490
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17491
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17492
|
+
});
|
|
17178
17493
|
var WorkspaceSettings = external_exports.object({
|
|
17179
17494
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17180
17495
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17186,38 +17501,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17186
17501
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17187
17502
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17188
17503
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17504
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17505
|
+
// Shares writes.
|
|
17506
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17189
17507
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17190
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17508
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17509
|
+
// Records that the user consented to sending findings to the model API for
|
|
17510
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17511
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17512
|
+
// covers the current payload and must be re-granted.
|
|
17513
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17191
17514
|
});
|
|
17192
17515
|
function defaultWorkspaceSettings() {
|
|
17193
17516
|
return WorkspaceSettings.parse({});
|
|
17194
17517
|
}
|
|
17195
|
-
function toEventRow(event) {
|
|
17196
|
-
return {
|
|
17197
|
-
id: event.id,
|
|
17198
|
-
sourceTool: event.sourceTool,
|
|
17199
|
-
kind: event.kind,
|
|
17200
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17201
|
-
contentHash: event.contentHash,
|
|
17202
|
-
content: event.content,
|
|
17203
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17204
|
-
};
|
|
17205
|
-
}
|
|
17206
|
-
function toFindingRow(finding2) {
|
|
17207
|
-
return {
|
|
17208
|
-
id: finding2.id,
|
|
17209
|
-
eventId: finding2.eventId,
|
|
17210
|
-
ruleId: finding2.ruleId,
|
|
17211
|
-
category: finding2.category,
|
|
17212
|
-
severity: finding2.severity,
|
|
17213
|
-
spanStart: finding2.span.start,
|
|
17214
|
-
spanEnd: finding2.span.end,
|
|
17215
|
-
maskedMatch: finding2.maskedMatch,
|
|
17216
|
-
actionTaken: finding2.actionTaken,
|
|
17217
|
-
confidence: finding2.confidence,
|
|
17218
|
-
findingKey: finding2.findingKey ?? null
|
|
17219
|
-
};
|
|
17220
|
-
}
|
|
17221
17518
|
function toInventoryRow(input, id, now) {
|
|
17222
17519
|
return {
|
|
17223
17520
|
id,
|
|
@@ -17287,7 +17584,42 @@ function toInspectionFindingRow(input) {
|
|
|
17287
17584
|
spanEnd: input.span.end,
|
|
17288
17585
|
maskedMatch: input.maskedMatch,
|
|
17289
17586
|
actionTaken: input.actionTaken,
|
|
17290
|
-
confidence: input.confidence
|
|
17587
|
+
confidence: input.confidence,
|
|
17588
|
+
findingKey: input.findingKey ?? null,
|
|
17589
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17590
|
+
};
|
|
17591
|
+
}
|
|
17592
|
+
function toCaptureAttributes(event) {
|
|
17593
|
+
const metadata = event.metadata;
|
|
17594
|
+
return {
|
|
17595
|
+
source_tool: event.sourceTool,
|
|
17596
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17597
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17598
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17599
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17600
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17601
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17602
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17603
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17604
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17605
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17606
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17607
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17608
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17609
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17610
|
+
};
|
|
17611
|
+
}
|
|
17612
|
+
function captureDefinitionVersion(finding2) {
|
|
17613
|
+
return `capture/${finding2.category}/${finding2.severity}`;
|
|
17614
|
+
}
|
|
17615
|
+
function toCaptureDefinitionInput(finding2) {
|
|
17616
|
+
return {
|
|
17617
|
+
ruleId: finding2.ruleId,
|
|
17618
|
+
version: captureDefinitionVersion(finding2),
|
|
17619
|
+
name: finding2.ruleId,
|
|
17620
|
+
category: finding2.category,
|
|
17621
|
+
severity: finding2.severity,
|
|
17622
|
+
definition: JSON.stringify({ ruleId: finding2.ruleId })
|
|
17291
17623
|
};
|
|
17292
17624
|
}
|
|
17293
17625
|
|
|
@@ -17669,145 +18001,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17669
18001
|
path: ["liveKeys"]
|
|
17670
18002
|
});
|
|
17671
18003
|
|
|
17672
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17673
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17674
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17675
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17676
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17677
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17678
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17679
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17680
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17681
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17682
|
-
var ReviewInfo = external_exports.object({
|
|
17683
|
-
needsReview: external_exports.boolean(),
|
|
17684
|
-
reasons: external_exports.array(ReviewReason)
|
|
17685
|
-
}).meta({ id: "ReviewInfo" });
|
|
17686
|
-
var DestinationNetwork = external_exports.object({
|
|
17687
|
-
port: external_exports.number().int().nullable(),
|
|
17688
|
-
geo: external_exports.string().nullable(),
|
|
17689
|
-
ptr: external_exports.string().nullable()
|
|
17690
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17691
|
-
var EndpointSummary = external_exports.object({
|
|
17692
|
-
id: external_exports.string(),
|
|
17693
|
-
method: HttpMethod,
|
|
17694
|
-
transport: Transport,
|
|
17695
|
-
url: external_exports.string(),
|
|
17696
|
-
template: external_exports.boolean(),
|
|
17697
|
-
dataClass: DataClass,
|
|
17698
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17699
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17700
|
-
}).meta({ id: "EndpointSummary" });
|
|
17701
|
-
var CallSite = external_exports.object({
|
|
17702
|
-
id: external_exports.string(),
|
|
17703
|
-
project: external_exports.string(),
|
|
17704
|
-
file: external_exports.string(),
|
|
17705
|
-
line: external_exports.number().int().nonnegative(),
|
|
17706
|
-
snippet: external_exports.string(),
|
|
17707
|
-
dynamic: external_exports.boolean(),
|
|
17708
|
-
vendored: external_exports.boolean(),
|
|
17709
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17710
|
-
projectId: external_exports.string().nullable()
|
|
17711
|
-
}).meta({ id: "CallSite" });
|
|
17712
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17713
|
-
sites: external_exports.array(CallSite)
|
|
17714
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17715
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17716
|
-
id: external_exports.string(),
|
|
17717
|
-
kind: DestinationKind,
|
|
17718
|
-
name: external_exports.string(),
|
|
17719
|
-
host: external_exports.string(),
|
|
17720
|
-
category: external_exports.string(),
|
|
17721
|
-
trust: ShareTrustLevel,
|
|
17722
|
-
/** Effective state (decision applied over the trust default). */
|
|
17723
|
-
status: EgressStatus,
|
|
17724
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17725
|
-
isCustom: external_exports.boolean(),
|
|
17726
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17727
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17728
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17729
|
-
transports: external_exports.array(Transport),
|
|
17730
|
-
/** Most-sensitive first. */
|
|
17731
|
-
dataClasses: external_exports.array(DataClass),
|
|
17732
|
-
review: ReviewInfo,
|
|
17733
|
-
/** Non-provider hosts only; null for providers. */
|
|
17734
|
-
network: DestinationNetwork.nullable(),
|
|
17735
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17736
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17737
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17738
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17739
|
-
endpointCount: true,
|
|
17740
|
-
callSiteCount: true,
|
|
17741
|
-
endpoints: true
|
|
17742
|
-
}).extend({
|
|
17743
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17744
|
-
note: external_exports.string().nullable(),
|
|
17745
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17746
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17747
|
-
var ReviewDestination = external_exports.object({
|
|
17748
|
-
id: external_exports.string(),
|
|
17749
|
-
kind: DestinationKind,
|
|
17750
|
-
name: external_exports.string(),
|
|
17751
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17752
|
-
host: external_exports.string(),
|
|
17753
|
-
trust: ShareTrustLevel,
|
|
17754
|
-
status: EgressStatus,
|
|
17755
|
-
review: ReviewInfo,
|
|
17756
|
-
topDataClass: DataClass,
|
|
17757
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17758
|
-
lastSeen: external_exports.iso.datetime()
|
|
17759
|
-
}).meta({ id: "ReviewDestination" });
|
|
17760
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17761
|
-
kind: DestinationKind,
|
|
17762
|
-
total: external_exports.number().int().nonnegative(),
|
|
17763
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17764
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17765
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17766
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17767
|
-
var SharesStats = external_exports.object({
|
|
17768
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17769
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17770
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17771
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17772
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17773
|
-
byKind: external_exports.object({
|
|
17774
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17775
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17776
|
-
ip: external_exports.number().int().nonnegative()
|
|
17777
|
-
}),
|
|
17778
|
-
byTrust: external_exports.object({
|
|
17779
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17780
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17781
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17782
|
-
ip: external_exports.number().int().nonnegative()
|
|
17783
|
-
})
|
|
17784
|
-
}).meta({ id: "SharesStats" });
|
|
17785
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17786
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17787
|
-
decision: EgressDecision.nullable()
|
|
17788
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17789
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17790
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17791
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17792
|
-
q: external_exports.string().optional(),
|
|
17793
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17794
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17795
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17796
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17797
|
-
/**
|
|
17798
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17799
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17800
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17801
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17802
|
-
*/
|
|
17803
|
-
review: external_exports.stringbool().default(false)
|
|
17804
|
-
});
|
|
17805
|
-
var ExportSharesQuery = external_exports.object({
|
|
17806
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17807
|
-
q: external_exports.string().optional(),
|
|
17808
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17809
|
-
});
|
|
17810
|
-
|
|
17811
18004
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17812
18005
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17813
18006
|
function trustDefaultStatus(trust) {
|
|
@@ -17827,7 +18020,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17827
18020
|
const reasons = [];
|
|
17828
18021
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17829
18022
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17830
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
18023
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17831
18024
|
return reasons;
|
|
17832
18025
|
}
|
|
17833
18026
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17854,6 +18047,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17854
18047
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17855
18048
|
}
|
|
17856
18049
|
|
|
18050
|
+
// ../../packages/persistence/src/ids.ts
|
|
18051
|
+
import { createHash } from "crypto";
|
|
18052
|
+
function sha256Hex(input) {
|
|
18053
|
+
return createHash("sha256").update(input).digest("hex");
|
|
18054
|
+
}
|
|
18055
|
+
function inventoryId(objectType, identityKey) {
|
|
18056
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
18057
|
+
}
|
|
18058
|
+
function sourceProjectId(url2) {
|
|
18059
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
18060
|
+
}
|
|
18061
|
+
function classifiedDataId(cls) {
|
|
18062
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
18063
|
+
}
|
|
18064
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
18065
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
18066
|
+
}
|
|
18067
|
+
function llmCallId(sessionId, messageId) {
|
|
18068
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
18069
|
+
}
|
|
18070
|
+
function toolCallId(sessionId, toolUseId) {
|
|
18071
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
18072
|
+
}
|
|
18073
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
18074
|
+
return sha256Hex(
|
|
18075
|
+
canonicalIdentity([
|
|
18076
|
+
"inspection_finding",
|
|
18077
|
+
auditEventId,
|
|
18078
|
+
ruleId,
|
|
18079
|
+
String(spanStart),
|
|
18080
|
+
String(spanEnd)
|
|
18081
|
+
])
|
|
18082
|
+
);
|
|
18083
|
+
}
|
|
18084
|
+
var NO_SESSION = "no_session";
|
|
18085
|
+
var NO_PATH = "no_path";
|
|
18086
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
18087
|
+
return sha256Hex(
|
|
18088
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
18089
|
+
);
|
|
18090
|
+
}
|
|
18091
|
+
|
|
17857
18092
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17858
18093
|
function escapeLikePattern(s) {
|
|
17859
18094
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17950,39 +18185,81 @@ function evidenceExists(db, object2) {
|
|
|
17950
18185
|
return schemaObjectExists(db, "table", object2.name);
|
|
17951
18186
|
}
|
|
17952
18187
|
|
|
17953
|
-
// ../../packages/persistence/src/
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
18188
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18189
|
+
function allRows(stmt, params) {
|
|
18190
|
+
if (params === void 0) return stmt.all();
|
|
18191
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18192
|
+
return stmt.all(params);
|
|
17957
18193
|
}
|
|
17958
|
-
function
|
|
17959
|
-
|
|
18194
|
+
function getRow(stmt, params) {
|
|
18195
|
+
if (params === void 0) return stmt.get();
|
|
18196
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18197
|
+
return stmt.get(params);
|
|
17960
18198
|
}
|
|
17961
|
-
function
|
|
17962
|
-
return
|
|
18199
|
+
function intToBool(raw) {
|
|
18200
|
+
return raw === 1 || raw === true;
|
|
17963
18201
|
}
|
|
17964
|
-
function
|
|
17965
|
-
return
|
|
18202
|
+
function boolToInt(b) {
|
|
18203
|
+
return b ? 1 : 0;
|
|
17966
18204
|
}
|
|
17967
|
-
function
|
|
17968
|
-
|
|
18205
|
+
function bindParams(row) {
|
|
18206
|
+
const out = {};
|
|
18207
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18208
|
+
out[key] = value === void 0 ? null : value;
|
|
18209
|
+
}
|
|
18210
|
+
return out;
|
|
17969
18211
|
}
|
|
17970
|
-
function
|
|
17971
|
-
return
|
|
18212
|
+
function countScalar(db, sql, params) {
|
|
18213
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17972
18214
|
}
|
|
17973
|
-
function
|
|
17974
|
-
|
|
18215
|
+
function countBy(db, sql, params) {
|
|
18216
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18217
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18218
|
+
map2.set(row.k, row.n);
|
|
18219
|
+
}
|
|
18220
|
+
return map2;
|
|
17975
18221
|
}
|
|
17976
|
-
function
|
|
17977
|
-
|
|
17978
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
18222
|
+
function mapRowsTolerant(rows, map2) {
|
|
18223
|
+
const out = [];
|
|
18224
|
+
for (const row of rows) {
|
|
18225
|
+
try {
|
|
18226
|
+
out.push(map2(row));
|
|
18227
|
+
} catch {
|
|
18228
|
+
}
|
|
18229
|
+
}
|
|
18230
|
+
return out;
|
|
18231
|
+
}
|
|
18232
|
+
|
|
18233
|
+
// ../../packages/persistence/src/paths.ts
|
|
18234
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18235
|
+
var DATA_DIR_MODE = 448;
|
|
18236
|
+
var DATA_FILE_MODE = 384;
|
|
18237
|
+
var DB_FILENAME = "aka.db";
|
|
18238
|
+
function chmodBestEffort(path, mode) {
|
|
18239
|
+
try {
|
|
18240
|
+
chmodSync(path, mode);
|
|
18241
|
+
} catch {
|
|
18242
|
+
}
|
|
18243
|
+
}
|
|
18244
|
+
function tightenDir(dir) {
|
|
18245
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18246
|
+
}
|
|
18247
|
+
function ensureDataDirSync(dir) {
|
|
18248
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18249
|
+
tightenDir(dir);
|
|
18250
|
+
}
|
|
18251
|
+
function dbSidecars(file2) {
|
|
18252
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18253
|
+
}
|
|
18254
|
+
function tightenFile(file2) {
|
|
18255
|
+
try {
|
|
18256
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18257
|
+
} catch {
|
|
18258
|
+
}
|
|
18259
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18260
|
+
}
|
|
18261
|
+
function tightenPerms(file2) {
|
|
18262
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
17986
18263
|
}
|
|
17987
18264
|
|
|
17988
18265
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17996,7 +18273,8 @@ function createdIndexName(statement) {
|
|
|
17996
18273
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17997
18274
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17998
18275
|
}
|
|
17999
|
-
|
|
18276
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18277
|
+
function applyMigrations(db, file2) {
|
|
18000
18278
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
18001
18279
|
db.exec(
|
|
18002
18280
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -18010,6 +18288,7 @@ function applyMigrations(db) {
|
|
|
18010
18288
|
);
|
|
18011
18289
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
18012
18290
|
if (applied.has(migration.tag)) continue;
|
|
18291
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
18013
18292
|
const evidence = evidenceObjects(migration.sql);
|
|
18014
18293
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
18015
18294
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18054,13 +18333,54 @@ function applyMigrations(db) {
|
|
|
18054
18333
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18055
18334
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18056
18335
|
}
|
|
18057
|
-
ensureSyncedAtColumn(db, "events");
|
|
18058
18336
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18059
18337
|
ensureScanLedgerTable(db);
|
|
18060
18338
|
ensureBlockedDetectionsTable(db);
|
|
18339
|
+
ensureRuleProbeCacheTable(db);
|
|
18061
18340
|
ensureWriteGateTrigger(db);
|
|
18062
18341
|
ensureTokenUsageColumns(db);
|
|
18063
18342
|
reconcileSourceProjectIds(db);
|
|
18343
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18344
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18345
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18346
|
+
}
|
|
18347
|
+
}
|
|
18348
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18349
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18350
|
+
if (!migration) return;
|
|
18351
|
+
if (file2) {
|
|
18352
|
+
try {
|
|
18353
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18354
|
+
} catch (error51) {
|
|
18355
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18356
|
+
return;
|
|
18357
|
+
}
|
|
18358
|
+
}
|
|
18359
|
+
try {
|
|
18360
|
+
withTransaction(
|
|
18361
|
+
db,
|
|
18362
|
+
() => {
|
|
18363
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18364
|
+
if (alreadyDropped) return;
|
|
18365
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18366
|
+
db.exec(statement);
|
|
18367
|
+
}
|
|
18368
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18369
|
+
migration.tag,
|
|
18370
|
+
Date.now()
|
|
18371
|
+
);
|
|
18372
|
+
},
|
|
18373
|
+
"IMMEDIATE"
|
|
18374
|
+
);
|
|
18375
|
+
} catch (error51) {
|
|
18376
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18377
|
+
}
|
|
18378
|
+
}
|
|
18379
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18380
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18381
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18382
|
+
tightenFile(backup);
|
|
18383
|
+
return backup;
|
|
18064
18384
|
}
|
|
18065
18385
|
var TOKEN_USAGE_COLUMNS = [
|
|
18066
18386
|
{
|
|
@@ -18089,6 +18409,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18089
18409
|
}
|
|
18090
18410
|
];
|
|
18091
18411
|
function ensureTokenUsageColumns(db) {
|
|
18412
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18092
18413
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18093
18414
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18094
18415
|
if (!existing.has(column.name)) {
|
|
@@ -18154,11 +18475,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18154
18475
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18155
18476
|
}
|
|
18156
18477
|
}
|
|
18478
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18479
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18480
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18481
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18482
|
+
return row?.lastRowid ?? 0;
|
|
18483
|
+
}
|
|
18484
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18485
|
+
db.prepare(
|
|
18486
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18487
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18488
|
+
).run(source, lastRowid);
|
|
18489
|
+
}
|
|
18490
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18491
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18492
|
+
let processed = 0;
|
|
18493
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18494
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18495
|
+
if (rows.length === 0) return true;
|
|
18496
|
+
withTransaction(
|
|
18497
|
+
db,
|
|
18498
|
+
() => {
|
|
18499
|
+
handleRows(rows);
|
|
18500
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18501
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18502
|
+
},
|
|
18503
|
+
"IMMEDIATE"
|
|
18504
|
+
);
|
|
18505
|
+
processed += rows.length;
|
|
18506
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18507
|
+
}
|
|
18508
|
+
return false;
|
|
18509
|
+
}
|
|
18510
|
+
function parseLegacyEventMetadata(raw) {
|
|
18511
|
+
if (raw === null) return void 0;
|
|
18512
|
+
try {
|
|
18513
|
+
return JSON.parse(raw);
|
|
18514
|
+
} catch {
|
|
18515
|
+
return void 0;
|
|
18516
|
+
}
|
|
18517
|
+
}
|
|
18518
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18519
|
+
return JSON.stringify(
|
|
18520
|
+
toCaptureAttributes({
|
|
18521
|
+
id: row.id,
|
|
18522
|
+
sourceTool: row.sourceTool,
|
|
18523
|
+
kind: row.kind,
|
|
18524
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18525
|
+
contentHash: row.contentHash,
|
|
18526
|
+
content: row.content,
|
|
18527
|
+
metadata: row.metadata
|
|
18528
|
+
})
|
|
18529
|
+
);
|
|
18530
|
+
}
|
|
18531
|
+
function copyLegacyEvents(db) {
|
|
18532
|
+
const selectStmt = db.prepare(
|
|
18533
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18534
|
+
content_hash AS contentHash, content, metadata
|
|
18535
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18536
|
+
);
|
|
18537
|
+
const insertStmt = db.prepare(
|
|
18538
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18539
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18540
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18541
|
+
);
|
|
18542
|
+
const stubRootStmt = db.prepare(
|
|
18543
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18544
|
+
);
|
|
18545
|
+
return drainLegacyTable(
|
|
18546
|
+
db,
|
|
18547
|
+
"events",
|
|
18548
|
+
selectStmt,
|
|
18549
|
+
(rows) => {
|
|
18550
|
+
for (const row of rows) {
|
|
18551
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18552
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18553
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18554
|
+
insertStmt.run(
|
|
18555
|
+
bindParams({
|
|
18556
|
+
id: row.id,
|
|
18557
|
+
parentId: sessionId,
|
|
18558
|
+
rootSessionId: sessionId,
|
|
18559
|
+
eventType: row.kind,
|
|
18560
|
+
startedAt: row.occurredAt,
|
|
18561
|
+
content: row.content,
|
|
18562
|
+
contentHash: row.contentHash,
|
|
18563
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18564
|
+
})
|
|
18565
|
+
);
|
|
18566
|
+
}
|
|
18567
|
+
}
|
|
18568
|
+
);
|
|
18569
|
+
}
|
|
18570
|
+
function copyLegacyFindings(db) {
|
|
18571
|
+
const selectStmt = db.prepare(
|
|
18572
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18573
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18574
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18575
|
+
first_detected_at AS firstDetectedAt
|
|
18576
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18577
|
+
);
|
|
18578
|
+
const definitionStmt = db.prepare(
|
|
18579
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18580
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18581
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18582
|
+
);
|
|
18583
|
+
const findingStmt = db.prepare(
|
|
18584
|
+
`INSERT INTO inspection_findings
|
|
18585
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18586
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18587
|
+
finding_key, first_detected_at)
|
|
18588
|
+
VALUES
|
|
18589
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18590
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18591
|
+
:findingKey, :firstDetectedAt)
|
|
18592
|
+
ON CONFLICT(id) DO NOTHING
|
|
18593
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18594
|
+
first_detected_at = CASE
|
|
18595
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18596
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18597
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18598
|
+
END`
|
|
18599
|
+
);
|
|
18600
|
+
return drainLegacyTable(
|
|
18601
|
+
db,
|
|
18602
|
+
"findings",
|
|
18603
|
+
selectStmt,
|
|
18604
|
+
(rows) => {
|
|
18605
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18606
|
+
for (const row of rows) {
|
|
18607
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18608
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18609
|
+
if (definitionId === void 0) {
|
|
18610
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18611
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18612
|
+
definitionStmt.run(
|
|
18613
|
+
bindParams({
|
|
18614
|
+
id: definitionId,
|
|
18615
|
+
ruleId: row.ruleId,
|
|
18616
|
+
name: row.ruleId,
|
|
18617
|
+
category: row.category,
|
|
18618
|
+
severity: row.severity,
|
|
18619
|
+
definition: "",
|
|
18620
|
+
version: version2
|
|
18621
|
+
})
|
|
18622
|
+
);
|
|
18623
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18624
|
+
}
|
|
18625
|
+
findingStmt.run(
|
|
18626
|
+
bindParams({
|
|
18627
|
+
id: row.id,
|
|
18628
|
+
auditEventId: row.eventId,
|
|
18629
|
+
inspectionDefinitionId: definitionId,
|
|
18630
|
+
spanStart: row.spanStart,
|
|
18631
|
+
spanEnd: row.spanEnd,
|
|
18632
|
+
maskedMatch: row.maskedMatch,
|
|
18633
|
+
actionTaken: row.actionTaken,
|
|
18634
|
+
confidence: row.confidence,
|
|
18635
|
+
findingKey: row.findingKey,
|
|
18636
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18637
|
+
})
|
|
18638
|
+
);
|
|
18639
|
+
}
|
|
18640
|
+
}
|
|
18641
|
+
);
|
|
18642
|
+
}
|
|
18643
|
+
function runLegacyHistoryBackfill(db) {
|
|
18644
|
+
try {
|
|
18645
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18646
|
+
if (!eventsCaughtUp) return false;
|
|
18647
|
+
return copyLegacyFindings(db);
|
|
18648
|
+
} catch (error51) {
|
|
18649
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18650
|
+
return false;
|
|
18651
|
+
}
|
|
18652
|
+
}
|
|
18157
18653
|
function isForeignSqliteLineage(db) {
|
|
18158
18654
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18159
18655
|
return columnNames(db, "events").includes("tenant_id");
|
|
18160
18656
|
}
|
|
18161
18657
|
function ensureSyncedAtColumn(db, table) {
|
|
18658
|
+
if (!schemaObjectExists(db, "table", table)) return;
|
|
18162
18659
|
if (!columnNames(db, table).includes("synced_at")) {
|
|
18163
18660
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
18164
18661
|
}
|
|
@@ -18179,6 +18676,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18179
18676
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18180
18677
|
)`);
|
|
18181
18678
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18679
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18182
18680
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18183
18681
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18184
18682
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18197,29 +18695,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18197
18695
|
blocked_at INTEGER NOT NULL
|
|
18198
18696
|
)`);
|
|
18199
18697
|
}
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18208
|
-
try {
|
|
18209
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18210
|
-
} catch {
|
|
18211
|
-
}
|
|
18212
|
-
}
|
|
18213
|
-
function walSidecars(file2) {
|
|
18214
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18215
|
-
}
|
|
18216
|
-
function tightenPerms(file2) {
|
|
18217
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18218
|
-
try {
|
|
18219
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18220
|
-
} catch {
|
|
18221
|
-
}
|
|
18222
|
-
}
|
|
18698
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18699
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18700
|
+
rule_key TEXT PRIMARY KEY,
|
|
18701
|
+
verdict TEXT NOT NULL,
|
|
18702
|
+
worst_probe_ms REAL NOT NULL,
|
|
18703
|
+
checked_at INTEGER NOT NULL
|
|
18704
|
+
)`);
|
|
18223
18705
|
}
|
|
18224
18706
|
|
|
18225
18707
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18241,51 +18723,6 @@ function parseJsonObject(s) {
|
|
|
18241
18723
|
return void 0;
|
|
18242
18724
|
}
|
|
18243
18725
|
|
|
18244
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18245
|
-
function allRows(stmt, params) {
|
|
18246
|
-
if (params === void 0) return stmt.all();
|
|
18247
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18248
|
-
return stmt.all(params);
|
|
18249
|
-
}
|
|
18250
|
-
function getRow(stmt, params) {
|
|
18251
|
-
if (params === void 0) return stmt.get();
|
|
18252
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18253
|
-
return stmt.get(params);
|
|
18254
|
-
}
|
|
18255
|
-
function intToBool(raw) {
|
|
18256
|
-
return raw === 1 || raw === true;
|
|
18257
|
-
}
|
|
18258
|
-
function boolToInt(b) {
|
|
18259
|
-
return b ? 1 : 0;
|
|
18260
|
-
}
|
|
18261
|
-
function bindParams(row) {
|
|
18262
|
-
const out = {};
|
|
18263
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18264
|
-
out[key] = value === void 0 ? null : value;
|
|
18265
|
-
}
|
|
18266
|
-
return out;
|
|
18267
|
-
}
|
|
18268
|
-
function countScalar(db, sql, params) {
|
|
18269
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18270
|
-
}
|
|
18271
|
-
function countBy(db, sql, params) {
|
|
18272
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18273
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18274
|
-
map2.set(row.k, row.n);
|
|
18275
|
-
}
|
|
18276
|
-
return map2;
|
|
18277
|
-
}
|
|
18278
|
-
function mapRowsTolerant(rows, map2) {
|
|
18279
|
-
const out = [];
|
|
18280
|
-
for (const row of rows) {
|
|
18281
|
-
try {
|
|
18282
|
-
out.push(map2(row));
|
|
18283
|
-
} catch {
|
|
18284
|
-
}
|
|
18285
|
-
}
|
|
18286
|
-
return out;
|
|
18287
|
-
}
|
|
18288
|
-
|
|
18289
18726
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18290
18727
|
var DAY_MS = 864e5;
|
|
18291
18728
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18902,6 +19339,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18902
19339
|
})
|
|
18903
19340
|
);
|
|
18904
19341
|
}
|
|
19342
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19343
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19344
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19345
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19346
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19347
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19348
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19349
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19350
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19351
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19352
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19353
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19354
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19355
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19356
|
+
}
|
|
18905
19357
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18906
19358
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18907
19359
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19363,8 +19815,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19363
19815
|
)
|
|
19364
19816
|
);
|
|
19365
19817
|
}
|
|
19366
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19367
|
-
//
|
|
19818
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19819
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19820
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19821
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19822
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19823
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19824
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19825
|
+
// to the old findings⋈events one.
|
|
19368
19826
|
countFindingsLast30d(ruleIds) {
|
|
19369
19827
|
if (ruleIds.length === 0) return 0;
|
|
19370
19828
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19372,8 +19830,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19372
19830
|
return countScalar(
|
|
19373
19831
|
this.db,
|
|
19374
19832
|
`SELECT count(*) AS n
|
|
19375
|
-
FROM
|
|
19376
|
-
|
|
19833
|
+
FROM inspection_findings f
|
|
19834
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19835
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19836
|
+
WHERE e.started_at >= ?
|
|
19837
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19838
|
+
AND d.rule_id IN (${inClause})`,
|
|
19377
19839
|
[since, ...ruleIds]
|
|
19378
19840
|
);
|
|
19379
19841
|
}
|
|
@@ -19383,35 +19845,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19383
19845
|
var SqliteEventsRepository = class {
|
|
19384
19846
|
constructor(db) {
|
|
19385
19847
|
this.db = db;
|
|
19386
|
-
this.insertStmt = db.prepare(
|
|
19387
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19388
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19389
|
-
);
|
|
19390
19848
|
}
|
|
19391
19849
|
db;
|
|
19392
|
-
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
this.insertStmt.run(
|
|
19396
|
-
bindParams({
|
|
19397
|
-
id: row.id,
|
|
19398
|
-
sourceTool: row.sourceTool,
|
|
19399
|
-
kind: row.kind,
|
|
19400
|
-
occurredAt: row.occurredAt,
|
|
19401
|
-
contentHash: row.contentHash,
|
|
19402
|
-
content: row.content,
|
|
19403
|
-
metadata: row.metadata
|
|
19404
|
-
})
|
|
19405
|
-
);
|
|
19406
|
-
}
|
|
19407
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19408
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19409
|
-
// never duplicates findings.
|
|
19850
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19851
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19852
|
+
// scan never duplicates findings.
|
|
19410
19853
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19411
19854
|
// async EventsReadPort contract.
|
|
19855
|
+
//
|
|
19856
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19857
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19858
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19859
|
+
// index-friendly rather than walking rows that can never match.
|
|
19412
19860
|
contentHashes() {
|
|
19413
19861
|
const rows = allRows(
|
|
19414
|
-
this.db.prepare(
|
|
19862
|
+
this.db.prepare(
|
|
19863
|
+
`SELECT content_hash FROM audit_events
|
|
19864
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19865
|
+
)
|
|
19415
19866
|
);
|
|
19416
19867
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19417
19868
|
}
|
|
@@ -19747,17 +20198,20 @@ function parseExceptionRow(row) {
|
|
|
19747
20198
|
}
|
|
19748
20199
|
|
|
19749
20200
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19750
|
-
function
|
|
20201
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19751
20202
|
return `(
|
|
19752
|
-
SELECT fr
|
|
20203
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19753
20204
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19754
20205
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19755
20206
|
LIMIT 1
|
|
19756
20207
|
)`;
|
|
19757
20208
|
}
|
|
20209
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20210
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20211
|
+
}
|
|
19758
20212
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19759
|
-
SELECT finding_key, status FROM (
|
|
19760
|
-
SELECT fr.finding_key, fr.status,
|
|
20213
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20214
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19761
20215
|
ROW_NUMBER() OVER (
|
|
19762
20216
|
PARTITION BY fr.finding_key
|
|
19763
20217
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19784,68 +20238,21 @@ var DAY_MS3 = 864e5;
|
|
|
19784
20238
|
var SqliteFindingsRepository = class {
|
|
19785
20239
|
constructor(db) {
|
|
19786
20240
|
this.db = db;
|
|
19787
|
-
this.insertStmt = db.prepare(
|
|
19788
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19789
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19790
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19791
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19792
|
-
event_id = excluded.event_id,
|
|
19793
|
-
category = excluded.category,
|
|
19794
|
-
severity = excluded.severity,
|
|
19795
|
-
span_start = excluded.span_start,
|
|
19796
|
-
span_end = excluded.span_end,
|
|
19797
|
-
masked_match = excluded.masked_match,
|
|
19798
|
-
action_taken = excluded.action_taken,
|
|
19799
|
-
confidence = excluded.confidence`
|
|
19800
|
-
);
|
|
19801
|
-
this.sessionDupStmt = db.prepare(
|
|
19802
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19803
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19804
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19805
|
-
LIMIT 1`
|
|
19806
|
-
);
|
|
19807
20241
|
}
|
|
19808
20242
|
db;
|
|
19809
|
-
insertStmt;
|
|
19810
|
-
sessionDupStmt;
|
|
19811
|
-
insertFindings(findings, scope = {}) {
|
|
19812
|
-
for (const finding2 of findings) {
|
|
19813
|
-
if (scope.sessionId && this.isSessionDuplicate(finding2, scope.sessionId)) continue;
|
|
19814
|
-
const row = toFindingRow(finding2);
|
|
19815
|
-
this.insertStmt.run({
|
|
19816
|
-
id: row.id,
|
|
19817
|
-
eventId: row.eventId,
|
|
19818
|
-
ruleId: row.ruleId,
|
|
19819
|
-
category: row.category,
|
|
19820
|
-
severity: row.severity,
|
|
19821
|
-
spanStart: row.spanStart,
|
|
19822
|
-
spanEnd: row.spanEnd,
|
|
19823
|
-
maskedMatch: row.maskedMatch,
|
|
19824
|
-
actionTaken: row.actionTaken,
|
|
19825
|
-
confidence: row.confidence,
|
|
19826
|
-
findingKey: row.findingKey ?? null
|
|
19827
|
-
});
|
|
19828
|
-
}
|
|
19829
|
-
}
|
|
19830
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19831
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19832
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19833
|
-
isSessionDuplicate(finding2, sessionId) {
|
|
19834
|
-
const hit = this.sessionDupStmt.get({
|
|
19835
|
-
ruleId: finding2.ruleId,
|
|
19836
|
-
maskedMatch: finding2.maskedMatch,
|
|
19837
|
-
sessionId
|
|
19838
|
-
});
|
|
19839
|
-
return hit !== void 0;
|
|
19840
|
-
}
|
|
19841
20243
|
recentFindings(opts) {
|
|
19842
20244
|
const limit = opts?.limit ?? 50;
|
|
19843
20245
|
const rows = allRows(
|
|
19844
20246
|
this.db.prepare(
|
|
19845
|
-
`SELECT f.id, f.event_id,
|
|
19846
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19847
|
-
|
|
19848
|
-
|
|
20247
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20248
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20249
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20250
|
+
e.event_type AS kind
|
|
20251
|
+
FROM inspection_findings f
|
|
20252
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20253
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20254
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20255
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19849
20256
|
LIMIT :limit`
|
|
19850
20257
|
),
|
|
19851
20258
|
{ limit }
|
|
@@ -19867,25 +20274,34 @@ var SqliteFindingsRepository = class {
|
|
|
19867
20274
|
);
|
|
19868
20275
|
}
|
|
19869
20276
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19870
|
-
* session-stamped
|
|
20277
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19871
20278
|
* page can label its findings link without the grouped pipeline. */
|
|
19872
20279
|
sessionFindingsCount(sessionId) {
|
|
19873
20280
|
if (!sessionId) return Promise.resolve(0);
|
|
19874
20281
|
return Promise.resolve(
|
|
19875
20282
|
countScalar(
|
|
19876
20283
|
this.db,
|
|
19877
|
-
`SELECT count(*) AS n FROM
|
|
19878
|
-
JOIN
|
|
19879
|
-
WHERE
|
|
20284
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20285
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20286
|
+
WHERE e.root_session_id = :sessionId
|
|
20287
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19880
20288
|
{ sessionId }
|
|
19881
20289
|
)
|
|
19882
20290
|
);
|
|
19883
20291
|
}
|
|
19884
|
-
/** Per-rule transcript firing tally for one session —
|
|
19885
|
-
*
|
|
19886
|
-
*
|
|
19887
|
-
*
|
|
19888
|
-
*
|
|
20292
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20293
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20294
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20295
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20296
|
+
* tally with the deduped groups it lists.
|
|
20297
|
+
*
|
|
20298
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20299
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20300
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20301
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20302
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20303
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20304
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19889
20305
|
sessionFirings(sessionId) {
|
|
19890
20306
|
return Object.fromEntries(
|
|
19891
20307
|
countBy(
|
|
@@ -19895,18 +20311,25 @@ var SqliteFindingsRepository = class {
|
|
|
19895
20311
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19896
20312
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19897
20313
|
WHERE e.root_session_id = :sessionId
|
|
20314
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19898
20315
|
GROUP BY d.rule_id`,
|
|
19899
20316
|
{ sessionId }
|
|
19900
20317
|
)
|
|
19901
20318
|
);
|
|
19902
20319
|
}
|
|
19903
20320
|
/**
|
|
19904
|
-
* Grouped findings for the dashboard — joins
|
|
19905
|
-
* toolName from
|
|
19906
|
-
*
|
|
20321
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20322
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20323
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20324
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20325
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20326
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20327
|
+
* severity then recency. Filtering
|
|
19907
20328
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19908
20329
|
* reflect the full filtered set; `items` is the requested
|
|
19909
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20330
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20331
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20332
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19910
20333
|
*
|
|
19911
20334
|
* Two reads, neither of which materializes a row per finding:
|
|
19912
20335
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19919,10 +20342,11 @@ var SqliteFindingsRepository = class {
|
|
|
19919
20342
|
* rule is ever restated in SQL.
|
|
19920
20343
|
*/
|
|
19921
20344
|
listGroupedFindings(query) {
|
|
19922
|
-
const sessionPredicate = query.sessionId ? `
|
|
20345
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20346
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19923
20347
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19924
20348
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19925
|
-
predicate
|
|
20349
|
+
predicate,
|
|
19926
20350
|
params: sessionParams
|
|
19927
20351
|
});
|
|
19928
20352
|
const rows = allRows(
|
|
@@ -19930,24 +20354,26 @@ var SqliteFindingsRepository = class {
|
|
|
19930
20354
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19931
20355
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19932
20356
|
FROM (
|
|
19933
|
-
SELECT f.id AS id,
|
|
19934
|
-
|
|
20357
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20358
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19935
20359
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19936
|
-
e.
|
|
19937
|
-
json_extract(e.
|
|
19938
|
-
json_extract(e.
|
|
19939
|
-
json_extract(e.
|
|
19940
|
-
e.
|
|
20360
|
+
e.started_at AS occurred_at,
|
|
20361
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20362
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20363
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20364
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20365
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19941
20366
|
latest.status AS latest_status,
|
|
19942
20367
|
ROW_NUMBER() OVER (
|
|
19943
|
-
PARTITION BY
|
|
19944
|
-
ORDER BY e.
|
|
20368
|
+
PARTITION BY d.rule_id
|
|
20369
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19945
20370
|
) AS rn
|
|
19946
|
-
FROM
|
|
19947
|
-
JOIN
|
|
20371
|
+
FROM inspection_findings f
|
|
20372
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20373
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19948
20374
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19949
20375
|
ON latest.finding_key = f.finding_key
|
|
19950
|
-
${
|
|
20376
|
+
${predicate}
|
|
19951
20377
|
)
|
|
19952
20378
|
WHERE rn <= :cap
|
|
19953
20379
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19974,17 +20400,29 @@ var SqliteFindingsRepository = class {
|
|
|
19974
20400
|
severity: query.severity,
|
|
19975
20401
|
providers: query.provider,
|
|
19976
20402
|
actions: query.action,
|
|
20403
|
+
statuses: query.status,
|
|
19977
20404
|
subtype: query.subtype,
|
|
19978
20405
|
q: query.q
|
|
19979
20406
|
};
|
|
19980
20407
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19981
20408
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20409
|
+
const statusFilter = query.status ?? [];
|
|
19982
20410
|
const totals = {
|
|
19983
|
-
findings: sorted.reduce((acc, g) =>
|
|
20411
|
+
findings: sorted.reduce((acc, g) => {
|
|
20412
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20413
|
+
const agg = aggregates.get(g.id);
|
|
20414
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20415
|
+
}, 0),
|
|
19984
20416
|
groups: sorted.length
|
|
19985
20417
|
};
|
|
19986
20418
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19987
|
-
const
|
|
20419
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20420
|
+
const items = sorted.slice(0, limit).map(
|
|
20421
|
+
(g) => statusSet ? {
|
|
20422
|
+
...g,
|
|
20423
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20424
|
+
} : g
|
|
20425
|
+
);
|
|
19988
20426
|
return Promise.resolve({
|
|
19989
20427
|
totals,
|
|
19990
20428
|
facets,
|
|
@@ -19998,45 +20436,62 @@ var SqliteFindingsRepository = class {
|
|
|
19998
20436
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19999
20437
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
20000
20438
|
*
|
|
20001
|
-
*
|
|
20002
|
-
*
|
|
20003
|
-
*
|
|
20004
|
-
* status
|
|
20005
|
-
*
|
|
20006
|
-
*
|
|
20439
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20440
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20441
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20442
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20443
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20444
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20445
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20446
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20447
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20448
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20449
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
20007
20450
|
* a group's row stays small however many findings it holds.
|
|
20008
20451
|
*
|
|
20009
20452
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
20010
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
20011
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
20012
|
-
* that is a string proportional to the store (~8MB over
|
|
20013
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
20014
|
-
* match an instance outside the preview, which searching
|
|
20015
|
-
* would silently lose, so it is fetched only when the
|
|
20016
|
-
* carries a `q`.
|
|
20453
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20454
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20455
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20456
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20457
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20458
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20459
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20460
|
+
* path repeating across tuples.)
|
|
20017
20461
|
*/
|
|
20018
20462
|
groupAggregates(withSearchText, scope) {
|
|
20019
|
-
const
|
|
20020
|
-
group_concat(DISTINCT json_extract(e.
|
|
20021
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20463
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20464
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20465
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
20022
20466
|
const rows = this.db.prepare(
|
|
20023
|
-
`SELECT
|
|
20024
|
-
|
|
20025
|
-
max(
|
|
20026
|
-
group_concat(
|
|
20027
|
-
group_concat(
|
|
20028
|
-
group_concat(
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
20467
|
+
`SELECT rule_id,
|
|
20468
|
+
sum(tuple_count) AS instance_count,
|
|
20469
|
+
max(latest_at) AS latest_at,
|
|
20470
|
+
group_concat(source_tools) AS source_tools,
|
|
20471
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20472
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20473
|
+
group_concat(repos) AS repos,
|
|
20474
|
+
group_concat(files) AS files,
|
|
20475
|
+
group_concat(tool_names) AS tool_names
|
|
20476
|
+
FROM (
|
|
20477
|
+
SELECT d.rule_id AS rule_id,
|
|
20478
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20479
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20480
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20481
|
+
count(*) AS tuple_count,
|
|
20482
|
+
max(e.started_at) AS latest_at,
|
|
20483
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20484
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20485
|
+
${innerSearchColumns}
|
|
20486
|
+
FROM inspection_findings f
|
|
20487
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20488
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20489
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20490
|
+
ON latest.finding_key = f.finding_key
|
|
20491
|
+
${scope.predicate}
|
|
20492
|
+
GROUP BY d.rule_id, status_tuple
|
|
20493
|
+
)
|
|
20494
|
+
GROUP BY rule_id`
|
|
20040
20495
|
).all(scope.params);
|
|
20041
20496
|
return new Map(
|
|
20042
20497
|
rows.map((r) => [
|
|
@@ -20046,13 +20501,14 @@ var SqliteFindingsRepository = class {
|
|
|
20046
20501
|
sourceTools: splitConcat(r.source_tools),
|
|
20047
20502
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20048
20503
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20049
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20504
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20050
20505
|
return {
|
|
20051
20506
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20052
20507
|
// so the marker stands in for the key itself (never rendered).
|
|
20053
20508
|
kind,
|
|
20054
20509
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20055
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20510
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20511
|
+
count: Number(count)
|
|
20056
20512
|
};
|
|
20057
20513
|
}),
|
|
20058
20514
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20069,10 +20525,21 @@ var SqliteFindingsRepository = class {
|
|
|
20069
20525
|
);
|
|
20070
20526
|
}
|
|
20071
20527
|
healthSummary() {
|
|
20072
|
-
const total = countScalar(
|
|
20528
|
+
const total = countScalar(
|
|
20529
|
+
this.db,
|
|
20530
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20531
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20532
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20533
|
+
);
|
|
20073
20534
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20074
20535
|
const grouped = allRows(
|
|
20075
|
-
this.db.prepare(
|
|
20536
|
+
this.db.prepare(
|
|
20537
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20538
|
+
FROM inspection_findings f
|
|
20539
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20540
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20541
|
+
GROUP BY f.action_taken`
|
|
20542
|
+
)
|
|
20076
20543
|
);
|
|
20077
20544
|
for (const row of grouped) {
|
|
20078
20545
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20080,12 +20547,15 @@ var SqliteFindingsRepository = class {
|
|
|
20080
20547
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20081
20548
|
const sevRows = allRows(
|
|
20082
20549
|
this.db.prepare(
|
|
20083
|
-
`SELECT
|
|
20084
|
-
FROM
|
|
20550
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20551
|
+
FROM inspection_findings f
|
|
20552
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20553
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20085
20554
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20086
20555
|
ON latest.finding_key = f.finding_key
|
|
20087
|
-
WHERE
|
|
20088
|
-
|
|
20556
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20557
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20558
|
+
GROUP BY d.severity`
|
|
20089
20559
|
)
|
|
20090
20560
|
);
|
|
20091
20561
|
for (const row of sevRows) {
|
|
@@ -20106,9 +20576,11 @@ var SqliteFindingsRepository = class {
|
|
|
20106
20576
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20107
20577
|
const rows = allRows(
|
|
20108
20578
|
this.db.prepare(
|
|
20109
|
-
`SELECT date(e.
|
|
20110
|
-
FROM
|
|
20111
|
-
|
|
20579
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20580
|
+
FROM inspection_findings f
|
|
20581
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20582
|
+
WHERE e.started_at >= :since
|
|
20583
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20112
20584
|
GROUP BY day, f.action_taken`
|
|
20113
20585
|
),
|
|
20114
20586
|
{ since }
|
|
@@ -20173,15 +20645,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20173
20645
|
this.insertStmt = db.prepare(
|
|
20174
20646
|
`INSERT INTO inspection_findings
|
|
20175
20647
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20176
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20648
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20649
|
+
finding_key, first_detected_at)
|
|
20177
20650
|
VALUES
|
|
20178
20651
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20179
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20180
|
-
|
|
20652
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20653
|
+
:findingKey,
|
|
20654
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20655
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20656
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20657
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20658
|
+
audit_event_id = excluded.audit_event_id,
|
|
20659
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20660
|
+
classified_data_id = excluded.classified_data_id,
|
|
20661
|
+
span_start = excluded.span_start,
|
|
20662
|
+
span_end = excluded.span_end,
|
|
20663
|
+
masked_match = excluded.masked_match,
|
|
20664
|
+
action_taken = excluded.action_taken,
|
|
20665
|
+
confidence = excluded.confidence`
|
|
20666
|
+
);
|
|
20667
|
+
this.sessionDupStmt = db.prepare(
|
|
20668
|
+
`SELECT 1 FROM inspection_findings f
|
|
20669
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20670
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20671
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20672
|
+
AND e.root_session_id = :sessionId
|
|
20673
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20674
|
+
LIMIT 1`
|
|
20675
|
+
);
|
|
20676
|
+
this.eventDupStmt = db.prepare(
|
|
20677
|
+
`SELECT 1 FROM inspection_findings f
|
|
20678
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20679
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20680
|
+
AND f.masked_match = :maskedMatch
|
|
20681
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20682
|
+
LIMIT 1`
|
|
20181
20683
|
);
|
|
20182
20684
|
}
|
|
20183
20685
|
db;
|
|
20184
20686
|
insertStmt;
|
|
20687
|
+
sessionDupStmt;
|
|
20688
|
+
eventDupStmt;
|
|
20689
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20690
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20691
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20692
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20693
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20694
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20695
|
+
}
|
|
20696
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20697
|
+
// recorded against the given audit event.
|
|
20698
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20699
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20700
|
+
}
|
|
20185
20701
|
insertFinding(input) {
|
|
20186
20702
|
const row = toInspectionFindingRow(input);
|
|
20187
20703
|
this.insertStmt.run(
|
|
@@ -20194,7 +20710,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20194
20710
|
spanEnd: row.spanEnd,
|
|
20195
20711
|
maskedMatch: row.maskedMatch,
|
|
20196
20712
|
actionTaken: row.actionTaken,
|
|
20197
|
-
confidence: row.confidence
|
|
20713
|
+
confidence: row.confidence,
|
|
20714
|
+
findingKey: row.findingKey,
|
|
20715
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20198
20716
|
})
|
|
20199
20717
|
);
|
|
20200
20718
|
}
|
|
@@ -20466,7 +20984,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20466
20984
|
installedRuleset() {
|
|
20467
20985
|
const rows = allRows(
|
|
20468
20986
|
this.db.prepare(
|
|
20469
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20987
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20470
20988
|
)
|
|
20471
20989
|
);
|
|
20472
20990
|
const out = {
|
|
@@ -20474,7 +20992,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20474
20992
|
enabledPacks: 0,
|
|
20475
20993
|
rules: [],
|
|
20476
20994
|
invalidRules: 0,
|
|
20477
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20995
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20996
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20478
20997
|
};
|
|
20479
20998
|
for (const row of rows) {
|
|
20480
20999
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20496,6 +21015,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20496
21015
|
if (parsed.success) {
|
|
20497
21016
|
out.rules.push(parsed.data);
|
|
20498
21017
|
out.ruleActions.set(parsed.data.id, action);
|
|
21018
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20499
21019
|
} else out.invalidRules += 1;
|
|
20500
21020
|
}
|
|
20501
21021
|
}
|
|
@@ -21670,19 +22190,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21670
22190
|
);
|
|
21671
22191
|
this.openAtRestStmt = db.prepare(
|
|
21672
22192
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21673
|
-
FROM
|
|
21674
|
-
JOIN
|
|
21675
|
-
WHERE e.
|
|
21676
|
-
AND json_extract(e.
|
|
22193
|
+
FROM inspection_findings f
|
|
22194
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22195
|
+
WHERE e.event_type = 'code_change'
|
|
22196
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21677
22197
|
AND f.finding_key IS NOT NULL
|
|
21678
22198
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21679
22199
|
);
|
|
21680
22200
|
this.resolvedAtRestStmt = db.prepare(
|
|
21681
22201
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21682
|
-
FROM
|
|
21683
|
-
JOIN
|
|
21684
|
-
WHERE e.
|
|
21685
|
-
AND json_extract(e.
|
|
22202
|
+
FROM inspection_findings f
|
|
22203
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22204
|
+
WHERE e.event_type = 'code_change'
|
|
22205
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21686
22206
|
AND f.finding_key IS NOT NULL
|
|
21687
22207
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21688
22208
|
);
|
|
@@ -21750,6 +22270,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21750
22270
|
}
|
|
21751
22271
|
};
|
|
21752
22272
|
|
|
22273
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22274
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22275
|
+
constructor(db) {
|
|
22276
|
+
this.db = db;
|
|
22277
|
+
this.upsertStmt = db.prepare(
|
|
22278
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22279
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22280
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22281
|
+
verdict = excluded.verdict,
|
|
22282
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22283
|
+
checked_at = excluded.checked_at`
|
|
22284
|
+
);
|
|
22285
|
+
this.readStmt = db.prepare(
|
|
22286
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22287
|
+
);
|
|
22288
|
+
}
|
|
22289
|
+
db;
|
|
22290
|
+
upsertStmt;
|
|
22291
|
+
readStmt;
|
|
22292
|
+
getVerdict(ruleKey) {
|
|
22293
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22294
|
+
}
|
|
22295
|
+
setVerdict(ruleKey, verdict, worstProbeMs) {
|
|
22296
|
+
failOpenTransaction(this.db, () => {
|
|
22297
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
|
|
22298
|
+
});
|
|
22299
|
+
}
|
|
22300
|
+
};
|
|
22301
|
+
|
|
21753
22302
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21754
22303
|
var SqliteScanLedgerRepository = class {
|
|
21755
22304
|
constructor(db) {
|
|
@@ -21876,25 +22425,27 @@ var SqliteSecurityRepository = class {
|
|
|
21876
22425
|
severitySummary() {
|
|
21877
22426
|
const rows = allRows(
|
|
21878
22427
|
this.db.prepare(
|
|
21879
|
-
`SELECT
|
|
22428
|
+
`SELECT d.severity AS severity,
|
|
21880
22429
|
COUNT(*) AS count,
|
|
21881
22430
|
SUM(CASE
|
|
21882
|
-
WHEN e.
|
|
22431
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21883
22432
|
WHEN f.finding_key IS NULL THEN 0
|
|
21884
22433
|
WHEN latest.status = 'resolved' THEN 1
|
|
21885
22434
|
ELSE 0
|
|
21886
22435
|
END) AS caught,
|
|
21887
22436
|
SUM(CASE
|
|
21888
|
-
WHEN e.
|
|
22437
|
+
WHEN e.event_type = 'code_change'
|
|
21889
22438
|
AND f.finding_key IS NOT NULL
|
|
21890
22439
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21891
22440
|
ELSE 0
|
|
21892
22441
|
END) AS open_at_rest
|
|
21893
|
-
FROM
|
|
21894
|
-
JOIN
|
|
22442
|
+
FROM inspection_findings f
|
|
22443
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22444
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21895
22445
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21896
22446
|
ON latest.finding_key = f.finding_key
|
|
21897
|
-
|
|
22447
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22448
|
+
GROUP BY d.severity`
|
|
21898
22449
|
)
|
|
21899
22450
|
);
|
|
21900
22451
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21960,7 +22511,7 @@ var SqliteSecurityRepository = class {
|
|
|
21960
22511
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21961
22512
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21962
22513
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21963
|
-
// (
|
|
22514
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21964
22515
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21965
22516
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21966
22517
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21985,30 +22536,20 @@ var SqliteSecurityRepository = class {
|
|
|
21985
22536
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21986
22537
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21987
22538
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21988
|
-
//
|
|
21989
|
-
// the parent event's
|
|
21990
|
-
// backfill left null.
|
|
21991
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
|
|
21998
|
-
|
|
21999
|
-
|
|
22000
|
-
WHERE fr.finding_key = f.finding_key
|
|
22001
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22002
|
-
LIMIT 1
|
|
22003
|
-
) AS latest_method,
|
|
22004
|
-
(
|
|
22005
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22006
|
-
WHERE fr.finding_key = f.finding_key
|
|
22007
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22008
|
-
LIMIT 1
|
|
22009
|
-
) AS latest_resolved_at
|
|
22010
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22539
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22540
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22541
|
+
// legacy/edge row the backfill left null.
|
|
22542
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22543
|
+
latest.status AS latest_status,
|
|
22544
|
+
latest.method AS latest_method,
|
|
22545
|
+
latest.resolved_at AS latest_resolved_at
|
|
22546
|
+
FROM inspection_findings f
|
|
22547
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22548
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22549
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22550
|
+
ON latest.finding_key = f.finding_key
|
|
22011
22551
|
WHERE f.finding_key IS NOT NULL
|
|
22552
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22012
22553
|
AND EXISTS (
|
|
22013
22554
|
SELECT 1 FROM finding_resolution fr
|
|
22014
22555
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22055,11 +22596,13 @@ var SqliteSecurityRepository = class {
|
|
|
22055
22596
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22056
22597
|
const rows = allRows(
|
|
22057
22598
|
this.db.prepare(
|
|
22058
|
-
`SELECT json_extract(e.
|
|
22059
|
-
FROM
|
|
22060
|
-
|
|
22061
|
-
|
|
22062
|
-
AND
|
|
22599
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22600
|
+
FROM inspection_findings f
|
|
22601
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22602
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22603
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22604
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22605
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22063
22606
|
GROUP BY repo
|
|
22064
22607
|
ORDER BY c DESC, repo
|
|
22065
22608
|
LIMIT :limit`
|
|
@@ -22083,44 +22626,28 @@ var SqliteSecurityRepository = class {
|
|
|
22083
22626
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22084
22627
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22085
22628
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22086
|
-
// finding's parent event (
|
|
22087
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22088
|
-
// capped at `limit`.
|
|
22629
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22630
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22631
|
+
// DESC, capped at `limit`.
|
|
22089
22632
|
recentlyResolved(limit = 20) {
|
|
22090
22633
|
const rows = allRows(
|
|
22091
22634
|
this.db.prepare(
|
|
22092
22635
|
`SELECT f.finding_key AS finding_key,
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
json_extract(e.
|
|
22096
|
-
COALESCE(f.first_detected_at, e.
|
|
22097
|
-
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
AND
|
|
22106
|
-
AND
|
|
22107
|
-
|
|
22108
|
-
WHERE fr.finding_key = f.finding_key
|
|
22109
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22110
|
-
LIMIT 1
|
|
22111
|
-
) = 'resolved'
|
|
22112
|
-
AND (
|
|
22113
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22114
|
-
WHERE fr.finding_key = f.finding_key
|
|
22115
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22116
|
-
LIMIT 1
|
|
22117
|
-
) = 'fixed-at-source'
|
|
22118
|
-
AND (
|
|
22119
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22120
|
-
WHERE fr.finding_key = f.finding_key
|
|
22121
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22122
|
-
LIMIT 1
|
|
22123
|
-
) IS NOT NULL
|
|
22636
|
+
d.rule_id AS rule_id,
|
|
22637
|
+
d.severity AS severity,
|
|
22638
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22639
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22640
|
+
latest.resolved_at AS latest_resolved_at
|
|
22641
|
+
FROM inspection_findings f
|
|
22642
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22643
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22644
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22645
|
+
ON latest.finding_key = f.finding_key
|
|
22646
|
+
WHERE e.event_type = 'code_change'
|
|
22647
|
+
AND f.finding_key IS NOT NULL
|
|
22648
|
+
AND latest.status = 'resolved'
|
|
22649
|
+
AND latest.method = 'fixed-at-source'
|
|
22650
|
+
AND latest.resolved_at IS NOT NULL
|
|
22124
22651
|
ORDER BY latest_resolved_at DESC
|
|
22125
22652
|
LIMIT :limit`
|
|
22126
22653
|
),
|
|
@@ -22139,15 +22666,18 @@ var SqliteSecurityRepository = class {
|
|
|
22139
22666
|
return Promise.resolve({ items });
|
|
22140
22667
|
}
|
|
22141
22668
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22142
|
-
// epoch-millis timestamp.
|
|
22669
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22143
22670
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22144
22671
|
findingsInRange(fromMs, toMs) {
|
|
22145
22672
|
const rows = allRows(
|
|
22146
22673
|
this.db.prepare(
|
|
22147
|
-
`SELECT e.
|
|
22148
|
-
FROM
|
|
22149
|
-
|
|
22150
|
-
|
|
22674
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22675
|
+
FROM inspection_findings f
|
|
22676
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22677
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22678
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22679
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22680
|
+
ORDER BY e.started_at`
|
|
22151
22681
|
),
|
|
22152
22682
|
{ from: fromMs, to: toMs }
|
|
22153
22683
|
);
|
|
@@ -22161,11 +22691,50 @@ var SqliteSecurityRepository = class {
|
|
|
22161
22691
|
|
|
22162
22692
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22163
22693
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22164
|
-
var
|
|
22694
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22695
|
+
var IN_CHUNK = 500;
|
|
22696
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22697
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22698
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22699
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22165
22700
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22166
22701
|
function parseNetwork(networkJson) {
|
|
22167
22702
|
return safeJson(networkJson, null);
|
|
22168
22703
|
}
|
|
22704
|
+
function capHits(all, mode) {
|
|
22705
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22706
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22707
|
+
}
|
|
22708
|
+
if (mode === "walk") {
|
|
22709
|
+
return {
|
|
22710
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22711
|
+
droppedFiles: [],
|
|
22712
|
+
truncated: true
|
|
22713
|
+
};
|
|
22714
|
+
}
|
|
22715
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22716
|
+
for (const hit of all) {
|
|
22717
|
+
const bucket = byFile.get(hit.site.file);
|
|
22718
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22719
|
+
else bucket.push(hit);
|
|
22720
|
+
}
|
|
22721
|
+
const hits = [];
|
|
22722
|
+
const droppedFiles = [];
|
|
22723
|
+
for (const [file2, bucket] of byFile) {
|
|
22724
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22725
|
+
else hits.push(...bucket);
|
|
22726
|
+
}
|
|
22727
|
+
return { hits, droppedFiles, truncated: true };
|
|
22728
|
+
}
|
|
22729
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22730
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22731
|
+
const dropped = new Set(droppedFiles);
|
|
22732
|
+
return {
|
|
22733
|
+
mode: "ledger",
|
|
22734
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22735
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22736
|
+
};
|
|
22737
|
+
}
|
|
22169
22738
|
function toEndpointSummary(row) {
|
|
22170
22739
|
return {
|
|
22171
22740
|
id: row.id,
|
|
@@ -22256,13 +22825,15 @@ var SqliteSharesRepository = class {
|
|
|
22256
22825
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22257
22826
|
const insecure = countScalar(
|
|
22258
22827
|
this.db,
|
|
22259
|
-
|
|
22828
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22829
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22260
22830
|
);
|
|
22261
22831
|
const needsReview = countScalar(
|
|
22262
22832
|
this.db,
|
|
22263
22833
|
`SELECT count(DISTINCT d.id) AS n
|
|
22264
22834
|
FROM share_destination d
|
|
22265
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22835
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22836
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22266
22837
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22267
22838
|
);
|
|
22268
22839
|
const kindCounts = countBy(
|
|
@@ -22272,6 +22843,7 @@ var SqliteSharesRepository = class {
|
|
|
22272
22843
|
const byKind = {
|
|
22273
22844
|
provider: kindCounts.get("provider") ?? 0,
|
|
22274
22845
|
internal: kindCounts.get("internal") ?? 0,
|
|
22846
|
+
external: kindCounts.get("external") ?? 0,
|
|
22275
22847
|
ip: kindCounts.get("ip") ?? 0
|
|
22276
22848
|
};
|
|
22277
22849
|
const trustCounts = countBy(
|
|
@@ -22347,23 +22919,316 @@ var SqliteSharesRepository = class {
|
|
|
22347
22919
|
// real edit from a no-such-destination.
|
|
22348
22920
|
/**
|
|
22349
22921
|
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22350
|
-
* `null` deletes the override
|
|
22922
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22923
|
+
*
|
|
22924
|
+
* The written row carries both the destination id and its host, so the
|
|
22925
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22926
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22927
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22928
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22929
|
+
* would otherwise race a concurrent prune.
|
|
22351
22930
|
*/
|
|
22352
22931
|
setEgressDecision(destinationId, decision) {
|
|
22353
|
-
|
|
22354
|
-
|
|
22355
|
-
|
|
22356
|
-
|
|
22357
|
-
|
|
22932
|
+
let existed = false;
|
|
22933
|
+
withTransaction(
|
|
22934
|
+
this.db,
|
|
22935
|
+
() => {
|
|
22936
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22937
|
+
if (dest === void 0) return;
|
|
22938
|
+
existed = true;
|
|
22939
|
+
this.db.prepare(
|
|
22940
|
+
`DELETE FROM egress_decision_override
|
|
22941
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22942
|
+
).run({ host: dest.host, destinationId });
|
|
22943
|
+
if (decision === null) return;
|
|
22944
|
+
this.db.prepare(
|
|
22945
|
+
`INSERT INTO egress_decision_override
|
|
22946
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22947
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22948
|
+
).run({
|
|
22949
|
+
id: randomUUID7(),
|
|
22950
|
+
destinationId,
|
|
22951
|
+
host: dest.host,
|
|
22952
|
+
decision,
|
|
22953
|
+
now: Date.now()
|
|
22954
|
+
});
|
|
22955
|
+
},
|
|
22956
|
+
"IMMEDIATE"
|
|
22957
|
+
);
|
|
22958
|
+
return existed;
|
|
22959
|
+
}
|
|
22960
|
+
/**
|
|
22961
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22962
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22963
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22964
|
+
* references, and drop what no longer has evidence.
|
|
22965
|
+
*
|
|
22966
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22967
|
+
* display payload and never scope a delete. The whole write is one
|
|
22968
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22969
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22970
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22971
|
+
* ledger commit so the next scan retries.
|
|
22972
|
+
*
|
|
22973
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22974
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22975
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22976
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22977
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22978
|
+
*/
|
|
22979
|
+
recordProjectEgress(input) {
|
|
22980
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22981
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22982
|
+
const now = Date.now();
|
|
22983
|
+
let summary = {
|
|
22984
|
+
destinations: 0,
|
|
22985
|
+
endpoints: 0,
|
|
22986
|
+
callSites: 0,
|
|
22987
|
+
truncated,
|
|
22988
|
+
droppedFiles
|
|
22989
|
+
};
|
|
22990
|
+
withTransaction(
|
|
22991
|
+
this.db,
|
|
22992
|
+
() => {
|
|
22993
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22994
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22995
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22996
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22997
|
+
this.pruneOrphans();
|
|
22998
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22999
|
+
},
|
|
23000
|
+
"IMMEDIATE"
|
|
23001
|
+
);
|
|
23002
|
+
return summary;
|
|
23003
|
+
}
|
|
23004
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
23005
|
+
/**
|
|
23006
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
23007
|
+
*
|
|
23008
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
23009
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
23010
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
23011
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
23012
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
23013
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
23014
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
23015
|
+
*/
|
|
23016
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
23017
|
+
if (reconcile.mode === "walk") {
|
|
23018
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
23019
|
+
this.db.prepare(
|
|
23020
|
+
`DELETE FROM share_call_site
|
|
23021
|
+
WHERE project_key = :key
|
|
23022
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
23023
|
+
AND file NOT LIKE '.%'
|
|
23024
|
+
AND file NOT LIKE '%/.%'`
|
|
23025
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
23026
|
+
return;
|
|
23027
|
+
}
|
|
23028
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
23029
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
23030
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
23031
|
+
this.db.prepare(
|
|
23032
|
+
`DELETE FROM share_call_site
|
|
23033
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
23034
|
+
).run(projectKey, ...chunk);
|
|
23035
|
+
}
|
|
23036
|
+
}
|
|
23037
|
+
/**
|
|
23038
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
23039
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
23040
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
23041
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
23042
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
23043
|
+
* classification for this batch.
|
|
23044
|
+
*/
|
|
23045
|
+
upsertHits(input, hits, projectId, now) {
|
|
23046
|
+
if (hits.length === 0) return;
|
|
23047
|
+
const destStmt = this.db.prepare(
|
|
23048
|
+
`INSERT INTO share_destination
|
|
23049
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
23050
|
+
created_at, updated_at)
|
|
23051
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
23052
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
23053
|
+
kind = excluded.kind,
|
|
23054
|
+
name = excluded.name,
|
|
23055
|
+
category = excluded.category,
|
|
23056
|
+
trust = excluded.trust,
|
|
23057
|
+
network_json = excluded.network_json,
|
|
23058
|
+
last_seen = excluded.last_seen,
|
|
23059
|
+
updated_at = excluded.updated_at`
|
|
23060
|
+
);
|
|
23061
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
23062
|
+
const endpointStmt = this.db.prepare(
|
|
23063
|
+
`INSERT INTO share_endpoint
|
|
23064
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
23065
|
+
created_at, updated_at)
|
|
23066
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
23067
|
+
:now, :now)
|
|
23068
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
23069
|
+
transport = excluded.transport,
|
|
23070
|
+
template = excluded.template,
|
|
23071
|
+
data_class = excluded.data_class,
|
|
23072
|
+
last_seen = excluded.last_seen,
|
|
23073
|
+
updated_at = excluded.updated_at`
|
|
23074
|
+
);
|
|
23075
|
+
const endpointIdStmt = this.db.prepare(
|
|
23076
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
23077
|
+
);
|
|
23078
|
+
const siteStmt = this.db.prepare(
|
|
23079
|
+
`INSERT INTO share_call_site
|
|
23080
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
23081
|
+
project_id, created_at, updated_at)
|
|
23082
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
23083
|
+
:vendored, :projectId, :now, :now)
|
|
23084
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
23085
|
+
snippet = excluded.snippet,
|
|
23086
|
+
dynamic = excluded.dynamic,
|
|
23087
|
+
vendored = excluded.vendored,
|
|
23088
|
+
project = excluded.project,
|
|
23089
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
23090
|
+
updated_at = excluded.updated_at`
|
|
23091
|
+
);
|
|
23092
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
23093
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
23094
|
+
for (const hit of hits) {
|
|
23095
|
+
let destinationId = destIds.get(hit.host);
|
|
23096
|
+
if (destinationId === void 0) {
|
|
23097
|
+
destStmt.run({
|
|
23098
|
+
id: randomUUID7(),
|
|
23099
|
+
kind: hit.kind,
|
|
23100
|
+
name: hit.name,
|
|
23101
|
+
host: hit.host,
|
|
23102
|
+
category: hit.category,
|
|
23103
|
+
trust: hit.trust,
|
|
23104
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
23105
|
+
now
|
|
23106
|
+
});
|
|
23107
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
23108
|
+
destIds.set(hit.host, destinationId);
|
|
23109
|
+
}
|
|
23110
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
23111
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
23112
|
+
if (endpointId === void 0) {
|
|
23113
|
+
endpointStmt.run({
|
|
23114
|
+
id: randomUUID7(),
|
|
23115
|
+
destinationId,
|
|
23116
|
+
method: hit.method,
|
|
23117
|
+
transport: hit.transport,
|
|
23118
|
+
url: hit.url,
|
|
23119
|
+
template: boolToInt(hit.template),
|
|
23120
|
+
dataClass: hit.dataClass,
|
|
23121
|
+
now
|
|
23122
|
+
});
|
|
23123
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23124
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23125
|
+
}
|
|
23126
|
+
siteStmt.run({
|
|
23127
|
+
id: randomUUID7(),
|
|
23128
|
+
endpointId,
|
|
23129
|
+
project: input.project,
|
|
23130
|
+
projectKey: input.projectKey,
|
|
23131
|
+
file: hit.site.file,
|
|
23132
|
+
line: hit.site.line,
|
|
23133
|
+
snippet: hit.site.snippet,
|
|
23134
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23135
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23136
|
+
projectId,
|
|
23137
|
+
now
|
|
23138
|
+
});
|
|
22358
23139
|
}
|
|
23140
|
+
}
|
|
23141
|
+
/**
|
|
23142
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23143
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23144
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23145
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23146
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23147
|
+
* representative.
|
|
23148
|
+
*/
|
|
23149
|
+
knownProjectId(projectKey) {
|
|
23150
|
+
return getRow(
|
|
23151
|
+
this.db.prepare(
|
|
23152
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23153
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23154
|
+
),
|
|
23155
|
+
[projectKey]
|
|
23156
|
+
)?.projectId ?? null;
|
|
23157
|
+
}
|
|
23158
|
+
/**
|
|
23159
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23160
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23161
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
23162
|
+
*/
|
|
23163
|
+
confirmLastSeen(projectKey, now) {
|
|
22359
23164
|
this.db.prepare(
|
|
22360
|
-
`
|
|
22361
|
-
|
|
22362
|
-
|
|
22363
|
-
|
|
22364
|
-
|
|
22365
|
-
|
|
22366
|
-
|
|
23165
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23166
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23167
|
+
).run({ now, key: projectKey });
|
|
23168
|
+
this.db.prepare(
|
|
23169
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23170
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23171
|
+
FROM share_endpoint e
|
|
23172
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23173
|
+
WHERE c.project_key = :key)`
|
|
23174
|
+
).run({ now, key: projectKey });
|
|
23175
|
+
}
|
|
23176
|
+
/**
|
|
23177
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23178
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23179
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23180
|
+
*
|
|
23181
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23182
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23183
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23184
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23185
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23186
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23187
|
+
*/
|
|
23188
|
+
pruneOrphans() {
|
|
23189
|
+
this.db.exec(
|
|
23190
|
+
`DELETE FROM share_endpoint
|
|
23191
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23192
|
+
);
|
|
23193
|
+
this.db.exec(
|
|
23194
|
+
`DELETE FROM egress_decision_override
|
|
23195
|
+
WHERE host IS NULL
|
|
23196
|
+
AND destination_id IN (
|
|
23197
|
+
SELECT d.id FROM share_destination d
|
|
23198
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23199
|
+
);
|
|
23200
|
+
this.db.exec(
|
|
23201
|
+
`DELETE FROM share_destination
|
|
23202
|
+
WHERE NOT EXISTS (
|
|
23203
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23204
|
+
);
|
|
23205
|
+
}
|
|
23206
|
+
/**
|
|
23207
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23208
|
+
* projects and carry no project column, so both are counted through the call
|
|
23209
|
+
* sites that reference them.
|
|
23210
|
+
*/
|
|
23211
|
+
projectTotals(projectKey) {
|
|
23212
|
+
return {
|
|
23213
|
+
destinations: countScalar(
|
|
23214
|
+
this.db,
|
|
23215
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23216
|
+
FROM share_endpoint e
|
|
23217
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23218
|
+
WHERE c.project_key = ?`,
|
|
23219
|
+
[projectKey]
|
|
23220
|
+
),
|
|
23221
|
+
endpoints: countScalar(
|
|
23222
|
+
this.db,
|
|
23223
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23224
|
+
[projectKey]
|
|
23225
|
+
),
|
|
23226
|
+
callSites: countScalar(
|
|
23227
|
+
this.db,
|
|
23228
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23229
|
+
[projectKey]
|
|
23230
|
+
)
|
|
23231
|
+
};
|
|
22367
23232
|
}
|
|
22368
23233
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22369
23234
|
mapDestRow(r) {
|
|
@@ -22383,7 +23248,8 @@ var SqliteSharesRepository = class {
|
|
|
22383
23248
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22384
23249
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22385
23250
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22386
|
-
d.created_at AS createdAt,
|
|
23251
|
+
d.created_at AS createdAt,
|
|
23252
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22387
23253
|
const conditions = [];
|
|
22388
23254
|
const params = [];
|
|
22389
23255
|
if (kinds && kinds.length > 0) {
|
|
@@ -22394,7 +23260,8 @@ var SqliteSharesRepository = class {
|
|
|
22394
23260
|
conditions.push(
|
|
22395
23261
|
`(d.trust IN ('unverified', 'ip')
|
|
22396
23262
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22397
|
-
WHERE re.destination_id = d.id
|
|
23263
|
+
WHERE re.destination_id = d.id
|
|
23264
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22398
23265
|
);
|
|
22399
23266
|
}
|
|
22400
23267
|
let sql;
|
|
@@ -22407,7 +23274,7 @@ var SqliteSharesRepository = class {
|
|
|
22407
23274
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22408
23275
|
sql = `SELECT DISTINCT ${cols}
|
|
22409
23276
|
FROM share_destination d
|
|
22410
|
-
|
|
23277
|
+
${OVERRIDE_JOIN}
|
|
22411
23278
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22412
23279
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22413
23280
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22415,7 +23282,7 @@ var SqliteSharesRepository = class {
|
|
|
22415
23282
|
} else {
|
|
22416
23283
|
sql = `SELECT ${cols}
|
|
22417
23284
|
FROM share_destination d
|
|
22418
|
-
|
|
23285
|
+
${OVERRIDE_JOIN}
|
|
22419
23286
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22420
23287
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22421
23288
|
}
|
|
@@ -22430,9 +23297,9 @@ var SqliteSharesRepository = class {
|
|
|
22430
23297
|
this.db.prepare(
|
|
22431
23298
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22432
23299
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22433
|
-
|
|
23300
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22434
23301
|
FROM share_destination d
|
|
22435
|
-
|
|
23302
|
+
${OVERRIDE_JOIN}
|
|
22436
23303
|
WHERE d.id = ?`
|
|
22437
23304
|
),
|
|
22438
23305
|
[destinationId]
|
|
@@ -22634,9 +23501,10 @@ function openWithPragmas(file2) {
|
|
|
22634
23501
|
}
|
|
22635
23502
|
function backupLegacyStore(file2) {
|
|
22636
23503
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
23504
|
+
renameSync2(file2, backup);
|
|
23505
|
+
tightenFile(backup);
|
|
23506
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23507
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22640
23508
|
}
|
|
22641
23509
|
return backup;
|
|
22642
23510
|
}
|
|
@@ -22652,7 +23520,7 @@ function openLocalDatabase(dir) {
|
|
|
22652
23520
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22653
23521
|
);
|
|
22654
23522
|
}
|
|
22655
|
-
applyMigrations(db);
|
|
23523
|
+
applyMigrations(db, file2);
|
|
22656
23524
|
tightenPerms(file2);
|
|
22657
23525
|
const events = new SqliteEventsRepository(db);
|
|
22658
23526
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22661,6 +23529,7 @@ function openLocalDatabase(dir) {
|
|
|
22661
23529
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22662
23530
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22663
23531
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23532
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22664
23533
|
const security = new SqliteSecurityRepository(db);
|
|
22665
23534
|
const detections = new SqliteDetectionsRepository(db);
|
|
22666
23535
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22678,9 +23547,56 @@ function openLocalDatabase(dir) {
|
|
|
22678
23547
|
policies.seedDefaults();
|
|
22679
23548
|
function recordCapture(event, detected) {
|
|
22680
23549
|
failOpenTransaction(db, () => {
|
|
22681
|
-
events.insertEvent(event);
|
|
22682
23550
|
const sessionId = event.metadata?.sessionId;
|
|
22683
|
-
|
|
23551
|
+
if (sessionId) {
|
|
23552
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23553
|
+
}
|
|
23554
|
+
const auditEventId = captureId(
|
|
23555
|
+
sessionId ?? null,
|
|
23556
|
+
event.contentHash,
|
|
23557
|
+
event.metadata?.filePath ?? null
|
|
23558
|
+
);
|
|
23559
|
+
auditEvents.insertAuditEvent({
|
|
23560
|
+
id: auditEventId,
|
|
23561
|
+
eventType: event.kind,
|
|
23562
|
+
startedAt: event.occurredAt,
|
|
23563
|
+
parentId: sessionId,
|
|
23564
|
+
rootSessionId: sessionId,
|
|
23565
|
+
content: event.content,
|
|
23566
|
+
contentHash: event.contentHash,
|
|
23567
|
+
attributes: toCaptureAttributes(event)
|
|
23568
|
+
});
|
|
23569
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23570
|
+
for (const finding2 of detected) {
|
|
23571
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding2.ruleId, finding2.maskedMatch, sessionId)) {
|
|
23572
|
+
continue;
|
|
23573
|
+
}
|
|
23574
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23575
|
+
auditEventId,
|
|
23576
|
+
finding2.ruleId,
|
|
23577
|
+
finding2.maskedMatch,
|
|
23578
|
+
finding2.span.start,
|
|
23579
|
+
finding2.span.end
|
|
23580
|
+
)) {
|
|
23581
|
+
continue;
|
|
23582
|
+
}
|
|
23583
|
+
const key = `${finding2.ruleId}@${captureDefinitionVersion(finding2)}`;
|
|
23584
|
+
let definitionId = definitionIds.get(key);
|
|
23585
|
+
if (!definitionId) {
|
|
23586
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding2));
|
|
23587
|
+
definitionIds.set(key, definitionId);
|
|
23588
|
+
}
|
|
23589
|
+
inspectionFindings.insertFinding({
|
|
23590
|
+
id: finding2.id,
|
|
23591
|
+
auditEventId,
|
|
23592
|
+
inspectionDefinitionId: definitionId,
|
|
23593
|
+
span: finding2.span,
|
|
23594
|
+
maskedMatch: finding2.maskedMatch,
|
|
23595
|
+
actionTaken: finding2.actionTaken,
|
|
23596
|
+
confidence: finding2.confidence,
|
|
23597
|
+
findingKey: finding2.findingKey ?? void 0
|
|
23598
|
+
});
|
|
23599
|
+
}
|
|
22684
23600
|
});
|
|
22685
23601
|
}
|
|
22686
23602
|
function ensureInventory(ctx) {
|
|
@@ -22798,6 +23714,7 @@ function openLocalDatabase(dir) {
|
|
|
22798
23714
|
scanLedger,
|
|
22799
23715
|
exceptions,
|
|
22800
23716
|
resolutions,
|
|
23717
|
+
ruleProbeCache,
|
|
22801
23718
|
security,
|
|
22802
23719
|
detections,
|
|
22803
23720
|
shares,
|
|
@@ -22827,9 +23744,12 @@ function openLocalDatabase(dir) {
|
|
|
22827
23744
|
};
|
|
22828
23745
|
}
|
|
22829
23746
|
|
|
23747
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23748
|
+
import { createHash as createHash3 } from "crypto";
|
|
23749
|
+
|
|
22830
23750
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22831
23751
|
import { createHmac, randomBytes } from "crypto";
|
|
22832
|
-
import {
|
|
23752
|
+
import { readFileSync } from "fs";
|
|
22833
23753
|
import { join as join2 } from "path";
|
|
22834
23754
|
var KEY_FILENAME = "exception.key";
|
|
22835
23755
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -22866,8 +23786,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
22866
23786
|
}
|
|
22867
23787
|
|
|
22868
23788
|
// ../../packages/persistence/src/local-layout.ts
|
|
22869
|
-
import {
|
|
22870
|
-
import {
|
|
23789
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23790
|
+
import { mkdir } from "fs/promises";
|
|
22871
23791
|
import { homedir } from "os";
|
|
22872
23792
|
import { join as join3 } from "path";
|
|
22873
23793
|
function defaultDataDir() {
|
|
@@ -22882,6 +23802,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22882
23802
|
function dbPath(base = defaultDataDir()) {
|
|
22883
23803
|
return join3(dataDir(base), "aka.db");
|
|
22884
23804
|
}
|
|
23805
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23806
|
+
ensureDataDirSync(dir);
|
|
23807
|
+
}
|
|
22885
23808
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22886
23809
|
const moves = [
|
|
22887
23810
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22889,19 +23812,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22889
23812
|
];
|
|
22890
23813
|
for (const { name, dest } of moves) {
|
|
22891
23814
|
try {
|
|
22892
|
-
|
|
22893
|
-
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
}
|
|
22897
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23815
|
+
ensureDataDirSync(dest);
|
|
23816
|
+
const moved = join3(dest, name);
|
|
23817
|
+
renameSync3(join3(base, name), moved);
|
|
23818
|
+
tightenFile(moved);
|
|
22898
23819
|
} catch {
|
|
22899
23820
|
}
|
|
22900
23821
|
}
|
|
22901
23822
|
}
|
|
22902
23823
|
|
|
22903
23824
|
// ../../packages/persistence/src/settings.ts
|
|
22904
|
-
import { readFileSync as readFileSync2
|
|
23825
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22905
23826
|
import { join as join4 } from "path";
|
|
22906
23827
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22907
23828
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22923,7 +23844,7 @@ function readJson(file2) {
|
|
|
22923
23844
|
}
|
|
22924
23845
|
|
|
22925
23846
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22926
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23847
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22927
23848
|
import { join as join5 } from "path";
|
|
22928
23849
|
var MARKER = "warn-era-capped";
|
|
22929
23850
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22931,7 +23852,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22931
23852
|
const marker = join5(dataDir2, MARKER);
|
|
22932
23853
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22933
23854
|
const capped = db.policies.capCategoryActions();
|
|
22934
|
-
|
|
23855
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22935
23856
|
`, { mode: DATA_FILE_MODE });
|
|
22936
23857
|
return { capped };
|
|
22937
23858
|
}
|
|
@@ -22986,6 +23907,12 @@ function resolveProvider() {
|
|
|
22986
23907
|
|
|
22987
23908
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22988
23909
|
function loadConfig(base = defaultDataDir()) {
|
|
23910
|
+
try {
|
|
23911
|
+
ensureLayoutDirSync(base);
|
|
23912
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23913
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23914
|
+
} catch {
|
|
23915
|
+
}
|
|
22989
23916
|
migrateLegacyLayout(base);
|
|
22990
23917
|
const settings = readWorkspaceSettings(base);
|
|
22991
23918
|
return {
|
|
@@ -23008,15 +23935,583 @@ function resolveProviderSafe() {
|
|
|
23008
23935
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23009
23936
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23010
23937
|
import { homedir as homedir2 } from "os";
|
|
23011
|
-
import { basename as basename3, join as
|
|
23938
|
+
import { basename as basename3, join as join8 } from "path";
|
|
23939
|
+
|
|
23940
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23941
|
+
var EXTRACTOR_VERSION = "1";
|
|
23942
|
+
var PROVIDER_REGISTRY = [
|
|
23943
|
+
{
|
|
23944
|
+
id: "stripe",
|
|
23945
|
+
name: "Stripe",
|
|
23946
|
+
category: "Payments",
|
|
23947
|
+
hostSuffixes: ["stripe.com"],
|
|
23948
|
+
apiBase: "https://api.stripe.com",
|
|
23949
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23950
|
+
sdks: {
|
|
23951
|
+
npm: ["stripe"],
|
|
23952
|
+
pypi: ["stripe"],
|
|
23953
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23954
|
+
maven: ["com.stripe"],
|
|
23955
|
+
rubygems: ["stripe"],
|
|
23956
|
+
composer: ["stripe/stripe-php"],
|
|
23957
|
+
nuget: ["Stripe.net"]
|
|
23958
|
+
}
|
|
23959
|
+
},
|
|
23960
|
+
{
|
|
23961
|
+
id: "datadog",
|
|
23962
|
+
name: "Datadog",
|
|
23963
|
+
category: "Observability",
|
|
23964
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23965
|
+
apiBase: "https://api.datadoghq.com",
|
|
23966
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23967
|
+
sdks: {
|
|
23968
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23969
|
+
pypi: ["datadog", "ddtrace"],
|
|
23970
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23971
|
+
maven: ["com.datadoghq"],
|
|
23972
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23973
|
+
nuget: ["Datadog.Trace"]
|
|
23974
|
+
}
|
|
23975
|
+
},
|
|
23976
|
+
{
|
|
23977
|
+
id: "newrelic",
|
|
23978
|
+
name: "New Relic",
|
|
23979
|
+
category: "Observability",
|
|
23980
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23981
|
+
apiBase: "https://api.newrelic.com",
|
|
23982
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23983
|
+
sdks: {
|
|
23984
|
+
npm: ["newrelic"],
|
|
23985
|
+
pypi: ["newrelic"],
|
|
23986
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23987
|
+
maven: ["com.newrelic.agent.java"],
|
|
23988
|
+
rubygems: ["newrelic_rpm"],
|
|
23989
|
+
nuget: ["NewRelic.Agent"]
|
|
23990
|
+
}
|
|
23991
|
+
},
|
|
23992
|
+
{
|
|
23993
|
+
id: "sentry",
|
|
23994
|
+
name: "Sentry",
|
|
23995
|
+
category: "Error tracking",
|
|
23996
|
+
hostSuffixes: ["sentry.io"],
|
|
23997
|
+
apiBase: "https://sentry.io",
|
|
23998
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23999
|
+
sdks: {
|
|
24000
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
24001
|
+
pypi: ["sentry-sdk"],
|
|
24002
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
24003
|
+
maven: ["io.sentry"],
|
|
24004
|
+
rubygems: ["sentry-ruby"],
|
|
24005
|
+
cargo: ["sentry"],
|
|
24006
|
+
composer: ["sentry/sentry"],
|
|
24007
|
+
nuget: ["Sentry"]
|
|
24008
|
+
}
|
|
24009
|
+
},
|
|
24010
|
+
{
|
|
24011
|
+
id: "openai",
|
|
24012
|
+
name: "OpenAI",
|
|
24013
|
+
category: "LLM provider",
|
|
24014
|
+
hostSuffixes: ["openai.com"],
|
|
24015
|
+
apiBase: "https://api.openai.com",
|
|
24016
|
+
defaultDataClasses: ["pii", "source"],
|
|
24017
|
+
sdks: {
|
|
24018
|
+
npm: ["openai"],
|
|
24019
|
+
pypi: ["openai"],
|
|
24020
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
24021
|
+
maven: ["com.openai"],
|
|
24022
|
+
rubygems: ["ruby-openai"],
|
|
24023
|
+
cargo: ["async-openai"],
|
|
24024
|
+
composer: ["openai-php/client"],
|
|
24025
|
+
nuget: ["OpenAI"]
|
|
24026
|
+
}
|
|
24027
|
+
},
|
|
24028
|
+
{
|
|
24029
|
+
id: "anthropic",
|
|
24030
|
+
name: "Anthropic",
|
|
24031
|
+
category: "LLM provider",
|
|
24032
|
+
hostSuffixes: ["anthropic.com"],
|
|
24033
|
+
apiBase: "https://api.anthropic.com",
|
|
24034
|
+
defaultDataClasses: ["pii", "source"],
|
|
24035
|
+
sdks: {
|
|
24036
|
+
npm: ["@anthropic-ai/sdk"],
|
|
24037
|
+
pypi: ["anthropic"],
|
|
24038
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
24039
|
+
nuget: ["Anthropic.SDK"]
|
|
24040
|
+
}
|
|
24041
|
+
},
|
|
24042
|
+
{
|
|
24043
|
+
id: "aws",
|
|
24044
|
+
name: "Amazon Web Services",
|
|
24045
|
+
category: "Cloud platform",
|
|
24046
|
+
hostSuffixes: ["amazonaws.com"],
|
|
24047
|
+
apiBase: "https://s3.amazonaws.com",
|
|
24048
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
24049
|
+
sdks: {
|
|
24050
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
24051
|
+
pypi: ["boto3"],
|
|
24052
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
24053
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
24054
|
+
rubygems: ["aws-sdk-s3"],
|
|
24055
|
+
cargo: ["aws-sdk-s3"],
|
|
24056
|
+
nuget: ["AWSSDK.S3"]
|
|
24057
|
+
}
|
|
24058
|
+
},
|
|
24059
|
+
{
|
|
24060
|
+
id: "gcp",
|
|
24061
|
+
name: "Google Cloud",
|
|
24062
|
+
category: "Cloud platform",
|
|
24063
|
+
hostSuffixes: ["googleapis.com"],
|
|
24064
|
+
apiBase: "https://storage.googleapis.com",
|
|
24065
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24066
|
+
sdks: {
|
|
24067
|
+
npm: ["@google-cloud/storage"],
|
|
24068
|
+
pypi: ["google-cloud-storage"],
|
|
24069
|
+
go: ["cloud.google.com/go"],
|
|
24070
|
+
maven: ["com.google.cloud"],
|
|
24071
|
+
rubygems: ["google-cloud-storage"],
|
|
24072
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
24073
|
+
}
|
|
24074
|
+
},
|
|
24075
|
+
{
|
|
24076
|
+
id: "azure",
|
|
24077
|
+
name: "Microsoft Azure",
|
|
24078
|
+
category: "Cloud platform",
|
|
24079
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
24080
|
+
apiBase: "https://management.azure.com",
|
|
24081
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24082
|
+
sdks: {
|
|
24083
|
+
npm: ["@azure/storage-blob"],
|
|
24084
|
+
pypi: ["azure-storage-blob"],
|
|
24085
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
24086
|
+
maven: ["com.azure"],
|
|
24087
|
+
rubygems: ["azure-storage-blob"],
|
|
24088
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
24089
|
+
}
|
|
24090
|
+
},
|
|
24091
|
+
{
|
|
24092
|
+
id: "slack",
|
|
24093
|
+
name: "Slack",
|
|
24094
|
+
category: "Notifications",
|
|
24095
|
+
hostSuffixes: ["slack.com"],
|
|
24096
|
+
apiBase: "https://slack.com/api",
|
|
24097
|
+
defaultDataClasses: ["logs"],
|
|
24098
|
+
sdks: {
|
|
24099
|
+
npm: ["@slack/web-api"],
|
|
24100
|
+
pypi: ["slack-sdk"],
|
|
24101
|
+
go: ["github.com/slack-go/slack"],
|
|
24102
|
+
maven: ["com.slack.api"],
|
|
24103
|
+
rubygems: ["slack-ruby-client"],
|
|
24104
|
+
composer: ["slack-php/slack-api"],
|
|
24105
|
+
nuget: ["SlackNet"]
|
|
24106
|
+
}
|
|
24107
|
+
},
|
|
24108
|
+
{
|
|
24109
|
+
id: "segment",
|
|
24110
|
+
name: "Segment",
|
|
24111
|
+
category: "Analytics",
|
|
24112
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
24113
|
+
apiBase: "https://api.segment.io",
|
|
24114
|
+
defaultDataClasses: ["customer"],
|
|
24115
|
+
sdks: {
|
|
24116
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
24117
|
+
pypi: ["segment-analytics-python"],
|
|
24118
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
24119
|
+
maven: ["com.segment.analytics.java"],
|
|
24120
|
+
rubygems: ["analytics-ruby"],
|
|
24121
|
+
nuget: ["Analytics"]
|
|
24122
|
+
}
|
|
24123
|
+
},
|
|
24124
|
+
{
|
|
24125
|
+
id: "twilio",
|
|
24126
|
+
name: "Twilio",
|
|
24127
|
+
category: "Communications",
|
|
24128
|
+
hostSuffixes: ["twilio.com"],
|
|
24129
|
+
apiBase: "https://api.twilio.com",
|
|
24130
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24131
|
+
sdks: {
|
|
24132
|
+
npm: ["twilio"],
|
|
24133
|
+
pypi: ["twilio"],
|
|
24134
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24135
|
+
maven: ["com.twilio.sdk"],
|
|
24136
|
+
rubygems: ["twilio-ruby"],
|
|
24137
|
+
composer: ["twilio/sdk"],
|
|
24138
|
+
nuget: ["Twilio"]
|
|
24139
|
+
}
|
|
24140
|
+
},
|
|
24141
|
+
{
|
|
24142
|
+
id: "sendgrid",
|
|
24143
|
+
name: "SendGrid",
|
|
24144
|
+
category: "Email",
|
|
24145
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24146
|
+
apiBase: "https://api.sendgrid.com",
|
|
24147
|
+
defaultDataClasses: ["pii"],
|
|
24148
|
+
sdks: {
|
|
24149
|
+
npm: ["@sendgrid/mail"],
|
|
24150
|
+
pypi: ["sendgrid"],
|
|
24151
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24152
|
+
maven: ["com.sendgrid"],
|
|
24153
|
+
rubygems: ["sendgrid-ruby"],
|
|
24154
|
+
composer: ["sendgrid/sendgrid"],
|
|
24155
|
+
nuget: ["SendGrid"]
|
|
24156
|
+
}
|
|
24157
|
+
},
|
|
24158
|
+
{
|
|
24159
|
+
id: "mailgun",
|
|
24160
|
+
name: "Mailgun",
|
|
24161
|
+
category: "Email",
|
|
24162
|
+
hostSuffixes: ["mailgun.net"],
|
|
24163
|
+
apiBase: "https://api.mailgun.net",
|
|
24164
|
+
defaultDataClasses: ["pii"],
|
|
24165
|
+
sdks: {
|
|
24166
|
+
npm: ["mailgun.js"],
|
|
24167
|
+
pypi: ["mailgun"],
|
|
24168
|
+
rubygems: ["mailgun-ruby"],
|
|
24169
|
+
composer: ["mailgun/mailgun-php"],
|
|
24170
|
+
nuget: ["Mailgun"]
|
|
24171
|
+
}
|
|
24172
|
+
},
|
|
24173
|
+
{
|
|
24174
|
+
id: "mixpanel",
|
|
24175
|
+
name: "Mixpanel",
|
|
24176
|
+
category: "Analytics",
|
|
24177
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24178
|
+
apiBase: "https://api.mixpanel.com",
|
|
24179
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24180
|
+
sdks: {
|
|
24181
|
+
npm: ["mixpanel"],
|
|
24182
|
+
pypi: ["mixpanel"],
|
|
24183
|
+
rubygems: ["mixpanel-ruby"],
|
|
24184
|
+
nuget: ["Mixpanel"]
|
|
24185
|
+
}
|
|
24186
|
+
},
|
|
24187
|
+
{
|
|
24188
|
+
id: "amplitude",
|
|
24189
|
+
name: "Amplitude",
|
|
24190
|
+
category: "Analytics",
|
|
24191
|
+
hostSuffixes: ["amplitude.com"],
|
|
24192
|
+
apiBase: "https://api2.amplitude.com",
|
|
24193
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24194
|
+
sdks: {
|
|
24195
|
+
npm: ["@amplitude/analytics-node"],
|
|
24196
|
+
pypi: ["amplitude-analytics"],
|
|
24197
|
+
nuget: ["Amplitude"]
|
|
24198
|
+
}
|
|
24199
|
+
},
|
|
24200
|
+
{
|
|
24201
|
+
id: "posthog",
|
|
24202
|
+
name: "PostHog",
|
|
24203
|
+
category: "Analytics",
|
|
24204
|
+
hostSuffixes: ["posthog.com"],
|
|
24205
|
+
apiBase: "https://us.i.posthog.com",
|
|
24206
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24207
|
+
sdks: {
|
|
24208
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24209
|
+
pypi: ["posthog"],
|
|
24210
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24211
|
+
rubygems: ["posthog-ruby"],
|
|
24212
|
+
composer: ["posthog/posthog-php"],
|
|
24213
|
+
nuget: ["PostHog"]
|
|
24214
|
+
}
|
|
24215
|
+
},
|
|
24216
|
+
{
|
|
24217
|
+
id: "honeycomb",
|
|
24218
|
+
name: "Honeycomb",
|
|
24219
|
+
category: "Observability",
|
|
24220
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24221
|
+
apiBase: "https://api.honeycomb.io",
|
|
24222
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24223
|
+
sdks: {
|
|
24224
|
+
npm: ["libhoney"],
|
|
24225
|
+
pypi: ["libhoney"],
|
|
24226
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24227
|
+
rubygems: ["libhoney"]
|
|
24228
|
+
}
|
|
24229
|
+
},
|
|
24230
|
+
{
|
|
24231
|
+
id: "grafana",
|
|
24232
|
+
name: "Grafana Cloud",
|
|
24233
|
+
category: "Observability",
|
|
24234
|
+
hostSuffixes: ["grafana.net"],
|
|
24235
|
+
apiBase: "https://grafana.net",
|
|
24236
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24237
|
+
sdks: {
|
|
24238
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24239
|
+
}
|
|
24240
|
+
},
|
|
24241
|
+
{
|
|
24242
|
+
id: "splunk",
|
|
24243
|
+
name: "Splunk",
|
|
24244
|
+
category: "Observability",
|
|
24245
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24246
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24247
|
+
defaultDataClasses: ["logs"],
|
|
24248
|
+
sdks: {
|
|
24249
|
+
npm: ["splunk-logging"],
|
|
24250
|
+
pypi: ["splunk-sdk"],
|
|
24251
|
+
maven: ["com.splunk"],
|
|
24252
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24253
|
+
}
|
|
24254
|
+
},
|
|
24255
|
+
{
|
|
24256
|
+
id: "pagerduty",
|
|
24257
|
+
name: "PagerDuty",
|
|
24258
|
+
category: "Incident response",
|
|
24259
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24260
|
+
apiBase: "https://api.pagerduty.com",
|
|
24261
|
+
defaultDataClasses: ["logs"],
|
|
24262
|
+
sdks: {
|
|
24263
|
+
npm: ["@pagerduty/pdjs"],
|
|
24264
|
+
pypi: ["pdpyras"],
|
|
24265
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24266
|
+
rubygems: ["pagerduty"]
|
|
24267
|
+
}
|
|
24268
|
+
},
|
|
24269
|
+
{
|
|
24270
|
+
id: "github",
|
|
24271
|
+
name: "GitHub",
|
|
24272
|
+
category: "Developer platform",
|
|
24273
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24274
|
+
apiBase: "https://api.github.com",
|
|
24275
|
+
defaultDataClasses: ["source"],
|
|
24276
|
+
sdks: {
|
|
24277
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24278
|
+
pypi: ["pygithub"],
|
|
24279
|
+
go: ["github.com/google/go-github"],
|
|
24280
|
+
maven: ["org.kohsuke.github-api"],
|
|
24281
|
+
rubygems: ["octokit"],
|
|
24282
|
+
cargo: ["octocrab"],
|
|
24283
|
+
composer: ["knplabs/github-api"],
|
|
24284
|
+
nuget: ["Octokit"]
|
|
24285
|
+
}
|
|
24286
|
+
},
|
|
24287
|
+
{
|
|
24288
|
+
id: "gitlab",
|
|
24289
|
+
name: "GitLab",
|
|
24290
|
+
category: "Developer platform",
|
|
24291
|
+
hostSuffixes: ["gitlab.com"],
|
|
24292
|
+
apiBase: "https://gitlab.com/api",
|
|
24293
|
+
defaultDataClasses: ["source"],
|
|
24294
|
+
sdks: {
|
|
24295
|
+
npm: ["@gitbeaker/rest"],
|
|
24296
|
+
pypi: ["python-gitlab"],
|
|
24297
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24298
|
+
rubygems: ["gitlab"],
|
|
24299
|
+
nuget: ["GitLabApiClient"]
|
|
24300
|
+
}
|
|
24301
|
+
},
|
|
24302
|
+
{
|
|
24303
|
+
id: "auth0",
|
|
24304
|
+
name: "Auth0",
|
|
24305
|
+
category: "Identity",
|
|
24306
|
+
hostSuffixes: ["auth0.com"],
|
|
24307
|
+
apiBase: "https://login.auth0.com",
|
|
24308
|
+
defaultDataClasses: ["pii"],
|
|
24309
|
+
sdks: {
|
|
24310
|
+
npm: ["auth0"],
|
|
24311
|
+
pypi: ["auth0-python"],
|
|
24312
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24313
|
+
maven: ["com.auth0"],
|
|
24314
|
+
rubygems: ["auth0"],
|
|
24315
|
+
composer: ["auth0/auth0-php"],
|
|
24316
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24317
|
+
}
|
|
24318
|
+
},
|
|
24319
|
+
{
|
|
24320
|
+
id: "okta",
|
|
24321
|
+
name: "Okta",
|
|
24322
|
+
category: "Identity",
|
|
24323
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24324
|
+
apiBase: "https://login.okta.com",
|
|
24325
|
+
defaultDataClasses: ["pii"],
|
|
24326
|
+
sdks: {
|
|
24327
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24328
|
+
pypi: ["okta"],
|
|
24329
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24330
|
+
maven: ["com.okta.sdk"],
|
|
24331
|
+
nuget: ["Okta.Sdk"]
|
|
24332
|
+
}
|
|
24333
|
+
},
|
|
24334
|
+
{
|
|
24335
|
+
id: "clerk",
|
|
24336
|
+
name: "Clerk",
|
|
24337
|
+
category: "Identity",
|
|
24338
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24339
|
+
apiBase: "https://api.clerk.com",
|
|
24340
|
+
defaultDataClasses: ["pii"],
|
|
24341
|
+
sdks: {
|
|
24342
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24343
|
+
pypi: ["clerk-backend-api"],
|
|
24344
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24345
|
+
}
|
|
24346
|
+
},
|
|
24347
|
+
{
|
|
24348
|
+
id: "supabase",
|
|
24349
|
+
name: "Supabase",
|
|
24350
|
+
category: "Backend platform",
|
|
24351
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24352
|
+
apiBase: "https://api.supabase.com",
|
|
24353
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24354
|
+
sdks: {
|
|
24355
|
+
npm: ["@supabase/supabase-js"],
|
|
24356
|
+
pypi: ["supabase"],
|
|
24357
|
+
cargo: ["postgrest"]
|
|
24358
|
+
}
|
|
24359
|
+
},
|
|
24360
|
+
{
|
|
24361
|
+
id: "firebase",
|
|
24362
|
+
name: "Firebase",
|
|
24363
|
+
category: "Backend platform",
|
|
24364
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24365
|
+
apiBase: "https://firebaseio.com",
|
|
24366
|
+
defaultDataClasses: ["customer"],
|
|
24367
|
+
sdks: {
|
|
24368
|
+
npm: ["firebase", "firebase-admin"],
|
|
24369
|
+
pypi: ["firebase-admin"],
|
|
24370
|
+
go: ["firebase.google.com/go"],
|
|
24371
|
+
maven: ["com.google.firebase"]
|
|
24372
|
+
}
|
|
24373
|
+
},
|
|
24374
|
+
{
|
|
24375
|
+
id: "mongodb-atlas",
|
|
24376
|
+
name: "MongoDB Atlas",
|
|
24377
|
+
category: "Database SaaS",
|
|
24378
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24379
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24380
|
+
defaultDataClasses: ["customer"],
|
|
24381
|
+
sdks: {
|
|
24382
|
+
npm: ["mongodb"],
|
|
24383
|
+
pypi: ["pymongo"],
|
|
24384
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24385
|
+
maven: ["org.mongodb"],
|
|
24386
|
+
rubygems: ["mongo"],
|
|
24387
|
+
cargo: ["mongodb"],
|
|
24388
|
+
nuget: ["MongoDB.Driver"]
|
|
24389
|
+
}
|
|
24390
|
+
},
|
|
24391
|
+
{
|
|
24392
|
+
id: "planetscale",
|
|
24393
|
+
name: "PlanetScale",
|
|
24394
|
+
category: "Database SaaS",
|
|
24395
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24396
|
+
apiBase: "https://api.planetscale.com",
|
|
24397
|
+
defaultDataClasses: ["customer"],
|
|
24398
|
+
sdks: {
|
|
24399
|
+
npm: ["@planetscale/database"],
|
|
24400
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24401
|
+
}
|
|
24402
|
+
},
|
|
24403
|
+
{
|
|
24404
|
+
id: "algolia",
|
|
24405
|
+
name: "Algolia",
|
|
24406
|
+
category: "Search SaaS",
|
|
24407
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24408
|
+
apiBase: "https://algolia.net",
|
|
24409
|
+
defaultDataClasses: ["customer"],
|
|
24410
|
+
sdks: {
|
|
24411
|
+
npm: ["algoliasearch"],
|
|
24412
|
+
pypi: ["algoliasearch"],
|
|
24413
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24414
|
+
maven: ["com.algolia"],
|
|
24415
|
+
rubygems: ["algolia"],
|
|
24416
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24417
|
+
nuget: ["Algolia.Search"]
|
|
24418
|
+
}
|
|
24419
|
+
},
|
|
24420
|
+
{
|
|
24421
|
+
id: "cloudflare",
|
|
24422
|
+
name: "Cloudflare",
|
|
24423
|
+
category: "CDN / edge",
|
|
24424
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24425
|
+
apiBase: "https://api.cloudflare.com",
|
|
24426
|
+
defaultDataClasses: ["logs"],
|
|
24427
|
+
sdks: {
|
|
24428
|
+
npm: ["cloudflare"],
|
|
24429
|
+
pypi: ["cloudflare"],
|
|
24430
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24431
|
+
nuget: ["CloudFlare.Client"]
|
|
24432
|
+
}
|
|
24433
|
+
},
|
|
24434
|
+
{
|
|
24435
|
+
id: "huggingface",
|
|
24436
|
+
name: "Hugging Face",
|
|
24437
|
+
category: "LLM provider",
|
|
24438
|
+
hostSuffixes: ["huggingface.co"],
|
|
24439
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24440
|
+
defaultDataClasses: ["source"],
|
|
24441
|
+
sdks: {
|
|
24442
|
+
npm: ["@huggingface/inference"],
|
|
24443
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24444
|
+
rubygems: ["hugging-face"]
|
|
24445
|
+
}
|
|
24446
|
+
},
|
|
24447
|
+
{
|
|
24448
|
+
id: "cohere",
|
|
24449
|
+
name: "Cohere",
|
|
24450
|
+
category: "LLM provider",
|
|
24451
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24452
|
+
apiBase: "https://api.cohere.com",
|
|
24453
|
+
defaultDataClasses: ["pii", "source"],
|
|
24454
|
+
sdks: {
|
|
24455
|
+
npm: ["cohere-ai"],
|
|
24456
|
+
pypi: ["cohere"],
|
|
24457
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24458
|
+
}
|
|
24459
|
+
},
|
|
24460
|
+
{
|
|
24461
|
+
id: "mistral",
|
|
24462
|
+
name: "Mistral AI",
|
|
24463
|
+
category: "LLM provider",
|
|
24464
|
+
hostSuffixes: ["mistral.ai"],
|
|
24465
|
+
apiBase: "https://api.mistral.ai",
|
|
24466
|
+
defaultDataClasses: ["pii", "source"],
|
|
24467
|
+
sdks: {
|
|
24468
|
+
npm: ["@mistralai/mistralai"],
|
|
24469
|
+
pypi: ["mistralai"],
|
|
24470
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24471
|
+
}
|
|
24472
|
+
}
|
|
24473
|
+
];
|
|
24474
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24475
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24476
|
+
|
|
24477
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24478
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24479
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24480
|
+
var SECRET_VALUE = new RegExp(
|
|
24481
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24482
|
+
"gi"
|
|
24483
|
+
);
|
|
24484
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24485
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24486
|
+
"gi"
|
|
24487
|
+
);
|
|
24488
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24489
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24490
|
+
{
|
|
24491
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24492
|
+
prefix: "/api/webhooks/"
|
|
24493
|
+
},
|
|
24494
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24495
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24496
|
+
];
|
|
24497
|
+
function escapeRegExp(literal2) {
|
|
24498
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24499
|
+
}
|
|
24500
|
+
var WEBHOOK_URL = new RegExp(
|
|
24501
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24502
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24503
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24504
|
+
"gi"
|
|
24505
|
+
);
|
|
23012
24506
|
|
|
23013
24507
|
// ../../packages/detections/src/escape-regexp.ts
|
|
23014
|
-
function
|
|
24508
|
+
function escapeRegExp2(value) {
|
|
23015
24509
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23016
24510
|
}
|
|
23017
24511
|
|
|
23018
24512
|
// ../../packages/detections/src/matchers/limits.ts
|
|
23019
24513
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24514
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
23020
24515
|
|
|
23021
24516
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
23022
24517
|
var KeywordMatcher2 = class {
|
|
@@ -23027,7 +24522,7 @@ var KeywordMatcher2 = class {
|
|
|
23027
24522
|
for (const kw of keywords) {
|
|
23028
24523
|
if (kw.length === 0) continue;
|
|
23029
24524
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
23030
|
-
const re = new RegExp(
|
|
24525
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
23031
24526
|
let m;
|
|
23032
24527
|
while ((m = re.exec(text)) !== null) {
|
|
23033
24528
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -23044,9 +24539,13 @@ var RegexMatcher2 = class {
|
|
|
23044
24539
|
if (rule.matcher.type !== "regex") return [];
|
|
23045
24540
|
const { pattern, flags, captureGroup } = rule.matcher;
|
|
23046
24541
|
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24542
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
23047
24543
|
const spans = [];
|
|
23048
24544
|
let m;
|
|
23049
|
-
|
|
24545
|
+
const maxIterations = scanText2.length + 1;
|
|
24546
|
+
let iterations = 0;
|
|
24547
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24548
|
+
if (++iterations > maxIterations) break;
|
|
23050
24549
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
23051
24550
|
if (m[0].length === 0) re.lastIndex++;
|
|
23052
24551
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -23150,7 +24649,7 @@ function isCorroborated(candidate, candidates, text) {
|
|
|
23150
24649
|
for (const label of labels) {
|
|
23151
24650
|
const trimmed = label.trim();
|
|
23152
24651
|
if (trimmed.length === 0) continue;
|
|
23153
|
-
const re = new RegExp(`(?<![A-Za-z0-9])${
|
|
24652
|
+
const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
|
|
23154
24653
|
if (re.test(haystack)) return true;
|
|
23155
24654
|
}
|
|
23156
24655
|
}
|
|
@@ -23397,6 +24896,31 @@ function whole(command) {
|
|
|
23397
24896
|
return { start: 0, end: command.length };
|
|
23398
24897
|
}
|
|
23399
24898
|
|
|
24899
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24900
|
+
var EXPONENTIAL_UNITS = [
|
|
24901
|
+
"a",
|
|
24902
|
+
"0",
|
|
24903
|
+
" ",
|
|
24904
|
+
"x",
|
|
24905
|
+
"ab",
|
|
24906
|
+
"a.",
|
|
24907
|
+
"a-",
|
|
24908
|
+
"a_",
|
|
24909
|
+
"a@",
|
|
24910
|
+
"a/",
|
|
24911
|
+
"a:",
|
|
24912
|
+
"a=",
|
|
24913
|
+
"a;",
|
|
24914
|
+
"aA0",
|
|
24915
|
+
" "
|
|
24916
|
+
];
|
|
24917
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24918
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24919
|
+
);
|
|
24920
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24921
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24922
|
+
);
|
|
24923
|
+
|
|
23400
24924
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23401
24925
|
var auth_jwt_no_verify_default = {
|
|
23402
24926
|
specVersion: 1,
|
|
@@ -25444,7 +26968,7 @@ function ensureBundledPacks() {
|
|
|
25444
26968
|
return false;
|
|
25445
26969
|
}
|
|
25446
26970
|
}
|
|
25447
|
-
function scanText(text) {
|
|
26971
|
+
function scanText(text, ruleVersions) {
|
|
25448
26972
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
25449
26973
|
try {
|
|
25450
26974
|
const rules = getLoadedRules();
|
|
@@ -25456,7 +26980,7 @@ function scanText(text) {
|
|
|
25456
26980
|
return {
|
|
25457
26981
|
ruleId: m.ruleId,
|
|
25458
26982
|
ruleName: rule?.name ?? m.ruleId,
|
|
25459
|
-
ruleVersion: String(rule?.specVersion ?? 1),
|
|
26983
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
25460
26984
|
category: m.category,
|
|
25461
26985
|
severity: m.severity,
|
|
25462
26986
|
span: m.span,
|
|
@@ -25474,8 +26998,8 @@ function maskText(text) {
|
|
|
25474
26998
|
}
|
|
25475
26999
|
|
|
25476
27000
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25477
|
-
import { existsSync as
|
|
25478
|
-
import { basename as basename2, dirname, isAbsolute, join as
|
|
27001
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
27002
|
+
import { basename as basename2, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
25479
27003
|
function resolveRepoIdentity(cwd) {
|
|
25480
27004
|
try {
|
|
25481
27005
|
const root = findGitRoot(cwd);
|
|
@@ -25525,7 +27049,7 @@ function resolveGitBranch(cwd) {
|
|
|
25525
27049
|
try {
|
|
25526
27050
|
const root = findGitRoot(cwd);
|
|
25527
27051
|
if (!root) return void 0;
|
|
25528
|
-
const dotGit =
|
|
27052
|
+
const dotGit = join7(root, ".git");
|
|
25529
27053
|
let gitdir;
|
|
25530
27054
|
try {
|
|
25531
27055
|
gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
|
|
@@ -25533,7 +27057,7 @@ function resolveGitBranch(cwd) {
|
|
|
25533
27057
|
return void 0;
|
|
25534
27058
|
}
|
|
25535
27059
|
if (gitdir === void 0) return void 0;
|
|
25536
|
-
const head = safeRead(
|
|
27060
|
+
const head = safeRead(join7(gitdir, "HEAD"));
|
|
25537
27061
|
if (!head) return void 0;
|
|
25538
27062
|
return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
|
|
25539
27063
|
} catch {
|
|
@@ -25543,37 +27067,37 @@ function resolveGitBranch(cwd) {
|
|
|
25543
27067
|
function resolveWorktreeGitdir(root, dotGitFile) {
|
|
25544
27068
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
|
|
25545
27069
|
if (!target) return void 0;
|
|
25546
|
-
return isAbsolute(target) ? target :
|
|
27070
|
+
return isAbsolute(target) ? target : join7(root, target);
|
|
25547
27071
|
}
|
|
25548
27072
|
function findGitRoot(start) {
|
|
25549
27073
|
let dir = start;
|
|
25550
27074
|
for (; ; ) {
|
|
25551
|
-
if (
|
|
27075
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
25552
27076
|
const parent = dirname(dir);
|
|
25553
27077
|
if (parent === dir) return void 0;
|
|
25554
27078
|
dir = parent;
|
|
25555
27079
|
}
|
|
25556
27080
|
}
|
|
25557
27081
|
function resolveGitContext(root) {
|
|
25558
|
-
const dotGit =
|
|
27082
|
+
const dotGit = join7(root, ".git");
|
|
25559
27083
|
try {
|
|
25560
27084
|
if (statSync(dotGit).isDirectory()) {
|
|
25561
|
-
return { configPath:
|
|
27085
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
25562
27086
|
}
|
|
25563
27087
|
} catch {
|
|
25564
27088
|
return void 0;
|
|
25565
27089
|
}
|
|
25566
27090
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
25567
27091
|
if (!target) return void 0;
|
|
25568
|
-
const gitdir = isAbsolute(target) ? target :
|
|
25569
|
-
if (
|
|
25570
|
-
return { configPath:
|
|
27092
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
27093
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
27094
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
25571
27095
|
}
|
|
25572
|
-
const commonRaw = safeRead(
|
|
27096
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
25573
27097
|
if (!commonRaw) return void 0;
|
|
25574
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
27098
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
25575
27099
|
const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
25576
|
-
return { configPath:
|
|
27100
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
25577
27101
|
}
|
|
25578
27102
|
function safeRead(path) {
|
|
25579
27103
|
try {
|
|
@@ -25639,31 +27163,31 @@ function resolveConfigInventory(input) {
|
|
|
25639
27163
|
};
|
|
25640
27164
|
try {
|
|
25641
27165
|
const home = input.homeDir ?? homedir2();
|
|
25642
|
-
const claudeDir =
|
|
27166
|
+
const claudeDir = join8(home, ".claude");
|
|
25643
27167
|
const repo = resolveRepoIdentity(input.cwd);
|
|
25644
27168
|
const repoIdentity = repo?.url ?? input.cwd;
|
|
25645
27169
|
const projectSource = `project:${repoIdentity}`;
|
|
25646
|
-
collectSettingsHooks(scan2,
|
|
25647
|
-
collectSettingsHooks(scan2,
|
|
25648
|
-
collectSettingsHooks(scan2,
|
|
27170
|
+
collectSettingsHooks(scan2, join8(claudeDir, "settings.json"), "user");
|
|
27171
|
+
collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.json"), "project");
|
|
27172
|
+
collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.local.json"), "local");
|
|
25649
27173
|
const projectOrigin = { scope: "project", project: repoIdentity };
|
|
25650
|
-
collectMcpFile(scan2,
|
|
25651
|
-
collectUserClaudeJson(scan2,
|
|
25652
|
-
collectMcpFile(scan2,
|
|
25653
|
-
collectMcpFile(scan2,
|
|
25654
|
-
collectMcpFile(scan2,
|
|
27174
|
+
collectMcpFile(scan2, join8(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
|
|
27175
|
+
collectUserClaudeJson(scan2, join8(home, ".claude.json"), input.cwd, repoIdentity);
|
|
27176
|
+
collectMcpFile(scan2, join8(claudeDir, "settings.json"), { scope: "user" });
|
|
27177
|
+
collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.json"), projectOrigin);
|
|
27178
|
+
collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.local.json"), {
|
|
25655
27179
|
scope: "local",
|
|
25656
27180
|
project: repoIdentity
|
|
25657
27181
|
});
|
|
25658
27182
|
collectConfigFiles(scan2, claudeDir, input.cwd);
|
|
25659
|
-
collectSkillsDir(scan2,
|
|
25660
|
-
collectSkillsDir(scan2,
|
|
27183
|
+
collectSkillsDir(scan2, join8(claudeDir, "skills"), { source: "local", scope: "user" });
|
|
27184
|
+
collectSkillsDir(scan2, join8(input.cwd, ".claude", "skills"), {
|
|
25661
27185
|
source: projectSource,
|
|
25662
27186
|
scope: "project"
|
|
25663
27187
|
});
|
|
25664
27188
|
collectInstalledPlugins(scan2, claudeDir);
|
|
25665
27189
|
collectMarketplaceSkills(scan2, claudeDir);
|
|
25666
|
-
collectSkillsDir(scan2,
|
|
27190
|
+
collectSkillsDir(scan2, join8(input.cwd, "skills"), { source: projectSource, scope: "project" });
|
|
25667
27191
|
scan2.skills = dedupeSkills(scan2.skills);
|
|
25668
27192
|
scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
|
|
25669
27193
|
} catch (err) {
|
|
@@ -25792,7 +27316,7 @@ function projectEntryFor(projects, cwd) {
|
|
|
25792
27316
|
return void 0;
|
|
25793
27317
|
}
|
|
25794
27318
|
function collectPluginManifestMcp(scan2, installPath, origin) {
|
|
25795
|
-
const manifestPath =
|
|
27319
|
+
const manifestPath = join8(installPath, ".claude-plugin", "plugin.json");
|
|
25796
27320
|
const raw = readOptional(manifestPath);
|
|
25797
27321
|
if (raw === void 0) return;
|
|
25798
27322
|
try {
|
|
@@ -25800,7 +27324,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
|
|
|
25800
27324
|
if (typeof parsed !== "object" || parsed === null) return;
|
|
25801
27325
|
const declared = parsed.mcpServers;
|
|
25802
27326
|
if (typeof declared === "string" && declared.length > 0) {
|
|
25803
|
-
collectMcpFile(scan2,
|
|
27327
|
+
collectMcpFile(scan2, join8(installPath, declared), origin, { recordErrors: true });
|
|
25804
27328
|
} else {
|
|
25805
27329
|
collectMcpObject(scan2, declared, manifestPath, origin);
|
|
25806
27330
|
}
|
|
@@ -25817,14 +27341,14 @@ var SETTINGS_KEY_LABELS = [
|
|
|
25817
27341
|
["statusLine", "status line"]
|
|
25818
27342
|
];
|
|
25819
27343
|
function collectConfigFiles(scan2, claudeDir, cwd) {
|
|
25820
|
-
settingsConfigFile(scan2,
|
|
25821
|
-
settingsConfigFile(scan2,
|
|
25822
|
-
settingsConfigFile(scan2,
|
|
25823
|
-
memoryConfigFile(scan2,
|
|
25824
|
-
memoryConfigFile(scan2,
|
|
25825
|
-
mcpJsonConfigFile(scan2,
|
|
25826
|
-
dirConfigFile(scan2,
|
|
25827
|
-
dirConfigFile(scan2,
|
|
27344
|
+
settingsConfigFile(scan2, join8(claudeDir, "settings.json"), "user", "User settings");
|
|
27345
|
+
settingsConfigFile(scan2, join8(cwd, ".claude", "settings.json"), "project", "Project settings");
|
|
27346
|
+
settingsConfigFile(scan2, join8(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
|
|
27347
|
+
memoryConfigFile(scan2, join8(claudeDir, "CLAUDE.md"), "user", "User memory");
|
|
27348
|
+
memoryConfigFile(scan2, join8(cwd, "CLAUDE.md"), "project", "Project memory");
|
|
27349
|
+
mcpJsonConfigFile(scan2, join8(cwd, ".mcp.json"));
|
|
27350
|
+
dirConfigFile(scan2, join8(cwd, ".claude", "commands"), "Slash commands", "command");
|
|
27351
|
+
dirConfigFile(scan2, join8(cwd, ".claude", "agents"), "Subagents", "subagent");
|
|
25828
27352
|
}
|
|
25829
27353
|
function configFileEntry(path, scope, kind) {
|
|
25830
27354
|
try {
|
|
@@ -25899,7 +27423,7 @@ function countMarkdownFiles(dir, depth) {
|
|
|
25899
27423
|
let count = 0;
|
|
25900
27424
|
for (const dirent of readdirSync(dir, { withFileTypes: true })) {
|
|
25901
27425
|
if (dirent.name.startsWith(".")) continue;
|
|
25902
|
-
if (dirent.isDirectory()) count += countMarkdownFiles(
|
|
27426
|
+
if (dirent.isDirectory()) count += countMarkdownFiles(join8(dir, dirent.name), depth + 1);
|
|
25903
27427
|
else if (dirent.name.endsWith(".md")) count += 1;
|
|
25904
27428
|
}
|
|
25905
27429
|
return count;
|
|
@@ -25912,7 +27436,7 @@ function collectSkillsDir(scan2, dir, origin) {
|
|
|
25912
27436
|
return;
|
|
25913
27437
|
}
|
|
25914
27438
|
for (const name of names) {
|
|
25915
|
-
const skillFile =
|
|
27439
|
+
const skillFile = join8(dir, name, "SKILL.md");
|
|
25916
27440
|
try {
|
|
25917
27441
|
const raw = readOptional(skillFile);
|
|
25918
27442
|
if (raw === void 0) continue;
|
|
@@ -25921,7 +27445,7 @@ function collectSkillsDir(scan2, dir, origin) {
|
|
|
25921
27445
|
name: front.name ?? name,
|
|
25922
27446
|
source: origin.source,
|
|
25923
27447
|
scope: origin.scope,
|
|
25924
|
-
location:
|
|
27448
|
+
location: join8(dir, name),
|
|
25925
27449
|
updatedAt: statSync2(skillFile).mtime.toISOString()
|
|
25926
27450
|
};
|
|
25927
27451
|
const version2 = front.version ?? origin.defaultVersion;
|
|
@@ -25940,10 +27464,10 @@ function parseFrontmatter(raw) {
|
|
|
25940
27464
|
if (lines[0]?.trim() !== "---") return out;
|
|
25941
27465
|
for (const line of lines.slice(1)) {
|
|
25942
27466
|
if (line.trim() === "---") break;
|
|
25943
|
-
const
|
|
25944
|
-
if (
|
|
25945
|
-
const key = line.slice(0,
|
|
25946
|
-
const value = line.slice(
|
|
27467
|
+
const sep5 = line.indexOf(":");
|
|
27468
|
+
if (sep5 === -1) continue;
|
|
27469
|
+
const key = line.slice(0, sep5).trim();
|
|
27470
|
+
const value = line.slice(sep5 + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
25947
27471
|
if (value === "") continue;
|
|
25948
27472
|
if (key === "name") out.name = value;
|
|
25949
27473
|
else if (key === "description") out.description = value;
|
|
@@ -25952,7 +27476,7 @@ function parseFrontmatter(raw) {
|
|
|
25952
27476
|
return out;
|
|
25953
27477
|
}
|
|
25954
27478
|
function collectInstalledPlugins(scan2, claudeDir) {
|
|
25955
|
-
const manifestPath =
|
|
27479
|
+
const manifestPath = join8(claudeDir, "plugins", "installed_plugins.json");
|
|
25956
27480
|
const raw = readOptional(manifestPath);
|
|
25957
27481
|
if (raw === void 0) return;
|
|
25958
27482
|
let plugins;
|
|
@@ -25977,7 +27501,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
|
|
|
25977
27501
|
if (typeof installPath !== "string" || seen.has(installPath)) continue;
|
|
25978
27502
|
seen.add(installPath);
|
|
25979
27503
|
const version2 = install.version;
|
|
25980
|
-
const hooksPath =
|
|
27504
|
+
const hooksPath = join8(installPath, "hooks", "hooks.json");
|
|
25981
27505
|
const hooksRaw = readOptional(hooksPath);
|
|
25982
27506
|
if (hooksRaw !== void 0) {
|
|
25983
27507
|
try {
|
|
@@ -25997,22 +27521,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
|
|
|
25997
27521
|
}
|
|
25998
27522
|
const origin = { source: marketplace, scope: "plugin", pluginName };
|
|
25999
27523
|
if (typeof version2 === "string") origin.defaultVersion = version2;
|
|
26000
|
-
collectSkillsDir(scan2,
|
|
27524
|
+
collectSkillsDir(scan2, join8(installPath, "skills"), origin);
|
|
26001
27525
|
const mcpOrigin = { scope: "plugin", pluginName, marketplace };
|
|
26002
|
-
collectMcpFile(scan2,
|
|
27526
|
+
collectMcpFile(scan2, join8(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
|
|
26003
27527
|
collectPluginManifestMcp(scan2, installPath, mcpOrigin);
|
|
26004
27528
|
}
|
|
26005
27529
|
}
|
|
26006
27530
|
}
|
|
26007
27531
|
function collectMarketplaceSkills(scan2, claudeDir) {
|
|
26008
|
-
for (const mp of readMarketplaces(
|
|
27532
|
+
for (const mp of readMarketplaces(join8(claudeDir, "plugins", "known_marketplaces.json"))) {
|
|
26009
27533
|
if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
|
|
26010
|
-
collectSkillsDir(scan2,
|
|
27534
|
+
collectSkillsDir(scan2, join8(mp.installLocation, "skills"), {
|
|
26011
27535
|
source: mp.name,
|
|
26012
27536
|
scope: "plugin"
|
|
26013
27537
|
});
|
|
26014
|
-
collectPluginSkillDirs(scan2,
|
|
26015
|
-
collectPluginSkillDirs(scan2,
|
|
27538
|
+
collectPluginSkillDirs(scan2, join8(mp.installLocation, "plugins"), mp.name);
|
|
27539
|
+
collectPluginSkillDirs(scan2, join8(mp.installLocation, "external_plugins"), mp.name);
|
|
26016
27540
|
}
|
|
26017
27541
|
}
|
|
26018
27542
|
function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
|
|
@@ -26023,7 +27547,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
|
|
|
26023
27547
|
return;
|
|
26024
27548
|
}
|
|
26025
27549
|
for (const plugin of plugins) {
|
|
26026
|
-
collectSkillsDir(scan2,
|
|
27550
|
+
collectSkillsDir(scan2, join8(pluginsDir, plugin, "skills"), {
|
|
26027
27551
|
source: marketplace,
|
|
26028
27552
|
scope: "plugin",
|
|
26029
27553
|
pluginName: plugin
|
|
@@ -26097,10 +27621,7 @@ function str2(value) {
|
|
|
26097
27621
|
}
|
|
26098
27622
|
|
|
26099
27623
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26100
|
-
import { createHash as
|
|
26101
|
-
|
|
26102
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
26103
|
-
import { createHash as createHash4 } from "crypto";
|
|
27624
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26104
27625
|
|
|
26105
27626
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26106
27627
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
@@ -26132,31 +27653,35 @@ function resolveInventoryContext(input) {
|
|
|
26132
27653
|
}
|
|
26133
27654
|
|
|
26134
27655
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26135
|
-
import { mkdirSync as
|
|
26136
|
-
import { join as
|
|
27656
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
27657
|
+
import { join as join9 } from "path";
|
|
26137
27658
|
var SESSION_START_MARKER = "session-start-last";
|
|
26138
27659
|
function claimSessionStart(dataDir2, sessionId) {
|
|
26139
27660
|
return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
|
|
26140
27661
|
}
|
|
26141
27662
|
function claimOncePerSession(dataDir2, marker, sessionId) {
|
|
26142
27663
|
if (!sessionId) return true;
|
|
26143
|
-
const path =
|
|
27664
|
+
const path = join9(dataDir2, marker);
|
|
26144
27665
|
try {
|
|
26145
27666
|
if (readFileSync5(path, "utf8") === sessionId) return false;
|
|
26146
27667
|
} catch {
|
|
26147
27668
|
}
|
|
26148
27669
|
try {
|
|
26149
|
-
|
|
26150
|
-
|
|
27670
|
+
mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
|
|
27671
|
+
writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
|
|
26151
27672
|
} catch {
|
|
26152
27673
|
}
|
|
26153
27674
|
return true;
|
|
26154
27675
|
}
|
|
26155
27676
|
|
|
27677
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
27678
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
27679
|
+
import { basename as basename4, dirname as dirname2, sep as sep3 } from "path";
|
|
27680
|
+
|
|
26156
27681
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
26157
27682
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
26158
|
-
import { existsSync as
|
|
26159
|
-
import { basename as
|
|
27683
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27684
|
+
import { basename as basename5, join as join10, relative, sep as sep4 } from "path";
|
|
26160
27685
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
26161
27686
|
".git",
|
|
26162
27687
|
"node_modules",
|
|
@@ -26176,7 +27701,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
26176
27701
|
var MAX_FILES = 2e4;
|
|
26177
27702
|
function readIgnoreLayer(dir) {
|
|
26178
27703
|
try {
|
|
26179
|
-
const content = readFileSync6(
|
|
27704
|
+
const content = readFileSync6(join10(dir, ".gitignore"), "utf8");
|
|
26180
27705
|
return { base: dir, matcher: (0, import_ignore.default)().add(content) };
|
|
26181
27706
|
} catch {
|
|
26182
27707
|
return void 0;
|
|
@@ -26185,7 +27710,7 @@ function readIgnoreLayer(dir) {
|
|
|
26185
27710
|
function isIgnored(layers, absPath, isDir) {
|
|
26186
27711
|
let ignored = false;
|
|
26187
27712
|
for (const layer of layers) {
|
|
26188
|
-
const rel = relative(layer.base, absPath).split(
|
|
27713
|
+
const rel = relative(layer.base, absPath).split(sep4).join("/") + (isDir ? "/" : "");
|
|
26189
27714
|
const verdict = layer.matcher.test(rel);
|
|
26190
27715
|
if (verdict.ignored) ignored = true;
|
|
26191
27716
|
else if (verdict.unignored) ignored = false;
|
|
@@ -26246,7 +27771,7 @@ function resolveProjectFiles(cwd) {
|
|
|
26246
27771
|
let visit2 = function(dir, layers) {
|
|
26247
27772
|
let dirents;
|
|
26248
27773
|
try {
|
|
26249
|
-
dirents =
|
|
27774
|
+
dirents = readdirSync3(dir, { withFileTypes: true, encoding: "utf8" });
|
|
26250
27775
|
} catch {
|
|
26251
27776
|
walk.lostSubtree = true;
|
|
26252
27777
|
return false;
|
|
@@ -26254,10 +27779,10 @@ function resolveProjectFiles(cwd) {
|
|
|
26254
27779
|
const layer = readIgnoreLayer(dir);
|
|
26255
27780
|
const dirLayers = layer ? [...layers, layer] : layers;
|
|
26256
27781
|
for (const entry of dirents) {
|
|
26257
|
-
const fullPath =
|
|
27782
|
+
const fullPath = join10(dir, entry.name);
|
|
26258
27783
|
if (entry.isDirectory()) {
|
|
26259
27784
|
if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
|
|
26260
|
-
if (
|
|
27785
|
+
if (existsSync5(join10(fullPath, ".git"))) continue;
|
|
26261
27786
|
if (visit2(fullPath, dirLayers)) return true;
|
|
26262
27787
|
continue;
|
|
26263
27788
|
}
|
|
@@ -26265,10 +27790,10 @@ function resolveProjectFiles(cwd) {
|
|
|
26265
27790
|
if (entry.name === ".git") continue;
|
|
26266
27791
|
if (isIgnored(dirLayers, fullPath, false)) continue;
|
|
26267
27792
|
if (files.length >= MAX_FILES) return true;
|
|
26268
|
-
const relPath = relative(root, fullPath).split(
|
|
27793
|
+
const relPath = relative(root, fullPath).split(sep4).join("/");
|
|
26269
27794
|
files.push({
|
|
26270
27795
|
path: relPath,
|
|
26271
|
-
name:
|
|
27796
|
+
name: basename5(entry.name),
|
|
26272
27797
|
origin: classifyOrigin(relPath, entry.name),
|
|
26273
27798
|
defaultAccess: "approved"
|
|
26274
27799
|
});
|
|
@@ -26298,17 +27823,17 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
26298
27823
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26299
27824
|
|
|
26300
27825
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
26301
|
-
import { mkdirSync as
|
|
26302
|
-
import { join as
|
|
27826
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
27827
|
+
import { join as join11 } from "path";
|
|
26303
27828
|
function throttled(dataDir2, markerName, windowMs) {
|
|
26304
|
-
const marker =
|
|
27829
|
+
const marker = join11(dataDir2, markerName);
|
|
26305
27830
|
try {
|
|
26306
27831
|
if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
|
|
26307
27832
|
} catch {
|
|
26308
27833
|
}
|
|
26309
27834
|
try {
|
|
26310
|
-
|
|
26311
|
-
|
|
27835
|
+
mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
|
|
27836
|
+
writeFileSync4(marker, String(Date.now()), { mode: DATA_FILE_MODE });
|
|
26312
27837
|
} catch {
|
|
26313
27838
|
}
|
|
26314
27839
|
return false;
|
|
@@ -26346,7 +27871,8 @@ var StandaloneDataGateway = class {
|
|
|
26346
27871
|
}
|
|
26347
27872
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
26348
27873
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
26349
|
-
// hands the natural key across.
|
|
27874
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27875
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
26350
27876
|
recordLlmCall(input) {
|
|
26351
27877
|
this.db.auditEvents.insertLlmCall(input);
|
|
26352
27878
|
return Promise.resolve();
|
|
@@ -26388,7 +27914,9 @@ var StandaloneDataGateway = class {
|
|
|
26388
27914
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
26389
27915
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
26390
27916
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
26391
|
-
// content-addressed
|
|
27917
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27918
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27919
|
+
// than no-opping.
|
|
26392
27920
|
writeToolCall(input) {
|
|
26393
27921
|
this.db.auditEvents.insertToolCall(input);
|
|
26394
27922
|
if (input.inspections.length === 0) return;
|
|
@@ -26408,7 +27936,7 @@ var StandaloneDataGateway = class {
|
|
|
26408
27936
|
});
|
|
26409
27937
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
26410
27938
|
this.db.inspectionFindings.insertFinding({
|
|
26411
|
-
id: inspectionFindingId(auditEventId,
|
|
27939
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
26412
27940
|
auditEventId,
|
|
26413
27941
|
inspectionDefinitionId: definitionId,
|
|
26414
27942
|
classifiedDataId: classifiedDataId2,
|
|
@@ -26457,10 +27985,17 @@ var StandaloneDataGateway = class {
|
|
|
26457
27985
|
try {
|
|
26458
27986
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
26459
27987
|
if (snapshot.installedPacks === 0) return void 0;
|
|
26460
|
-
if (snapshot.enabledPacks === 0)
|
|
27988
|
+
if (snapshot.enabledPacks === 0) {
|
|
27989
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27990
|
+
}
|
|
26461
27991
|
if (snapshot.invalidRules > 0) return void 0;
|
|
26462
27992
|
if (snapshot.rules.length === 0) return void 0;
|
|
26463
|
-
return {
|
|
27993
|
+
return {
|
|
27994
|
+
rules: snapshot.rules,
|
|
27995
|
+
ruleActions: snapshot.ruleActions,
|
|
27996
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27997
|
+
complete: true
|
|
27998
|
+
};
|
|
26464
27999
|
} catch {
|
|
26465
28000
|
return void 0;
|
|
26466
28001
|
}
|
|
@@ -26488,6 +28023,7 @@ var StandaloneDataGateway = class {
|
|
|
26488
28023
|
policies: [...policies, ...rulePolicies],
|
|
26489
28024
|
rules: installed ? installed.rules : [],
|
|
26490
28025
|
...installed ? { rulesComplete: true } : {},
|
|
28026
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
26491
28027
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
26492
28028
|
customKeywords,
|
|
26493
28029
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -26586,6 +28122,13 @@ var StandaloneDataGateway = class {
|
|
|
26586
28122
|
this.db.scanLedger.upsertEntries(entries);
|
|
26587
28123
|
return Promise.resolve();
|
|
26588
28124
|
}
|
|
28125
|
+
getRuleProbeVerdict(ruleKey) {
|
|
28126
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
28127
|
+
}
|
|
28128
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
|
|
28129
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
|
|
28130
|
+
return Promise.resolve();
|
|
28131
|
+
}
|
|
26589
28132
|
openAtRestKeysForPath(path) {
|
|
26590
28133
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
26591
28134
|
}
|
|
@@ -26596,6 +28139,12 @@ var StandaloneDataGateway = class {
|
|
|
26596
28139
|
this.db.resolutions.insertResolution(input);
|
|
26597
28140
|
return Promise.resolve();
|
|
26598
28141
|
}
|
|
28142
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
28143
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
28144
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
28145
|
+
recordProjectEgress(input) {
|
|
28146
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
28147
|
+
}
|
|
26599
28148
|
close() {
|
|
26600
28149
|
this.db.close();
|
|
26601
28150
|
return Promise.resolve();
|
|
@@ -26740,7 +28289,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
|
|
|
26740
28289
|
|
|
26741
28290
|
// src/history/reconcile-trigger.ts
|
|
26742
28291
|
import { spawn } from "child_process";
|
|
26743
|
-
import { dirname as
|
|
28292
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
26744
28293
|
import { fileURLToPath } from "url";
|
|
26745
28294
|
|
|
26746
28295
|
// src/history/tail.ts
|
|
@@ -26748,13 +28297,13 @@ import { createHash as createHash5 } from "crypto";
|
|
|
26748
28297
|
import {
|
|
26749
28298
|
closeSync,
|
|
26750
28299
|
fstatSync,
|
|
26751
|
-
mkdirSync as
|
|
28300
|
+
mkdirSync as mkdirSync4,
|
|
26752
28301
|
openSync,
|
|
26753
28302
|
readFileSync as readFileSync7,
|
|
26754
28303
|
readSync,
|
|
26755
|
-
writeFileSync as
|
|
28304
|
+
writeFileSync as writeFileSync5
|
|
26756
28305
|
} from "fs";
|
|
26757
|
-
import { join as
|
|
28306
|
+
import { join as join12 } from "path";
|
|
26758
28307
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
26759
28308
|
function safeSessionId(sessionId) {
|
|
26760
28309
|
if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
|
|
@@ -26770,8 +28319,8 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
|
|
|
26770
28319
|
try {
|
|
26771
28320
|
const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
|
|
26772
28321
|
if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
|
|
26773
|
-
const here =
|
|
26774
|
-
const child = spawn(process.execPath, [
|
|
28322
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
28323
|
+
const child = spawn(process.execPath, [join13(here, "reconcile.js"), sessionId, transcriptPath], {
|
|
26775
28324
|
detached: true,
|
|
26776
28325
|
stdio: "ignore"
|
|
26777
28326
|
});
|
|
@@ -26784,13 +28333,23 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
|
|
|
26784
28333
|
async function readStdin() {
|
|
26785
28334
|
return new Promise((resolve) => {
|
|
26786
28335
|
let data = "";
|
|
26787
|
-
|
|
26788
|
-
|
|
26789
|
-
|
|
26790
|
-
|
|
26791
|
-
|
|
28336
|
+
let settled = false;
|
|
28337
|
+
const finish = () => {
|
|
28338
|
+
if (settled) return;
|
|
28339
|
+
settled = true;
|
|
28340
|
+
clearTimeout(timer);
|
|
28341
|
+
process.stdin.removeListener("data", onData);
|
|
28342
|
+
process.stdin.removeListener("end", finish);
|
|
26792
28343
|
resolve(data);
|
|
26793
|
-
}
|
|
28344
|
+
};
|
|
28345
|
+
const onData = (chunk) => {
|
|
28346
|
+
data += chunk;
|
|
28347
|
+
};
|
|
28348
|
+
const timer = setTimeout(finish, 5e3);
|
|
28349
|
+
process.stdin.setEncoding("utf8");
|
|
28350
|
+
process.stdin.on("data", onData);
|
|
28351
|
+
process.stdin.on("end", finish);
|
|
28352
|
+
process.stdin.on("error", finish);
|
|
26794
28353
|
});
|
|
26795
28354
|
}
|
|
26796
28355
|
function parseJson(raw) {
|