@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/remediate.js
CHANGED
|
@@ -497,7 +497,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
497
497
|
|
|
498
498
|
// ../../packages/persistence/src/database.ts
|
|
499
499
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
500
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
500
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
501
501
|
import { join, sep } from "path";
|
|
502
502
|
import { DatabaseSync } from "node:sqlite";
|
|
503
503
|
|
|
@@ -546,6 +546,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
546
546
|
{
|
|
547
547
|
tag: "0010_events_session_expression_index",
|
|
548
548
|
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tag: "0011_egress_writer",
|
|
552
|
+
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0012_handy_the_captain",
|
|
556
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0013_legacy_history_backfill_support",
|
|
560
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
tag: "0014_drop_legacy_events_findings",
|
|
564
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
549
565
|
}
|
|
550
566
|
];
|
|
551
567
|
|
|
@@ -15376,7 +15392,12 @@ var FindingFacets = external_exports.object({
|
|
|
15376
15392
|
severity: external_exports.array(FindingFacetItem),
|
|
15377
15393
|
subtype: external_exports.array(FindingFacetItem),
|
|
15378
15394
|
provider: external_exports.array(FindingFacetItem),
|
|
15379
|
-
action: external_exports.array(FindingFacetItem)
|
|
15395
|
+
action: external_exports.array(FindingFacetItem),
|
|
15396
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15397
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15398
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15399
|
+
// counted under no value.
|
|
15400
|
+
status: external_exports.array(FindingFacetItem)
|
|
15380
15401
|
}).meta({ id: "FindingFacets" });
|
|
15381
15402
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15382
15403
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15386,6 +15407,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15386
15407
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15387
15408
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15388
15409
|
action: external_exports.array(FindingAction).optional(),
|
|
15410
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15411
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15412
|
+
// one of the requested values.
|
|
15413
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15389
15414
|
q: external_exports.string().optional(),
|
|
15390
15415
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15391
15416
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15573,6 +15598,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15573
15598
|
parent_uuid: external_exports.string().optional(),
|
|
15574
15599
|
run_key: external_exports.string().optional()
|
|
15575
15600
|
}).catchall(external_exports.unknown());
|
|
15601
|
+
var CaptureAttributes = external_exports.object({
|
|
15602
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15603
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15604
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15605
|
+
source_tool: external_exports.string().optional(),
|
|
15606
|
+
file_path: external_exports.string().optional(),
|
|
15607
|
+
repo: external_exports.string().optional(),
|
|
15608
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15609
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15610
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15611
|
+
tool_name: external_exports.string().optional(),
|
|
15612
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15613
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15614
|
+
gitignored: external_exports.boolean().optional(),
|
|
15615
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15616
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15617
|
+
whole_file: external_exports.boolean().optional(),
|
|
15618
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15619
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15620
|
+
// trace id when telemetry is enabled.
|
|
15621
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15622
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15623
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15624
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15625
|
+
// authorized the bypass.
|
|
15626
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15627
|
+
}).catchall(external_exports.unknown());
|
|
15576
15628
|
var ToolCallInspection = external_exports.object({
|
|
15577
15629
|
ruleId: external_exports.string().min(1),
|
|
15578
15630
|
ruleName: external_exports.string(),
|
|
@@ -15659,7 +15711,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15659
15711
|
span: Span,
|
|
15660
15712
|
maskedMatch: external_exports.string(),
|
|
15661
15713
|
actionTaken: ActionTaken,
|
|
15662
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15714
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15715
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15716
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15717
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15718
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15719
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15720
|
+
findingKey: external_exports.string().optional(),
|
|
15721
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15722
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15723
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15724
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15725
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15663
15726
|
});
|
|
15664
15727
|
var InventoryContext = external_exports.object({
|
|
15665
15728
|
host: InventoryInput.optional(),
|
|
@@ -15861,6 +15924,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15861
15924
|
|
|
15862
15925
|
// ../../packages/schema/src/zod/event.ts
|
|
15863
15926
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15927
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15864
15928
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15865
15929
|
var EventMetadata = external_exports.object({
|
|
15866
15930
|
sessionId: external_exports.string().optional(),
|
|
@@ -16201,6 +16265,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16201
16265
|
|
|
16202
16266
|
// ../../packages/schema/src/zod/rule.ts
|
|
16203
16267
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16268
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16204
16269
|
var KeywordMatcher = external_exports.object({
|
|
16205
16270
|
type: external_exports.literal("keyword"),
|
|
16206
16271
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16225,9 +16290,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16225
16290
|
return false;
|
|
16226
16291
|
}
|
|
16227
16292
|
}
|
|
16293
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16228
16294
|
var RegexMatcher = external_exports.object({
|
|
16229
16295
|
type: external_exports.literal("regex"),
|
|
16230
|
-
pattern: external_exports.string(),
|
|
16296
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16231
16297
|
flags: external_exports.string().default("gi"),
|
|
16232
16298
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16233
16299
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16348,6 +16414,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16348
16414
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16349
16415
|
// `bundle.exceptions ?? []`.
|
|
16350
16416
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16417
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16418
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16419
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16420
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16421
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16422
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16351
16423
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16352
16424
|
fetchedAt: external_exports.iso.datetime()
|
|
16353
16425
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16794,6 +16866,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16794
16866
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16795
16867
|
}
|
|
16796
16868
|
|
|
16869
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16870
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16871
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16872
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16873
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16874
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16875
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16876
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16877
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16878
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16879
|
+
var ReviewInfo = external_exports.object({
|
|
16880
|
+
needsReview: external_exports.boolean(),
|
|
16881
|
+
reasons: external_exports.array(ReviewReason)
|
|
16882
|
+
}).meta({ id: "ReviewInfo" });
|
|
16883
|
+
var DestinationNetwork = external_exports.object({
|
|
16884
|
+
port: external_exports.number().int().nullable(),
|
|
16885
|
+
geo: external_exports.string().nullable(),
|
|
16886
|
+
ptr: external_exports.string().nullable()
|
|
16887
|
+
}).meta({ id: "DestinationNetwork" });
|
|
16888
|
+
var EndpointSummary = external_exports.object({
|
|
16889
|
+
id: external_exports.string(),
|
|
16890
|
+
method: HttpMethod,
|
|
16891
|
+
transport: Transport,
|
|
16892
|
+
url: external_exports.string(),
|
|
16893
|
+
template: external_exports.boolean(),
|
|
16894
|
+
dataClass: DataClass,
|
|
16895
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16896
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
16897
|
+
}).meta({ id: "EndpointSummary" });
|
|
16898
|
+
var CallSite = external_exports.object({
|
|
16899
|
+
id: external_exports.string(),
|
|
16900
|
+
project: external_exports.string(),
|
|
16901
|
+
file: external_exports.string(),
|
|
16902
|
+
line: external_exports.number().int().nonnegative(),
|
|
16903
|
+
snippet: external_exports.string(),
|
|
16904
|
+
dynamic: external_exports.boolean(),
|
|
16905
|
+
vendored: external_exports.boolean(),
|
|
16906
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
16907
|
+
projectId: external_exports.string().nullable()
|
|
16908
|
+
}).meta({ id: "CallSite" });
|
|
16909
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
16910
|
+
sites: external_exports.array(CallSite)
|
|
16911
|
+
}).meta({ id: "EndpointWithSites" });
|
|
16912
|
+
var ShareDestinationSummary = external_exports.object({
|
|
16913
|
+
id: external_exports.string(),
|
|
16914
|
+
kind: DestinationKind,
|
|
16915
|
+
name: external_exports.string(),
|
|
16916
|
+
host: external_exports.string(),
|
|
16917
|
+
category: external_exports.string(),
|
|
16918
|
+
trust: ShareTrustLevel,
|
|
16919
|
+
/** Effective state (decision applied over the trust default). */
|
|
16920
|
+
status: EgressStatus,
|
|
16921
|
+
/** True when an egress decision override differs from the trust default. */
|
|
16922
|
+
isCustom: external_exports.boolean(),
|
|
16923
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16924
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
16925
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16926
|
+
transports: external_exports.array(Transport),
|
|
16927
|
+
/** Most-sensitive first. */
|
|
16928
|
+
dataClasses: external_exports.array(DataClass),
|
|
16929
|
+
review: ReviewInfo,
|
|
16930
|
+
/** Non-provider hosts only; null for providers. */
|
|
16931
|
+
network: DestinationNetwork.nullable(),
|
|
16932
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
16933
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
16934
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
16935
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
16936
|
+
endpointCount: true,
|
|
16937
|
+
callSiteCount: true,
|
|
16938
|
+
endpoints: true
|
|
16939
|
+
}).extend({
|
|
16940
|
+
/** Ownership/geo rationale; null for providers. */
|
|
16941
|
+
note: external_exports.string().nullable(),
|
|
16942
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
16943
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
16944
|
+
var ReviewDestination = external_exports.object({
|
|
16945
|
+
id: external_exports.string(),
|
|
16946
|
+
kind: DestinationKind,
|
|
16947
|
+
name: external_exports.string(),
|
|
16948
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
16949
|
+
host: external_exports.string(),
|
|
16950
|
+
trust: ShareTrustLevel,
|
|
16951
|
+
status: EgressStatus,
|
|
16952
|
+
review: ReviewInfo,
|
|
16953
|
+
topDataClass: DataClass,
|
|
16954
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16955
|
+
lastSeen: external_exports.iso.datetime()
|
|
16956
|
+
}).meta({ id: "ReviewDestination" });
|
|
16957
|
+
var ShareDestinationGroup = external_exports.object({
|
|
16958
|
+
kind: DestinationKind,
|
|
16959
|
+
total: external_exports.number().int().nonnegative(),
|
|
16960
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
16961
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
16962
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
16963
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
16964
|
+
var SharesStats = external_exports.object({
|
|
16965
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
16966
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
16967
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
16968
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
16969
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
16970
|
+
byKind: external_exports.object({
|
|
16971
|
+
provider: external_exports.number().int().nonnegative(),
|
|
16972
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16973
|
+
external: external_exports.number().int().nonnegative(),
|
|
16974
|
+
ip: external_exports.number().int().nonnegative()
|
|
16975
|
+
}),
|
|
16976
|
+
byTrust: external_exports.object({
|
|
16977
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
16978
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16979
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
16980
|
+
ip: external_exports.number().int().nonnegative()
|
|
16981
|
+
})
|
|
16982
|
+
}).meta({ id: "SharesStats" });
|
|
16983
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
16984
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
16985
|
+
decision: EgressDecision.nullable()
|
|
16986
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
16987
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
16988
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
16989
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
16990
|
+
q: external_exports.string().optional(),
|
|
16991
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
16992
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
16993
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
16994
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
16995
|
+
/**
|
|
16996
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
16997
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
16998
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
16999
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17000
|
+
*/
|
|
17001
|
+
review: external_exports.stringbool().default(false)
|
|
17002
|
+
});
|
|
17003
|
+
var ExportSharesQuery = external_exports.object({
|
|
17004
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17005
|
+
q: external_exports.string().optional(),
|
|
17006
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
17007
|
+
});
|
|
17008
|
+
|
|
17009
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
17010
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
17011
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
17012
|
+
id: external_exports.string(),
|
|
17013
|
+
name: external_exports.string(),
|
|
17014
|
+
category: external_exports.string(),
|
|
17015
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
17016
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
17017
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
17018
|
+
apiBase: external_exports.string(),
|
|
17019
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
17020
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
17021
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
17022
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
17023
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
17024
|
+
var EgressCallSiteHit = external_exports.object({
|
|
17025
|
+
file: external_exports.string(),
|
|
17026
|
+
line: external_exports.number().int().positive(),
|
|
17027
|
+
snippet: external_exports.string(),
|
|
17028
|
+
dynamic: external_exports.boolean(),
|
|
17029
|
+
vendored: external_exports.boolean()
|
|
17030
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
17031
|
+
var ResolvedEgressHit = external_exports.object({
|
|
17032
|
+
host: external_exports.string(),
|
|
17033
|
+
kind: DestinationKind,
|
|
17034
|
+
name: external_exports.string(),
|
|
17035
|
+
category: external_exports.string(),
|
|
17036
|
+
trust: ShareTrustLevel,
|
|
17037
|
+
network: DestinationNetwork.nullable(),
|
|
17038
|
+
method: HttpMethod,
|
|
17039
|
+
transport: Transport,
|
|
17040
|
+
url: external_exports.string(),
|
|
17041
|
+
template: external_exports.boolean(),
|
|
17042
|
+
dataClass: DataClass,
|
|
17043
|
+
site: EgressCallSiteHit
|
|
17044
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
17045
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
17046
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
17047
|
+
external_exports.object({
|
|
17048
|
+
mode: external_exports.literal("ledger"),
|
|
17049
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
17050
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
17051
|
+
})
|
|
17052
|
+
]).meta({ id: "EgressReconcile" });
|
|
17053
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
17054
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
17055
|
+
projectKey: external_exports.string().min(1),
|
|
17056
|
+
/** Display name only — never keys reconciliation. */
|
|
17057
|
+
project: external_exports.string(),
|
|
17058
|
+
projectId: external_exports.string().nullable(),
|
|
17059
|
+
reconcile: EgressReconcile,
|
|
17060
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
17061
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
17062
|
+
var EgressWriteSummary = external_exports.object({
|
|
17063
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17064
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17065
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17066
|
+
truncated: external_exports.boolean(),
|
|
17067
|
+
/**
|
|
17068
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
17069
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17070
|
+
* again next scan.
|
|
17071
|
+
*/
|
|
17072
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17073
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17074
|
+
|
|
16797
17075
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16798
17076
|
function toApiAction(dbVal) {
|
|
16799
17077
|
const map2 = {
|
|
@@ -16941,6 +17219,15 @@ function groupActions(g) {
|
|
|
16941
17219
|
actionsCache.set(g, actions);
|
|
16942
17220
|
return actions;
|
|
16943
17221
|
}
|
|
17222
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17223
|
+
const statusSet = new Set(statuses);
|
|
17224
|
+
let sum = 0;
|
|
17225
|
+
for (const input of statusInputs) {
|
|
17226
|
+
if (input.count === void 0) return null;
|
|
17227
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17228
|
+
}
|
|
17229
|
+
return sum;
|
|
17230
|
+
}
|
|
16944
17231
|
function applyFindingFilters(groups, opts) {
|
|
16945
17232
|
let filtered = groups;
|
|
16946
17233
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16959,6 +17246,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
16959
17246
|
const subtypeSet = new Set(opts.subtype);
|
|
16960
17247
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
16961
17248
|
}
|
|
17249
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17250
|
+
const statusSet = new Set(opts.statuses);
|
|
17251
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17252
|
+
}
|
|
16962
17253
|
if (opts.q) {
|
|
16963
17254
|
const q = opts.q.toLowerCase();
|
|
16964
17255
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -16980,6 +17271,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16980
17271
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
16981
17272
|
providers: opts.providers,
|
|
16982
17273
|
actions: opts.actions,
|
|
17274
|
+
statuses: opts.statuses,
|
|
16983
17275
|
q: opts.q,
|
|
16984
17276
|
subtype: opts.subtype
|
|
16985
17277
|
});
|
|
@@ -16989,6 +17281,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16989
17281
|
}
|
|
16990
17282
|
const forProvider = applyFindingFilters(allGroups, {
|
|
16991
17283
|
actions: opts.actions,
|
|
17284
|
+
statuses: opts.statuses,
|
|
16992
17285
|
q: opts.q,
|
|
16993
17286
|
subtype: opts.subtype,
|
|
16994
17287
|
severity: opts.severity
|
|
@@ -16999,6 +17292,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16999
17292
|
}
|
|
17000
17293
|
const forAction = applyFindingFilters(allGroups, {
|
|
17001
17294
|
providers: opts.providers,
|
|
17295
|
+
statuses: opts.statuses,
|
|
17002
17296
|
q: opts.q,
|
|
17003
17297
|
subtype: opts.subtype,
|
|
17004
17298
|
severity: opts.severity
|
|
@@ -17010,17 +17304,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17010
17304
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17011
17305
|
providers: opts.providers,
|
|
17012
17306
|
actions: opts.actions,
|
|
17307
|
+
statuses: opts.statuses,
|
|
17013
17308
|
q: opts.q,
|
|
17014
17309
|
severity: opts.severity
|
|
17015
17310
|
});
|
|
17016
17311
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17017
17312
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17313
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17314
|
+
providers: opts.providers,
|
|
17315
|
+
actions: opts.actions,
|
|
17316
|
+
q: opts.q,
|
|
17317
|
+
subtype: opts.subtype,
|
|
17318
|
+
severity: opts.severity
|
|
17319
|
+
});
|
|
17320
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17321
|
+
for (const g of forStatus) {
|
|
17322
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17323
|
+
}
|
|
17018
17324
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17019
17325
|
return {
|
|
17020
17326
|
severity: toItems(severityMap),
|
|
17021
17327
|
provider: toItems(providerMap),
|
|
17022
17328
|
action: toItems(actionMap),
|
|
17023
|
-
subtype: toItems(subtypeMap)
|
|
17329
|
+
subtype: toItems(subtypeMap),
|
|
17330
|
+
status: toItems(statusMap)
|
|
17024
17331
|
};
|
|
17025
17332
|
}
|
|
17026
17333
|
|
|
@@ -17055,10 +17362,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17055
17362
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17056
17363
|
|
|
17057
17364
|
// ../../packages/schema/src/zod/local.ts
|
|
17058
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17365
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17059
17366
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17060
17367
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17061
17368
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17369
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17370
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17371
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17372
|
+
});
|
|
17062
17373
|
var WorkspaceSettings = external_exports.object({
|
|
17063
17374
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17064
17375
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17070,38 +17381,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17070
17381
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17071
17382
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17072
17383
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17384
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17385
|
+
// Shares writes.
|
|
17386
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17073
17387
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17074
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17388
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17389
|
+
// Records that the user consented to sending findings to the model API for
|
|
17390
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17391
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17392
|
+
// covers the current payload and must be re-granted.
|
|
17393
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17075
17394
|
});
|
|
17076
17395
|
function defaultWorkspaceSettings() {
|
|
17077
17396
|
return WorkspaceSettings.parse({});
|
|
17078
17397
|
}
|
|
17079
|
-
function toEventRow(event) {
|
|
17080
|
-
return {
|
|
17081
|
-
id: event.id,
|
|
17082
|
-
sourceTool: event.sourceTool,
|
|
17083
|
-
kind: event.kind,
|
|
17084
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17085
|
-
contentHash: event.contentHash,
|
|
17086
|
-
content: event.content,
|
|
17087
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17088
|
-
};
|
|
17089
|
-
}
|
|
17090
|
-
function toFindingRow(finding) {
|
|
17091
|
-
return {
|
|
17092
|
-
id: finding.id,
|
|
17093
|
-
eventId: finding.eventId,
|
|
17094
|
-
ruleId: finding.ruleId,
|
|
17095
|
-
category: finding.category,
|
|
17096
|
-
severity: finding.severity,
|
|
17097
|
-
spanStart: finding.span.start,
|
|
17098
|
-
spanEnd: finding.span.end,
|
|
17099
|
-
maskedMatch: finding.maskedMatch,
|
|
17100
|
-
actionTaken: finding.actionTaken,
|
|
17101
|
-
confidence: finding.confidence,
|
|
17102
|
-
findingKey: finding.findingKey ?? null
|
|
17103
|
-
};
|
|
17104
|
-
}
|
|
17105
17398
|
function toInventoryRow(input, id, now) {
|
|
17106
17399
|
return {
|
|
17107
17400
|
id,
|
|
@@ -17171,7 +17464,42 @@ function toInspectionFindingRow(input) {
|
|
|
17171
17464
|
spanEnd: input.span.end,
|
|
17172
17465
|
maskedMatch: input.maskedMatch,
|
|
17173
17466
|
actionTaken: input.actionTaken,
|
|
17174
|
-
confidence: input.confidence
|
|
17467
|
+
confidence: input.confidence,
|
|
17468
|
+
findingKey: input.findingKey ?? null,
|
|
17469
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17470
|
+
};
|
|
17471
|
+
}
|
|
17472
|
+
function toCaptureAttributes(event) {
|
|
17473
|
+
const metadata = event.metadata;
|
|
17474
|
+
return {
|
|
17475
|
+
source_tool: event.sourceTool,
|
|
17476
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17477
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17478
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17479
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17480
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17481
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17482
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17483
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17484
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17485
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17486
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17487
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17488
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17489
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17490
|
+
};
|
|
17491
|
+
}
|
|
17492
|
+
function captureDefinitionVersion(finding) {
|
|
17493
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17494
|
+
}
|
|
17495
|
+
function toCaptureDefinitionInput(finding) {
|
|
17496
|
+
return {
|
|
17497
|
+
ruleId: finding.ruleId,
|
|
17498
|
+
version: captureDefinitionVersion(finding),
|
|
17499
|
+
name: finding.ruleId,
|
|
17500
|
+
category: finding.category,
|
|
17501
|
+
severity: finding.severity,
|
|
17502
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17175
17503
|
};
|
|
17176
17504
|
}
|
|
17177
17505
|
|
|
@@ -17553,145 +17881,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17553
17881
|
path: ["liveKeys"]
|
|
17554
17882
|
});
|
|
17555
17883
|
|
|
17556
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17557
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17558
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17559
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17560
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17561
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17562
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17563
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17564
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17565
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17566
|
-
var ReviewInfo = external_exports.object({
|
|
17567
|
-
needsReview: external_exports.boolean(),
|
|
17568
|
-
reasons: external_exports.array(ReviewReason)
|
|
17569
|
-
}).meta({ id: "ReviewInfo" });
|
|
17570
|
-
var DestinationNetwork = external_exports.object({
|
|
17571
|
-
port: external_exports.number().int().nullable(),
|
|
17572
|
-
geo: external_exports.string().nullable(),
|
|
17573
|
-
ptr: external_exports.string().nullable()
|
|
17574
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17575
|
-
var EndpointSummary = external_exports.object({
|
|
17576
|
-
id: external_exports.string(),
|
|
17577
|
-
method: HttpMethod,
|
|
17578
|
-
transport: Transport,
|
|
17579
|
-
url: external_exports.string(),
|
|
17580
|
-
template: external_exports.boolean(),
|
|
17581
|
-
dataClass: DataClass,
|
|
17582
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17583
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17584
|
-
}).meta({ id: "EndpointSummary" });
|
|
17585
|
-
var CallSite = external_exports.object({
|
|
17586
|
-
id: external_exports.string(),
|
|
17587
|
-
project: external_exports.string(),
|
|
17588
|
-
file: external_exports.string(),
|
|
17589
|
-
line: external_exports.number().int().nonnegative(),
|
|
17590
|
-
snippet: external_exports.string(),
|
|
17591
|
-
dynamic: external_exports.boolean(),
|
|
17592
|
-
vendored: external_exports.boolean(),
|
|
17593
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17594
|
-
projectId: external_exports.string().nullable()
|
|
17595
|
-
}).meta({ id: "CallSite" });
|
|
17596
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17597
|
-
sites: external_exports.array(CallSite)
|
|
17598
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17599
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17600
|
-
id: external_exports.string(),
|
|
17601
|
-
kind: DestinationKind,
|
|
17602
|
-
name: external_exports.string(),
|
|
17603
|
-
host: external_exports.string(),
|
|
17604
|
-
category: external_exports.string(),
|
|
17605
|
-
trust: ShareTrustLevel,
|
|
17606
|
-
/** Effective state (decision applied over the trust default). */
|
|
17607
|
-
status: EgressStatus,
|
|
17608
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17609
|
-
isCustom: external_exports.boolean(),
|
|
17610
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17611
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17612
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17613
|
-
transports: external_exports.array(Transport),
|
|
17614
|
-
/** Most-sensitive first. */
|
|
17615
|
-
dataClasses: external_exports.array(DataClass),
|
|
17616
|
-
review: ReviewInfo,
|
|
17617
|
-
/** Non-provider hosts only; null for providers. */
|
|
17618
|
-
network: DestinationNetwork.nullable(),
|
|
17619
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17620
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17621
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17622
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17623
|
-
endpointCount: true,
|
|
17624
|
-
callSiteCount: true,
|
|
17625
|
-
endpoints: true
|
|
17626
|
-
}).extend({
|
|
17627
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17628
|
-
note: external_exports.string().nullable(),
|
|
17629
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17630
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17631
|
-
var ReviewDestination = external_exports.object({
|
|
17632
|
-
id: external_exports.string(),
|
|
17633
|
-
kind: DestinationKind,
|
|
17634
|
-
name: external_exports.string(),
|
|
17635
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17636
|
-
host: external_exports.string(),
|
|
17637
|
-
trust: ShareTrustLevel,
|
|
17638
|
-
status: EgressStatus,
|
|
17639
|
-
review: ReviewInfo,
|
|
17640
|
-
topDataClass: DataClass,
|
|
17641
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17642
|
-
lastSeen: external_exports.iso.datetime()
|
|
17643
|
-
}).meta({ id: "ReviewDestination" });
|
|
17644
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17645
|
-
kind: DestinationKind,
|
|
17646
|
-
total: external_exports.number().int().nonnegative(),
|
|
17647
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17648
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17649
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17650
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17651
|
-
var SharesStats = external_exports.object({
|
|
17652
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17653
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17654
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17655
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17656
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17657
|
-
byKind: external_exports.object({
|
|
17658
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17659
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17660
|
-
ip: external_exports.number().int().nonnegative()
|
|
17661
|
-
}),
|
|
17662
|
-
byTrust: external_exports.object({
|
|
17663
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17664
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17665
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17666
|
-
ip: external_exports.number().int().nonnegative()
|
|
17667
|
-
})
|
|
17668
|
-
}).meta({ id: "SharesStats" });
|
|
17669
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17670
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17671
|
-
decision: EgressDecision.nullable()
|
|
17672
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17673
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17674
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17675
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17676
|
-
q: external_exports.string().optional(),
|
|
17677
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17678
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17679
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17680
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17681
|
-
/**
|
|
17682
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17683
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17684
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17685
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17686
|
-
*/
|
|
17687
|
-
review: external_exports.stringbool().default(false)
|
|
17688
|
-
});
|
|
17689
|
-
var ExportSharesQuery = external_exports.object({
|
|
17690
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17691
|
-
q: external_exports.string().optional(),
|
|
17692
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17693
|
-
});
|
|
17694
|
-
|
|
17695
17884
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17696
17885
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17697
17886
|
function trustDefaultStatus(trust) {
|
|
@@ -17711,7 +17900,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17711
17900
|
const reasons = [];
|
|
17712
17901
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17713
17902
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17714
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17903
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17715
17904
|
return reasons;
|
|
17716
17905
|
}
|
|
17717
17906
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17738,6 +17927,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17738
17927
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17739
17928
|
}
|
|
17740
17929
|
|
|
17930
|
+
// ../../packages/persistence/src/ids.ts
|
|
17931
|
+
import { createHash } from "crypto";
|
|
17932
|
+
function sha256Hex(input) {
|
|
17933
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17934
|
+
}
|
|
17935
|
+
function inventoryId(objectType, identityKey) {
|
|
17936
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17937
|
+
}
|
|
17938
|
+
function sourceProjectId(url2) {
|
|
17939
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17940
|
+
}
|
|
17941
|
+
function classifiedDataId(cls) {
|
|
17942
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17943
|
+
}
|
|
17944
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17945
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17946
|
+
}
|
|
17947
|
+
function llmCallId(sessionId, messageId) {
|
|
17948
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17949
|
+
}
|
|
17950
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17951
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17952
|
+
}
|
|
17953
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
17954
|
+
return sha256Hex(
|
|
17955
|
+
canonicalIdentity([
|
|
17956
|
+
"inspection_finding",
|
|
17957
|
+
auditEventId,
|
|
17958
|
+
ruleId,
|
|
17959
|
+
String(spanStart),
|
|
17960
|
+
String(spanEnd)
|
|
17961
|
+
])
|
|
17962
|
+
);
|
|
17963
|
+
}
|
|
17964
|
+
var NO_SESSION = "no_session";
|
|
17965
|
+
var NO_PATH = "no_path";
|
|
17966
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17967
|
+
return sha256Hex(
|
|
17968
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17969
|
+
);
|
|
17970
|
+
}
|
|
17971
|
+
|
|
17741
17972
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17742
17973
|
function escapeLikePattern(s) {
|
|
17743
17974
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17834,39 +18065,98 @@ function evidenceExists(db, object2) {
|
|
|
17834
18065
|
return schemaObjectExists(db, "table", object2.name);
|
|
17835
18066
|
}
|
|
17836
18067
|
|
|
17837
|
-
// ../../packages/persistence/src/
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
18068
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18069
|
+
function allRows(stmt, params) {
|
|
18070
|
+
if (params === void 0) return stmt.all();
|
|
18071
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18072
|
+
return stmt.all(params);
|
|
17841
18073
|
}
|
|
17842
|
-
function
|
|
17843
|
-
|
|
18074
|
+
function getRow(stmt, params) {
|
|
18075
|
+
if (params === void 0) return stmt.get();
|
|
18076
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18077
|
+
return stmt.get(params);
|
|
17844
18078
|
}
|
|
17845
|
-
function
|
|
17846
|
-
return
|
|
18079
|
+
function intToBool(raw) {
|
|
18080
|
+
return raw === 1 || raw === true;
|
|
17847
18081
|
}
|
|
17848
|
-
function
|
|
17849
|
-
return
|
|
18082
|
+
function boolToInt(b) {
|
|
18083
|
+
return b ? 1 : 0;
|
|
17850
18084
|
}
|
|
17851
|
-
function
|
|
17852
|
-
|
|
18085
|
+
function bindParams(row) {
|
|
18086
|
+
const out = {};
|
|
18087
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18088
|
+
out[key] = value === void 0 ? null : value;
|
|
18089
|
+
}
|
|
18090
|
+
return out;
|
|
17853
18091
|
}
|
|
17854
|
-
function
|
|
17855
|
-
return
|
|
18092
|
+
function countScalar(db, sql, params) {
|
|
18093
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17856
18094
|
}
|
|
17857
|
-
function
|
|
17858
|
-
|
|
18095
|
+
function countBy(db, sql, params) {
|
|
18096
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18097
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18098
|
+
map2.set(row.k, row.n);
|
|
18099
|
+
}
|
|
18100
|
+
return map2;
|
|
17859
18101
|
}
|
|
17860
|
-
function
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
18102
|
+
function mapRowsTolerant(rows, map2) {
|
|
18103
|
+
const out = [];
|
|
18104
|
+
for (const row of rows) {
|
|
18105
|
+
try {
|
|
18106
|
+
out.push(map2(row));
|
|
18107
|
+
} catch {
|
|
18108
|
+
}
|
|
18109
|
+
}
|
|
18110
|
+
return out;
|
|
18111
|
+
}
|
|
18112
|
+
|
|
18113
|
+
// ../../packages/persistence/src/paths.ts
|
|
18114
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18115
|
+
var DATA_DIR_MODE = 448;
|
|
18116
|
+
var DATA_FILE_MODE = 384;
|
|
18117
|
+
var DB_FILENAME = "aka.db";
|
|
18118
|
+
function chmodBestEffort(path, mode) {
|
|
18119
|
+
try {
|
|
18120
|
+
chmodSync(path, mode);
|
|
18121
|
+
} catch {
|
|
18122
|
+
}
|
|
18123
|
+
}
|
|
18124
|
+
function tightenDir(dir) {
|
|
18125
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18126
|
+
}
|
|
18127
|
+
function ensureDataDirSync(dir) {
|
|
18128
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18129
|
+
tightenDir(dir);
|
|
18130
|
+
}
|
|
18131
|
+
function dbSidecars(file2) {
|
|
18132
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18133
|
+
}
|
|
18134
|
+
function tightenFile(file2) {
|
|
18135
|
+
try {
|
|
18136
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18137
|
+
} catch {
|
|
18138
|
+
}
|
|
18139
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18140
|
+
}
|
|
18141
|
+
function tightenPerms(file2) {
|
|
18142
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18143
|
+
}
|
|
18144
|
+
function writeOwnerOnlyFileSync(file2, data) {
|
|
18145
|
+
const tmp = `${file2}.${String(process.pid)}.tmp`;
|
|
18146
|
+
try {
|
|
18147
|
+
rmSync(tmp, { force: true });
|
|
18148
|
+
} catch {
|
|
18149
|
+
}
|
|
18150
|
+
try {
|
|
18151
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18152
|
+
renameSync(tmp, file2);
|
|
18153
|
+
} finally {
|
|
18154
|
+
try {
|
|
18155
|
+
rmSync(tmp, { force: true });
|
|
18156
|
+
} catch {
|
|
18157
|
+
}
|
|
18158
|
+
}
|
|
18159
|
+
tightenFile(file2);
|
|
17870
18160
|
}
|
|
17871
18161
|
|
|
17872
18162
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17880,7 +18170,8 @@ function createdIndexName(statement) {
|
|
|
17880
18170
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17881
18171
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17882
18172
|
}
|
|
17883
|
-
|
|
18173
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18174
|
+
function applyMigrations(db, file2) {
|
|
17884
18175
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17885
18176
|
db.exec(
|
|
17886
18177
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17894,6 +18185,7 @@ function applyMigrations(db) {
|
|
|
17894
18185
|
);
|
|
17895
18186
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17896
18187
|
if (applied.has(migration.tag)) continue;
|
|
18188
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17897
18189
|
const evidence = evidenceObjects(migration.sql);
|
|
17898
18190
|
const present2 = evidence.filter((o) => evidenceExists(db, o));
|
|
17899
18191
|
if (present2.length > 0 && present2.length < evidence.length) {
|
|
@@ -17938,13 +18230,54 @@ function applyMigrations(db) {
|
|
|
17938
18230
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
17939
18231
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
17940
18232
|
}
|
|
17941
|
-
ensureSyncedAtColumn(db, "events");
|
|
17942
18233
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17943
18234
|
ensureScanLedgerTable(db);
|
|
17944
18235
|
ensureBlockedDetectionsTable(db);
|
|
18236
|
+
ensureRuleProbeCacheTable(db);
|
|
17945
18237
|
ensureWriteGateTrigger(db);
|
|
17946
18238
|
ensureTokenUsageColumns(db);
|
|
17947
18239
|
reconcileSourceProjectIds(db);
|
|
18240
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18241
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18242
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18245
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18246
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18247
|
+
if (!migration) return;
|
|
18248
|
+
if (file2) {
|
|
18249
|
+
try {
|
|
18250
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18251
|
+
} catch (error51) {
|
|
18252
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18253
|
+
return;
|
|
18254
|
+
}
|
|
18255
|
+
}
|
|
18256
|
+
try {
|
|
18257
|
+
withTransaction(
|
|
18258
|
+
db,
|
|
18259
|
+
() => {
|
|
18260
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18261
|
+
if (alreadyDropped) return;
|
|
18262
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18263
|
+
db.exec(statement);
|
|
18264
|
+
}
|
|
18265
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18266
|
+
migration.tag,
|
|
18267
|
+
Date.now()
|
|
18268
|
+
);
|
|
18269
|
+
},
|
|
18270
|
+
"IMMEDIATE"
|
|
18271
|
+
);
|
|
18272
|
+
} catch (error51) {
|
|
18273
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18274
|
+
}
|
|
18275
|
+
}
|
|
18276
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18277
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18278
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18279
|
+
tightenFile(backup);
|
|
18280
|
+
return backup;
|
|
17948
18281
|
}
|
|
17949
18282
|
var TOKEN_USAGE_COLUMNS = [
|
|
17950
18283
|
{
|
|
@@ -17973,6 +18306,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17973
18306
|
}
|
|
17974
18307
|
];
|
|
17975
18308
|
function ensureTokenUsageColumns(db) {
|
|
18309
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
17976
18310
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17977
18311
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17978
18312
|
if (!existing.has(column.name)) {
|
|
@@ -18035,7 +18369,182 @@ function reconcileSourceProjectIds(db) {
|
|
|
18035
18369
|
"IMMEDIATE"
|
|
18036
18370
|
);
|
|
18037
18371
|
} catch (error51) {
|
|
18038
|
-
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18372
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18373
|
+
}
|
|
18374
|
+
}
|
|
18375
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18376
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18377
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18378
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18379
|
+
return row?.lastRowid ?? 0;
|
|
18380
|
+
}
|
|
18381
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18382
|
+
db.prepare(
|
|
18383
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18384
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18385
|
+
).run(source, lastRowid);
|
|
18386
|
+
}
|
|
18387
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18388
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18389
|
+
let processed = 0;
|
|
18390
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18391
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18392
|
+
if (rows.length === 0) return true;
|
|
18393
|
+
withTransaction(
|
|
18394
|
+
db,
|
|
18395
|
+
() => {
|
|
18396
|
+
handleRows(rows);
|
|
18397
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18398
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18399
|
+
},
|
|
18400
|
+
"IMMEDIATE"
|
|
18401
|
+
);
|
|
18402
|
+
processed += rows.length;
|
|
18403
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18404
|
+
}
|
|
18405
|
+
return false;
|
|
18406
|
+
}
|
|
18407
|
+
function parseLegacyEventMetadata(raw) {
|
|
18408
|
+
if (raw === null) return void 0;
|
|
18409
|
+
try {
|
|
18410
|
+
return JSON.parse(raw);
|
|
18411
|
+
} catch {
|
|
18412
|
+
return void 0;
|
|
18413
|
+
}
|
|
18414
|
+
}
|
|
18415
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18416
|
+
return JSON.stringify(
|
|
18417
|
+
toCaptureAttributes({
|
|
18418
|
+
id: row.id,
|
|
18419
|
+
sourceTool: row.sourceTool,
|
|
18420
|
+
kind: row.kind,
|
|
18421
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18422
|
+
contentHash: row.contentHash,
|
|
18423
|
+
content: row.content,
|
|
18424
|
+
metadata: row.metadata
|
|
18425
|
+
})
|
|
18426
|
+
);
|
|
18427
|
+
}
|
|
18428
|
+
function copyLegacyEvents(db) {
|
|
18429
|
+
const selectStmt = db.prepare(
|
|
18430
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18431
|
+
content_hash AS contentHash, content, metadata
|
|
18432
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18433
|
+
);
|
|
18434
|
+
const insertStmt = db.prepare(
|
|
18435
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18436
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18437
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18438
|
+
);
|
|
18439
|
+
const stubRootStmt = db.prepare(
|
|
18440
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18441
|
+
);
|
|
18442
|
+
return drainLegacyTable(
|
|
18443
|
+
db,
|
|
18444
|
+
"events",
|
|
18445
|
+
selectStmt,
|
|
18446
|
+
(rows) => {
|
|
18447
|
+
for (const row of rows) {
|
|
18448
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18449
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18450
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18451
|
+
insertStmt.run(
|
|
18452
|
+
bindParams({
|
|
18453
|
+
id: row.id,
|
|
18454
|
+
parentId: sessionId,
|
|
18455
|
+
rootSessionId: sessionId,
|
|
18456
|
+
eventType: row.kind,
|
|
18457
|
+
startedAt: row.occurredAt,
|
|
18458
|
+
content: row.content,
|
|
18459
|
+
contentHash: row.contentHash,
|
|
18460
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18461
|
+
})
|
|
18462
|
+
);
|
|
18463
|
+
}
|
|
18464
|
+
}
|
|
18465
|
+
);
|
|
18466
|
+
}
|
|
18467
|
+
function copyLegacyFindings(db) {
|
|
18468
|
+
const selectStmt = db.prepare(
|
|
18469
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18470
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18471
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18472
|
+
first_detected_at AS firstDetectedAt
|
|
18473
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18474
|
+
);
|
|
18475
|
+
const definitionStmt = db.prepare(
|
|
18476
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18477
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18478
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18479
|
+
);
|
|
18480
|
+
const findingStmt = db.prepare(
|
|
18481
|
+
`INSERT INTO inspection_findings
|
|
18482
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18483
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18484
|
+
finding_key, first_detected_at)
|
|
18485
|
+
VALUES
|
|
18486
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18487
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18488
|
+
:findingKey, :firstDetectedAt)
|
|
18489
|
+
ON CONFLICT(id) DO NOTHING
|
|
18490
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18491
|
+
first_detected_at = CASE
|
|
18492
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18493
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18494
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18495
|
+
END`
|
|
18496
|
+
);
|
|
18497
|
+
return drainLegacyTable(
|
|
18498
|
+
db,
|
|
18499
|
+
"findings",
|
|
18500
|
+
selectStmt,
|
|
18501
|
+
(rows) => {
|
|
18502
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18503
|
+
for (const row of rows) {
|
|
18504
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18505
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18506
|
+
if (definitionId === void 0) {
|
|
18507
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18508
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18509
|
+
definitionStmt.run(
|
|
18510
|
+
bindParams({
|
|
18511
|
+
id: definitionId,
|
|
18512
|
+
ruleId: row.ruleId,
|
|
18513
|
+
name: row.ruleId,
|
|
18514
|
+
category: row.category,
|
|
18515
|
+
severity: row.severity,
|
|
18516
|
+
definition: "",
|
|
18517
|
+
version: version2
|
|
18518
|
+
})
|
|
18519
|
+
);
|
|
18520
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18521
|
+
}
|
|
18522
|
+
findingStmt.run(
|
|
18523
|
+
bindParams({
|
|
18524
|
+
id: row.id,
|
|
18525
|
+
auditEventId: row.eventId,
|
|
18526
|
+
inspectionDefinitionId: definitionId,
|
|
18527
|
+
spanStart: row.spanStart,
|
|
18528
|
+
spanEnd: row.spanEnd,
|
|
18529
|
+
maskedMatch: row.maskedMatch,
|
|
18530
|
+
actionTaken: row.actionTaken,
|
|
18531
|
+
confidence: row.confidence,
|
|
18532
|
+
findingKey: row.findingKey,
|
|
18533
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18534
|
+
})
|
|
18535
|
+
);
|
|
18536
|
+
}
|
|
18537
|
+
}
|
|
18538
|
+
);
|
|
18539
|
+
}
|
|
18540
|
+
function runLegacyHistoryBackfill(db) {
|
|
18541
|
+
try {
|
|
18542
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18543
|
+
if (!eventsCaughtUp) return false;
|
|
18544
|
+
return copyLegacyFindings(db);
|
|
18545
|
+
} catch (error51) {
|
|
18546
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18547
|
+
return false;
|
|
18039
18548
|
}
|
|
18040
18549
|
}
|
|
18041
18550
|
function isForeignSqliteLineage(db) {
|
|
@@ -18043,6 +18552,7 @@ function isForeignSqliteLineage(db) {
|
|
|
18043
18552
|
return columnNames(db, "events").includes("tenant_id");
|
|
18044
18553
|
}
|
|
18045
18554
|
function ensureSyncedAtColumn(db, table2) {
|
|
18555
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18046
18556
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18047
18557
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18048
18558
|
}
|
|
@@ -18063,6 +18573,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18063
18573
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18064
18574
|
)`);
|
|
18065
18575
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18576
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18066
18577
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18067
18578
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18068
18579
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18081,29 +18592,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18081
18592
|
blocked_at INTEGER NOT NULL
|
|
18082
18593
|
)`);
|
|
18083
18594
|
}
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18092
|
-
try {
|
|
18093
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18094
|
-
} catch {
|
|
18095
|
-
}
|
|
18096
|
-
}
|
|
18097
|
-
function walSidecars(file2) {
|
|
18098
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18099
|
-
}
|
|
18100
|
-
function tightenPerms(file2) {
|
|
18101
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18102
|
-
try {
|
|
18103
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18104
|
-
} catch {
|
|
18105
|
-
}
|
|
18106
|
-
}
|
|
18595
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18596
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18597
|
+
rule_key TEXT PRIMARY KEY,
|
|
18598
|
+
verdict TEXT NOT NULL,
|
|
18599
|
+
worst_probe_ms REAL NOT NULL,
|
|
18600
|
+
checked_at INTEGER NOT NULL
|
|
18601
|
+
)`);
|
|
18107
18602
|
}
|
|
18108
18603
|
|
|
18109
18604
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18125,51 +18620,6 @@ function parseJsonObject(s) {
|
|
|
18125
18620
|
return void 0;
|
|
18126
18621
|
}
|
|
18127
18622
|
|
|
18128
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18129
|
-
function allRows(stmt, params) {
|
|
18130
|
-
if (params === void 0) return stmt.all();
|
|
18131
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18132
|
-
return stmt.all(params);
|
|
18133
|
-
}
|
|
18134
|
-
function getRow(stmt, params) {
|
|
18135
|
-
if (params === void 0) return stmt.get();
|
|
18136
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18137
|
-
return stmt.get(params);
|
|
18138
|
-
}
|
|
18139
|
-
function intToBool(raw) {
|
|
18140
|
-
return raw === 1 || raw === true;
|
|
18141
|
-
}
|
|
18142
|
-
function boolToInt(b) {
|
|
18143
|
-
return b ? 1 : 0;
|
|
18144
|
-
}
|
|
18145
|
-
function bindParams(row) {
|
|
18146
|
-
const out = {};
|
|
18147
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18148
|
-
out[key] = value === void 0 ? null : value;
|
|
18149
|
-
}
|
|
18150
|
-
return out;
|
|
18151
|
-
}
|
|
18152
|
-
function countScalar(db, sql, params) {
|
|
18153
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18154
|
-
}
|
|
18155
|
-
function countBy(db, sql, params) {
|
|
18156
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18157
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18158
|
-
map2.set(row.k, row.n);
|
|
18159
|
-
}
|
|
18160
|
-
return map2;
|
|
18161
|
-
}
|
|
18162
|
-
function mapRowsTolerant(rows, map2) {
|
|
18163
|
-
const out = [];
|
|
18164
|
-
for (const row of rows) {
|
|
18165
|
-
try {
|
|
18166
|
-
out.push(map2(row));
|
|
18167
|
-
} catch {
|
|
18168
|
-
}
|
|
18169
|
-
}
|
|
18170
|
-
return out;
|
|
18171
|
-
}
|
|
18172
|
-
|
|
18173
18623
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18174
18624
|
var DAY_MS = 864e5;
|
|
18175
18625
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18786,6 +19236,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18786
19236
|
})
|
|
18787
19237
|
);
|
|
18788
19238
|
}
|
|
19239
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19240
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19241
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19242
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19243
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19244
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19245
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19246
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19247
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19248
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19249
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19250
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19251
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19252
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19253
|
+
}
|
|
18789
19254
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18790
19255
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18791
19256
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19247,8 +19712,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19247
19712
|
)
|
|
19248
19713
|
);
|
|
19249
19714
|
}
|
|
19250
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19251
|
-
//
|
|
19715
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19716
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19717
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19718
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19719
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19720
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19721
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19722
|
+
// to the old findings⋈events one.
|
|
19252
19723
|
countFindingsLast30d(ruleIds) {
|
|
19253
19724
|
if (ruleIds.length === 0) return 0;
|
|
19254
19725
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19256,8 +19727,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19256
19727
|
return countScalar(
|
|
19257
19728
|
this.db,
|
|
19258
19729
|
`SELECT count(*) AS n
|
|
19259
|
-
FROM
|
|
19260
|
-
|
|
19730
|
+
FROM inspection_findings f
|
|
19731
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19732
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19733
|
+
WHERE e.started_at >= ?
|
|
19734
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19735
|
+
AND d.rule_id IN (${inClause})`,
|
|
19261
19736
|
[since, ...ruleIds]
|
|
19262
19737
|
);
|
|
19263
19738
|
}
|
|
@@ -19267,35 +19742,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19267
19742
|
var SqliteEventsRepository = class {
|
|
19268
19743
|
constructor(db) {
|
|
19269
19744
|
this.db = db;
|
|
19270
|
-
this.insertStmt = db.prepare(
|
|
19271
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19272
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19273
|
-
);
|
|
19274
19745
|
}
|
|
19275
19746
|
db;
|
|
19276
|
-
|
|
19277
|
-
|
|
19278
|
-
|
|
19279
|
-
this.insertStmt.run(
|
|
19280
|
-
bindParams({
|
|
19281
|
-
id: row.id,
|
|
19282
|
-
sourceTool: row.sourceTool,
|
|
19283
|
-
kind: row.kind,
|
|
19284
|
-
occurredAt: row.occurredAt,
|
|
19285
|
-
contentHash: row.contentHash,
|
|
19286
|
-
content: row.content,
|
|
19287
|
-
metadata: row.metadata
|
|
19288
|
-
})
|
|
19289
|
-
);
|
|
19290
|
-
}
|
|
19291
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19292
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19293
|
-
// never duplicates findings.
|
|
19747
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19748
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19749
|
+
// scan never duplicates findings.
|
|
19294
19750
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19295
19751
|
// async EventsReadPort contract.
|
|
19752
|
+
//
|
|
19753
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19754
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19755
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19756
|
+
// index-friendly rather than walking rows that can never match.
|
|
19296
19757
|
contentHashes() {
|
|
19297
19758
|
const rows = allRows(
|
|
19298
|
-
this.db.prepare(
|
|
19759
|
+
this.db.prepare(
|
|
19760
|
+
`SELECT content_hash FROM audit_events
|
|
19761
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19762
|
+
)
|
|
19299
19763
|
);
|
|
19300
19764
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19301
19765
|
}
|
|
@@ -19631,17 +20095,20 @@ function parseExceptionRow(row) {
|
|
|
19631
20095
|
}
|
|
19632
20096
|
|
|
19633
20097
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19634
|
-
function
|
|
20098
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19635
20099
|
return `(
|
|
19636
|
-
SELECT fr
|
|
20100
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19637
20101
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19638
20102
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19639
20103
|
LIMIT 1
|
|
19640
20104
|
)`;
|
|
19641
20105
|
}
|
|
20106
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20107
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20108
|
+
}
|
|
19642
20109
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19643
|
-
SELECT finding_key, status FROM (
|
|
19644
|
-
SELECT fr.finding_key, fr.status,
|
|
20110
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20111
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19645
20112
|
ROW_NUMBER() OVER (
|
|
19646
20113
|
PARTITION BY fr.finding_key
|
|
19647
20114
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19668,68 +20135,21 @@ var DAY_MS3 = 864e5;
|
|
|
19668
20135
|
var SqliteFindingsRepository = class {
|
|
19669
20136
|
constructor(db) {
|
|
19670
20137
|
this.db = db;
|
|
19671
|
-
this.insertStmt = db.prepare(
|
|
19672
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19673
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19674
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19675
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19676
|
-
event_id = excluded.event_id,
|
|
19677
|
-
category = excluded.category,
|
|
19678
|
-
severity = excluded.severity,
|
|
19679
|
-
span_start = excluded.span_start,
|
|
19680
|
-
span_end = excluded.span_end,
|
|
19681
|
-
masked_match = excluded.masked_match,
|
|
19682
|
-
action_taken = excluded.action_taken,
|
|
19683
|
-
confidence = excluded.confidence`
|
|
19684
|
-
);
|
|
19685
|
-
this.sessionDupStmt = db.prepare(
|
|
19686
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19687
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19688
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19689
|
-
LIMIT 1`
|
|
19690
|
-
);
|
|
19691
20138
|
}
|
|
19692
20139
|
db;
|
|
19693
|
-
insertStmt;
|
|
19694
|
-
sessionDupStmt;
|
|
19695
|
-
insertFindings(findings, scope = {}) {
|
|
19696
|
-
for (const finding of findings) {
|
|
19697
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19698
|
-
const row = toFindingRow(finding);
|
|
19699
|
-
this.insertStmt.run({
|
|
19700
|
-
id: row.id,
|
|
19701
|
-
eventId: row.eventId,
|
|
19702
|
-
ruleId: row.ruleId,
|
|
19703
|
-
category: row.category,
|
|
19704
|
-
severity: row.severity,
|
|
19705
|
-
spanStart: row.spanStart,
|
|
19706
|
-
spanEnd: row.spanEnd,
|
|
19707
|
-
maskedMatch: row.maskedMatch,
|
|
19708
|
-
actionTaken: row.actionTaken,
|
|
19709
|
-
confidence: row.confidence,
|
|
19710
|
-
findingKey: row.findingKey ?? null
|
|
19711
|
-
});
|
|
19712
|
-
}
|
|
19713
|
-
}
|
|
19714
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19715
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19716
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19717
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19718
|
-
const hit = this.sessionDupStmt.get({
|
|
19719
|
-
ruleId: finding.ruleId,
|
|
19720
|
-
maskedMatch: finding.maskedMatch,
|
|
19721
|
-
sessionId
|
|
19722
|
-
});
|
|
19723
|
-
return hit !== void 0;
|
|
19724
|
-
}
|
|
19725
20140
|
recentFindings(opts) {
|
|
19726
20141
|
const limit = opts?.limit ?? 50;
|
|
19727
20142
|
const rows = allRows(
|
|
19728
20143
|
this.db.prepare(
|
|
19729
|
-
`SELECT f.id, f.event_id,
|
|
19730
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19731
|
-
|
|
19732
|
-
|
|
20144
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20145
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20146
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20147
|
+
e.event_type AS kind
|
|
20148
|
+
FROM inspection_findings f
|
|
20149
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20150
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20151
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20152
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19733
20153
|
LIMIT :limit`
|
|
19734
20154
|
),
|
|
19735
20155
|
{ limit }
|
|
@@ -19751,25 +20171,34 @@ var SqliteFindingsRepository = class {
|
|
|
19751
20171
|
);
|
|
19752
20172
|
}
|
|
19753
20173
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19754
|
-
* session-stamped
|
|
20174
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19755
20175
|
* page can label its findings link without the grouped pipeline. */
|
|
19756
20176
|
sessionFindingsCount(sessionId) {
|
|
19757
20177
|
if (!sessionId) return Promise.resolve(0);
|
|
19758
20178
|
return Promise.resolve(
|
|
19759
20179
|
countScalar(
|
|
19760
20180
|
this.db,
|
|
19761
|
-
`SELECT count(*) AS n FROM
|
|
19762
|
-
JOIN
|
|
19763
|
-
WHERE
|
|
20181
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20182
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20183
|
+
WHERE e.root_session_id = :sessionId
|
|
20184
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19764
20185
|
{ sessionId }
|
|
19765
20186
|
)
|
|
19766
20187
|
);
|
|
19767
20188
|
}
|
|
19768
|
-
/** Per-rule transcript firing tally for one session —
|
|
19769
|
-
*
|
|
19770
|
-
*
|
|
19771
|
-
*
|
|
19772
|
-
*
|
|
20189
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20190
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20191
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20192
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20193
|
+
* tally with the deduped groups it lists.
|
|
20194
|
+
*
|
|
20195
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20196
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20197
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20198
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20199
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20200
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20201
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19773
20202
|
sessionFirings(sessionId) {
|
|
19774
20203
|
return Object.fromEntries(
|
|
19775
20204
|
countBy(
|
|
@@ -19779,18 +20208,25 @@ var SqliteFindingsRepository = class {
|
|
|
19779
20208
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19780
20209
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19781
20210
|
WHERE e.root_session_id = :sessionId
|
|
20211
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19782
20212
|
GROUP BY d.rule_id`,
|
|
19783
20213
|
{ sessionId }
|
|
19784
20214
|
)
|
|
19785
20215
|
);
|
|
19786
20216
|
}
|
|
19787
20217
|
/**
|
|
19788
|
-
* Grouped findings for the dashboard — joins
|
|
19789
|
-
* toolName from
|
|
19790
|
-
*
|
|
20218
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20219
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20220
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20221
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20222
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20223
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20224
|
+
* severity then recency. Filtering
|
|
19791
20225
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19792
20226
|
* reflect the full filtered set; `items` is the requested
|
|
19793
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20227
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20228
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20229
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19794
20230
|
*
|
|
19795
20231
|
* Two reads, neither of which materializes a row per finding:
|
|
19796
20232
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19803,10 +20239,11 @@ var SqliteFindingsRepository = class {
|
|
|
19803
20239
|
* rule is ever restated in SQL.
|
|
19804
20240
|
*/
|
|
19805
20241
|
listGroupedFindings(query) {
|
|
19806
|
-
const sessionPredicate = query.sessionId ? `
|
|
20242
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20243
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19807
20244
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19808
20245
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19809
|
-
predicate
|
|
20246
|
+
predicate,
|
|
19810
20247
|
params: sessionParams
|
|
19811
20248
|
});
|
|
19812
20249
|
const rows = allRows(
|
|
@@ -19814,24 +20251,26 @@ var SqliteFindingsRepository = class {
|
|
|
19814
20251
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19815
20252
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19816
20253
|
FROM (
|
|
19817
|
-
SELECT f.id AS id,
|
|
19818
|
-
|
|
20254
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20255
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19819
20256
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19820
|
-
e.
|
|
19821
|
-
json_extract(e.
|
|
19822
|
-
json_extract(e.
|
|
19823
|
-
json_extract(e.
|
|
19824
|
-
e.
|
|
20257
|
+
e.started_at AS occurred_at,
|
|
20258
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20259
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20260
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20261
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20262
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19825
20263
|
latest.status AS latest_status,
|
|
19826
20264
|
ROW_NUMBER() OVER (
|
|
19827
|
-
PARTITION BY
|
|
19828
|
-
ORDER BY e.
|
|
20265
|
+
PARTITION BY d.rule_id
|
|
20266
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19829
20267
|
) AS rn
|
|
19830
|
-
FROM
|
|
19831
|
-
JOIN
|
|
20268
|
+
FROM inspection_findings f
|
|
20269
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20270
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19832
20271
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19833
20272
|
ON latest.finding_key = f.finding_key
|
|
19834
|
-
${
|
|
20273
|
+
${predicate}
|
|
19835
20274
|
)
|
|
19836
20275
|
WHERE rn <= :cap
|
|
19837
20276
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19858,17 +20297,29 @@ var SqliteFindingsRepository = class {
|
|
|
19858
20297
|
severity: query.severity,
|
|
19859
20298
|
providers: query.provider,
|
|
19860
20299
|
actions: query.action,
|
|
20300
|
+
statuses: query.status,
|
|
19861
20301
|
subtype: query.subtype,
|
|
19862
20302
|
q: query.q
|
|
19863
20303
|
};
|
|
19864
20304
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19865
20305
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20306
|
+
const statusFilter = query.status ?? [];
|
|
19866
20307
|
const totals = {
|
|
19867
|
-
findings: sorted.reduce((acc, g) =>
|
|
20308
|
+
findings: sorted.reduce((acc, g) => {
|
|
20309
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20310
|
+
const agg = aggregates.get(g.id);
|
|
20311
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20312
|
+
}, 0),
|
|
19868
20313
|
groups: sorted.length
|
|
19869
20314
|
};
|
|
19870
20315
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19871
|
-
const
|
|
20316
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20317
|
+
const items = sorted.slice(0, limit).map(
|
|
20318
|
+
(g) => statusSet ? {
|
|
20319
|
+
...g,
|
|
20320
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20321
|
+
} : g
|
|
20322
|
+
);
|
|
19872
20323
|
return Promise.resolve({
|
|
19873
20324
|
totals,
|
|
19874
20325
|
facets,
|
|
@@ -19882,45 +20333,62 @@ var SqliteFindingsRepository = class {
|
|
|
19882
20333
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19883
20334
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19884
20335
|
*
|
|
19885
|
-
*
|
|
19886
|
-
*
|
|
19887
|
-
*
|
|
19888
|
-
* status
|
|
19889
|
-
*
|
|
19890
|
-
*
|
|
20336
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20337
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20338
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20339
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20340
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20341
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20342
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20343
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20344
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20345
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20346
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19891
20347
|
* a group's row stays small however many findings it holds.
|
|
19892
20348
|
*
|
|
19893
20349
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19894
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19895
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19896
|
-
* that is a string proportional to the store (~8MB over
|
|
19897
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19898
|
-
* match an instance outside the preview, which searching
|
|
19899
|
-
* would silently lose, so it is fetched only when the
|
|
19900
|
-
* carries a `q`.
|
|
20350
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20351
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20352
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20353
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20354
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20355
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20356
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20357
|
+
* path repeating across tuples.)
|
|
19901
20358
|
*/
|
|
19902
20359
|
groupAggregates(withSearchText, scope) {
|
|
19903
|
-
const
|
|
19904
|
-
group_concat(DISTINCT json_extract(e.
|
|
19905
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20360
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20361
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20362
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19906
20363
|
const rows = this.db.prepare(
|
|
19907
|
-
`SELECT
|
|
19908
|
-
|
|
19909
|
-
max(
|
|
19910
|
-
group_concat(
|
|
19911
|
-
group_concat(
|
|
19912
|
-
group_concat(
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
20364
|
+
`SELECT rule_id,
|
|
20365
|
+
sum(tuple_count) AS instance_count,
|
|
20366
|
+
max(latest_at) AS latest_at,
|
|
20367
|
+
group_concat(source_tools) AS source_tools,
|
|
20368
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20369
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20370
|
+
group_concat(repos) AS repos,
|
|
20371
|
+
group_concat(files) AS files,
|
|
20372
|
+
group_concat(tool_names) AS tool_names
|
|
20373
|
+
FROM (
|
|
20374
|
+
SELECT d.rule_id AS rule_id,
|
|
20375
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20376
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20377
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20378
|
+
count(*) AS tuple_count,
|
|
20379
|
+
max(e.started_at) AS latest_at,
|
|
20380
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20381
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20382
|
+
${innerSearchColumns}
|
|
20383
|
+
FROM inspection_findings f
|
|
20384
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20385
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20386
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20387
|
+
ON latest.finding_key = f.finding_key
|
|
20388
|
+
${scope.predicate}
|
|
20389
|
+
GROUP BY d.rule_id, status_tuple
|
|
20390
|
+
)
|
|
20391
|
+
GROUP BY rule_id`
|
|
19924
20392
|
).all(scope.params);
|
|
19925
20393
|
return new Map(
|
|
19926
20394
|
rows.map((r) => [
|
|
@@ -19930,13 +20398,14 @@ var SqliteFindingsRepository = class {
|
|
|
19930
20398
|
sourceTools: splitConcat(r.source_tools),
|
|
19931
20399
|
actionsTaken: splitConcat(r.actions_taken),
|
|
19932
20400
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19933
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20401
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
19934
20402
|
return {
|
|
19935
20403
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19936
20404
|
// so the marker stands in for the key itself (never rendered).
|
|
19937
20405
|
kind,
|
|
19938
20406
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19939
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20407
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20408
|
+
count: Number(count)
|
|
19940
20409
|
};
|
|
19941
20410
|
}),
|
|
19942
20411
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -19953,10 +20422,21 @@ var SqliteFindingsRepository = class {
|
|
|
19953
20422
|
);
|
|
19954
20423
|
}
|
|
19955
20424
|
healthSummary() {
|
|
19956
|
-
const total = countScalar(
|
|
20425
|
+
const total = countScalar(
|
|
20426
|
+
this.db,
|
|
20427
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20428
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20429
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20430
|
+
);
|
|
19957
20431
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19958
20432
|
const grouped = allRows(
|
|
19959
|
-
this.db.prepare(
|
|
20433
|
+
this.db.prepare(
|
|
20434
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20435
|
+
FROM inspection_findings f
|
|
20436
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20437
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20438
|
+
GROUP BY f.action_taken`
|
|
20439
|
+
)
|
|
19960
20440
|
);
|
|
19961
20441
|
for (const row of grouped) {
|
|
19962
20442
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -19964,12 +20444,15 @@ var SqliteFindingsRepository = class {
|
|
|
19964
20444
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19965
20445
|
const sevRows = allRows(
|
|
19966
20446
|
this.db.prepare(
|
|
19967
|
-
`SELECT
|
|
19968
|
-
FROM
|
|
20447
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20448
|
+
FROM inspection_findings f
|
|
20449
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20450
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19969
20451
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19970
20452
|
ON latest.finding_key = f.finding_key
|
|
19971
|
-
WHERE
|
|
19972
|
-
|
|
20453
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20454
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20455
|
+
GROUP BY d.severity`
|
|
19973
20456
|
)
|
|
19974
20457
|
);
|
|
19975
20458
|
for (const row of sevRows) {
|
|
@@ -19990,9 +20473,11 @@ var SqliteFindingsRepository = class {
|
|
|
19990
20473
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19991
20474
|
const rows = allRows(
|
|
19992
20475
|
this.db.prepare(
|
|
19993
|
-
`SELECT date(e.
|
|
19994
|
-
FROM
|
|
19995
|
-
|
|
20476
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20477
|
+
FROM inspection_findings f
|
|
20478
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20479
|
+
WHERE e.started_at >= :since
|
|
20480
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19996
20481
|
GROUP BY day, f.action_taken`
|
|
19997
20482
|
),
|
|
19998
20483
|
{ since }
|
|
@@ -20057,15 +20542,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20057
20542
|
this.insertStmt = db.prepare(
|
|
20058
20543
|
`INSERT INTO inspection_findings
|
|
20059
20544
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20060
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20545
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20546
|
+
finding_key, first_detected_at)
|
|
20061
20547
|
VALUES
|
|
20062
20548
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20063
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20064
|
-
|
|
20549
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20550
|
+
:findingKey,
|
|
20551
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20552
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20553
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20554
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20555
|
+
audit_event_id = excluded.audit_event_id,
|
|
20556
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20557
|
+
classified_data_id = excluded.classified_data_id,
|
|
20558
|
+
span_start = excluded.span_start,
|
|
20559
|
+
span_end = excluded.span_end,
|
|
20560
|
+
masked_match = excluded.masked_match,
|
|
20561
|
+
action_taken = excluded.action_taken,
|
|
20562
|
+
confidence = excluded.confidence`
|
|
20563
|
+
);
|
|
20564
|
+
this.sessionDupStmt = db.prepare(
|
|
20565
|
+
`SELECT 1 FROM inspection_findings f
|
|
20566
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20567
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20568
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20569
|
+
AND e.root_session_id = :sessionId
|
|
20570
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20571
|
+
LIMIT 1`
|
|
20572
|
+
);
|
|
20573
|
+
this.eventDupStmt = db.prepare(
|
|
20574
|
+
`SELECT 1 FROM inspection_findings f
|
|
20575
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20576
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20577
|
+
AND f.masked_match = :maskedMatch
|
|
20578
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20579
|
+
LIMIT 1`
|
|
20065
20580
|
);
|
|
20066
20581
|
}
|
|
20067
20582
|
db;
|
|
20068
20583
|
insertStmt;
|
|
20584
|
+
sessionDupStmt;
|
|
20585
|
+
eventDupStmt;
|
|
20586
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20587
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20588
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20589
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20590
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20591
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20592
|
+
}
|
|
20593
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20594
|
+
// recorded against the given audit event.
|
|
20595
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20596
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20597
|
+
}
|
|
20069
20598
|
insertFinding(input) {
|
|
20070
20599
|
const row = toInspectionFindingRow(input);
|
|
20071
20600
|
this.insertStmt.run(
|
|
@@ -20078,7 +20607,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20078
20607
|
spanEnd: row.spanEnd,
|
|
20079
20608
|
maskedMatch: row.maskedMatch,
|
|
20080
20609
|
actionTaken: row.actionTaken,
|
|
20081
|
-
confidence: row.confidence
|
|
20610
|
+
confidence: row.confidence,
|
|
20611
|
+
findingKey: row.findingKey,
|
|
20612
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20082
20613
|
})
|
|
20083
20614
|
);
|
|
20084
20615
|
}
|
|
@@ -20350,7 +20881,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20350
20881
|
installedRuleset() {
|
|
20351
20882
|
const rows = allRows(
|
|
20352
20883
|
this.db.prepare(
|
|
20353
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20884
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20354
20885
|
)
|
|
20355
20886
|
);
|
|
20356
20887
|
const out = {
|
|
@@ -20358,7 +20889,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20358
20889
|
enabledPacks: 0,
|
|
20359
20890
|
rules: [],
|
|
20360
20891
|
invalidRules: 0,
|
|
20361
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20892
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20893
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20362
20894
|
};
|
|
20363
20895
|
for (const row of rows) {
|
|
20364
20896
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20380,6 +20912,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20380
20912
|
if (parsed.success) {
|
|
20381
20913
|
out.rules.push(parsed.data);
|
|
20382
20914
|
out.ruleActions.set(parsed.data.id, action);
|
|
20915
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20383
20916
|
} else out.invalidRules += 1;
|
|
20384
20917
|
}
|
|
20385
20918
|
}
|
|
@@ -21554,19 +22087,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21554
22087
|
);
|
|
21555
22088
|
this.openAtRestStmt = db.prepare(
|
|
21556
22089
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21557
|
-
FROM
|
|
21558
|
-
JOIN
|
|
21559
|
-
WHERE e.
|
|
21560
|
-
AND json_extract(e.
|
|
22090
|
+
FROM inspection_findings f
|
|
22091
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22092
|
+
WHERE e.event_type = 'code_change'
|
|
22093
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21561
22094
|
AND f.finding_key IS NOT NULL
|
|
21562
22095
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21563
22096
|
);
|
|
21564
22097
|
this.resolvedAtRestStmt = db.prepare(
|
|
21565
22098
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21566
|
-
FROM
|
|
21567
|
-
JOIN
|
|
21568
|
-
WHERE e.
|
|
21569
|
-
AND json_extract(e.
|
|
22099
|
+
FROM inspection_findings f
|
|
22100
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22101
|
+
WHERE e.event_type = 'code_change'
|
|
22102
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21570
22103
|
AND f.finding_key IS NOT NULL
|
|
21571
22104
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21572
22105
|
);
|
|
@@ -21634,6 +22167,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21634
22167
|
}
|
|
21635
22168
|
};
|
|
21636
22169
|
|
|
22170
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22171
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22172
|
+
constructor(db) {
|
|
22173
|
+
this.db = db;
|
|
22174
|
+
this.upsertStmt = db.prepare(
|
|
22175
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22176
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22177
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22178
|
+
verdict = excluded.verdict,
|
|
22179
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22180
|
+
checked_at = excluded.checked_at`
|
|
22181
|
+
);
|
|
22182
|
+
this.readStmt = db.prepare(
|
|
22183
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22184
|
+
);
|
|
22185
|
+
}
|
|
22186
|
+
db;
|
|
22187
|
+
upsertStmt;
|
|
22188
|
+
readStmt;
|
|
22189
|
+
getVerdict(ruleKey) {
|
|
22190
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22191
|
+
}
|
|
22192
|
+
setVerdict(ruleKey, verdict, worstProbeMs2) {
|
|
22193
|
+
failOpenTransaction(this.db, () => {
|
|
22194
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
|
|
22195
|
+
});
|
|
22196
|
+
}
|
|
22197
|
+
};
|
|
22198
|
+
|
|
21637
22199
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21638
22200
|
var SqliteScanLedgerRepository = class {
|
|
21639
22201
|
constructor(db) {
|
|
@@ -21760,25 +22322,27 @@ var SqliteSecurityRepository = class {
|
|
|
21760
22322
|
severitySummary() {
|
|
21761
22323
|
const rows = allRows(
|
|
21762
22324
|
this.db.prepare(
|
|
21763
|
-
`SELECT
|
|
22325
|
+
`SELECT d.severity AS severity,
|
|
21764
22326
|
COUNT(*) AS count,
|
|
21765
22327
|
SUM(CASE
|
|
21766
|
-
WHEN e.
|
|
22328
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21767
22329
|
WHEN f.finding_key IS NULL THEN 0
|
|
21768
22330
|
WHEN latest.status = 'resolved' THEN 1
|
|
21769
22331
|
ELSE 0
|
|
21770
22332
|
END) AS caught,
|
|
21771
22333
|
SUM(CASE
|
|
21772
|
-
WHEN e.
|
|
22334
|
+
WHEN e.event_type = 'code_change'
|
|
21773
22335
|
AND f.finding_key IS NOT NULL
|
|
21774
22336
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21775
22337
|
ELSE 0
|
|
21776
22338
|
END) AS open_at_rest
|
|
21777
|
-
FROM
|
|
21778
|
-
JOIN
|
|
22339
|
+
FROM inspection_findings f
|
|
22340
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22341
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21779
22342
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21780
22343
|
ON latest.finding_key = f.finding_key
|
|
21781
|
-
|
|
22344
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22345
|
+
GROUP BY d.severity`
|
|
21782
22346
|
)
|
|
21783
22347
|
);
|
|
21784
22348
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21844,7 +22408,7 @@ var SqliteSecurityRepository = class {
|
|
|
21844
22408
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21845
22409
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21846
22410
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21847
|
-
// (
|
|
22411
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21848
22412
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21849
22413
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21850
22414
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21869,30 +22433,20 @@ var SqliteSecurityRepository = class {
|
|
|
21869
22433
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21870
22434
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21871
22435
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21872
|
-
//
|
|
21873
|
-
// the parent event's
|
|
21874
|
-
// backfill left null.
|
|
21875
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21876
|
-
|
|
21877
|
-
|
|
21878
|
-
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
WHERE fr.finding_key = f.finding_key
|
|
21885
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21886
|
-
LIMIT 1
|
|
21887
|
-
) AS latest_method,
|
|
21888
|
-
(
|
|
21889
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21890
|
-
WHERE fr.finding_key = f.finding_key
|
|
21891
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21892
|
-
LIMIT 1
|
|
21893
|
-
) AS latest_resolved_at
|
|
21894
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22436
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22437
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22438
|
+
// legacy/edge row the backfill left null.
|
|
22439
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22440
|
+
latest.status AS latest_status,
|
|
22441
|
+
latest.method AS latest_method,
|
|
22442
|
+
latest.resolved_at AS latest_resolved_at
|
|
22443
|
+
FROM inspection_findings f
|
|
22444
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22445
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22446
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22447
|
+
ON latest.finding_key = f.finding_key
|
|
21895
22448
|
WHERE f.finding_key IS NOT NULL
|
|
22449
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21896
22450
|
AND EXISTS (
|
|
21897
22451
|
SELECT 1 FROM finding_resolution fr
|
|
21898
22452
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21939,11 +22493,13 @@ var SqliteSecurityRepository = class {
|
|
|
21939
22493
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21940
22494
|
const rows = allRows(
|
|
21941
22495
|
this.db.prepare(
|
|
21942
|
-
`SELECT json_extract(e.
|
|
21943
|
-
FROM
|
|
21944
|
-
|
|
21945
|
-
|
|
21946
|
-
AND
|
|
22496
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22497
|
+
FROM inspection_findings f
|
|
22498
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22499
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22500
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22501
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22502
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
21947
22503
|
GROUP BY repo
|
|
21948
22504
|
ORDER BY c DESC, repo
|
|
21949
22505
|
LIMIT :limit`
|
|
@@ -21967,44 +22523,28 @@ var SqliteSecurityRepository = class {
|
|
|
21967
22523
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
21968
22524
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
21969
22525
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
21970
|
-
// finding's parent event (
|
|
21971
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
21972
|
-
// capped at `limit`.
|
|
22526
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22527
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22528
|
+
// DESC, capped at `limit`.
|
|
21973
22529
|
recentlyResolved(limit = 20) {
|
|
21974
22530
|
const rows = allRows(
|
|
21975
22531
|
this.db.prepare(
|
|
21976
22532
|
`SELECT f.finding_key AS finding_key,
|
|
21977
|
-
|
|
21978
|
-
|
|
21979
|
-
json_extract(e.
|
|
21980
|
-
COALESCE(f.first_detected_at, e.
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
WHERE e.kind = 'code_change'
|
|
22533
|
+
d.rule_id AS rule_id,
|
|
22534
|
+
d.severity AS severity,
|
|
22535
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22536
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22537
|
+
latest.resolved_at AS latest_resolved_at
|
|
22538
|
+
FROM inspection_findings f
|
|
22539
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22540
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22541
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22542
|
+
ON latest.finding_key = f.finding_key
|
|
22543
|
+
WHERE e.event_type = 'code_change'
|
|
21989
22544
|
AND f.finding_key IS NOT NULL
|
|
21990
|
-
AND
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21994
|
-
LIMIT 1
|
|
21995
|
-
) = 'resolved'
|
|
21996
|
-
AND (
|
|
21997
|
-
SELECT fr.method FROM finding_resolution fr
|
|
21998
|
-
WHERE fr.finding_key = f.finding_key
|
|
21999
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22000
|
-
LIMIT 1
|
|
22001
|
-
) = 'fixed-at-source'
|
|
22002
|
-
AND (
|
|
22003
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22004
|
-
WHERE fr.finding_key = f.finding_key
|
|
22005
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22006
|
-
LIMIT 1
|
|
22007
|
-
) IS NOT NULL
|
|
22545
|
+
AND latest.status = 'resolved'
|
|
22546
|
+
AND latest.method = 'fixed-at-source'
|
|
22547
|
+
AND latest.resolved_at IS NOT NULL
|
|
22008
22548
|
ORDER BY latest_resolved_at DESC
|
|
22009
22549
|
LIMIT :limit`
|
|
22010
22550
|
),
|
|
@@ -22023,15 +22563,18 @@ var SqliteSecurityRepository = class {
|
|
|
22023
22563
|
return Promise.resolve({ items });
|
|
22024
22564
|
}
|
|
22025
22565
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22026
|
-
// epoch-millis timestamp.
|
|
22566
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22027
22567
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22028
22568
|
findingsInRange(fromMs, toMs) {
|
|
22029
22569
|
const rows = allRows(
|
|
22030
22570
|
this.db.prepare(
|
|
22031
|
-
`SELECT e.
|
|
22032
|
-
FROM
|
|
22033
|
-
|
|
22034
|
-
|
|
22571
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22572
|
+
FROM inspection_findings f
|
|
22573
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22574
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22575
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22576
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22577
|
+
ORDER BY e.started_at`
|
|
22035
22578
|
),
|
|
22036
22579
|
{ from: fromMs, to: toMs }
|
|
22037
22580
|
);
|
|
@@ -22045,11 +22588,50 @@ var SqliteSecurityRepository = class {
|
|
|
22045
22588
|
|
|
22046
22589
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22047
22590
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22048
|
-
var
|
|
22591
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22592
|
+
var IN_CHUNK = 500;
|
|
22593
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22594
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22595
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22596
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22049
22597
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22050
22598
|
function parseNetwork(networkJson) {
|
|
22051
22599
|
return safeJson(networkJson, null);
|
|
22052
22600
|
}
|
|
22601
|
+
function capHits(all, mode) {
|
|
22602
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22603
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22604
|
+
}
|
|
22605
|
+
if (mode === "walk") {
|
|
22606
|
+
return {
|
|
22607
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22608
|
+
droppedFiles: [],
|
|
22609
|
+
truncated: true
|
|
22610
|
+
};
|
|
22611
|
+
}
|
|
22612
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22613
|
+
for (const hit of all) {
|
|
22614
|
+
const bucket = byFile.get(hit.site.file);
|
|
22615
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22616
|
+
else bucket.push(hit);
|
|
22617
|
+
}
|
|
22618
|
+
const hits = [];
|
|
22619
|
+
const droppedFiles = [];
|
|
22620
|
+
for (const [file2, bucket] of byFile) {
|
|
22621
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22622
|
+
else hits.push(...bucket);
|
|
22623
|
+
}
|
|
22624
|
+
return { hits, droppedFiles, truncated: true };
|
|
22625
|
+
}
|
|
22626
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22627
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22628
|
+
const dropped = new Set(droppedFiles);
|
|
22629
|
+
return {
|
|
22630
|
+
mode: "ledger",
|
|
22631
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22632
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22633
|
+
};
|
|
22634
|
+
}
|
|
22053
22635
|
function toEndpointSummary(row) {
|
|
22054
22636
|
return {
|
|
22055
22637
|
id: row.id,
|
|
@@ -22140,13 +22722,15 @@ var SqliteSharesRepository = class {
|
|
|
22140
22722
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22141
22723
|
const insecure = countScalar(
|
|
22142
22724
|
this.db,
|
|
22143
|
-
|
|
22725
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22726
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22144
22727
|
);
|
|
22145
22728
|
const needsReview = countScalar(
|
|
22146
22729
|
this.db,
|
|
22147
22730
|
`SELECT count(DISTINCT d.id) AS n
|
|
22148
22731
|
FROM share_destination d
|
|
22149
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22732
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22733
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22150
22734
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22151
22735
|
);
|
|
22152
22736
|
const kindCounts = countBy(
|
|
@@ -22156,6 +22740,7 @@ var SqliteSharesRepository = class {
|
|
|
22156
22740
|
const byKind = {
|
|
22157
22741
|
provider: kindCounts.get("provider") ?? 0,
|
|
22158
22742
|
internal: kindCounts.get("internal") ?? 0,
|
|
22743
|
+
external: kindCounts.get("external") ?? 0,
|
|
22159
22744
|
ip: kindCounts.get("ip") ?? 0
|
|
22160
22745
|
};
|
|
22161
22746
|
const trustCounts = countBy(
|
|
@@ -22218,36 +22803,329 @@ var SqliteSharesRepository = class {
|
|
|
22218
22803
|
});
|
|
22219
22804
|
return Promise.resolve({ items });
|
|
22220
22805
|
}
|
|
22221
|
-
getDestination(destinationId) {
|
|
22222
|
-
const dest = this.fetchDestinationById(destinationId);
|
|
22223
|
-
if (!dest) return Promise.resolve(null);
|
|
22224
|
-
const endpoints = this.fetchEndpoints([dest.id]);
|
|
22225
|
-
const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
|
|
22226
|
-
return Promise.resolve(buildDetail(dest, endpoints, callSites));
|
|
22806
|
+
getDestination(destinationId) {
|
|
22807
|
+
const dest = this.fetchDestinationById(destinationId);
|
|
22808
|
+
if (!dest) return Promise.resolve(null);
|
|
22809
|
+
const endpoints = this.fetchEndpoints([dest.id]);
|
|
22810
|
+
const callSites = this.fetchCallSites(endpoints.map((e) => e.id));
|
|
22811
|
+
return Promise.resolve(buildDetail(dest, endpoints, callSites));
|
|
22812
|
+
}
|
|
22813
|
+
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
22814
|
+
// Driven by a web-ui Server Action, not the hook path — errors surface to the
|
|
22815
|
+
// caller. Returns whether the destination existed, so the caller can tell a
|
|
22816
|
+
// real edit from a no-such-destination.
|
|
22817
|
+
/**
|
|
22818
|
+
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22819
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22820
|
+
*
|
|
22821
|
+
* The written row carries both the destination id and its host, so the
|
|
22822
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22823
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22824
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22825
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22826
|
+
* would otherwise race a concurrent prune.
|
|
22827
|
+
*/
|
|
22828
|
+
setEgressDecision(destinationId, decision) {
|
|
22829
|
+
let existed = false;
|
|
22830
|
+
withTransaction(
|
|
22831
|
+
this.db,
|
|
22832
|
+
() => {
|
|
22833
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22834
|
+
if (dest === void 0) return;
|
|
22835
|
+
existed = true;
|
|
22836
|
+
this.db.prepare(
|
|
22837
|
+
`DELETE FROM egress_decision_override
|
|
22838
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22839
|
+
).run({ host: dest.host, destinationId });
|
|
22840
|
+
if (decision === null) return;
|
|
22841
|
+
this.db.prepare(
|
|
22842
|
+
`INSERT INTO egress_decision_override
|
|
22843
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22844
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22845
|
+
).run({
|
|
22846
|
+
id: randomUUID7(),
|
|
22847
|
+
destinationId,
|
|
22848
|
+
host: dest.host,
|
|
22849
|
+
decision,
|
|
22850
|
+
now: Date.now()
|
|
22851
|
+
});
|
|
22852
|
+
},
|
|
22853
|
+
"IMMEDIATE"
|
|
22854
|
+
);
|
|
22855
|
+
return existed;
|
|
22856
|
+
}
|
|
22857
|
+
/**
|
|
22858
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22859
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22860
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22861
|
+
* references, and drop what no longer has evidence.
|
|
22862
|
+
*
|
|
22863
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22864
|
+
* display payload and never scope a delete. The whole write is one
|
|
22865
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22866
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22867
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22868
|
+
* ledger commit so the next scan retries.
|
|
22869
|
+
*
|
|
22870
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22871
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22872
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22873
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22874
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22875
|
+
*/
|
|
22876
|
+
recordProjectEgress(input) {
|
|
22877
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22878
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22879
|
+
const now = Date.now();
|
|
22880
|
+
let summary = {
|
|
22881
|
+
destinations: 0,
|
|
22882
|
+
endpoints: 0,
|
|
22883
|
+
callSites: 0,
|
|
22884
|
+
truncated,
|
|
22885
|
+
droppedFiles
|
|
22886
|
+
};
|
|
22887
|
+
withTransaction(
|
|
22888
|
+
this.db,
|
|
22889
|
+
() => {
|
|
22890
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22891
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22892
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22893
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22894
|
+
this.pruneOrphans();
|
|
22895
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22896
|
+
},
|
|
22897
|
+
"IMMEDIATE"
|
|
22898
|
+
);
|
|
22899
|
+
return summary;
|
|
22900
|
+
}
|
|
22901
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22902
|
+
/**
|
|
22903
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22904
|
+
*
|
|
22905
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22906
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22907
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22908
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22909
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22910
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22911
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22912
|
+
*/
|
|
22913
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22914
|
+
if (reconcile.mode === "walk") {
|
|
22915
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22916
|
+
this.db.prepare(
|
|
22917
|
+
`DELETE FROM share_call_site
|
|
22918
|
+
WHERE project_key = :key
|
|
22919
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22920
|
+
AND file NOT LIKE '.%'
|
|
22921
|
+
AND file NOT LIKE '%/.%'`
|
|
22922
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22923
|
+
return;
|
|
22924
|
+
}
|
|
22925
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22926
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22927
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22928
|
+
this.db.prepare(
|
|
22929
|
+
`DELETE FROM share_call_site
|
|
22930
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22931
|
+
).run(projectKey, ...chunk);
|
|
22932
|
+
}
|
|
22933
|
+
}
|
|
22934
|
+
/**
|
|
22935
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22936
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22937
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22938
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22939
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22940
|
+
* classification for this batch.
|
|
22941
|
+
*/
|
|
22942
|
+
upsertHits(input, hits, projectId, now) {
|
|
22943
|
+
if (hits.length === 0) return;
|
|
22944
|
+
const destStmt = this.db.prepare(
|
|
22945
|
+
`INSERT INTO share_destination
|
|
22946
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22947
|
+
created_at, updated_at)
|
|
22948
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22949
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22950
|
+
kind = excluded.kind,
|
|
22951
|
+
name = excluded.name,
|
|
22952
|
+
category = excluded.category,
|
|
22953
|
+
trust = excluded.trust,
|
|
22954
|
+
network_json = excluded.network_json,
|
|
22955
|
+
last_seen = excluded.last_seen,
|
|
22956
|
+
updated_at = excluded.updated_at`
|
|
22957
|
+
);
|
|
22958
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22959
|
+
const endpointStmt = this.db.prepare(
|
|
22960
|
+
`INSERT INTO share_endpoint
|
|
22961
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
22962
|
+
created_at, updated_at)
|
|
22963
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
22964
|
+
:now, :now)
|
|
22965
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
22966
|
+
transport = excluded.transport,
|
|
22967
|
+
template = excluded.template,
|
|
22968
|
+
data_class = excluded.data_class,
|
|
22969
|
+
last_seen = excluded.last_seen,
|
|
22970
|
+
updated_at = excluded.updated_at`
|
|
22971
|
+
);
|
|
22972
|
+
const endpointIdStmt = this.db.prepare(
|
|
22973
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
22974
|
+
);
|
|
22975
|
+
const siteStmt = this.db.prepare(
|
|
22976
|
+
`INSERT INTO share_call_site
|
|
22977
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
22978
|
+
project_id, created_at, updated_at)
|
|
22979
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
22980
|
+
:vendored, :projectId, :now, :now)
|
|
22981
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
22982
|
+
snippet = excluded.snippet,
|
|
22983
|
+
dynamic = excluded.dynamic,
|
|
22984
|
+
vendored = excluded.vendored,
|
|
22985
|
+
project = excluded.project,
|
|
22986
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
22987
|
+
updated_at = excluded.updated_at`
|
|
22988
|
+
);
|
|
22989
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
22990
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
22991
|
+
for (const hit of hits) {
|
|
22992
|
+
let destinationId = destIds.get(hit.host);
|
|
22993
|
+
if (destinationId === void 0) {
|
|
22994
|
+
destStmt.run({
|
|
22995
|
+
id: randomUUID7(),
|
|
22996
|
+
kind: hit.kind,
|
|
22997
|
+
name: hit.name,
|
|
22998
|
+
host: hit.host,
|
|
22999
|
+
category: hit.category,
|
|
23000
|
+
trust: hit.trust,
|
|
23001
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
23002
|
+
now
|
|
23003
|
+
});
|
|
23004
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
23005
|
+
destIds.set(hit.host, destinationId);
|
|
23006
|
+
}
|
|
23007
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
23008
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
23009
|
+
if (endpointId === void 0) {
|
|
23010
|
+
endpointStmt.run({
|
|
23011
|
+
id: randomUUID7(),
|
|
23012
|
+
destinationId,
|
|
23013
|
+
method: hit.method,
|
|
23014
|
+
transport: hit.transport,
|
|
23015
|
+
url: hit.url,
|
|
23016
|
+
template: boolToInt(hit.template),
|
|
23017
|
+
dataClass: hit.dataClass,
|
|
23018
|
+
now
|
|
23019
|
+
});
|
|
23020
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23021
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23022
|
+
}
|
|
23023
|
+
siteStmt.run({
|
|
23024
|
+
id: randomUUID7(),
|
|
23025
|
+
endpointId,
|
|
23026
|
+
project: input.project,
|
|
23027
|
+
projectKey: input.projectKey,
|
|
23028
|
+
file: hit.site.file,
|
|
23029
|
+
line: hit.site.line,
|
|
23030
|
+
snippet: hit.site.snippet,
|
|
23031
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23032
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23033
|
+
projectId,
|
|
23034
|
+
now
|
|
23035
|
+
});
|
|
23036
|
+
}
|
|
23037
|
+
}
|
|
23038
|
+
/**
|
|
23039
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23040
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23041
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23042
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23043
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23044
|
+
* representative.
|
|
23045
|
+
*/
|
|
23046
|
+
knownProjectId(projectKey) {
|
|
23047
|
+
return getRow(
|
|
23048
|
+
this.db.prepare(
|
|
23049
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23050
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23051
|
+
),
|
|
23052
|
+
[projectKey]
|
|
23053
|
+
)?.projectId ?? null;
|
|
22227
23054
|
}
|
|
22228
|
-
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
22229
|
-
// Driven by a web-ui Server Action, not the hook path — errors surface to the
|
|
22230
|
-
// caller. Returns whether the destination existed, so the caller can tell a
|
|
22231
|
-
// real edit from a no-such-destination.
|
|
22232
23055
|
/**
|
|
22233
|
-
*
|
|
22234
|
-
*
|
|
23056
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23057
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23058
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
22235
23059
|
*/
|
|
22236
|
-
|
|
22237
|
-
const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
|
|
22238
|
-
if (exists === void 0) return false;
|
|
22239
|
-
if (decision === null) {
|
|
22240
|
-
this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
|
|
22241
|
-
return true;
|
|
22242
|
-
}
|
|
23060
|
+
confirmLastSeen(projectKey, now) {
|
|
22243
23061
|
this.db.prepare(
|
|
22244
|
-
`
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
23062
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23063
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23064
|
+
).run({ now, key: projectKey });
|
|
23065
|
+
this.db.prepare(
|
|
23066
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23067
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23068
|
+
FROM share_endpoint e
|
|
23069
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23070
|
+
WHERE c.project_key = :key)`
|
|
23071
|
+
).run({ now, key: projectKey });
|
|
23072
|
+
}
|
|
23073
|
+
/**
|
|
23074
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23075
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23076
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23077
|
+
*
|
|
23078
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23079
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23080
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23081
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23082
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23083
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23084
|
+
*/
|
|
23085
|
+
pruneOrphans() {
|
|
23086
|
+
this.db.exec(
|
|
23087
|
+
`DELETE FROM share_endpoint
|
|
23088
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23089
|
+
);
|
|
23090
|
+
this.db.exec(
|
|
23091
|
+
`DELETE FROM egress_decision_override
|
|
23092
|
+
WHERE host IS NULL
|
|
23093
|
+
AND destination_id IN (
|
|
23094
|
+
SELECT d.id FROM share_destination d
|
|
23095
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23096
|
+
);
|
|
23097
|
+
this.db.exec(
|
|
23098
|
+
`DELETE FROM share_destination
|
|
23099
|
+
WHERE NOT EXISTS (
|
|
23100
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23101
|
+
);
|
|
23102
|
+
}
|
|
23103
|
+
/**
|
|
23104
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23105
|
+
* projects and carry no project column, so both are counted through the call
|
|
23106
|
+
* sites that reference them.
|
|
23107
|
+
*/
|
|
23108
|
+
projectTotals(projectKey) {
|
|
23109
|
+
return {
|
|
23110
|
+
destinations: countScalar(
|
|
23111
|
+
this.db,
|
|
23112
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23113
|
+
FROM share_endpoint e
|
|
23114
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23115
|
+
WHERE c.project_key = ?`,
|
|
23116
|
+
[projectKey]
|
|
23117
|
+
),
|
|
23118
|
+
endpoints: countScalar(
|
|
23119
|
+
this.db,
|
|
23120
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23121
|
+
[projectKey]
|
|
23122
|
+
),
|
|
23123
|
+
callSites: countScalar(
|
|
23124
|
+
this.db,
|
|
23125
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23126
|
+
[projectKey]
|
|
23127
|
+
)
|
|
23128
|
+
};
|
|
22251
23129
|
}
|
|
22252
23130
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22253
23131
|
mapDestRow(r) {
|
|
@@ -22267,7 +23145,8 @@ var SqliteSharesRepository = class {
|
|
|
22267
23145
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22268
23146
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22269
23147
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22270
|
-
d.created_at AS createdAt,
|
|
23148
|
+
d.created_at AS createdAt,
|
|
23149
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22271
23150
|
const conditions = [];
|
|
22272
23151
|
const params = [];
|
|
22273
23152
|
if (kinds && kinds.length > 0) {
|
|
@@ -22278,7 +23157,8 @@ var SqliteSharesRepository = class {
|
|
|
22278
23157
|
conditions.push(
|
|
22279
23158
|
`(d.trust IN ('unverified', 'ip')
|
|
22280
23159
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22281
|
-
WHERE re.destination_id = d.id
|
|
23160
|
+
WHERE re.destination_id = d.id
|
|
23161
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22282
23162
|
);
|
|
22283
23163
|
}
|
|
22284
23164
|
let sql;
|
|
@@ -22291,7 +23171,7 @@ var SqliteSharesRepository = class {
|
|
|
22291
23171
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22292
23172
|
sql = `SELECT DISTINCT ${cols}
|
|
22293
23173
|
FROM share_destination d
|
|
22294
|
-
|
|
23174
|
+
${OVERRIDE_JOIN}
|
|
22295
23175
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22296
23176
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22297
23177
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22299,7 +23179,7 @@ var SqliteSharesRepository = class {
|
|
|
22299
23179
|
} else {
|
|
22300
23180
|
sql = `SELECT ${cols}
|
|
22301
23181
|
FROM share_destination d
|
|
22302
|
-
|
|
23182
|
+
${OVERRIDE_JOIN}
|
|
22303
23183
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22304
23184
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22305
23185
|
}
|
|
@@ -22314,9 +23194,9 @@ var SqliteSharesRepository = class {
|
|
|
22314
23194
|
this.db.prepare(
|
|
22315
23195
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22316
23196
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22317
|
-
|
|
23197
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22318
23198
|
FROM share_destination d
|
|
22319
|
-
|
|
23199
|
+
${OVERRIDE_JOIN}
|
|
22320
23200
|
WHERE d.id = ?`
|
|
22321
23201
|
),
|
|
22322
23202
|
[destinationId]
|
|
@@ -22518,9 +23398,10 @@ function openWithPragmas(file2) {
|
|
|
22518
23398
|
}
|
|
22519
23399
|
function backupLegacyStore(file2) {
|
|
22520
23400
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
|
|
23401
|
+
renameSync2(file2, backup);
|
|
23402
|
+
tightenFile(backup);
|
|
23403
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23404
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22524
23405
|
}
|
|
22525
23406
|
return backup;
|
|
22526
23407
|
}
|
|
@@ -22536,7 +23417,7 @@ function openLocalDatabase(dir) {
|
|
|
22536
23417
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22537
23418
|
);
|
|
22538
23419
|
}
|
|
22539
|
-
applyMigrations(db);
|
|
23420
|
+
applyMigrations(db, file2);
|
|
22540
23421
|
tightenPerms(file2);
|
|
22541
23422
|
const events = new SqliteEventsRepository(db);
|
|
22542
23423
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22545,6 +23426,7 @@ function openLocalDatabase(dir) {
|
|
|
22545
23426
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22546
23427
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22547
23428
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23429
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22548
23430
|
const security = new SqliteSecurityRepository(db);
|
|
22549
23431
|
const detections = new SqliteDetectionsRepository(db);
|
|
22550
23432
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22562,9 +23444,56 @@ function openLocalDatabase(dir) {
|
|
|
22562
23444
|
policies.seedDefaults();
|
|
22563
23445
|
function recordCapture(event, detected) {
|
|
22564
23446
|
failOpenTransaction(db, () => {
|
|
22565
|
-
events.insertEvent(event);
|
|
22566
23447
|
const sessionId = event.metadata?.sessionId;
|
|
22567
|
-
|
|
23448
|
+
if (sessionId) {
|
|
23449
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23450
|
+
}
|
|
23451
|
+
const auditEventId = captureId(
|
|
23452
|
+
sessionId ?? null,
|
|
23453
|
+
event.contentHash,
|
|
23454
|
+
event.metadata?.filePath ?? null
|
|
23455
|
+
);
|
|
23456
|
+
auditEvents.insertAuditEvent({
|
|
23457
|
+
id: auditEventId,
|
|
23458
|
+
eventType: event.kind,
|
|
23459
|
+
startedAt: event.occurredAt,
|
|
23460
|
+
parentId: sessionId,
|
|
23461
|
+
rootSessionId: sessionId,
|
|
23462
|
+
content: event.content,
|
|
23463
|
+
contentHash: event.contentHash,
|
|
23464
|
+
attributes: toCaptureAttributes(event)
|
|
23465
|
+
});
|
|
23466
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23467
|
+
for (const finding of detected) {
|
|
23468
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23469
|
+
continue;
|
|
23470
|
+
}
|
|
23471
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23472
|
+
auditEventId,
|
|
23473
|
+
finding.ruleId,
|
|
23474
|
+
finding.maskedMatch,
|
|
23475
|
+
finding.span.start,
|
|
23476
|
+
finding.span.end
|
|
23477
|
+
)) {
|
|
23478
|
+
continue;
|
|
23479
|
+
}
|
|
23480
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23481
|
+
let definitionId = definitionIds.get(key);
|
|
23482
|
+
if (!definitionId) {
|
|
23483
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23484
|
+
definitionIds.set(key, definitionId);
|
|
23485
|
+
}
|
|
23486
|
+
inspectionFindings.insertFinding({
|
|
23487
|
+
id: finding.id,
|
|
23488
|
+
auditEventId,
|
|
23489
|
+
inspectionDefinitionId: definitionId,
|
|
23490
|
+
span: finding.span,
|
|
23491
|
+
maskedMatch: finding.maskedMatch,
|
|
23492
|
+
actionTaken: finding.actionTaken,
|
|
23493
|
+
confidence: finding.confidence,
|
|
23494
|
+
findingKey: finding.findingKey ?? void 0
|
|
23495
|
+
});
|
|
23496
|
+
}
|
|
22568
23497
|
});
|
|
22569
23498
|
}
|
|
22570
23499
|
function ensureInventory(ctx) {
|
|
@@ -22682,6 +23611,7 @@ function openLocalDatabase(dir) {
|
|
|
22682
23611
|
scanLedger,
|
|
22683
23612
|
exceptions,
|
|
22684
23613
|
resolutions,
|
|
23614
|
+
ruleProbeCache,
|
|
22685
23615
|
security,
|
|
22686
23616
|
detections,
|
|
22687
23617
|
shares,
|
|
@@ -22711,9 +23641,19 @@ function openLocalDatabase(dir) {
|
|
|
22711
23641
|
};
|
|
22712
23642
|
}
|
|
22713
23643
|
|
|
23644
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23645
|
+
import { createHash as createHash3 } from "crypto";
|
|
23646
|
+
function normalizeFilePath(filePath) {
|
|
23647
|
+
return filePath.replaceAll("\\", "/");
|
|
23648
|
+
}
|
|
23649
|
+
function computeFindingKey(input) {
|
|
23650
|
+
const normalizedPath = normalizeFilePath(input.filePath);
|
|
23651
|
+
return createHash3("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
23652
|
+
}
|
|
23653
|
+
|
|
22714
23654
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22715
23655
|
import { createHmac, randomBytes } from "crypto";
|
|
22716
|
-
import {
|
|
23656
|
+
import { readFileSync } from "fs";
|
|
22717
23657
|
import { join as join2 } from "path";
|
|
22718
23658
|
var KEY_FILENAME = "exception.key";
|
|
22719
23659
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -22741,15 +23681,9 @@ function parseKeyFile(raw) {
|
|
|
22741
23681
|
function writeKeyFile(dataDir2, key) {
|
|
22742
23682
|
ensureDataDirSync(dataDir2);
|
|
22743
23683
|
const file2 = keyFilePath(dataDir2);
|
|
22744
|
-
const tmp = `${file2}.tmp`;
|
|
22745
23684
|
const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
|
|
22746
|
-
|
|
22747
|
-
|
|
22748
|
-
renameSync2(tmp, file2);
|
|
22749
|
-
try {
|
|
22750
|
-
chmodSync2(file2, DATA_FILE_MODE);
|
|
22751
|
-
} catch {
|
|
22752
|
-
}
|
|
23685
|
+
writeOwnerOnlyFileSync(file2, `${body}
|
|
23686
|
+
`);
|
|
22753
23687
|
return key;
|
|
22754
23688
|
}
|
|
22755
23689
|
function readFingerprintKey(dataDir2) {
|
|
@@ -22765,10 +23699,7 @@ function readFingerprintKey(dataDir2) {
|
|
|
22765
23699
|
function loadOrCreateFingerprintKey(dataDir2) {
|
|
22766
23700
|
const existing = readFingerprintKey(dataDir2);
|
|
22767
23701
|
if (existing) {
|
|
22768
|
-
|
|
22769
|
-
chmodSync2(keyFilePath(dataDir2), DATA_FILE_MODE);
|
|
22770
|
-
} catch {
|
|
22771
|
-
}
|
|
23702
|
+
tightenFile(keyFilePath(dataDir2));
|
|
22772
23703
|
return existing;
|
|
22773
23704
|
}
|
|
22774
23705
|
return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
|
|
@@ -22778,8 +23709,8 @@ function fingerprintValue(key, raw) {
|
|
|
22778
23709
|
}
|
|
22779
23710
|
|
|
22780
23711
|
// ../../packages/persistence/src/local-layout.ts
|
|
22781
|
-
import {
|
|
22782
|
-
import {
|
|
23712
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23713
|
+
import { mkdir } from "fs/promises";
|
|
22783
23714
|
import { homedir } from "os";
|
|
22784
23715
|
import { join as join3 } from "path";
|
|
22785
23716
|
function defaultDataDir() {
|
|
@@ -22794,6 +23725,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22794
23725
|
function dbPath(base = defaultDataDir()) {
|
|
22795
23726
|
return join3(dataDir(base), "aka.db");
|
|
22796
23727
|
}
|
|
23728
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23729
|
+
ensureDataDirSync(dir);
|
|
23730
|
+
}
|
|
22797
23731
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22798
23732
|
const moves = [
|
|
22799
23733
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22801,19 +23735,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22801
23735
|
];
|
|
22802
23736
|
for (const { name, dest } of moves) {
|
|
22803
23737
|
try {
|
|
22804
|
-
|
|
22805
|
-
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
}
|
|
22809
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23738
|
+
ensureDataDirSync(dest);
|
|
23739
|
+
const moved = join3(dest, name);
|
|
23740
|
+
renameSync3(join3(base, name), moved);
|
|
23741
|
+
tightenFile(moved);
|
|
22810
23742
|
} catch {
|
|
22811
23743
|
}
|
|
22812
23744
|
}
|
|
22813
23745
|
}
|
|
22814
23746
|
|
|
22815
23747
|
// ../../packages/persistence/src/settings.ts
|
|
22816
|
-
import { readFileSync as readFileSync2
|
|
23748
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22817
23749
|
import { join as join4 } from "path";
|
|
22818
23750
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22819
23751
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22835,7 +23767,7 @@ function readJson(file2) {
|
|
|
22835
23767
|
}
|
|
22836
23768
|
|
|
22837
23769
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22838
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23770
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22839
23771
|
import { join as join5 } from "path";
|
|
22840
23772
|
var MARKER = "warn-era-capped";
|
|
22841
23773
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22843,11 +23775,15 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22843
23775
|
const marker = join5(dataDir2, MARKER);
|
|
22844
23776
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22845
23777
|
const capped = db.policies.capCategoryActions();
|
|
22846
|
-
|
|
23778
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22847
23779
|
`, { mode: DATA_FILE_MODE });
|
|
22848
23780
|
return { capped };
|
|
22849
23781
|
}
|
|
22850
23782
|
|
|
23783
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23784
|
+
import { existsSync as existsSync3 } from "fs";
|
|
23785
|
+
import { join as join6 } from "path";
|
|
23786
|
+
|
|
22851
23787
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
22852
23788
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
22853
23789
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -22898,6 +23834,12 @@ function resolveProvider() {
|
|
|
22898
23834
|
|
|
22899
23835
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22900
23836
|
function loadConfig(base = defaultDataDir()) {
|
|
23837
|
+
try {
|
|
23838
|
+
ensureLayoutDirSync(base);
|
|
23839
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23840
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23841
|
+
} catch {
|
|
23842
|
+
}
|
|
22901
23843
|
migrateLegacyLayout(base);
|
|
22902
23844
|
const settings = readWorkspaceSettings(base);
|
|
22903
23845
|
return {
|
|
@@ -22920,15 +23862,583 @@ function resolveProviderSafe() {
|
|
|
22920
23862
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22921
23863
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22922
23864
|
import { homedir as homedir2 } from "os";
|
|
22923
|
-
import { basename as basename2, join as
|
|
23865
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23866
|
+
|
|
23867
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23868
|
+
var EXTRACTOR_VERSION = "1";
|
|
23869
|
+
var PROVIDER_REGISTRY = [
|
|
23870
|
+
{
|
|
23871
|
+
id: "stripe",
|
|
23872
|
+
name: "Stripe",
|
|
23873
|
+
category: "Payments",
|
|
23874
|
+
hostSuffixes: ["stripe.com"],
|
|
23875
|
+
apiBase: "https://api.stripe.com",
|
|
23876
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23877
|
+
sdks: {
|
|
23878
|
+
npm: ["stripe"],
|
|
23879
|
+
pypi: ["stripe"],
|
|
23880
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23881
|
+
maven: ["com.stripe"],
|
|
23882
|
+
rubygems: ["stripe"],
|
|
23883
|
+
composer: ["stripe/stripe-php"],
|
|
23884
|
+
nuget: ["Stripe.net"]
|
|
23885
|
+
}
|
|
23886
|
+
},
|
|
23887
|
+
{
|
|
23888
|
+
id: "datadog",
|
|
23889
|
+
name: "Datadog",
|
|
23890
|
+
category: "Observability",
|
|
23891
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23892
|
+
apiBase: "https://api.datadoghq.com",
|
|
23893
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23894
|
+
sdks: {
|
|
23895
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23896
|
+
pypi: ["datadog", "ddtrace"],
|
|
23897
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23898
|
+
maven: ["com.datadoghq"],
|
|
23899
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23900
|
+
nuget: ["Datadog.Trace"]
|
|
23901
|
+
}
|
|
23902
|
+
},
|
|
23903
|
+
{
|
|
23904
|
+
id: "newrelic",
|
|
23905
|
+
name: "New Relic",
|
|
23906
|
+
category: "Observability",
|
|
23907
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23908
|
+
apiBase: "https://api.newrelic.com",
|
|
23909
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23910
|
+
sdks: {
|
|
23911
|
+
npm: ["newrelic"],
|
|
23912
|
+
pypi: ["newrelic"],
|
|
23913
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23914
|
+
maven: ["com.newrelic.agent.java"],
|
|
23915
|
+
rubygems: ["newrelic_rpm"],
|
|
23916
|
+
nuget: ["NewRelic.Agent"]
|
|
23917
|
+
}
|
|
23918
|
+
},
|
|
23919
|
+
{
|
|
23920
|
+
id: "sentry",
|
|
23921
|
+
name: "Sentry",
|
|
23922
|
+
category: "Error tracking",
|
|
23923
|
+
hostSuffixes: ["sentry.io"],
|
|
23924
|
+
apiBase: "https://sentry.io",
|
|
23925
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23926
|
+
sdks: {
|
|
23927
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23928
|
+
pypi: ["sentry-sdk"],
|
|
23929
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23930
|
+
maven: ["io.sentry"],
|
|
23931
|
+
rubygems: ["sentry-ruby"],
|
|
23932
|
+
cargo: ["sentry"],
|
|
23933
|
+
composer: ["sentry/sentry"],
|
|
23934
|
+
nuget: ["Sentry"]
|
|
23935
|
+
}
|
|
23936
|
+
},
|
|
23937
|
+
{
|
|
23938
|
+
id: "openai",
|
|
23939
|
+
name: "OpenAI",
|
|
23940
|
+
category: "LLM provider",
|
|
23941
|
+
hostSuffixes: ["openai.com"],
|
|
23942
|
+
apiBase: "https://api.openai.com",
|
|
23943
|
+
defaultDataClasses: ["pii", "source"],
|
|
23944
|
+
sdks: {
|
|
23945
|
+
npm: ["openai"],
|
|
23946
|
+
pypi: ["openai"],
|
|
23947
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23948
|
+
maven: ["com.openai"],
|
|
23949
|
+
rubygems: ["ruby-openai"],
|
|
23950
|
+
cargo: ["async-openai"],
|
|
23951
|
+
composer: ["openai-php/client"],
|
|
23952
|
+
nuget: ["OpenAI"]
|
|
23953
|
+
}
|
|
23954
|
+
},
|
|
23955
|
+
{
|
|
23956
|
+
id: "anthropic",
|
|
23957
|
+
name: "Anthropic",
|
|
23958
|
+
category: "LLM provider",
|
|
23959
|
+
hostSuffixes: ["anthropic.com"],
|
|
23960
|
+
apiBase: "https://api.anthropic.com",
|
|
23961
|
+
defaultDataClasses: ["pii", "source"],
|
|
23962
|
+
sdks: {
|
|
23963
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23964
|
+
pypi: ["anthropic"],
|
|
23965
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23966
|
+
nuget: ["Anthropic.SDK"]
|
|
23967
|
+
}
|
|
23968
|
+
},
|
|
23969
|
+
{
|
|
23970
|
+
id: "aws",
|
|
23971
|
+
name: "Amazon Web Services",
|
|
23972
|
+
category: "Cloud platform",
|
|
23973
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23974
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23975
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23976
|
+
sdks: {
|
|
23977
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23978
|
+
pypi: ["boto3"],
|
|
23979
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23980
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23981
|
+
rubygems: ["aws-sdk-s3"],
|
|
23982
|
+
cargo: ["aws-sdk-s3"],
|
|
23983
|
+
nuget: ["AWSSDK.S3"]
|
|
23984
|
+
}
|
|
23985
|
+
},
|
|
23986
|
+
{
|
|
23987
|
+
id: "gcp",
|
|
23988
|
+
name: "Google Cloud",
|
|
23989
|
+
category: "Cloud platform",
|
|
23990
|
+
hostSuffixes: ["googleapis.com"],
|
|
23991
|
+
apiBase: "https://storage.googleapis.com",
|
|
23992
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23993
|
+
sdks: {
|
|
23994
|
+
npm: ["@google-cloud/storage"],
|
|
23995
|
+
pypi: ["google-cloud-storage"],
|
|
23996
|
+
go: ["cloud.google.com/go"],
|
|
23997
|
+
maven: ["com.google.cloud"],
|
|
23998
|
+
rubygems: ["google-cloud-storage"],
|
|
23999
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
24000
|
+
}
|
|
24001
|
+
},
|
|
24002
|
+
{
|
|
24003
|
+
id: "azure",
|
|
24004
|
+
name: "Microsoft Azure",
|
|
24005
|
+
category: "Cloud platform",
|
|
24006
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
24007
|
+
apiBase: "https://management.azure.com",
|
|
24008
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24009
|
+
sdks: {
|
|
24010
|
+
npm: ["@azure/storage-blob"],
|
|
24011
|
+
pypi: ["azure-storage-blob"],
|
|
24012
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
24013
|
+
maven: ["com.azure"],
|
|
24014
|
+
rubygems: ["azure-storage-blob"],
|
|
24015
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
24016
|
+
}
|
|
24017
|
+
},
|
|
24018
|
+
{
|
|
24019
|
+
id: "slack",
|
|
24020
|
+
name: "Slack",
|
|
24021
|
+
category: "Notifications",
|
|
24022
|
+
hostSuffixes: ["slack.com"],
|
|
24023
|
+
apiBase: "https://slack.com/api",
|
|
24024
|
+
defaultDataClasses: ["logs"],
|
|
24025
|
+
sdks: {
|
|
24026
|
+
npm: ["@slack/web-api"],
|
|
24027
|
+
pypi: ["slack-sdk"],
|
|
24028
|
+
go: ["github.com/slack-go/slack"],
|
|
24029
|
+
maven: ["com.slack.api"],
|
|
24030
|
+
rubygems: ["slack-ruby-client"],
|
|
24031
|
+
composer: ["slack-php/slack-api"],
|
|
24032
|
+
nuget: ["SlackNet"]
|
|
24033
|
+
}
|
|
24034
|
+
},
|
|
24035
|
+
{
|
|
24036
|
+
id: "segment",
|
|
24037
|
+
name: "Segment",
|
|
24038
|
+
category: "Analytics",
|
|
24039
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
24040
|
+
apiBase: "https://api.segment.io",
|
|
24041
|
+
defaultDataClasses: ["customer"],
|
|
24042
|
+
sdks: {
|
|
24043
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
24044
|
+
pypi: ["segment-analytics-python"],
|
|
24045
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
24046
|
+
maven: ["com.segment.analytics.java"],
|
|
24047
|
+
rubygems: ["analytics-ruby"],
|
|
24048
|
+
nuget: ["Analytics"]
|
|
24049
|
+
}
|
|
24050
|
+
},
|
|
24051
|
+
{
|
|
24052
|
+
id: "twilio",
|
|
24053
|
+
name: "Twilio",
|
|
24054
|
+
category: "Communications",
|
|
24055
|
+
hostSuffixes: ["twilio.com"],
|
|
24056
|
+
apiBase: "https://api.twilio.com",
|
|
24057
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24058
|
+
sdks: {
|
|
24059
|
+
npm: ["twilio"],
|
|
24060
|
+
pypi: ["twilio"],
|
|
24061
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24062
|
+
maven: ["com.twilio.sdk"],
|
|
24063
|
+
rubygems: ["twilio-ruby"],
|
|
24064
|
+
composer: ["twilio/sdk"],
|
|
24065
|
+
nuget: ["Twilio"]
|
|
24066
|
+
}
|
|
24067
|
+
},
|
|
24068
|
+
{
|
|
24069
|
+
id: "sendgrid",
|
|
24070
|
+
name: "SendGrid",
|
|
24071
|
+
category: "Email",
|
|
24072
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24073
|
+
apiBase: "https://api.sendgrid.com",
|
|
24074
|
+
defaultDataClasses: ["pii"],
|
|
24075
|
+
sdks: {
|
|
24076
|
+
npm: ["@sendgrid/mail"],
|
|
24077
|
+
pypi: ["sendgrid"],
|
|
24078
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24079
|
+
maven: ["com.sendgrid"],
|
|
24080
|
+
rubygems: ["sendgrid-ruby"],
|
|
24081
|
+
composer: ["sendgrid/sendgrid"],
|
|
24082
|
+
nuget: ["SendGrid"]
|
|
24083
|
+
}
|
|
24084
|
+
},
|
|
24085
|
+
{
|
|
24086
|
+
id: "mailgun",
|
|
24087
|
+
name: "Mailgun",
|
|
24088
|
+
category: "Email",
|
|
24089
|
+
hostSuffixes: ["mailgun.net"],
|
|
24090
|
+
apiBase: "https://api.mailgun.net",
|
|
24091
|
+
defaultDataClasses: ["pii"],
|
|
24092
|
+
sdks: {
|
|
24093
|
+
npm: ["mailgun.js"],
|
|
24094
|
+
pypi: ["mailgun"],
|
|
24095
|
+
rubygems: ["mailgun-ruby"],
|
|
24096
|
+
composer: ["mailgun/mailgun-php"],
|
|
24097
|
+
nuget: ["Mailgun"]
|
|
24098
|
+
}
|
|
24099
|
+
},
|
|
24100
|
+
{
|
|
24101
|
+
id: "mixpanel",
|
|
24102
|
+
name: "Mixpanel",
|
|
24103
|
+
category: "Analytics",
|
|
24104
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24105
|
+
apiBase: "https://api.mixpanel.com",
|
|
24106
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24107
|
+
sdks: {
|
|
24108
|
+
npm: ["mixpanel"],
|
|
24109
|
+
pypi: ["mixpanel"],
|
|
24110
|
+
rubygems: ["mixpanel-ruby"],
|
|
24111
|
+
nuget: ["Mixpanel"]
|
|
24112
|
+
}
|
|
24113
|
+
},
|
|
24114
|
+
{
|
|
24115
|
+
id: "amplitude",
|
|
24116
|
+
name: "Amplitude",
|
|
24117
|
+
category: "Analytics",
|
|
24118
|
+
hostSuffixes: ["amplitude.com"],
|
|
24119
|
+
apiBase: "https://api2.amplitude.com",
|
|
24120
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24121
|
+
sdks: {
|
|
24122
|
+
npm: ["@amplitude/analytics-node"],
|
|
24123
|
+
pypi: ["amplitude-analytics"],
|
|
24124
|
+
nuget: ["Amplitude"]
|
|
24125
|
+
}
|
|
24126
|
+
},
|
|
24127
|
+
{
|
|
24128
|
+
id: "posthog",
|
|
24129
|
+
name: "PostHog",
|
|
24130
|
+
category: "Analytics",
|
|
24131
|
+
hostSuffixes: ["posthog.com"],
|
|
24132
|
+
apiBase: "https://us.i.posthog.com",
|
|
24133
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24134
|
+
sdks: {
|
|
24135
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24136
|
+
pypi: ["posthog"],
|
|
24137
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24138
|
+
rubygems: ["posthog-ruby"],
|
|
24139
|
+
composer: ["posthog/posthog-php"],
|
|
24140
|
+
nuget: ["PostHog"]
|
|
24141
|
+
}
|
|
24142
|
+
},
|
|
24143
|
+
{
|
|
24144
|
+
id: "honeycomb",
|
|
24145
|
+
name: "Honeycomb",
|
|
24146
|
+
category: "Observability",
|
|
24147
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24148
|
+
apiBase: "https://api.honeycomb.io",
|
|
24149
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24150
|
+
sdks: {
|
|
24151
|
+
npm: ["libhoney"],
|
|
24152
|
+
pypi: ["libhoney"],
|
|
24153
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24154
|
+
rubygems: ["libhoney"]
|
|
24155
|
+
}
|
|
24156
|
+
},
|
|
24157
|
+
{
|
|
24158
|
+
id: "grafana",
|
|
24159
|
+
name: "Grafana Cloud",
|
|
24160
|
+
category: "Observability",
|
|
24161
|
+
hostSuffixes: ["grafana.net"],
|
|
24162
|
+
apiBase: "https://grafana.net",
|
|
24163
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24164
|
+
sdks: {
|
|
24165
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24166
|
+
}
|
|
24167
|
+
},
|
|
24168
|
+
{
|
|
24169
|
+
id: "splunk",
|
|
24170
|
+
name: "Splunk",
|
|
24171
|
+
category: "Observability",
|
|
24172
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24173
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24174
|
+
defaultDataClasses: ["logs"],
|
|
24175
|
+
sdks: {
|
|
24176
|
+
npm: ["splunk-logging"],
|
|
24177
|
+
pypi: ["splunk-sdk"],
|
|
24178
|
+
maven: ["com.splunk"],
|
|
24179
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24180
|
+
}
|
|
24181
|
+
},
|
|
24182
|
+
{
|
|
24183
|
+
id: "pagerduty",
|
|
24184
|
+
name: "PagerDuty",
|
|
24185
|
+
category: "Incident response",
|
|
24186
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24187
|
+
apiBase: "https://api.pagerduty.com",
|
|
24188
|
+
defaultDataClasses: ["logs"],
|
|
24189
|
+
sdks: {
|
|
24190
|
+
npm: ["@pagerduty/pdjs"],
|
|
24191
|
+
pypi: ["pdpyras"],
|
|
24192
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24193
|
+
rubygems: ["pagerduty"]
|
|
24194
|
+
}
|
|
24195
|
+
},
|
|
24196
|
+
{
|
|
24197
|
+
id: "github",
|
|
24198
|
+
name: "GitHub",
|
|
24199
|
+
category: "Developer platform",
|
|
24200
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24201
|
+
apiBase: "https://api.github.com",
|
|
24202
|
+
defaultDataClasses: ["source"],
|
|
24203
|
+
sdks: {
|
|
24204
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24205
|
+
pypi: ["pygithub"],
|
|
24206
|
+
go: ["github.com/google/go-github"],
|
|
24207
|
+
maven: ["org.kohsuke.github-api"],
|
|
24208
|
+
rubygems: ["octokit"],
|
|
24209
|
+
cargo: ["octocrab"],
|
|
24210
|
+
composer: ["knplabs/github-api"],
|
|
24211
|
+
nuget: ["Octokit"]
|
|
24212
|
+
}
|
|
24213
|
+
},
|
|
24214
|
+
{
|
|
24215
|
+
id: "gitlab",
|
|
24216
|
+
name: "GitLab",
|
|
24217
|
+
category: "Developer platform",
|
|
24218
|
+
hostSuffixes: ["gitlab.com"],
|
|
24219
|
+
apiBase: "https://gitlab.com/api",
|
|
24220
|
+
defaultDataClasses: ["source"],
|
|
24221
|
+
sdks: {
|
|
24222
|
+
npm: ["@gitbeaker/rest"],
|
|
24223
|
+
pypi: ["python-gitlab"],
|
|
24224
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24225
|
+
rubygems: ["gitlab"],
|
|
24226
|
+
nuget: ["GitLabApiClient"]
|
|
24227
|
+
}
|
|
24228
|
+
},
|
|
24229
|
+
{
|
|
24230
|
+
id: "auth0",
|
|
24231
|
+
name: "Auth0",
|
|
24232
|
+
category: "Identity",
|
|
24233
|
+
hostSuffixes: ["auth0.com"],
|
|
24234
|
+
apiBase: "https://login.auth0.com",
|
|
24235
|
+
defaultDataClasses: ["pii"],
|
|
24236
|
+
sdks: {
|
|
24237
|
+
npm: ["auth0"],
|
|
24238
|
+
pypi: ["auth0-python"],
|
|
24239
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24240
|
+
maven: ["com.auth0"],
|
|
24241
|
+
rubygems: ["auth0"],
|
|
24242
|
+
composer: ["auth0/auth0-php"],
|
|
24243
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24244
|
+
}
|
|
24245
|
+
},
|
|
24246
|
+
{
|
|
24247
|
+
id: "okta",
|
|
24248
|
+
name: "Okta",
|
|
24249
|
+
category: "Identity",
|
|
24250
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24251
|
+
apiBase: "https://login.okta.com",
|
|
24252
|
+
defaultDataClasses: ["pii"],
|
|
24253
|
+
sdks: {
|
|
24254
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24255
|
+
pypi: ["okta"],
|
|
24256
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24257
|
+
maven: ["com.okta.sdk"],
|
|
24258
|
+
nuget: ["Okta.Sdk"]
|
|
24259
|
+
}
|
|
24260
|
+
},
|
|
24261
|
+
{
|
|
24262
|
+
id: "clerk",
|
|
24263
|
+
name: "Clerk",
|
|
24264
|
+
category: "Identity",
|
|
24265
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24266
|
+
apiBase: "https://api.clerk.com",
|
|
24267
|
+
defaultDataClasses: ["pii"],
|
|
24268
|
+
sdks: {
|
|
24269
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24270
|
+
pypi: ["clerk-backend-api"],
|
|
24271
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24272
|
+
}
|
|
24273
|
+
},
|
|
24274
|
+
{
|
|
24275
|
+
id: "supabase",
|
|
24276
|
+
name: "Supabase",
|
|
24277
|
+
category: "Backend platform",
|
|
24278
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24279
|
+
apiBase: "https://api.supabase.com",
|
|
24280
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24281
|
+
sdks: {
|
|
24282
|
+
npm: ["@supabase/supabase-js"],
|
|
24283
|
+
pypi: ["supabase"],
|
|
24284
|
+
cargo: ["postgrest"]
|
|
24285
|
+
}
|
|
24286
|
+
},
|
|
24287
|
+
{
|
|
24288
|
+
id: "firebase",
|
|
24289
|
+
name: "Firebase",
|
|
24290
|
+
category: "Backend platform",
|
|
24291
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24292
|
+
apiBase: "https://firebaseio.com",
|
|
24293
|
+
defaultDataClasses: ["customer"],
|
|
24294
|
+
sdks: {
|
|
24295
|
+
npm: ["firebase", "firebase-admin"],
|
|
24296
|
+
pypi: ["firebase-admin"],
|
|
24297
|
+
go: ["firebase.google.com/go"],
|
|
24298
|
+
maven: ["com.google.firebase"]
|
|
24299
|
+
}
|
|
24300
|
+
},
|
|
24301
|
+
{
|
|
24302
|
+
id: "mongodb-atlas",
|
|
24303
|
+
name: "MongoDB Atlas",
|
|
24304
|
+
category: "Database SaaS",
|
|
24305
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24306
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24307
|
+
defaultDataClasses: ["customer"],
|
|
24308
|
+
sdks: {
|
|
24309
|
+
npm: ["mongodb"],
|
|
24310
|
+
pypi: ["pymongo"],
|
|
24311
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24312
|
+
maven: ["org.mongodb"],
|
|
24313
|
+
rubygems: ["mongo"],
|
|
24314
|
+
cargo: ["mongodb"],
|
|
24315
|
+
nuget: ["MongoDB.Driver"]
|
|
24316
|
+
}
|
|
24317
|
+
},
|
|
24318
|
+
{
|
|
24319
|
+
id: "planetscale",
|
|
24320
|
+
name: "PlanetScale",
|
|
24321
|
+
category: "Database SaaS",
|
|
24322
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24323
|
+
apiBase: "https://api.planetscale.com",
|
|
24324
|
+
defaultDataClasses: ["customer"],
|
|
24325
|
+
sdks: {
|
|
24326
|
+
npm: ["@planetscale/database"],
|
|
24327
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24328
|
+
}
|
|
24329
|
+
},
|
|
24330
|
+
{
|
|
24331
|
+
id: "algolia",
|
|
24332
|
+
name: "Algolia",
|
|
24333
|
+
category: "Search SaaS",
|
|
24334
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24335
|
+
apiBase: "https://algolia.net",
|
|
24336
|
+
defaultDataClasses: ["customer"],
|
|
24337
|
+
sdks: {
|
|
24338
|
+
npm: ["algoliasearch"],
|
|
24339
|
+
pypi: ["algoliasearch"],
|
|
24340
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24341
|
+
maven: ["com.algolia"],
|
|
24342
|
+
rubygems: ["algolia"],
|
|
24343
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24344
|
+
nuget: ["Algolia.Search"]
|
|
24345
|
+
}
|
|
24346
|
+
},
|
|
24347
|
+
{
|
|
24348
|
+
id: "cloudflare",
|
|
24349
|
+
name: "Cloudflare",
|
|
24350
|
+
category: "CDN / edge",
|
|
24351
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24352
|
+
apiBase: "https://api.cloudflare.com",
|
|
24353
|
+
defaultDataClasses: ["logs"],
|
|
24354
|
+
sdks: {
|
|
24355
|
+
npm: ["cloudflare"],
|
|
24356
|
+
pypi: ["cloudflare"],
|
|
24357
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24358
|
+
nuget: ["CloudFlare.Client"]
|
|
24359
|
+
}
|
|
24360
|
+
},
|
|
24361
|
+
{
|
|
24362
|
+
id: "huggingface",
|
|
24363
|
+
name: "Hugging Face",
|
|
24364
|
+
category: "LLM provider",
|
|
24365
|
+
hostSuffixes: ["huggingface.co"],
|
|
24366
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24367
|
+
defaultDataClasses: ["source"],
|
|
24368
|
+
sdks: {
|
|
24369
|
+
npm: ["@huggingface/inference"],
|
|
24370
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24371
|
+
rubygems: ["hugging-face"]
|
|
24372
|
+
}
|
|
24373
|
+
},
|
|
24374
|
+
{
|
|
24375
|
+
id: "cohere",
|
|
24376
|
+
name: "Cohere",
|
|
24377
|
+
category: "LLM provider",
|
|
24378
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24379
|
+
apiBase: "https://api.cohere.com",
|
|
24380
|
+
defaultDataClasses: ["pii", "source"],
|
|
24381
|
+
sdks: {
|
|
24382
|
+
npm: ["cohere-ai"],
|
|
24383
|
+
pypi: ["cohere"],
|
|
24384
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24385
|
+
}
|
|
24386
|
+
},
|
|
24387
|
+
{
|
|
24388
|
+
id: "mistral",
|
|
24389
|
+
name: "Mistral AI",
|
|
24390
|
+
category: "LLM provider",
|
|
24391
|
+
hostSuffixes: ["mistral.ai"],
|
|
24392
|
+
apiBase: "https://api.mistral.ai",
|
|
24393
|
+
defaultDataClasses: ["pii", "source"],
|
|
24394
|
+
sdks: {
|
|
24395
|
+
npm: ["@mistralai/mistralai"],
|
|
24396
|
+
pypi: ["mistralai"],
|
|
24397
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24398
|
+
}
|
|
24399
|
+
}
|
|
24400
|
+
];
|
|
24401
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24402
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24403
|
+
|
|
24404
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24405
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24406
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24407
|
+
var SECRET_VALUE = new RegExp(
|
|
24408
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24409
|
+
"gi"
|
|
24410
|
+
);
|
|
24411
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24412
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24413
|
+
"gi"
|
|
24414
|
+
);
|
|
24415
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24416
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24417
|
+
{
|
|
24418
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24419
|
+
prefix: "/api/webhooks/"
|
|
24420
|
+
},
|
|
24421
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24422
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24423
|
+
];
|
|
24424
|
+
function escapeRegExp(literal2) {
|
|
24425
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24426
|
+
}
|
|
24427
|
+
var WEBHOOK_URL = new RegExp(
|
|
24428
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24429
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24430
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24431
|
+
"gi"
|
|
24432
|
+
);
|
|
22924
24433
|
|
|
22925
24434
|
// ../../packages/detections/src/escape-regexp.ts
|
|
22926
|
-
function
|
|
24435
|
+
function escapeRegExp2(value) {
|
|
22927
24436
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22928
24437
|
}
|
|
22929
24438
|
|
|
22930
24439
|
// ../../packages/detections/src/matchers/limits.ts
|
|
22931
24440
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24441
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
22932
24442
|
|
|
22933
24443
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22934
24444
|
var KeywordMatcher2 = class {
|
|
@@ -22939,7 +24449,7 @@ var KeywordMatcher2 = class {
|
|
|
22939
24449
|
for (const kw of keywords) {
|
|
22940
24450
|
if (kw.length === 0) continue;
|
|
22941
24451
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22942
|
-
const re = new RegExp(
|
|
24452
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
22943
24453
|
let m;
|
|
22944
24454
|
while ((m = re.exec(text)) !== null) {
|
|
22945
24455
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -22956,9 +24466,13 @@ var RegexMatcher2 = class {
|
|
|
22956
24466
|
if (rule.matcher.type !== "regex") return [];
|
|
22957
24467
|
const { pattern, flags, captureGroup } = rule.matcher;
|
|
22958
24468
|
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24469
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
22959
24470
|
const spans = [];
|
|
22960
24471
|
let m;
|
|
22961
|
-
|
|
24472
|
+
const maxIterations = scanText2.length + 1;
|
|
24473
|
+
let iterations = 0;
|
|
24474
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24475
|
+
if (++iterations > maxIterations) break;
|
|
22962
24476
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
22963
24477
|
if (m[0].length === 0) re.lastIndex++;
|
|
22964
24478
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -23062,7 +24576,7 @@ function isCorroborated(candidate, candidates, text) {
|
|
|
23062
24576
|
for (const label of labels) {
|
|
23063
24577
|
const trimmed = label.trim();
|
|
23064
24578
|
if (trimmed.length === 0) continue;
|
|
23065
|
-
const re = new RegExp(`(?<![A-Za-z0-9])${
|
|
24579
|
+
const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
|
|
23066
24580
|
if (re.test(haystack)) return true;
|
|
23067
24581
|
}
|
|
23068
24582
|
}
|
|
@@ -23227,6 +24741,112 @@ var CONFIG_POSTURE_RULES = [
|
|
|
23227
24741
|
}
|
|
23228
24742
|
];
|
|
23229
24743
|
|
|
24744
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24745
|
+
var BUDGET_MS = 100;
|
|
24746
|
+
var EXPONENTIAL_UNITS = [
|
|
24747
|
+
"a",
|
|
24748
|
+
"0",
|
|
24749
|
+
" ",
|
|
24750
|
+
"x",
|
|
24751
|
+
"ab",
|
|
24752
|
+
"a.",
|
|
24753
|
+
"a-",
|
|
24754
|
+
"a_",
|
|
24755
|
+
"a@",
|
|
24756
|
+
"a/",
|
|
24757
|
+
"a:",
|
|
24758
|
+
"a=",
|
|
24759
|
+
"a;",
|
|
24760
|
+
"aA0",
|
|
24761
|
+
" "
|
|
24762
|
+
];
|
|
24763
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24764
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24765
|
+
);
|
|
24766
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24767
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24768
|
+
);
|
|
24769
|
+
function literalPrefix(pattern) {
|
|
24770
|
+
let prefix = "";
|
|
24771
|
+
let i = 0;
|
|
24772
|
+
if (pattern[i] === "^") i++;
|
|
24773
|
+
while (i < pattern.length) {
|
|
24774
|
+
const c = pattern[i];
|
|
24775
|
+
if (c === void 0) break;
|
|
24776
|
+
if (c === "\\") {
|
|
24777
|
+
const next = pattern[i + 1];
|
|
24778
|
+
if (next === "b" || next === "B") {
|
|
24779
|
+
i += 2;
|
|
24780
|
+
continue;
|
|
24781
|
+
}
|
|
24782
|
+
if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
|
|
24783
|
+
prefix += next;
|
|
24784
|
+
i += 2;
|
|
24785
|
+
continue;
|
|
24786
|
+
}
|
|
24787
|
+
if ("([{.*+?|)]}^$".includes(c)) break;
|
|
24788
|
+
prefix += c;
|
|
24789
|
+
i++;
|
|
24790
|
+
}
|
|
24791
|
+
return prefix;
|
|
24792
|
+
}
|
|
24793
|
+
function fuelChars(pattern) {
|
|
24794
|
+
const fuel = /* @__PURE__ */ new Set();
|
|
24795
|
+
for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
|
|
24796
|
+
const body = m[1];
|
|
24797
|
+
if (body === void 0) continue;
|
|
24798
|
+
const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
|
|
24799
|
+
const rangeStart = range?.[1];
|
|
24800
|
+
if (rangeStart !== void 0) fuel.add(rangeStart);
|
|
24801
|
+
else {
|
|
24802
|
+
const literal2 = body.replace(/\\/g, "")[0];
|
|
24803
|
+
if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
|
|
24804
|
+
}
|
|
24805
|
+
}
|
|
24806
|
+
if (pattern.includes("\\w")) fuel.add("a");
|
|
24807
|
+
if (pattern.includes("\\d")) fuel.add("0");
|
|
24808
|
+
if (pattern.includes("\\s")) fuel.add(" ");
|
|
24809
|
+
if (/(?<!\\)\./.test(pattern)) fuel.add("a");
|
|
24810
|
+
if (fuel.size === 0) fuel.add("a");
|
|
24811
|
+
return [...fuel];
|
|
24812
|
+
}
|
|
24813
|
+
function derivedProbes(pattern) {
|
|
24814
|
+
const prefix = literalPrefix(pattern);
|
|
24815
|
+
const fuel = fuelChars(pattern);
|
|
24816
|
+
const terminators = ["!", "#", "~", "\n"];
|
|
24817
|
+
const probes = [];
|
|
24818
|
+
for (const f of fuel) {
|
|
24819
|
+
for (const term of terminators) {
|
|
24820
|
+
if (term === f) continue;
|
|
24821
|
+
for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
|
|
24822
|
+
}
|
|
24823
|
+
}
|
|
24824
|
+
return probes;
|
|
24825
|
+
}
|
|
24826
|
+
function probesFor(rule) {
|
|
24827
|
+
const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
|
|
24828
|
+
return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
|
|
24829
|
+
}
|
|
24830
|
+
function worstProbeMs(rule) {
|
|
24831
|
+
let ms = 0;
|
|
24832
|
+
let probe = "";
|
|
24833
|
+
for (const text of probesFor(rule)) {
|
|
24834
|
+
const start = performance.now();
|
|
24835
|
+
scan(text, [rule]);
|
|
24836
|
+
const elapsed = performance.now() - start;
|
|
24837
|
+
if (elapsed > ms) {
|
|
24838
|
+
ms = elapsed;
|
|
24839
|
+
probe = text;
|
|
24840
|
+
}
|
|
24841
|
+
if (ms >= BUDGET_MS) break;
|
|
24842
|
+
}
|
|
24843
|
+
return { ms, probe };
|
|
24844
|
+
}
|
|
24845
|
+
function checkRuleTiming(rule) {
|
|
24846
|
+
const { ms, probe } = worstProbeMs(rule);
|
|
24847
|
+
return { safe: ms < BUDGET_MS, worstMs: ms, probe };
|
|
24848
|
+
}
|
|
24849
|
+
|
|
23230
24850
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23231
24851
|
var auth_jwt_no_verify_default = {
|
|
23232
24852
|
specVersion: 1,
|
|
@@ -25260,8 +26880,8 @@ function bundledDetections() {
|
|
|
25260
26880
|
}
|
|
25261
26881
|
|
|
25262
26882
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25263
|
-
import { existsSync as
|
|
25264
|
-
import { basename, dirname, isAbsolute, join as
|
|
26883
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26884
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
25265
26885
|
function resolveRepo(cwd) {
|
|
25266
26886
|
try {
|
|
25267
26887
|
const root = findGitRoot(cwd);
|
|
@@ -25283,32 +26903,32 @@ function resolveWorktreeRoot(cwd) {
|
|
|
25283
26903
|
function findGitRoot(start) {
|
|
25284
26904
|
let dir = start;
|
|
25285
26905
|
for (; ; ) {
|
|
25286
|
-
if (
|
|
26906
|
+
if (existsSync4(join7(dir, ".git"))) return dir;
|
|
25287
26907
|
const parent = dirname(dir);
|
|
25288
26908
|
if (parent === dir) return void 0;
|
|
25289
26909
|
dir = parent;
|
|
25290
26910
|
}
|
|
25291
26911
|
}
|
|
25292
26912
|
function resolveGitContext(root) {
|
|
25293
|
-
const dotGit =
|
|
26913
|
+
const dotGit = join7(root, ".git");
|
|
25294
26914
|
try {
|
|
25295
26915
|
if (statSync(dotGit).isDirectory()) {
|
|
25296
|
-
return { configPath:
|
|
26916
|
+
return { configPath: join7(dotGit, "config"), headRoot: root };
|
|
25297
26917
|
}
|
|
25298
26918
|
} catch {
|
|
25299
26919
|
return void 0;
|
|
25300
26920
|
}
|
|
25301
26921
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
25302
26922
|
if (!target) return void 0;
|
|
25303
|
-
const gitdir = isAbsolute(target) ? target :
|
|
25304
|
-
if (
|
|
25305
|
-
return { configPath:
|
|
26923
|
+
const gitdir = isAbsolute(target) ? target : join7(root, target);
|
|
26924
|
+
if (existsSync4(join7(gitdir, "config"))) {
|
|
26925
|
+
return { configPath: join7(gitdir, "config"), headRoot: root };
|
|
25306
26926
|
}
|
|
25307
|
-
const commonRaw = safeRead(
|
|
26927
|
+
const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
|
|
25308
26928
|
if (!commonRaw) return void 0;
|
|
25309
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
26929
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
|
|
25310
26930
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
25311
|
-
return { configPath:
|
|
26931
|
+
return { configPath: join7(commonGitDir, "config"), headRoot };
|
|
25312
26932
|
}
|
|
25313
26933
|
function safeRead(path) {
|
|
25314
26934
|
try {
|
|
@@ -25350,9 +26970,9 @@ function slugFromUrl(url2) {
|
|
|
25350
26970
|
}
|
|
25351
26971
|
|
|
25352
26972
|
// ../../packages/plugin-sdk/src/events.ts
|
|
25353
|
-
import { createHash as
|
|
26973
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
25354
26974
|
function contentHashOf(text) {
|
|
25355
|
-
return
|
|
26975
|
+
return createHash4("sha256").update(text).digest("hex");
|
|
25356
26976
|
}
|
|
25357
26977
|
function buildIngestEvent(input) {
|
|
25358
26978
|
return {
|
|
@@ -25372,22 +26992,16 @@ function buildIngestEvent(input) {
|
|
|
25372
26992
|
};
|
|
25373
26993
|
}
|
|
25374
26994
|
|
|
25375
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
25376
|
-
import { createHash as createHash4 } from "crypto";
|
|
25377
|
-
function normalizeFilePath(filePath) {
|
|
25378
|
-
return filePath.replaceAll("\\", "/");
|
|
25379
|
-
}
|
|
25380
|
-
function computeFindingKey(input) {
|
|
25381
|
-
const normalizedPath = normalizeFilePath(input.filePath);
|
|
25382
|
-
return createHash4("sha256").update(`${input.ruleId}\0${normalizedPath}\0${input.valueFingerprint}`).digest("hex");
|
|
25383
|
-
}
|
|
25384
|
-
|
|
25385
26995
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25386
26996
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
25387
26997
|
|
|
25388
26998
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
25389
|
-
import { mkdirSync as
|
|
25390
|
-
import { join as
|
|
26999
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
27000
|
+
import { join as join9 } from "path";
|
|
27001
|
+
|
|
27002
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
27003
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
27004
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
25391
27005
|
|
|
25392
27006
|
// ../../packages/plugin-sdk/src/posture.ts
|
|
25393
27007
|
function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
@@ -25401,8 +27015,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
25401
27015
|
|
|
25402
27016
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25403
27017
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25404
|
-
import { existsSync as
|
|
25405
|
-
import { basename as
|
|
27018
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27019
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
25406
27020
|
|
|
25407
27021
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
25408
27022
|
var MIN_RAW_LEN = 4;
|
|
@@ -25414,6 +27028,59 @@ function safeMaskedMatch(rawMatch) {
|
|
|
25414
27028
|
return masked;
|
|
25415
27029
|
}
|
|
25416
27030
|
|
|
27031
|
+
// ../../packages/plugin-sdk/src/rule-quarantine.ts
|
|
27032
|
+
var PASS_BUDGET_MS = 2e3;
|
|
27033
|
+
function ruleProbeKey(rule) {
|
|
27034
|
+
if (rule.matcher.type !== "regex") return void 0;
|
|
27035
|
+
return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
|
|
27036
|
+
}
|
|
27037
|
+
function warnQuarantined(rule, worstMs) {
|
|
27038
|
+
const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
|
|
27039
|
+
process.stderr.write(
|
|
27040
|
+
`[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
|
|
27041
|
+
`
|
|
27042
|
+
);
|
|
27043
|
+
}
|
|
27044
|
+
async function filterUnsafeRules(rules, gateway, opts) {
|
|
27045
|
+
const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
|
|
27046
|
+
const passStart = performance.now();
|
|
27047
|
+
const safe = [];
|
|
27048
|
+
for (const rule of rules) {
|
|
27049
|
+
const key = ruleProbeKey(rule);
|
|
27050
|
+
if (key === void 0) {
|
|
27051
|
+
safe.push(rule);
|
|
27052
|
+
continue;
|
|
27053
|
+
}
|
|
27054
|
+
let cached2;
|
|
27055
|
+
try {
|
|
27056
|
+
cached2 = await gateway.getRuleProbeVerdict(key);
|
|
27057
|
+
} catch {
|
|
27058
|
+
cached2 = void 0;
|
|
27059
|
+
}
|
|
27060
|
+
if (cached2) {
|
|
27061
|
+
if (cached2.verdict === "safe") safe.push(rule);
|
|
27062
|
+
else warnQuarantined(rule, cached2.worstProbeMs);
|
|
27063
|
+
continue;
|
|
27064
|
+
}
|
|
27065
|
+
if (performance.now() - passStart >= passBudgetMs) {
|
|
27066
|
+
warnQuarantined(rule, void 0);
|
|
27067
|
+
continue;
|
|
27068
|
+
}
|
|
27069
|
+
let isSafe;
|
|
27070
|
+
let worstMs;
|
|
27071
|
+
try {
|
|
27072
|
+
({ safe: isSafe, worstMs } = checkRuleTiming(rule));
|
|
27073
|
+
} catch {
|
|
27074
|
+
isSafe = false;
|
|
27075
|
+
worstMs = Number.POSITIVE_INFINITY;
|
|
27076
|
+
}
|
|
27077
|
+
await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
|
|
27078
|
+
if (isSafe) safe.push(rule);
|
|
27079
|
+
else warnQuarantined(rule, worstMs);
|
|
27080
|
+
}
|
|
27081
|
+
return safe;
|
|
27082
|
+
}
|
|
27083
|
+
|
|
25417
27084
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25418
27085
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
25419
27086
|
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
@@ -25456,7 +27123,17 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25456
27123
|
categoryActionIndex.set(p.target.category, p.action);
|
|
25457
27124
|
}
|
|
25458
27125
|
}
|
|
25459
|
-
|
|
27126
|
+
const bundledProbeKeys = new Set(
|
|
27127
|
+
getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
|
|
27128
|
+
);
|
|
27129
|
+
const incoming = bundle.rules ?? [];
|
|
27130
|
+
const ciVerified = incoming.filter((rule) => {
|
|
27131
|
+
const key = ruleProbeKey(rule);
|
|
27132
|
+
return key !== void 0 && bundledProbeKeys.has(key);
|
|
27133
|
+
});
|
|
27134
|
+
const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
|
|
27135
|
+
const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
|
|
27136
|
+
rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
|
|
25460
27137
|
bundleExceptions = bundle.exceptions ?? [];
|
|
25461
27138
|
initialized = true;
|
|
25462
27139
|
}
|
|
@@ -25699,16 +27376,16 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25699
27376
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25700
27377
|
|
|
25701
27378
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25702
|
-
import { mkdirSync as
|
|
25703
|
-
import { join as
|
|
27379
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
27380
|
+
import { join as join11 } from "path";
|
|
25704
27381
|
|
|
25705
27382
|
// src/command-registry.ts
|
|
25706
|
-
import { readdirSync as
|
|
27383
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
25707
27384
|
import { fileURLToPath } from "url";
|
|
25708
27385
|
var COMMAND_NAMESPACE = "aka";
|
|
25709
27386
|
var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
|
|
25710
27387
|
function readRegisteredCommands() {
|
|
25711
|
-
return
|
|
27388
|
+
return readdirSync4(COMMANDS_DIR).filter((f) => f.endsWith(".md")).map((f) => `/${COMMAND_NAMESPACE}:${f.replace(/\.md$/, "")}`);
|
|
25712
27389
|
}
|
|
25713
27390
|
function selectRegisteredCommands(curated, registry2) {
|
|
25714
27391
|
const registered = new Set(registry2);
|
|
@@ -25802,8 +27479,8 @@ function table(headers, rows, opts = {}) {
|
|
|
25802
27479
|
const widths = headers.map(
|
|
25803
27480
|
(h, i) => Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i] ?? "")))
|
|
25804
27481
|
);
|
|
25805
|
-
const
|
|
25806
|
-
const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(
|
|
27482
|
+
const sep5 = " ".repeat(gap);
|
|
27483
|
+
const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep5);
|
|
25807
27484
|
const headerLine = fmt(headers.map((h) => h.toUpperCase()));
|
|
25808
27485
|
if (opts.rowSep === true) {
|
|
25809
27486
|
const fullWidth = widths.reduce((n, w) => n + w, 0) + gap * Math.max(0, widths.length - 1);
|
|
@@ -25815,7 +27492,7 @@ function table(headers, rows, opts = {}) {
|
|
|
25815
27492
|
});
|
|
25816
27493
|
return [headerLine, rule, ...body].join("\n");
|
|
25817
27494
|
}
|
|
25818
|
-
const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(
|
|
27495
|
+
const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep5);
|
|
25819
27496
|
return [headerLine, ruleLine, ...rows.map(fmt)].join("\n");
|
|
25820
27497
|
}
|
|
25821
27498
|
function fenced(body) {
|
|
@@ -25867,8 +27544,8 @@ function routeRemediationOption(option, handlers) {
|
|
|
25867
27544
|
}
|
|
25868
27545
|
|
|
25869
27546
|
// src/remediation/rotation-checklist.ts
|
|
25870
|
-
import { writeFileSync as
|
|
25871
|
-
import { join as
|
|
27547
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
27548
|
+
import { join as join12 } from "path";
|
|
25872
27549
|
var GENERIC_CONSOLE_PATH = "rotate via the provider's own console";
|
|
25873
27550
|
var CONSOLE_PATHS = {
|
|
25874
27551
|
anthropic: "console.anthropic.com \u2192 Settings \u2192 API keys",
|
|
@@ -25953,7 +27630,7 @@ function renderRotationChecklistResolvedLine(location) {
|
|
|
25953
27630
|
return `\u2713 I drafted a rotation checklist for you (${location}).`;
|
|
25954
27631
|
}
|
|
25955
27632
|
function writeRotationChecklist(entries, targetDirectory) {
|
|
25956
|
-
|
|
27633
|
+
writeFileSync5(
|
|
25957
27634
|
`${targetDirectory}/rotation-checklist.md`,
|
|
25958
27635
|
renderChecklistMarkdown(entries),
|
|
25959
27636
|
"utf8"
|
|
@@ -25968,7 +27645,7 @@ function generateRotationChecklist(input) {
|
|
|
25968
27645
|
try {
|
|
25969
27646
|
const target = resolveRotationChecklistTarget(input.cwd);
|
|
25970
27647
|
targetDirectory = target.directory;
|
|
25971
|
-
const filePath =
|
|
27648
|
+
const filePath = join12(target.directory, "rotation-checklist.md");
|
|
25972
27649
|
writeRotationChecklist(input.entries, target.directory);
|
|
25973
27650
|
return {
|
|
25974
27651
|
status: "written",
|
|
@@ -26115,7 +27792,8 @@ var StandaloneDataGateway = class {
|
|
|
26115
27792
|
}
|
|
26116
27793
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
26117
27794
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
26118
|
-
// hands the natural key across.
|
|
27795
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
27796
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
26119
27797
|
recordLlmCall(input) {
|
|
26120
27798
|
this.db.auditEvents.insertLlmCall(input);
|
|
26121
27799
|
return Promise.resolve();
|
|
@@ -26157,7 +27835,9 @@ var StandaloneDataGateway = class {
|
|
|
26157
27835
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
26158
27836
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
26159
27837
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
26160
|
-
// content-addressed
|
|
27838
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
27839
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
27840
|
+
// than no-opping.
|
|
26161
27841
|
writeToolCall(input) {
|
|
26162
27842
|
this.db.auditEvents.insertToolCall(input);
|
|
26163
27843
|
if (input.inspections.length === 0) return;
|
|
@@ -26177,7 +27857,7 @@ var StandaloneDataGateway = class {
|
|
|
26177
27857
|
});
|
|
26178
27858
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
26179
27859
|
this.db.inspectionFindings.insertFinding({
|
|
26180
|
-
id: inspectionFindingId(auditEventId,
|
|
27860
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
26181
27861
|
auditEventId,
|
|
26182
27862
|
inspectionDefinitionId: definitionId,
|
|
26183
27863
|
classifiedDataId: classifiedDataId2,
|
|
@@ -26226,10 +27906,17 @@ var StandaloneDataGateway = class {
|
|
|
26226
27906
|
try {
|
|
26227
27907
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
26228
27908
|
if (snapshot.installedPacks === 0) return void 0;
|
|
26229
|
-
if (snapshot.enabledPacks === 0)
|
|
27909
|
+
if (snapshot.enabledPacks === 0) {
|
|
27910
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
27911
|
+
}
|
|
26230
27912
|
if (snapshot.invalidRules > 0) return void 0;
|
|
26231
27913
|
if (snapshot.rules.length === 0) return void 0;
|
|
26232
|
-
return {
|
|
27914
|
+
return {
|
|
27915
|
+
rules: snapshot.rules,
|
|
27916
|
+
ruleActions: snapshot.ruleActions,
|
|
27917
|
+
ruleVersions: snapshot.ruleVersions,
|
|
27918
|
+
complete: true
|
|
27919
|
+
};
|
|
26233
27920
|
} catch {
|
|
26234
27921
|
return void 0;
|
|
26235
27922
|
}
|
|
@@ -26257,6 +27944,7 @@ var StandaloneDataGateway = class {
|
|
|
26257
27944
|
policies: [...policies, ...rulePolicies],
|
|
26258
27945
|
rules: installed ? installed.rules : [],
|
|
26259
27946
|
...installed ? { rulesComplete: true } : {},
|
|
27947
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
26260
27948
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
26261
27949
|
customKeywords,
|
|
26262
27950
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -26355,6 +28043,13 @@ var StandaloneDataGateway = class {
|
|
|
26355
28043
|
this.db.scanLedger.upsertEntries(entries);
|
|
26356
28044
|
return Promise.resolve();
|
|
26357
28045
|
}
|
|
28046
|
+
getRuleProbeVerdict(ruleKey) {
|
|
28047
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
28048
|
+
}
|
|
28049
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
|
|
28050
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
|
|
28051
|
+
return Promise.resolve();
|
|
28052
|
+
}
|
|
26358
28053
|
openAtRestKeysForPath(path) {
|
|
26359
28054
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
26360
28055
|
}
|
|
@@ -26365,6 +28060,12 @@ var StandaloneDataGateway = class {
|
|
|
26365
28060
|
this.db.resolutions.insertResolution(input);
|
|
26366
28061
|
return Promise.resolve();
|
|
26367
28062
|
}
|
|
28063
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
28064
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
28065
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
28066
|
+
recordProjectEgress(input) {
|
|
28067
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
28068
|
+
}
|
|
26368
28069
|
close() {
|
|
26369
28070
|
this.db.close();
|
|
26370
28071
|
return Promise.resolve();
|
|
@@ -26382,11 +28083,11 @@ import { randomUUID as randomUUID12 } from "crypto";
|
|
|
26382
28083
|
var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
26383
28084
|
|
|
26384
28085
|
// src/history/transcripts.ts
|
|
26385
|
-
import { readdirSync as
|
|
28086
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7 } from "fs";
|
|
26386
28087
|
import { homedir as homedir3 } from "os";
|
|
26387
|
-
import { join as
|
|
28088
|
+
import { join as join13 } from "path";
|
|
26388
28089
|
function transcriptsDir(home) {
|
|
26389
|
-
return
|
|
28090
|
+
return join13(home ?? homedir3(), ".claude", "projects");
|
|
26390
28091
|
}
|
|
26391
28092
|
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
26392
28093
|
|
|
@@ -26398,7 +28099,7 @@ function deriveProvider(ruleId) {
|
|
|
26398
28099
|
}
|
|
26399
28100
|
|
|
26400
28101
|
// src/remediation/redact.ts
|
|
26401
|
-
import { readFileSync as readFileSync8, realpathSync as
|
|
28102
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
26402
28103
|
import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
|
|
26403
28104
|
var REDACTED_PLACEHOLDER = "[REDACTED:SECRET]";
|
|
26404
28105
|
function platformRedactionScope(home) {
|
|
@@ -26406,7 +28107,7 @@ function platformRedactionScope(home) {
|
|
|
26406
28107
|
}
|
|
26407
28108
|
function realPathOrNull(path) {
|
|
26408
28109
|
try {
|
|
26409
|
-
return
|
|
28110
|
+
return realpathSync3(path);
|
|
26410
28111
|
} catch {
|
|
26411
28112
|
return null;
|
|
26412
28113
|
}
|
|
@@ -26456,11 +28157,11 @@ function redactLeakedKeysDetailed(targets, scope = platformRedactionScope()) {
|
|
|
26456
28157
|
if (struckHere.length === 0) continue;
|
|
26457
28158
|
const tmpPath = `${filePath}.aka-redact.tmp`;
|
|
26458
28159
|
try {
|
|
26459
|
-
|
|
26460
|
-
|
|
28160
|
+
writeFileSync6(tmpPath, content);
|
|
28161
|
+
renameSync4(tmpPath, filePath);
|
|
26461
28162
|
} catch {
|
|
26462
28163
|
try {
|
|
26463
|
-
|
|
28164
|
+
rmSync3(tmpPath, { force: true, recursive: true });
|
|
26464
28165
|
} catch {
|
|
26465
28166
|
}
|
|
26466
28167
|
continue;
|