@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/statusline.js
CHANGED
|
@@ -491,9 +491,13 @@ var require_ignore = __commonJS({
|
|
|
491
491
|
}
|
|
492
492
|
});
|
|
493
493
|
|
|
494
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
495
|
+
import { existsSync as existsSync3 } from "fs";
|
|
496
|
+
import { join as join6 } from "path";
|
|
497
|
+
|
|
494
498
|
// ../../packages/persistence/src/database.ts
|
|
495
499
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
500
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
497
501
|
import { join, sep } from "path";
|
|
498
502
|
import { DatabaseSync } from "node:sqlite";
|
|
499
503
|
|
|
@@ -542,6 +546,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
542
546
|
{
|
|
543
547
|
tag: "0010_events_session_expression_index",
|
|
544
548
|
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tag: "0011_egress_writer",
|
|
552
|
+
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0012_handy_the_captain",
|
|
556
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0013_legacy_history_backfill_support",
|
|
560
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
tag: "0014_drop_legacy_events_findings",
|
|
564
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
545
565
|
}
|
|
546
566
|
];
|
|
547
567
|
|
|
@@ -15372,7 +15392,12 @@ var FindingFacets = external_exports.object({
|
|
|
15372
15392
|
severity: external_exports.array(FindingFacetItem),
|
|
15373
15393
|
subtype: external_exports.array(FindingFacetItem),
|
|
15374
15394
|
provider: external_exports.array(FindingFacetItem),
|
|
15375
|
-
action: external_exports.array(FindingFacetItem)
|
|
15395
|
+
action: external_exports.array(FindingFacetItem),
|
|
15396
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15397
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15398
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15399
|
+
// counted under no value.
|
|
15400
|
+
status: external_exports.array(FindingFacetItem)
|
|
15376
15401
|
}).meta({ id: "FindingFacets" });
|
|
15377
15402
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15378
15403
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15382,6 +15407,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15382
15407
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15383
15408
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15384
15409
|
action: external_exports.array(FindingAction).optional(),
|
|
15410
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15411
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15412
|
+
// one of the requested values.
|
|
15413
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15385
15414
|
q: external_exports.string().optional(),
|
|
15386
15415
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15387
15416
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15569,6 +15598,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15569
15598
|
parent_uuid: external_exports.string().optional(),
|
|
15570
15599
|
run_key: external_exports.string().optional()
|
|
15571
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());
|
|
15572
15628
|
var ToolCallInspection = external_exports.object({
|
|
15573
15629
|
ruleId: external_exports.string().min(1),
|
|
15574
15630
|
ruleName: external_exports.string(),
|
|
@@ -15655,7 +15711,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15655
15711
|
span: Span,
|
|
15656
15712
|
maskedMatch: external_exports.string(),
|
|
15657
15713
|
actionTaken: ActionTaken,
|
|
15658
|
-
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()
|
|
15659
15726
|
});
|
|
15660
15727
|
var InventoryContext = external_exports.object({
|
|
15661
15728
|
host: InventoryInput.optional(),
|
|
@@ -15857,6 +15924,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15857
15924
|
|
|
15858
15925
|
// ../../packages/schema/src/zod/event.ts
|
|
15859
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(",");
|
|
15860
15928
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15861
15929
|
var EventMetadata = external_exports.object({
|
|
15862
15930
|
sessionId: external_exports.string().optional(),
|
|
@@ -16197,6 +16265,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16197
16265
|
|
|
16198
16266
|
// ../../packages/schema/src/zod/rule.ts
|
|
16199
16267
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16268
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16200
16269
|
var KeywordMatcher = external_exports.object({
|
|
16201
16270
|
type: external_exports.literal("keyword"),
|
|
16202
16271
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16221,9 +16290,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16221
16290
|
return false;
|
|
16222
16291
|
}
|
|
16223
16292
|
}
|
|
16293
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16224
16294
|
var RegexMatcher = external_exports.object({
|
|
16225
16295
|
type: external_exports.literal("regex"),
|
|
16226
|
-
pattern: external_exports.string(),
|
|
16296
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16227
16297
|
flags: external_exports.string().default("gi"),
|
|
16228
16298
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16229
16299
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16344,6 +16414,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16344
16414
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16345
16415
|
// `bundle.exceptions ?? []`.
|
|
16346
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(),
|
|
16347
16423
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16348
16424
|
fetchedAt: external_exports.iso.datetime()
|
|
16349
16425
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16790,6 +16866,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16790
16866
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16791
16867
|
}
|
|
16792
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
|
+
|
|
16793
17075
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16794
17076
|
function toApiAction(dbVal) {
|
|
16795
17077
|
const map2 = {
|
|
@@ -16937,6 +17219,15 @@ function groupActions(g) {
|
|
|
16937
17219
|
actionsCache.set(g, actions);
|
|
16938
17220
|
return actions;
|
|
16939
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
|
+
}
|
|
16940
17231
|
function applyFindingFilters(groups, opts) {
|
|
16941
17232
|
let filtered = groups;
|
|
16942
17233
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16955,6 +17246,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
16955
17246
|
const subtypeSet = new Set(opts.subtype);
|
|
16956
17247
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
16957
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
|
+
}
|
|
16958
17253
|
if (opts.q) {
|
|
16959
17254
|
const q = opts.q.toLowerCase();
|
|
16960
17255
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -16976,6 +17271,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16976
17271
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
16977
17272
|
providers: opts.providers,
|
|
16978
17273
|
actions: opts.actions,
|
|
17274
|
+
statuses: opts.statuses,
|
|
16979
17275
|
q: opts.q,
|
|
16980
17276
|
subtype: opts.subtype
|
|
16981
17277
|
});
|
|
@@ -16985,6 +17281,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16985
17281
|
}
|
|
16986
17282
|
const forProvider = applyFindingFilters(allGroups, {
|
|
16987
17283
|
actions: opts.actions,
|
|
17284
|
+
statuses: opts.statuses,
|
|
16988
17285
|
q: opts.q,
|
|
16989
17286
|
subtype: opts.subtype,
|
|
16990
17287
|
severity: opts.severity
|
|
@@ -16995,6 +17292,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16995
17292
|
}
|
|
16996
17293
|
const forAction = applyFindingFilters(allGroups, {
|
|
16997
17294
|
providers: opts.providers,
|
|
17295
|
+
statuses: opts.statuses,
|
|
16998
17296
|
q: opts.q,
|
|
16999
17297
|
subtype: opts.subtype,
|
|
17000
17298
|
severity: opts.severity
|
|
@@ -17006,17 +17304,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17006
17304
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17007
17305
|
providers: opts.providers,
|
|
17008
17306
|
actions: opts.actions,
|
|
17307
|
+
statuses: opts.statuses,
|
|
17009
17308
|
q: opts.q,
|
|
17010
17309
|
severity: opts.severity
|
|
17011
17310
|
});
|
|
17012
17311
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17013
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
|
+
}
|
|
17014
17324
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17015
17325
|
return {
|
|
17016
17326
|
severity: toItems(severityMap),
|
|
17017
17327
|
provider: toItems(providerMap),
|
|
17018
17328
|
action: toItems(actionMap),
|
|
17019
|
-
subtype: toItems(subtypeMap)
|
|
17329
|
+
subtype: toItems(subtypeMap),
|
|
17330
|
+
status: toItems(statusMap)
|
|
17020
17331
|
};
|
|
17021
17332
|
}
|
|
17022
17333
|
|
|
@@ -17051,10 +17362,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17051
17362
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17052
17363
|
|
|
17053
17364
|
// ../../packages/schema/src/zod/local.ts
|
|
17054
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17365
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17055
17366
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17056
17367
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17057
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
|
+
});
|
|
17058
17373
|
var WorkspaceSettings = external_exports.object({
|
|
17059
17374
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17060
17375
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17066,38 +17381,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17066
17381
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17067
17382
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17068
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),
|
|
17069
17387
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17070
|
-
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()
|
|
17071
17394
|
});
|
|
17072
17395
|
function defaultWorkspaceSettings() {
|
|
17073
17396
|
return WorkspaceSettings.parse({});
|
|
17074
17397
|
}
|
|
17075
|
-
function toEventRow(event) {
|
|
17076
|
-
return {
|
|
17077
|
-
id: event.id,
|
|
17078
|
-
sourceTool: event.sourceTool,
|
|
17079
|
-
kind: event.kind,
|
|
17080
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17081
|
-
contentHash: event.contentHash,
|
|
17082
|
-
content: event.content,
|
|
17083
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17084
|
-
};
|
|
17085
|
-
}
|
|
17086
|
-
function toFindingRow(finding) {
|
|
17087
|
-
return {
|
|
17088
|
-
id: finding.id,
|
|
17089
|
-
eventId: finding.eventId,
|
|
17090
|
-
ruleId: finding.ruleId,
|
|
17091
|
-
category: finding.category,
|
|
17092
|
-
severity: finding.severity,
|
|
17093
|
-
spanStart: finding.span.start,
|
|
17094
|
-
spanEnd: finding.span.end,
|
|
17095
|
-
maskedMatch: finding.maskedMatch,
|
|
17096
|
-
actionTaken: finding.actionTaken,
|
|
17097
|
-
confidence: finding.confidence,
|
|
17098
|
-
findingKey: finding.findingKey ?? null
|
|
17099
|
-
};
|
|
17100
|
-
}
|
|
17101
17398
|
function toInventoryRow(input, id, now) {
|
|
17102
17399
|
return {
|
|
17103
17400
|
id,
|
|
@@ -17167,7 +17464,42 @@ function toInspectionFindingRow(input) {
|
|
|
17167
17464
|
spanEnd: input.span.end,
|
|
17168
17465
|
maskedMatch: input.maskedMatch,
|
|
17169
17466
|
actionTaken: input.actionTaken,
|
|
17170
|
-
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 })
|
|
17171
17503
|
};
|
|
17172
17504
|
}
|
|
17173
17505
|
|
|
@@ -17549,145 +17881,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17549
17881
|
path: ["liveKeys"]
|
|
17550
17882
|
});
|
|
17551
17883
|
|
|
17552
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17553
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17554
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17555
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17556
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17557
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17558
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17559
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17560
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17561
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17562
|
-
var ReviewInfo = external_exports.object({
|
|
17563
|
-
needsReview: external_exports.boolean(),
|
|
17564
|
-
reasons: external_exports.array(ReviewReason)
|
|
17565
|
-
}).meta({ id: "ReviewInfo" });
|
|
17566
|
-
var DestinationNetwork = external_exports.object({
|
|
17567
|
-
port: external_exports.number().int().nullable(),
|
|
17568
|
-
geo: external_exports.string().nullable(),
|
|
17569
|
-
ptr: external_exports.string().nullable()
|
|
17570
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17571
|
-
var EndpointSummary = external_exports.object({
|
|
17572
|
-
id: external_exports.string(),
|
|
17573
|
-
method: HttpMethod,
|
|
17574
|
-
transport: Transport,
|
|
17575
|
-
url: external_exports.string(),
|
|
17576
|
-
template: external_exports.boolean(),
|
|
17577
|
-
dataClass: DataClass,
|
|
17578
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17579
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17580
|
-
}).meta({ id: "EndpointSummary" });
|
|
17581
|
-
var CallSite = external_exports.object({
|
|
17582
|
-
id: external_exports.string(),
|
|
17583
|
-
project: external_exports.string(),
|
|
17584
|
-
file: external_exports.string(),
|
|
17585
|
-
line: external_exports.number().int().nonnegative(),
|
|
17586
|
-
snippet: external_exports.string(),
|
|
17587
|
-
dynamic: external_exports.boolean(),
|
|
17588
|
-
vendored: external_exports.boolean(),
|
|
17589
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17590
|
-
projectId: external_exports.string().nullable()
|
|
17591
|
-
}).meta({ id: "CallSite" });
|
|
17592
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17593
|
-
sites: external_exports.array(CallSite)
|
|
17594
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17595
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17596
|
-
id: external_exports.string(),
|
|
17597
|
-
kind: DestinationKind,
|
|
17598
|
-
name: external_exports.string(),
|
|
17599
|
-
host: external_exports.string(),
|
|
17600
|
-
category: external_exports.string(),
|
|
17601
|
-
trust: ShareTrustLevel,
|
|
17602
|
-
/** Effective state (decision applied over the trust default). */
|
|
17603
|
-
status: EgressStatus,
|
|
17604
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17605
|
-
isCustom: external_exports.boolean(),
|
|
17606
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17607
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17608
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17609
|
-
transports: external_exports.array(Transport),
|
|
17610
|
-
/** Most-sensitive first. */
|
|
17611
|
-
dataClasses: external_exports.array(DataClass),
|
|
17612
|
-
review: ReviewInfo,
|
|
17613
|
-
/** Non-provider hosts only; null for providers. */
|
|
17614
|
-
network: DestinationNetwork.nullable(),
|
|
17615
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17616
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17617
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17618
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17619
|
-
endpointCount: true,
|
|
17620
|
-
callSiteCount: true,
|
|
17621
|
-
endpoints: true
|
|
17622
|
-
}).extend({
|
|
17623
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17624
|
-
note: external_exports.string().nullable(),
|
|
17625
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17626
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17627
|
-
var ReviewDestination = external_exports.object({
|
|
17628
|
-
id: external_exports.string(),
|
|
17629
|
-
kind: DestinationKind,
|
|
17630
|
-
name: external_exports.string(),
|
|
17631
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17632
|
-
host: external_exports.string(),
|
|
17633
|
-
trust: ShareTrustLevel,
|
|
17634
|
-
status: EgressStatus,
|
|
17635
|
-
review: ReviewInfo,
|
|
17636
|
-
topDataClass: DataClass,
|
|
17637
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17638
|
-
lastSeen: external_exports.iso.datetime()
|
|
17639
|
-
}).meta({ id: "ReviewDestination" });
|
|
17640
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17641
|
-
kind: DestinationKind,
|
|
17642
|
-
total: external_exports.number().int().nonnegative(),
|
|
17643
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17644
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17645
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17646
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17647
|
-
var SharesStats = external_exports.object({
|
|
17648
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17649
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17650
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17651
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17652
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17653
|
-
byKind: external_exports.object({
|
|
17654
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17655
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17656
|
-
ip: external_exports.number().int().nonnegative()
|
|
17657
|
-
}),
|
|
17658
|
-
byTrust: external_exports.object({
|
|
17659
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17660
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17661
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17662
|
-
ip: external_exports.number().int().nonnegative()
|
|
17663
|
-
})
|
|
17664
|
-
}).meta({ id: "SharesStats" });
|
|
17665
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17666
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17667
|
-
decision: EgressDecision.nullable()
|
|
17668
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17669
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17670
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17671
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17672
|
-
q: external_exports.string().optional(),
|
|
17673
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17674
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17675
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17676
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17677
|
-
/**
|
|
17678
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17679
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17680
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17681
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17682
|
-
*/
|
|
17683
|
-
review: external_exports.stringbool().default(false)
|
|
17684
|
-
});
|
|
17685
|
-
var ExportSharesQuery = external_exports.object({
|
|
17686
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17687
|
-
q: external_exports.string().optional(),
|
|
17688
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17689
|
-
});
|
|
17690
|
-
|
|
17691
17884
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17692
17885
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17693
17886
|
function trustDefaultStatus(trust) {
|
|
@@ -17707,7 +17900,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17707
17900
|
const reasons = [];
|
|
17708
17901
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17709
17902
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17710
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17903
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17711
17904
|
return reasons;
|
|
17712
17905
|
}
|
|
17713
17906
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17734,6 +17927,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17734
17927
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17735
17928
|
}
|
|
17736
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
|
+
|
|
17737
17972
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17738
17973
|
function escapeLikePattern(s) {
|
|
17739
17974
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17830,39 +18065,81 @@ function evidenceExists(db, object2) {
|
|
|
17830
18065
|
return schemaObjectExists(db, "table", object2.name);
|
|
17831
18066
|
}
|
|
17832
18067
|
|
|
17833
|
-
// ../../packages/persistence/src/
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
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);
|
|
17837
18073
|
}
|
|
17838
|
-
function
|
|
17839
|
-
|
|
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);
|
|
17840
18078
|
}
|
|
17841
|
-
function
|
|
17842
|
-
return
|
|
18079
|
+
function intToBool(raw) {
|
|
18080
|
+
return raw === 1 || raw === true;
|
|
17843
18081
|
}
|
|
17844
|
-
function
|
|
17845
|
-
return
|
|
18082
|
+
function boolToInt(b) {
|
|
18083
|
+
return b ? 1 : 0;
|
|
17846
18084
|
}
|
|
17847
|
-
function
|
|
17848
|
-
|
|
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;
|
|
17849
18091
|
}
|
|
17850
|
-
function
|
|
17851
|
-
return
|
|
18092
|
+
function countScalar(db, sql, params) {
|
|
18093
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17852
18094
|
}
|
|
17853
|
-
function
|
|
17854
|
-
|
|
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;
|
|
17855
18101
|
}
|
|
17856
|
-
function
|
|
17857
|
-
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
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);
|
|
17866
18143
|
}
|
|
17867
18144
|
|
|
17868
18145
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17876,7 +18153,8 @@ function createdIndexName(statement) {
|
|
|
17876
18153
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17877
18154
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17878
18155
|
}
|
|
17879
|
-
|
|
18156
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18157
|
+
function applyMigrations(db, file2) {
|
|
17880
18158
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17881
18159
|
db.exec(
|
|
17882
18160
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17890,6 +18168,7 @@ function applyMigrations(db) {
|
|
|
17890
18168
|
);
|
|
17891
18169
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17892
18170
|
if (applied.has(migration.tag)) continue;
|
|
18171
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17893
18172
|
const evidence = evidenceObjects(migration.sql);
|
|
17894
18173
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17895
18174
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -17934,13 +18213,54 @@ function applyMigrations(db) {
|
|
|
17934
18213
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
17935
18214
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
17936
18215
|
}
|
|
17937
|
-
ensureSyncedAtColumn(db, "events");
|
|
17938
18216
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17939
18217
|
ensureScanLedgerTable(db);
|
|
17940
18218
|
ensureBlockedDetectionsTable(db);
|
|
18219
|
+
ensureRuleProbeCacheTable(db);
|
|
17941
18220
|
ensureWriteGateTrigger(db);
|
|
17942
18221
|
ensureTokenUsageColumns(db);
|
|
17943
18222
|
reconcileSourceProjectIds(db);
|
|
18223
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18224
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18225
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18226
|
+
}
|
|
18227
|
+
}
|
|
18228
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18229
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18230
|
+
if (!migration) return;
|
|
18231
|
+
if (file2) {
|
|
18232
|
+
try {
|
|
18233
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18234
|
+
} catch (error51) {
|
|
18235
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18236
|
+
return;
|
|
18237
|
+
}
|
|
18238
|
+
}
|
|
18239
|
+
try {
|
|
18240
|
+
withTransaction(
|
|
18241
|
+
db,
|
|
18242
|
+
() => {
|
|
18243
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18244
|
+
if (alreadyDropped) return;
|
|
18245
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18246
|
+
db.exec(statement);
|
|
18247
|
+
}
|
|
18248
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18249
|
+
migration.tag,
|
|
18250
|
+
Date.now()
|
|
18251
|
+
);
|
|
18252
|
+
},
|
|
18253
|
+
"IMMEDIATE"
|
|
18254
|
+
);
|
|
18255
|
+
} catch (error51) {
|
|
18256
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18257
|
+
}
|
|
18258
|
+
}
|
|
18259
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18260
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18261
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18262
|
+
tightenFile(backup);
|
|
18263
|
+
return backup;
|
|
17944
18264
|
}
|
|
17945
18265
|
var TOKEN_USAGE_COLUMNS = [
|
|
17946
18266
|
{
|
|
@@ -17969,6 +18289,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17969
18289
|
}
|
|
17970
18290
|
];
|
|
17971
18291
|
function ensureTokenUsageColumns(db) {
|
|
18292
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
17972
18293
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17973
18294
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17974
18295
|
if (!existing.has(column.name)) {
|
|
@@ -18034,11 +18355,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18034
18355
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18035
18356
|
}
|
|
18036
18357
|
}
|
|
18358
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18359
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18360
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18361
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18362
|
+
return row?.lastRowid ?? 0;
|
|
18363
|
+
}
|
|
18364
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18365
|
+
db.prepare(
|
|
18366
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18367
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18368
|
+
).run(source, lastRowid);
|
|
18369
|
+
}
|
|
18370
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18371
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18372
|
+
let processed = 0;
|
|
18373
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18374
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18375
|
+
if (rows.length === 0) return true;
|
|
18376
|
+
withTransaction(
|
|
18377
|
+
db,
|
|
18378
|
+
() => {
|
|
18379
|
+
handleRows(rows);
|
|
18380
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18381
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18382
|
+
},
|
|
18383
|
+
"IMMEDIATE"
|
|
18384
|
+
);
|
|
18385
|
+
processed += rows.length;
|
|
18386
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18387
|
+
}
|
|
18388
|
+
return false;
|
|
18389
|
+
}
|
|
18390
|
+
function parseLegacyEventMetadata(raw) {
|
|
18391
|
+
if (raw === null) return void 0;
|
|
18392
|
+
try {
|
|
18393
|
+
return JSON.parse(raw);
|
|
18394
|
+
} catch {
|
|
18395
|
+
return void 0;
|
|
18396
|
+
}
|
|
18397
|
+
}
|
|
18398
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18399
|
+
return JSON.stringify(
|
|
18400
|
+
toCaptureAttributes({
|
|
18401
|
+
id: row.id,
|
|
18402
|
+
sourceTool: row.sourceTool,
|
|
18403
|
+
kind: row.kind,
|
|
18404
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18405
|
+
contentHash: row.contentHash,
|
|
18406
|
+
content: row.content,
|
|
18407
|
+
metadata: row.metadata
|
|
18408
|
+
})
|
|
18409
|
+
);
|
|
18410
|
+
}
|
|
18411
|
+
function copyLegacyEvents(db) {
|
|
18412
|
+
const selectStmt = db.prepare(
|
|
18413
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18414
|
+
content_hash AS contentHash, content, metadata
|
|
18415
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18416
|
+
);
|
|
18417
|
+
const insertStmt = db.prepare(
|
|
18418
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18419
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18420
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18421
|
+
);
|
|
18422
|
+
const stubRootStmt = db.prepare(
|
|
18423
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18424
|
+
);
|
|
18425
|
+
return drainLegacyTable(
|
|
18426
|
+
db,
|
|
18427
|
+
"events",
|
|
18428
|
+
selectStmt,
|
|
18429
|
+
(rows) => {
|
|
18430
|
+
for (const row of rows) {
|
|
18431
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18432
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18433
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18434
|
+
insertStmt.run(
|
|
18435
|
+
bindParams({
|
|
18436
|
+
id: row.id,
|
|
18437
|
+
parentId: sessionId,
|
|
18438
|
+
rootSessionId: sessionId,
|
|
18439
|
+
eventType: row.kind,
|
|
18440
|
+
startedAt: row.occurredAt,
|
|
18441
|
+
content: row.content,
|
|
18442
|
+
contentHash: row.contentHash,
|
|
18443
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18444
|
+
})
|
|
18445
|
+
);
|
|
18446
|
+
}
|
|
18447
|
+
}
|
|
18448
|
+
);
|
|
18449
|
+
}
|
|
18450
|
+
function copyLegacyFindings(db) {
|
|
18451
|
+
const selectStmt = db.prepare(
|
|
18452
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18453
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18454
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18455
|
+
first_detected_at AS firstDetectedAt
|
|
18456
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18457
|
+
);
|
|
18458
|
+
const definitionStmt = db.prepare(
|
|
18459
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18460
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18461
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18462
|
+
);
|
|
18463
|
+
const findingStmt = db.prepare(
|
|
18464
|
+
`INSERT INTO inspection_findings
|
|
18465
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18466
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18467
|
+
finding_key, first_detected_at)
|
|
18468
|
+
VALUES
|
|
18469
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18470
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18471
|
+
:findingKey, :firstDetectedAt)
|
|
18472
|
+
ON CONFLICT(id) DO NOTHING
|
|
18473
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18474
|
+
first_detected_at = CASE
|
|
18475
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18476
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18477
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18478
|
+
END`
|
|
18479
|
+
);
|
|
18480
|
+
return drainLegacyTable(
|
|
18481
|
+
db,
|
|
18482
|
+
"findings",
|
|
18483
|
+
selectStmt,
|
|
18484
|
+
(rows) => {
|
|
18485
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18486
|
+
for (const row of rows) {
|
|
18487
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18488
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18489
|
+
if (definitionId === void 0) {
|
|
18490
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18491
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18492
|
+
definitionStmt.run(
|
|
18493
|
+
bindParams({
|
|
18494
|
+
id: definitionId,
|
|
18495
|
+
ruleId: row.ruleId,
|
|
18496
|
+
name: row.ruleId,
|
|
18497
|
+
category: row.category,
|
|
18498
|
+
severity: row.severity,
|
|
18499
|
+
definition: "",
|
|
18500
|
+
version: version2
|
|
18501
|
+
})
|
|
18502
|
+
);
|
|
18503
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18504
|
+
}
|
|
18505
|
+
findingStmt.run(
|
|
18506
|
+
bindParams({
|
|
18507
|
+
id: row.id,
|
|
18508
|
+
auditEventId: row.eventId,
|
|
18509
|
+
inspectionDefinitionId: definitionId,
|
|
18510
|
+
spanStart: row.spanStart,
|
|
18511
|
+
spanEnd: row.spanEnd,
|
|
18512
|
+
maskedMatch: row.maskedMatch,
|
|
18513
|
+
actionTaken: row.actionTaken,
|
|
18514
|
+
confidence: row.confidence,
|
|
18515
|
+
findingKey: row.findingKey,
|
|
18516
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18517
|
+
})
|
|
18518
|
+
);
|
|
18519
|
+
}
|
|
18520
|
+
}
|
|
18521
|
+
);
|
|
18522
|
+
}
|
|
18523
|
+
function runLegacyHistoryBackfill(db) {
|
|
18524
|
+
try {
|
|
18525
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18526
|
+
if (!eventsCaughtUp) return false;
|
|
18527
|
+
return copyLegacyFindings(db);
|
|
18528
|
+
} catch (error51) {
|
|
18529
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18530
|
+
return false;
|
|
18531
|
+
}
|
|
18532
|
+
}
|
|
18037
18533
|
function isForeignSqliteLineage(db) {
|
|
18038
18534
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18039
18535
|
return columnNames(db, "events").includes("tenant_id");
|
|
18040
18536
|
}
|
|
18041
18537
|
function ensureSyncedAtColumn(db, table2) {
|
|
18538
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18042
18539
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18043
18540
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18044
18541
|
}
|
|
@@ -18059,6 +18556,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18059
18556
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18060
18557
|
)`);
|
|
18061
18558
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18559
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18062
18560
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18063
18561
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18064
18562
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18077,29 +18575,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18077
18575
|
blocked_at INTEGER NOT NULL
|
|
18078
18576
|
)`);
|
|
18079
18577
|
}
|
|
18080
|
-
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18088
|
-
try {
|
|
18089
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18090
|
-
} catch {
|
|
18091
|
-
}
|
|
18092
|
-
}
|
|
18093
|
-
function walSidecars(file2) {
|
|
18094
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18095
|
-
}
|
|
18096
|
-
function tightenPerms(file2) {
|
|
18097
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18098
|
-
try {
|
|
18099
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18100
|
-
} catch {
|
|
18101
|
-
}
|
|
18102
|
-
}
|
|
18578
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18579
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18580
|
+
rule_key TEXT PRIMARY KEY,
|
|
18581
|
+
verdict TEXT NOT NULL,
|
|
18582
|
+
worst_probe_ms REAL NOT NULL,
|
|
18583
|
+
checked_at INTEGER NOT NULL
|
|
18584
|
+
)`);
|
|
18103
18585
|
}
|
|
18104
18586
|
|
|
18105
18587
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18121,51 +18603,6 @@ function parseJsonObject(s) {
|
|
|
18121
18603
|
return void 0;
|
|
18122
18604
|
}
|
|
18123
18605
|
|
|
18124
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18125
|
-
function allRows(stmt, params) {
|
|
18126
|
-
if (params === void 0) return stmt.all();
|
|
18127
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18128
|
-
return stmt.all(params);
|
|
18129
|
-
}
|
|
18130
|
-
function getRow(stmt, params) {
|
|
18131
|
-
if (params === void 0) return stmt.get();
|
|
18132
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18133
|
-
return stmt.get(params);
|
|
18134
|
-
}
|
|
18135
|
-
function intToBool(raw) {
|
|
18136
|
-
return raw === 1 || raw === true;
|
|
18137
|
-
}
|
|
18138
|
-
function boolToInt(b) {
|
|
18139
|
-
return b ? 1 : 0;
|
|
18140
|
-
}
|
|
18141
|
-
function bindParams(row) {
|
|
18142
|
-
const out = {};
|
|
18143
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18144
|
-
out[key] = value === void 0 ? null : value;
|
|
18145
|
-
}
|
|
18146
|
-
return out;
|
|
18147
|
-
}
|
|
18148
|
-
function countScalar(db, sql, params) {
|
|
18149
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18150
|
-
}
|
|
18151
|
-
function countBy(db, sql, params) {
|
|
18152
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18153
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18154
|
-
map2.set(row.k, row.n);
|
|
18155
|
-
}
|
|
18156
|
-
return map2;
|
|
18157
|
-
}
|
|
18158
|
-
function mapRowsTolerant(rows, map2) {
|
|
18159
|
-
const out = [];
|
|
18160
|
-
for (const row of rows) {
|
|
18161
|
-
try {
|
|
18162
|
-
out.push(map2(row));
|
|
18163
|
-
} catch {
|
|
18164
|
-
}
|
|
18165
|
-
}
|
|
18166
|
-
return out;
|
|
18167
|
-
}
|
|
18168
|
-
|
|
18169
18606
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18170
18607
|
var DAY_MS = 864e5;
|
|
18171
18608
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18782,6 +19219,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18782
19219
|
})
|
|
18783
19220
|
);
|
|
18784
19221
|
}
|
|
19222
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19223
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19224
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19225
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19226
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19227
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19228
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19229
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19230
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19231
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19232
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19233
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19234
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19235
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19236
|
+
}
|
|
18785
19237
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18786
19238
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18787
19239
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19243,8 +19695,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19243
19695
|
)
|
|
19244
19696
|
);
|
|
19245
19697
|
}
|
|
19246
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19247
|
-
//
|
|
19698
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19699
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19700
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19701
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19702
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19703
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19704
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19705
|
+
// to the old findings⋈events one.
|
|
19248
19706
|
countFindingsLast30d(ruleIds) {
|
|
19249
19707
|
if (ruleIds.length === 0) return 0;
|
|
19250
19708
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19252,8 +19710,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19252
19710
|
return countScalar(
|
|
19253
19711
|
this.db,
|
|
19254
19712
|
`SELECT count(*) AS n
|
|
19255
|
-
FROM
|
|
19256
|
-
|
|
19713
|
+
FROM inspection_findings f
|
|
19714
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19715
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19716
|
+
WHERE e.started_at >= ?
|
|
19717
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19718
|
+
AND d.rule_id IN (${inClause})`,
|
|
19257
19719
|
[since, ...ruleIds]
|
|
19258
19720
|
);
|
|
19259
19721
|
}
|
|
@@ -19263,35 +19725,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19263
19725
|
var SqliteEventsRepository = class {
|
|
19264
19726
|
constructor(db) {
|
|
19265
19727
|
this.db = db;
|
|
19266
|
-
this.insertStmt = db.prepare(
|
|
19267
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19268
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19269
|
-
);
|
|
19270
19728
|
}
|
|
19271
19729
|
db;
|
|
19272
|
-
|
|
19273
|
-
|
|
19274
|
-
|
|
19275
|
-
this.insertStmt.run(
|
|
19276
|
-
bindParams({
|
|
19277
|
-
id: row.id,
|
|
19278
|
-
sourceTool: row.sourceTool,
|
|
19279
|
-
kind: row.kind,
|
|
19280
|
-
occurredAt: row.occurredAt,
|
|
19281
|
-
contentHash: row.contentHash,
|
|
19282
|
-
content: row.content,
|
|
19283
|
-
metadata: row.metadata
|
|
19284
|
-
})
|
|
19285
|
-
);
|
|
19286
|
-
}
|
|
19287
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19288
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19289
|
-
// never duplicates findings.
|
|
19730
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19731
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19732
|
+
// scan never duplicates findings.
|
|
19290
19733
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19291
19734
|
// async EventsReadPort contract.
|
|
19735
|
+
//
|
|
19736
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19737
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19738
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19739
|
+
// index-friendly rather than walking rows that can never match.
|
|
19292
19740
|
contentHashes() {
|
|
19293
19741
|
const rows = allRows(
|
|
19294
|
-
this.db.prepare(
|
|
19742
|
+
this.db.prepare(
|
|
19743
|
+
`SELECT content_hash FROM audit_events
|
|
19744
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19745
|
+
)
|
|
19295
19746
|
);
|
|
19296
19747
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19297
19748
|
}
|
|
@@ -19627,17 +20078,20 @@ function parseExceptionRow(row) {
|
|
|
19627
20078
|
}
|
|
19628
20079
|
|
|
19629
20080
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19630
|
-
function
|
|
20081
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19631
20082
|
return `(
|
|
19632
|
-
SELECT fr
|
|
20083
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19633
20084
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19634
20085
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19635
20086
|
LIMIT 1
|
|
19636
20087
|
)`;
|
|
19637
20088
|
}
|
|
20089
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20090
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20091
|
+
}
|
|
19638
20092
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19639
|
-
SELECT finding_key, status FROM (
|
|
19640
|
-
SELECT fr.finding_key, fr.status,
|
|
20093
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20094
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19641
20095
|
ROW_NUMBER() OVER (
|
|
19642
20096
|
PARTITION BY fr.finding_key
|
|
19643
20097
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19664,68 +20118,21 @@ var DAY_MS3 = 864e5;
|
|
|
19664
20118
|
var SqliteFindingsRepository = class {
|
|
19665
20119
|
constructor(db) {
|
|
19666
20120
|
this.db = db;
|
|
19667
|
-
this.insertStmt = db.prepare(
|
|
19668
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19669
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19670
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19671
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19672
|
-
event_id = excluded.event_id,
|
|
19673
|
-
category = excluded.category,
|
|
19674
|
-
severity = excluded.severity,
|
|
19675
|
-
span_start = excluded.span_start,
|
|
19676
|
-
span_end = excluded.span_end,
|
|
19677
|
-
masked_match = excluded.masked_match,
|
|
19678
|
-
action_taken = excluded.action_taken,
|
|
19679
|
-
confidence = excluded.confidence`
|
|
19680
|
-
);
|
|
19681
|
-
this.sessionDupStmt = db.prepare(
|
|
19682
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19683
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19684
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19685
|
-
LIMIT 1`
|
|
19686
|
-
);
|
|
19687
20121
|
}
|
|
19688
20122
|
db;
|
|
19689
|
-
insertStmt;
|
|
19690
|
-
sessionDupStmt;
|
|
19691
|
-
insertFindings(findings, scope = {}) {
|
|
19692
|
-
for (const finding of findings) {
|
|
19693
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19694
|
-
const row = toFindingRow(finding);
|
|
19695
|
-
this.insertStmt.run({
|
|
19696
|
-
id: row.id,
|
|
19697
|
-
eventId: row.eventId,
|
|
19698
|
-
ruleId: row.ruleId,
|
|
19699
|
-
category: row.category,
|
|
19700
|
-
severity: row.severity,
|
|
19701
|
-
spanStart: row.spanStart,
|
|
19702
|
-
spanEnd: row.spanEnd,
|
|
19703
|
-
maskedMatch: row.maskedMatch,
|
|
19704
|
-
actionTaken: row.actionTaken,
|
|
19705
|
-
confidence: row.confidence,
|
|
19706
|
-
findingKey: row.findingKey ?? null
|
|
19707
|
-
});
|
|
19708
|
-
}
|
|
19709
|
-
}
|
|
19710
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19711
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19712
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19713
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19714
|
-
const hit = this.sessionDupStmt.get({
|
|
19715
|
-
ruleId: finding.ruleId,
|
|
19716
|
-
maskedMatch: finding.maskedMatch,
|
|
19717
|
-
sessionId
|
|
19718
|
-
});
|
|
19719
|
-
return hit !== void 0;
|
|
19720
|
-
}
|
|
19721
20123
|
recentFindings(opts) {
|
|
19722
20124
|
const limit = opts?.limit ?? 50;
|
|
19723
20125
|
const rows = allRows(
|
|
19724
20126
|
this.db.prepare(
|
|
19725
|
-
`SELECT f.id, f.event_id,
|
|
19726
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19727
|
-
|
|
19728
|
-
|
|
20127
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20128
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20129
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20130
|
+
e.event_type AS kind
|
|
20131
|
+
FROM inspection_findings f
|
|
20132
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20133
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20134
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20135
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19729
20136
|
LIMIT :limit`
|
|
19730
20137
|
),
|
|
19731
20138
|
{ limit }
|
|
@@ -19747,25 +20154,34 @@ var SqliteFindingsRepository = class {
|
|
|
19747
20154
|
);
|
|
19748
20155
|
}
|
|
19749
20156
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19750
|
-
* session-stamped
|
|
20157
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19751
20158
|
* page can label its findings link without the grouped pipeline. */
|
|
19752
20159
|
sessionFindingsCount(sessionId) {
|
|
19753
20160
|
if (!sessionId) return Promise.resolve(0);
|
|
19754
20161
|
return Promise.resolve(
|
|
19755
20162
|
countScalar(
|
|
19756
20163
|
this.db,
|
|
19757
|
-
`SELECT count(*) AS n FROM
|
|
19758
|
-
JOIN
|
|
19759
|
-
WHERE
|
|
20164
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20165
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20166
|
+
WHERE e.root_session_id = :sessionId
|
|
20167
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19760
20168
|
{ sessionId }
|
|
19761
20169
|
)
|
|
19762
20170
|
);
|
|
19763
20171
|
}
|
|
19764
|
-
/** Per-rule transcript firing tally for one session —
|
|
19765
|
-
*
|
|
19766
|
-
*
|
|
19767
|
-
*
|
|
19768
|
-
*
|
|
20172
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20173
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20174
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20175
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20176
|
+
* tally with the deduped groups it lists.
|
|
20177
|
+
*
|
|
20178
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20179
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20180
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20181
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20182
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20183
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20184
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19769
20185
|
sessionFirings(sessionId) {
|
|
19770
20186
|
return Object.fromEntries(
|
|
19771
20187
|
countBy(
|
|
@@ -19775,18 +20191,25 @@ var SqliteFindingsRepository = class {
|
|
|
19775
20191
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19776
20192
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19777
20193
|
WHERE e.root_session_id = :sessionId
|
|
20194
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19778
20195
|
GROUP BY d.rule_id`,
|
|
19779
20196
|
{ sessionId }
|
|
19780
20197
|
)
|
|
19781
20198
|
);
|
|
19782
20199
|
}
|
|
19783
20200
|
/**
|
|
19784
|
-
* Grouped findings for the dashboard — joins
|
|
19785
|
-
* toolName from
|
|
19786
|
-
*
|
|
20201
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20202
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20203
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20204
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20205
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20206
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20207
|
+
* severity then recency. Filtering
|
|
19787
20208
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19788
20209
|
* reflect the full filtered set; `items` is the requested
|
|
19789
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20210
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20211
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20212
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19790
20213
|
*
|
|
19791
20214
|
* Two reads, neither of which materializes a row per finding:
|
|
19792
20215
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19799,10 +20222,11 @@ var SqliteFindingsRepository = class {
|
|
|
19799
20222
|
* rule is ever restated in SQL.
|
|
19800
20223
|
*/
|
|
19801
20224
|
listGroupedFindings(query) {
|
|
19802
|
-
const sessionPredicate = query.sessionId ? `
|
|
20225
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20226
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19803
20227
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19804
20228
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19805
|
-
predicate
|
|
20229
|
+
predicate,
|
|
19806
20230
|
params: sessionParams
|
|
19807
20231
|
});
|
|
19808
20232
|
const rows = allRows(
|
|
@@ -19810,24 +20234,26 @@ var SqliteFindingsRepository = class {
|
|
|
19810
20234
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19811
20235
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19812
20236
|
FROM (
|
|
19813
|
-
SELECT f.id AS id,
|
|
19814
|
-
|
|
20237
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20238
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19815
20239
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19816
|
-
e.
|
|
19817
|
-
json_extract(e.
|
|
19818
|
-
json_extract(e.
|
|
19819
|
-
json_extract(e.
|
|
19820
|
-
e.
|
|
20240
|
+
e.started_at AS occurred_at,
|
|
20241
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20242
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20243
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20244
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20245
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19821
20246
|
latest.status AS latest_status,
|
|
19822
20247
|
ROW_NUMBER() OVER (
|
|
19823
|
-
PARTITION BY
|
|
19824
|
-
ORDER BY e.
|
|
20248
|
+
PARTITION BY d.rule_id
|
|
20249
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19825
20250
|
) AS rn
|
|
19826
|
-
FROM
|
|
19827
|
-
JOIN
|
|
20251
|
+
FROM inspection_findings f
|
|
20252
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20253
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19828
20254
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19829
20255
|
ON latest.finding_key = f.finding_key
|
|
19830
|
-
${
|
|
20256
|
+
${predicate}
|
|
19831
20257
|
)
|
|
19832
20258
|
WHERE rn <= :cap
|
|
19833
20259
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19854,17 +20280,29 @@ var SqliteFindingsRepository = class {
|
|
|
19854
20280
|
severity: query.severity,
|
|
19855
20281
|
providers: query.provider,
|
|
19856
20282
|
actions: query.action,
|
|
20283
|
+
statuses: query.status,
|
|
19857
20284
|
subtype: query.subtype,
|
|
19858
20285
|
q: query.q
|
|
19859
20286
|
};
|
|
19860
20287
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19861
20288
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20289
|
+
const statusFilter = query.status ?? [];
|
|
19862
20290
|
const totals = {
|
|
19863
|
-
findings: sorted.reduce((acc, g) =>
|
|
20291
|
+
findings: sorted.reduce((acc, g) => {
|
|
20292
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20293
|
+
const agg = aggregates.get(g.id);
|
|
20294
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20295
|
+
}, 0),
|
|
19864
20296
|
groups: sorted.length
|
|
19865
20297
|
};
|
|
19866
20298
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19867
|
-
const
|
|
20299
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20300
|
+
const items = sorted.slice(0, limit).map(
|
|
20301
|
+
(g) => statusSet ? {
|
|
20302
|
+
...g,
|
|
20303
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20304
|
+
} : g
|
|
20305
|
+
);
|
|
19868
20306
|
return Promise.resolve({
|
|
19869
20307
|
totals,
|
|
19870
20308
|
facets,
|
|
@@ -19878,45 +20316,62 @@ var SqliteFindingsRepository = class {
|
|
|
19878
20316
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19879
20317
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19880
20318
|
*
|
|
19881
|
-
*
|
|
19882
|
-
*
|
|
19883
|
-
*
|
|
19884
|
-
* status
|
|
19885
|
-
*
|
|
19886
|
-
*
|
|
20319
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20320
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20321
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20322
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20323
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20324
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20325
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20326
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20327
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20328
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20329
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19887
20330
|
* a group's row stays small however many findings it holds.
|
|
19888
20331
|
*
|
|
19889
20332
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19890
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19891
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19892
|
-
* that is a string proportional to the store (~8MB over
|
|
19893
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19894
|
-
* match an instance outside the preview, which searching
|
|
19895
|
-
* would silently lose, so it is fetched only when the
|
|
19896
|
-
* carries a `q`.
|
|
20333
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20334
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20335
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20336
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20337
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20338
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20339
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20340
|
+
* path repeating across tuples.)
|
|
19897
20341
|
*/
|
|
19898
20342
|
groupAggregates(withSearchText, scope) {
|
|
19899
|
-
const
|
|
19900
|
-
group_concat(DISTINCT json_extract(e.
|
|
19901
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20343
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20344
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20345
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19902
20346
|
const rows = this.db.prepare(
|
|
19903
|
-
`SELECT
|
|
19904
|
-
|
|
19905
|
-
max(
|
|
19906
|
-
group_concat(
|
|
19907
|
-
group_concat(
|
|
19908
|
-
group_concat(
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
20347
|
+
`SELECT rule_id,
|
|
20348
|
+
sum(tuple_count) AS instance_count,
|
|
20349
|
+
max(latest_at) AS latest_at,
|
|
20350
|
+
group_concat(source_tools) AS source_tools,
|
|
20351
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20352
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20353
|
+
group_concat(repos) AS repos,
|
|
20354
|
+
group_concat(files) AS files,
|
|
20355
|
+
group_concat(tool_names) AS tool_names
|
|
20356
|
+
FROM (
|
|
20357
|
+
SELECT d.rule_id AS rule_id,
|
|
20358
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20359
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20360
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20361
|
+
count(*) AS tuple_count,
|
|
20362
|
+
max(e.started_at) AS latest_at,
|
|
20363
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20364
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20365
|
+
${innerSearchColumns}
|
|
20366
|
+
FROM inspection_findings f
|
|
20367
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20368
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20369
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20370
|
+
ON latest.finding_key = f.finding_key
|
|
20371
|
+
${scope.predicate}
|
|
20372
|
+
GROUP BY d.rule_id, status_tuple
|
|
20373
|
+
)
|
|
20374
|
+
GROUP BY rule_id`
|
|
19920
20375
|
).all(scope.params);
|
|
19921
20376
|
return new Map(
|
|
19922
20377
|
rows.map((r) => [
|
|
@@ -19926,13 +20381,14 @@ var SqliteFindingsRepository = class {
|
|
|
19926
20381
|
sourceTools: splitConcat(r.source_tools),
|
|
19927
20382
|
actionsTaken: splitConcat(r.actions_taken),
|
|
19928
20383
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19929
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20384
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
19930
20385
|
return {
|
|
19931
20386
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19932
20387
|
// so the marker stands in for the key itself (never rendered).
|
|
19933
20388
|
kind,
|
|
19934
20389
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19935
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20390
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20391
|
+
count: Number(count)
|
|
19936
20392
|
};
|
|
19937
20393
|
}),
|
|
19938
20394
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -19949,10 +20405,21 @@ var SqliteFindingsRepository = class {
|
|
|
19949
20405
|
);
|
|
19950
20406
|
}
|
|
19951
20407
|
healthSummary() {
|
|
19952
|
-
const total = countScalar(
|
|
20408
|
+
const total = countScalar(
|
|
20409
|
+
this.db,
|
|
20410
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20411
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20412
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20413
|
+
);
|
|
19953
20414
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19954
20415
|
const grouped = allRows(
|
|
19955
|
-
this.db.prepare(
|
|
20416
|
+
this.db.prepare(
|
|
20417
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20418
|
+
FROM inspection_findings f
|
|
20419
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20420
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20421
|
+
GROUP BY f.action_taken`
|
|
20422
|
+
)
|
|
19956
20423
|
);
|
|
19957
20424
|
for (const row of grouped) {
|
|
19958
20425
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -19960,12 +20427,15 @@ var SqliteFindingsRepository = class {
|
|
|
19960
20427
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19961
20428
|
const sevRows = allRows(
|
|
19962
20429
|
this.db.prepare(
|
|
19963
|
-
`SELECT
|
|
19964
|
-
FROM
|
|
20430
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20431
|
+
FROM inspection_findings f
|
|
20432
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20433
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19965
20434
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19966
20435
|
ON latest.finding_key = f.finding_key
|
|
19967
|
-
WHERE
|
|
19968
|
-
|
|
20436
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20437
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20438
|
+
GROUP BY d.severity`
|
|
19969
20439
|
)
|
|
19970
20440
|
);
|
|
19971
20441
|
for (const row of sevRows) {
|
|
@@ -19986,9 +20456,11 @@ var SqliteFindingsRepository = class {
|
|
|
19986
20456
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19987
20457
|
const rows = allRows(
|
|
19988
20458
|
this.db.prepare(
|
|
19989
|
-
`SELECT date(e.
|
|
19990
|
-
FROM
|
|
19991
|
-
|
|
20459
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20460
|
+
FROM inspection_findings f
|
|
20461
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20462
|
+
WHERE e.started_at >= :since
|
|
20463
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19992
20464
|
GROUP BY day, f.action_taken`
|
|
19993
20465
|
),
|
|
19994
20466
|
{ since }
|
|
@@ -20053,15 +20525,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20053
20525
|
this.insertStmt = db.prepare(
|
|
20054
20526
|
`INSERT INTO inspection_findings
|
|
20055
20527
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20056
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20528
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20529
|
+
finding_key, first_detected_at)
|
|
20057
20530
|
VALUES
|
|
20058
20531
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20059
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20060
|
-
|
|
20532
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20533
|
+
:findingKey,
|
|
20534
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20535
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20536
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20537
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20538
|
+
audit_event_id = excluded.audit_event_id,
|
|
20539
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20540
|
+
classified_data_id = excluded.classified_data_id,
|
|
20541
|
+
span_start = excluded.span_start,
|
|
20542
|
+
span_end = excluded.span_end,
|
|
20543
|
+
masked_match = excluded.masked_match,
|
|
20544
|
+
action_taken = excluded.action_taken,
|
|
20545
|
+
confidence = excluded.confidence`
|
|
20546
|
+
);
|
|
20547
|
+
this.sessionDupStmt = db.prepare(
|
|
20548
|
+
`SELECT 1 FROM inspection_findings f
|
|
20549
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20550
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20551
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20552
|
+
AND e.root_session_id = :sessionId
|
|
20553
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20554
|
+
LIMIT 1`
|
|
20555
|
+
);
|
|
20556
|
+
this.eventDupStmt = db.prepare(
|
|
20557
|
+
`SELECT 1 FROM inspection_findings f
|
|
20558
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20559
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20560
|
+
AND f.masked_match = :maskedMatch
|
|
20561
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20562
|
+
LIMIT 1`
|
|
20061
20563
|
);
|
|
20062
20564
|
}
|
|
20063
20565
|
db;
|
|
20064
20566
|
insertStmt;
|
|
20567
|
+
sessionDupStmt;
|
|
20568
|
+
eventDupStmt;
|
|
20569
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20570
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20571
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20572
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20573
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20574
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20575
|
+
}
|
|
20576
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20577
|
+
// recorded against the given audit event.
|
|
20578
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20579
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20580
|
+
}
|
|
20065
20581
|
insertFinding(input) {
|
|
20066
20582
|
const row = toInspectionFindingRow(input);
|
|
20067
20583
|
this.insertStmt.run(
|
|
@@ -20074,7 +20590,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20074
20590
|
spanEnd: row.spanEnd,
|
|
20075
20591
|
maskedMatch: row.maskedMatch,
|
|
20076
20592
|
actionTaken: row.actionTaken,
|
|
20077
|
-
confidence: row.confidence
|
|
20593
|
+
confidence: row.confidence,
|
|
20594
|
+
findingKey: row.findingKey,
|
|
20595
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20078
20596
|
})
|
|
20079
20597
|
);
|
|
20080
20598
|
}
|
|
@@ -20346,7 +20864,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20346
20864
|
installedRuleset() {
|
|
20347
20865
|
const rows = allRows(
|
|
20348
20866
|
this.db.prepare(
|
|
20349
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20867
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20350
20868
|
)
|
|
20351
20869
|
);
|
|
20352
20870
|
const out = {
|
|
@@ -20354,7 +20872,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20354
20872
|
enabledPacks: 0,
|
|
20355
20873
|
rules: [],
|
|
20356
20874
|
invalidRules: 0,
|
|
20357
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20875
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20876
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20358
20877
|
};
|
|
20359
20878
|
for (const row of rows) {
|
|
20360
20879
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20376,6 +20895,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20376
20895
|
if (parsed.success) {
|
|
20377
20896
|
out.rules.push(parsed.data);
|
|
20378
20897
|
out.ruleActions.set(parsed.data.id, action);
|
|
20898
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20379
20899
|
} else out.invalidRules += 1;
|
|
20380
20900
|
}
|
|
20381
20901
|
}
|
|
@@ -21550,19 +22070,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21550
22070
|
);
|
|
21551
22071
|
this.openAtRestStmt = db.prepare(
|
|
21552
22072
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21553
|
-
FROM
|
|
21554
|
-
JOIN
|
|
21555
|
-
WHERE e.
|
|
21556
|
-
AND json_extract(e.
|
|
22073
|
+
FROM inspection_findings f
|
|
22074
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22075
|
+
WHERE e.event_type = 'code_change'
|
|
22076
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21557
22077
|
AND f.finding_key IS NOT NULL
|
|
21558
22078
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21559
22079
|
);
|
|
21560
22080
|
this.resolvedAtRestStmt = db.prepare(
|
|
21561
22081
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21562
|
-
FROM
|
|
21563
|
-
JOIN
|
|
21564
|
-
WHERE e.
|
|
21565
|
-
AND json_extract(e.
|
|
22082
|
+
FROM inspection_findings f
|
|
22083
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22084
|
+
WHERE e.event_type = 'code_change'
|
|
22085
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21566
22086
|
AND f.finding_key IS NOT NULL
|
|
21567
22087
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21568
22088
|
);
|
|
@@ -21630,6 +22150,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21630
22150
|
}
|
|
21631
22151
|
};
|
|
21632
22152
|
|
|
22153
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22154
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22155
|
+
constructor(db) {
|
|
22156
|
+
this.db = db;
|
|
22157
|
+
this.upsertStmt = db.prepare(
|
|
22158
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22159
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22160
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22161
|
+
verdict = excluded.verdict,
|
|
22162
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22163
|
+
checked_at = excluded.checked_at`
|
|
22164
|
+
);
|
|
22165
|
+
this.readStmt = db.prepare(
|
|
22166
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22167
|
+
);
|
|
22168
|
+
}
|
|
22169
|
+
db;
|
|
22170
|
+
upsertStmt;
|
|
22171
|
+
readStmt;
|
|
22172
|
+
getVerdict(ruleKey) {
|
|
22173
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22174
|
+
}
|
|
22175
|
+
setVerdict(ruleKey, verdict, worstProbeMs) {
|
|
22176
|
+
failOpenTransaction(this.db, () => {
|
|
22177
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
|
|
22178
|
+
});
|
|
22179
|
+
}
|
|
22180
|
+
};
|
|
22181
|
+
|
|
21633
22182
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21634
22183
|
var SqliteScanLedgerRepository = class {
|
|
21635
22184
|
constructor(db) {
|
|
@@ -21756,25 +22305,27 @@ var SqliteSecurityRepository = class {
|
|
|
21756
22305
|
severitySummary() {
|
|
21757
22306
|
const rows = allRows(
|
|
21758
22307
|
this.db.prepare(
|
|
21759
|
-
`SELECT
|
|
22308
|
+
`SELECT d.severity AS severity,
|
|
21760
22309
|
COUNT(*) AS count,
|
|
21761
22310
|
SUM(CASE
|
|
21762
|
-
WHEN e.
|
|
22311
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21763
22312
|
WHEN f.finding_key IS NULL THEN 0
|
|
21764
22313
|
WHEN latest.status = 'resolved' THEN 1
|
|
21765
22314
|
ELSE 0
|
|
21766
22315
|
END) AS caught,
|
|
21767
22316
|
SUM(CASE
|
|
21768
|
-
WHEN e.
|
|
22317
|
+
WHEN e.event_type = 'code_change'
|
|
21769
22318
|
AND f.finding_key IS NOT NULL
|
|
21770
22319
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21771
22320
|
ELSE 0
|
|
21772
22321
|
END) AS open_at_rest
|
|
21773
|
-
FROM
|
|
21774
|
-
JOIN
|
|
22322
|
+
FROM inspection_findings f
|
|
22323
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22324
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21775
22325
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21776
22326
|
ON latest.finding_key = f.finding_key
|
|
21777
|
-
|
|
22327
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22328
|
+
GROUP BY d.severity`
|
|
21778
22329
|
)
|
|
21779
22330
|
);
|
|
21780
22331
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21840,7 +22391,7 @@ var SqliteSecurityRepository = class {
|
|
|
21840
22391
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21841
22392
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21842
22393
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21843
|
-
// (
|
|
22394
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21844
22395
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21845
22396
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21846
22397
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21865,30 +22416,20 @@ var SqliteSecurityRepository = class {
|
|
|
21865
22416
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21866
22417
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21867
22418
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21868
|
-
//
|
|
21869
|
-
// the parent event's
|
|
21870
|
-
// backfill left null.
|
|
21871
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
|
|
21875
|
-
|
|
21876
|
-
|
|
21877
|
-
|
|
21878
|
-
|
|
21879
|
-
|
|
21880
|
-
WHERE fr.finding_key = f.finding_key
|
|
21881
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21882
|
-
LIMIT 1
|
|
21883
|
-
) AS latest_method,
|
|
21884
|
-
(
|
|
21885
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21886
|
-
WHERE fr.finding_key = f.finding_key
|
|
21887
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21888
|
-
LIMIT 1
|
|
21889
|
-
) AS latest_resolved_at
|
|
21890
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22419
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22420
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22421
|
+
// legacy/edge row the backfill left null.
|
|
22422
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22423
|
+
latest.status AS latest_status,
|
|
22424
|
+
latest.method AS latest_method,
|
|
22425
|
+
latest.resolved_at AS latest_resolved_at
|
|
22426
|
+
FROM inspection_findings f
|
|
22427
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22428
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22429
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22430
|
+
ON latest.finding_key = f.finding_key
|
|
21891
22431
|
WHERE f.finding_key IS NOT NULL
|
|
22432
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21892
22433
|
AND EXISTS (
|
|
21893
22434
|
SELECT 1 FROM finding_resolution fr
|
|
21894
22435
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21935,11 +22476,13 @@ var SqliteSecurityRepository = class {
|
|
|
21935
22476
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21936
22477
|
const rows = allRows(
|
|
21937
22478
|
this.db.prepare(
|
|
21938
|
-
`SELECT json_extract(e.
|
|
21939
|
-
FROM
|
|
21940
|
-
|
|
21941
|
-
|
|
21942
|
-
AND
|
|
22479
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22480
|
+
FROM inspection_findings f
|
|
22481
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22482
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22483
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22484
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22485
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
21943
22486
|
GROUP BY repo
|
|
21944
22487
|
ORDER BY c DESC, repo
|
|
21945
22488
|
LIMIT :limit`
|
|
@@ -21963,44 +22506,28 @@ var SqliteSecurityRepository = class {
|
|
|
21963
22506
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
21964
22507
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
21965
22508
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
21966
|
-
// finding's parent event (
|
|
21967
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
21968
|
-
// capped at `limit`.
|
|
22509
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22510
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22511
|
+
// DESC, capped at `limit`.
|
|
21969
22512
|
recentlyResolved(limit = 20) {
|
|
21970
22513
|
const rows = allRows(
|
|
21971
22514
|
this.db.prepare(
|
|
21972
22515
|
`SELECT f.finding_key AS finding_key,
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
json_extract(e.
|
|
21976
|
-
COALESCE(f.first_detected_at, e.
|
|
21977
|
-
|
|
21978
|
-
|
|
21979
|
-
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
WHERE e.kind = 'code_change'
|
|
22516
|
+
d.rule_id AS rule_id,
|
|
22517
|
+
d.severity AS severity,
|
|
22518
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22519
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22520
|
+
latest.resolved_at AS latest_resolved_at
|
|
22521
|
+
FROM inspection_findings f
|
|
22522
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22523
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22524
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22525
|
+
ON latest.finding_key = f.finding_key
|
|
22526
|
+
WHERE e.event_type = 'code_change'
|
|
21985
22527
|
AND f.finding_key IS NOT NULL
|
|
21986
|
-
AND
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21990
|
-
LIMIT 1
|
|
21991
|
-
) = 'resolved'
|
|
21992
|
-
AND (
|
|
21993
|
-
SELECT fr.method FROM finding_resolution fr
|
|
21994
|
-
WHERE fr.finding_key = f.finding_key
|
|
21995
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21996
|
-
LIMIT 1
|
|
21997
|
-
) = 'fixed-at-source'
|
|
21998
|
-
AND (
|
|
21999
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22000
|
-
WHERE fr.finding_key = f.finding_key
|
|
22001
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22002
|
-
LIMIT 1
|
|
22003
|
-
) IS NOT NULL
|
|
22528
|
+
AND latest.status = 'resolved'
|
|
22529
|
+
AND latest.method = 'fixed-at-source'
|
|
22530
|
+
AND latest.resolved_at IS NOT NULL
|
|
22004
22531
|
ORDER BY latest_resolved_at DESC
|
|
22005
22532
|
LIMIT :limit`
|
|
22006
22533
|
),
|
|
@@ -22019,15 +22546,18 @@ var SqliteSecurityRepository = class {
|
|
|
22019
22546
|
return Promise.resolve({ items });
|
|
22020
22547
|
}
|
|
22021
22548
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22022
|
-
// epoch-millis timestamp.
|
|
22549
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22023
22550
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22024
22551
|
findingsInRange(fromMs, toMs) {
|
|
22025
22552
|
const rows = allRows(
|
|
22026
22553
|
this.db.prepare(
|
|
22027
|
-
`SELECT e.
|
|
22028
|
-
FROM
|
|
22029
|
-
|
|
22030
|
-
|
|
22554
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22555
|
+
FROM inspection_findings f
|
|
22556
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22557
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22558
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22559
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22560
|
+
ORDER BY e.started_at`
|
|
22031
22561
|
),
|
|
22032
22562
|
{ from: fromMs, to: toMs }
|
|
22033
22563
|
);
|
|
@@ -22041,11 +22571,50 @@ var SqliteSecurityRepository = class {
|
|
|
22041
22571
|
|
|
22042
22572
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22043
22573
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22044
|
-
var
|
|
22574
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22575
|
+
var IN_CHUNK = 500;
|
|
22576
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22577
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22578
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22579
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22045
22580
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22046
22581
|
function parseNetwork(networkJson) {
|
|
22047
22582
|
return safeJson(networkJson, null);
|
|
22048
22583
|
}
|
|
22584
|
+
function capHits(all, mode) {
|
|
22585
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22586
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22587
|
+
}
|
|
22588
|
+
if (mode === "walk") {
|
|
22589
|
+
return {
|
|
22590
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22591
|
+
droppedFiles: [],
|
|
22592
|
+
truncated: true
|
|
22593
|
+
};
|
|
22594
|
+
}
|
|
22595
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22596
|
+
for (const hit of all) {
|
|
22597
|
+
const bucket = byFile.get(hit.site.file);
|
|
22598
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22599
|
+
else bucket.push(hit);
|
|
22600
|
+
}
|
|
22601
|
+
const hits = [];
|
|
22602
|
+
const droppedFiles = [];
|
|
22603
|
+
for (const [file2, bucket] of byFile) {
|
|
22604
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22605
|
+
else hits.push(...bucket);
|
|
22606
|
+
}
|
|
22607
|
+
return { hits, droppedFiles, truncated: true };
|
|
22608
|
+
}
|
|
22609
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22610
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22611
|
+
const dropped = new Set(droppedFiles);
|
|
22612
|
+
return {
|
|
22613
|
+
mode: "ledger",
|
|
22614
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22615
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22616
|
+
};
|
|
22617
|
+
}
|
|
22049
22618
|
function toEndpointSummary(row) {
|
|
22050
22619
|
return {
|
|
22051
22620
|
id: row.id,
|
|
@@ -22136,13 +22705,15 @@ var SqliteSharesRepository = class {
|
|
|
22136
22705
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22137
22706
|
const insecure = countScalar(
|
|
22138
22707
|
this.db,
|
|
22139
|
-
|
|
22708
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22709
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22140
22710
|
);
|
|
22141
22711
|
const needsReview = countScalar(
|
|
22142
22712
|
this.db,
|
|
22143
22713
|
`SELECT count(DISTINCT d.id) AS n
|
|
22144
22714
|
FROM share_destination d
|
|
22145
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22715
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22716
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22146
22717
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22147
22718
|
);
|
|
22148
22719
|
const kindCounts = countBy(
|
|
@@ -22152,6 +22723,7 @@ var SqliteSharesRepository = class {
|
|
|
22152
22723
|
const byKind = {
|
|
22153
22724
|
provider: kindCounts.get("provider") ?? 0,
|
|
22154
22725
|
internal: kindCounts.get("internal") ?? 0,
|
|
22726
|
+
external: kindCounts.get("external") ?? 0,
|
|
22155
22727
|
ip: kindCounts.get("ip") ?? 0
|
|
22156
22728
|
};
|
|
22157
22729
|
const trustCounts = countBy(
|
|
@@ -22227,23 +22799,316 @@ var SqliteSharesRepository = class {
|
|
|
22227
22799
|
// real edit from a no-such-destination.
|
|
22228
22800
|
/**
|
|
22229
22801
|
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22230
|
-
* `null` deletes the override
|
|
22802
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22803
|
+
*
|
|
22804
|
+
* The written row carries both the destination id and its host, so the
|
|
22805
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22806
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22807
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22808
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22809
|
+
* would otherwise race a concurrent prune.
|
|
22231
22810
|
*/
|
|
22232
22811
|
setEgressDecision(destinationId, decision) {
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22812
|
+
let existed = false;
|
|
22813
|
+
withTransaction(
|
|
22814
|
+
this.db,
|
|
22815
|
+
() => {
|
|
22816
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22817
|
+
if (dest === void 0) return;
|
|
22818
|
+
existed = true;
|
|
22819
|
+
this.db.prepare(
|
|
22820
|
+
`DELETE FROM egress_decision_override
|
|
22821
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22822
|
+
).run({ host: dest.host, destinationId });
|
|
22823
|
+
if (decision === null) return;
|
|
22824
|
+
this.db.prepare(
|
|
22825
|
+
`INSERT INTO egress_decision_override
|
|
22826
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22827
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22828
|
+
).run({
|
|
22829
|
+
id: randomUUID7(),
|
|
22830
|
+
destinationId,
|
|
22831
|
+
host: dest.host,
|
|
22832
|
+
decision,
|
|
22833
|
+
now: Date.now()
|
|
22834
|
+
});
|
|
22835
|
+
},
|
|
22836
|
+
"IMMEDIATE"
|
|
22837
|
+
);
|
|
22838
|
+
return existed;
|
|
22839
|
+
}
|
|
22840
|
+
/**
|
|
22841
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22842
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22843
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22844
|
+
* references, and drop what no longer has evidence.
|
|
22845
|
+
*
|
|
22846
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22847
|
+
* display payload and never scope a delete. The whole write is one
|
|
22848
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22849
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22850
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22851
|
+
* ledger commit so the next scan retries.
|
|
22852
|
+
*
|
|
22853
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22854
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22855
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22856
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22857
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22858
|
+
*/
|
|
22859
|
+
recordProjectEgress(input) {
|
|
22860
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22861
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22862
|
+
const now = Date.now();
|
|
22863
|
+
let summary = {
|
|
22864
|
+
destinations: 0,
|
|
22865
|
+
endpoints: 0,
|
|
22866
|
+
callSites: 0,
|
|
22867
|
+
truncated,
|
|
22868
|
+
droppedFiles
|
|
22869
|
+
};
|
|
22870
|
+
withTransaction(
|
|
22871
|
+
this.db,
|
|
22872
|
+
() => {
|
|
22873
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22874
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22875
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22876
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22877
|
+
this.pruneOrphans();
|
|
22878
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22879
|
+
},
|
|
22880
|
+
"IMMEDIATE"
|
|
22881
|
+
);
|
|
22882
|
+
return summary;
|
|
22883
|
+
}
|
|
22884
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22885
|
+
/**
|
|
22886
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22887
|
+
*
|
|
22888
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22889
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22890
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22891
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22892
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22893
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22894
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22895
|
+
*/
|
|
22896
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22897
|
+
if (reconcile.mode === "walk") {
|
|
22898
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22899
|
+
this.db.prepare(
|
|
22900
|
+
`DELETE FROM share_call_site
|
|
22901
|
+
WHERE project_key = :key
|
|
22902
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22903
|
+
AND file NOT LIKE '.%'
|
|
22904
|
+
AND file NOT LIKE '%/.%'`
|
|
22905
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22906
|
+
return;
|
|
22907
|
+
}
|
|
22908
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22909
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22910
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22911
|
+
this.db.prepare(
|
|
22912
|
+
`DELETE FROM share_call_site
|
|
22913
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22914
|
+
).run(projectKey, ...chunk);
|
|
22915
|
+
}
|
|
22916
|
+
}
|
|
22917
|
+
/**
|
|
22918
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22919
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22920
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22921
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22922
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22923
|
+
* classification for this batch.
|
|
22924
|
+
*/
|
|
22925
|
+
upsertHits(input, hits, projectId, now) {
|
|
22926
|
+
if (hits.length === 0) return;
|
|
22927
|
+
const destStmt = this.db.prepare(
|
|
22928
|
+
`INSERT INTO share_destination
|
|
22929
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22930
|
+
created_at, updated_at)
|
|
22931
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22932
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22933
|
+
kind = excluded.kind,
|
|
22934
|
+
name = excluded.name,
|
|
22935
|
+
category = excluded.category,
|
|
22936
|
+
trust = excluded.trust,
|
|
22937
|
+
network_json = excluded.network_json,
|
|
22938
|
+
last_seen = excluded.last_seen,
|
|
22939
|
+
updated_at = excluded.updated_at`
|
|
22940
|
+
);
|
|
22941
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22942
|
+
const endpointStmt = this.db.prepare(
|
|
22943
|
+
`INSERT INTO share_endpoint
|
|
22944
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
22945
|
+
created_at, updated_at)
|
|
22946
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
22947
|
+
:now, :now)
|
|
22948
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
22949
|
+
transport = excluded.transport,
|
|
22950
|
+
template = excluded.template,
|
|
22951
|
+
data_class = excluded.data_class,
|
|
22952
|
+
last_seen = excluded.last_seen,
|
|
22953
|
+
updated_at = excluded.updated_at`
|
|
22954
|
+
);
|
|
22955
|
+
const endpointIdStmt = this.db.prepare(
|
|
22956
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
22957
|
+
);
|
|
22958
|
+
const siteStmt = this.db.prepare(
|
|
22959
|
+
`INSERT INTO share_call_site
|
|
22960
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
22961
|
+
project_id, created_at, updated_at)
|
|
22962
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
22963
|
+
:vendored, :projectId, :now, :now)
|
|
22964
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
22965
|
+
snippet = excluded.snippet,
|
|
22966
|
+
dynamic = excluded.dynamic,
|
|
22967
|
+
vendored = excluded.vendored,
|
|
22968
|
+
project = excluded.project,
|
|
22969
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
22970
|
+
updated_at = excluded.updated_at`
|
|
22971
|
+
);
|
|
22972
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
22973
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
22974
|
+
for (const hit of hits) {
|
|
22975
|
+
let destinationId = destIds.get(hit.host);
|
|
22976
|
+
if (destinationId === void 0) {
|
|
22977
|
+
destStmt.run({
|
|
22978
|
+
id: randomUUID7(),
|
|
22979
|
+
kind: hit.kind,
|
|
22980
|
+
name: hit.name,
|
|
22981
|
+
host: hit.host,
|
|
22982
|
+
category: hit.category,
|
|
22983
|
+
trust: hit.trust,
|
|
22984
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
22985
|
+
now
|
|
22986
|
+
});
|
|
22987
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
22988
|
+
destIds.set(hit.host, destinationId);
|
|
22989
|
+
}
|
|
22990
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
22991
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
22992
|
+
if (endpointId === void 0) {
|
|
22993
|
+
endpointStmt.run({
|
|
22994
|
+
id: randomUUID7(),
|
|
22995
|
+
destinationId,
|
|
22996
|
+
method: hit.method,
|
|
22997
|
+
transport: hit.transport,
|
|
22998
|
+
url: hit.url,
|
|
22999
|
+
template: boolToInt(hit.template),
|
|
23000
|
+
dataClass: hit.dataClass,
|
|
23001
|
+
now
|
|
23002
|
+
});
|
|
23003
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23004
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23005
|
+
}
|
|
23006
|
+
siteStmt.run({
|
|
23007
|
+
id: randomUUID7(),
|
|
23008
|
+
endpointId,
|
|
23009
|
+
project: input.project,
|
|
23010
|
+
projectKey: input.projectKey,
|
|
23011
|
+
file: hit.site.file,
|
|
23012
|
+
line: hit.site.line,
|
|
23013
|
+
snippet: hit.site.snippet,
|
|
23014
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23015
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23016
|
+
projectId,
|
|
23017
|
+
now
|
|
23018
|
+
});
|
|
22238
23019
|
}
|
|
23020
|
+
}
|
|
23021
|
+
/**
|
|
23022
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23023
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23024
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23025
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23026
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23027
|
+
* representative.
|
|
23028
|
+
*/
|
|
23029
|
+
knownProjectId(projectKey) {
|
|
23030
|
+
return getRow(
|
|
23031
|
+
this.db.prepare(
|
|
23032
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23033
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23034
|
+
),
|
|
23035
|
+
[projectKey]
|
|
23036
|
+
)?.projectId ?? null;
|
|
23037
|
+
}
|
|
23038
|
+
/**
|
|
23039
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23040
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23041
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
23042
|
+
*/
|
|
23043
|
+
confirmLastSeen(projectKey, now) {
|
|
22239
23044
|
this.db.prepare(
|
|
22240
|
-
`
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
23045
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23046
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23047
|
+
).run({ now, key: projectKey });
|
|
23048
|
+
this.db.prepare(
|
|
23049
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23050
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23051
|
+
FROM share_endpoint e
|
|
23052
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23053
|
+
WHERE c.project_key = :key)`
|
|
23054
|
+
).run({ now, key: projectKey });
|
|
23055
|
+
}
|
|
23056
|
+
/**
|
|
23057
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23058
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23059
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23060
|
+
*
|
|
23061
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23062
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23063
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23064
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23065
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23066
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23067
|
+
*/
|
|
23068
|
+
pruneOrphans() {
|
|
23069
|
+
this.db.exec(
|
|
23070
|
+
`DELETE FROM share_endpoint
|
|
23071
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23072
|
+
);
|
|
23073
|
+
this.db.exec(
|
|
23074
|
+
`DELETE FROM egress_decision_override
|
|
23075
|
+
WHERE host IS NULL
|
|
23076
|
+
AND destination_id IN (
|
|
23077
|
+
SELECT d.id FROM share_destination d
|
|
23078
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23079
|
+
);
|
|
23080
|
+
this.db.exec(
|
|
23081
|
+
`DELETE FROM share_destination
|
|
23082
|
+
WHERE NOT EXISTS (
|
|
23083
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23084
|
+
);
|
|
23085
|
+
}
|
|
23086
|
+
/**
|
|
23087
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23088
|
+
* projects and carry no project column, so both are counted through the call
|
|
23089
|
+
* sites that reference them.
|
|
23090
|
+
*/
|
|
23091
|
+
projectTotals(projectKey) {
|
|
23092
|
+
return {
|
|
23093
|
+
destinations: countScalar(
|
|
23094
|
+
this.db,
|
|
23095
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23096
|
+
FROM share_endpoint e
|
|
23097
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23098
|
+
WHERE c.project_key = ?`,
|
|
23099
|
+
[projectKey]
|
|
23100
|
+
),
|
|
23101
|
+
endpoints: countScalar(
|
|
23102
|
+
this.db,
|
|
23103
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23104
|
+
[projectKey]
|
|
23105
|
+
),
|
|
23106
|
+
callSites: countScalar(
|
|
23107
|
+
this.db,
|
|
23108
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23109
|
+
[projectKey]
|
|
23110
|
+
)
|
|
23111
|
+
};
|
|
22247
23112
|
}
|
|
22248
23113
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22249
23114
|
mapDestRow(r) {
|
|
@@ -22263,7 +23128,8 @@ var SqliteSharesRepository = class {
|
|
|
22263
23128
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22264
23129
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22265
23130
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22266
|
-
d.created_at AS createdAt,
|
|
23131
|
+
d.created_at AS createdAt,
|
|
23132
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22267
23133
|
const conditions = [];
|
|
22268
23134
|
const params = [];
|
|
22269
23135
|
if (kinds && kinds.length > 0) {
|
|
@@ -22274,7 +23140,8 @@ var SqliteSharesRepository = class {
|
|
|
22274
23140
|
conditions.push(
|
|
22275
23141
|
`(d.trust IN ('unverified', 'ip')
|
|
22276
23142
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22277
|
-
WHERE re.destination_id = d.id
|
|
23143
|
+
WHERE re.destination_id = d.id
|
|
23144
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22278
23145
|
);
|
|
22279
23146
|
}
|
|
22280
23147
|
let sql;
|
|
@@ -22287,7 +23154,7 @@ var SqliteSharesRepository = class {
|
|
|
22287
23154
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22288
23155
|
sql = `SELECT DISTINCT ${cols}
|
|
22289
23156
|
FROM share_destination d
|
|
22290
|
-
|
|
23157
|
+
${OVERRIDE_JOIN}
|
|
22291
23158
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22292
23159
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22293
23160
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22295,7 +23162,7 @@ var SqliteSharesRepository = class {
|
|
|
22295
23162
|
} else {
|
|
22296
23163
|
sql = `SELECT ${cols}
|
|
22297
23164
|
FROM share_destination d
|
|
22298
|
-
|
|
23165
|
+
${OVERRIDE_JOIN}
|
|
22299
23166
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22300
23167
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22301
23168
|
}
|
|
@@ -22310,9 +23177,9 @@ var SqliteSharesRepository = class {
|
|
|
22310
23177
|
this.db.prepare(
|
|
22311
23178
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22312
23179
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22313
|
-
|
|
23180
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22314
23181
|
FROM share_destination d
|
|
22315
|
-
|
|
23182
|
+
${OVERRIDE_JOIN}
|
|
22316
23183
|
WHERE d.id = ?`
|
|
22317
23184
|
),
|
|
22318
23185
|
[destinationId]
|
|
@@ -22514,9 +23381,10 @@ function openWithPragmas(file2) {
|
|
|
22514
23381
|
}
|
|
22515
23382
|
function backupLegacyStore(file2) {
|
|
22516
23383
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
|
|
23384
|
+
renameSync2(file2, backup);
|
|
23385
|
+
tightenFile(backup);
|
|
23386
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23387
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22520
23388
|
}
|
|
22521
23389
|
return backup;
|
|
22522
23390
|
}
|
|
@@ -22532,7 +23400,7 @@ function openLocalDatabase(dir) {
|
|
|
22532
23400
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22533
23401
|
);
|
|
22534
23402
|
}
|
|
22535
|
-
applyMigrations(db);
|
|
23403
|
+
applyMigrations(db, file2);
|
|
22536
23404
|
tightenPerms(file2);
|
|
22537
23405
|
const events = new SqliteEventsRepository(db);
|
|
22538
23406
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22541,6 +23409,7 @@ function openLocalDatabase(dir) {
|
|
|
22541
23409
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22542
23410
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22543
23411
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23412
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22544
23413
|
const security = new SqliteSecurityRepository(db);
|
|
22545
23414
|
const detections = new SqliteDetectionsRepository(db);
|
|
22546
23415
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22558,9 +23427,56 @@ function openLocalDatabase(dir) {
|
|
|
22558
23427
|
policies.seedDefaults();
|
|
22559
23428
|
function recordCapture(event, detected) {
|
|
22560
23429
|
failOpenTransaction(db, () => {
|
|
22561
|
-
events.insertEvent(event);
|
|
22562
23430
|
const sessionId = event.metadata?.sessionId;
|
|
22563
|
-
|
|
23431
|
+
if (sessionId) {
|
|
23432
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23433
|
+
}
|
|
23434
|
+
const auditEventId = captureId(
|
|
23435
|
+
sessionId ?? null,
|
|
23436
|
+
event.contentHash,
|
|
23437
|
+
event.metadata?.filePath ?? null
|
|
23438
|
+
);
|
|
23439
|
+
auditEvents.insertAuditEvent({
|
|
23440
|
+
id: auditEventId,
|
|
23441
|
+
eventType: event.kind,
|
|
23442
|
+
startedAt: event.occurredAt,
|
|
23443
|
+
parentId: sessionId,
|
|
23444
|
+
rootSessionId: sessionId,
|
|
23445
|
+
content: event.content,
|
|
23446
|
+
contentHash: event.contentHash,
|
|
23447
|
+
attributes: toCaptureAttributes(event)
|
|
23448
|
+
});
|
|
23449
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23450
|
+
for (const finding of detected) {
|
|
23451
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23452
|
+
continue;
|
|
23453
|
+
}
|
|
23454
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23455
|
+
auditEventId,
|
|
23456
|
+
finding.ruleId,
|
|
23457
|
+
finding.maskedMatch,
|
|
23458
|
+
finding.span.start,
|
|
23459
|
+
finding.span.end
|
|
23460
|
+
)) {
|
|
23461
|
+
continue;
|
|
23462
|
+
}
|
|
23463
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23464
|
+
let definitionId = definitionIds.get(key);
|
|
23465
|
+
if (!definitionId) {
|
|
23466
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23467
|
+
definitionIds.set(key, definitionId);
|
|
23468
|
+
}
|
|
23469
|
+
inspectionFindings.insertFinding({
|
|
23470
|
+
id: finding.id,
|
|
23471
|
+
auditEventId,
|
|
23472
|
+
inspectionDefinitionId: definitionId,
|
|
23473
|
+
span: finding.span,
|
|
23474
|
+
maskedMatch: finding.maskedMatch,
|
|
23475
|
+
actionTaken: finding.actionTaken,
|
|
23476
|
+
confidence: finding.confidence,
|
|
23477
|
+
findingKey: finding.findingKey ?? void 0
|
|
23478
|
+
});
|
|
23479
|
+
}
|
|
22564
23480
|
});
|
|
22565
23481
|
}
|
|
22566
23482
|
function ensureInventory(ctx) {
|
|
@@ -22678,6 +23594,7 @@ function openLocalDatabase(dir) {
|
|
|
22678
23594
|
scanLedger,
|
|
22679
23595
|
exceptions,
|
|
22680
23596
|
resolutions,
|
|
23597
|
+
ruleProbeCache,
|
|
22681
23598
|
security,
|
|
22682
23599
|
detections,
|
|
22683
23600
|
shares,
|
|
@@ -22707,9 +23624,12 @@ function openLocalDatabase(dir) {
|
|
|
22707
23624
|
};
|
|
22708
23625
|
}
|
|
22709
23626
|
|
|
23627
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23628
|
+
import { createHash as createHash3 } from "crypto";
|
|
23629
|
+
|
|
22710
23630
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22711
23631
|
import { createHmac, randomBytes } from "crypto";
|
|
22712
|
-
import {
|
|
23632
|
+
import { readFileSync } from "fs";
|
|
22713
23633
|
import { join as join2 } from "path";
|
|
22714
23634
|
var KEY_FILENAME = "exception.key";
|
|
22715
23635
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -22746,8 +23666,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
22746
23666
|
}
|
|
22747
23667
|
|
|
22748
23668
|
// ../../packages/persistence/src/local-layout.ts
|
|
22749
|
-
import {
|
|
22750
|
-
import {
|
|
23669
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23670
|
+
import { mkdir } from "fs/promises";
|
|
22751
23671
|
import { homedir } from "os";
|
|
22752
23672
|
import { join as join3 } from "path";
|
|
22753
23673
|
function defaultDataDir() {
|
|
@@ -22762,6 +23682,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22762
23682
|
function dbPath(base = defaultDataDir()) {
|
|
22763
23683
|
return join3(dataDir(base), "aka.db");
|
|
22764
23684
|
}
|
|
23685
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23686
|
+
ensureDataDirSync(dir);
|
|
23687
|
+
}
|
|
22765
23688
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22766
23689
|
const moves = [
|
|
22767
23690
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22769,19 +23692,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22769
23692
|
];
|
|
22770
23693
|
for (const { name, dest } of moves) {
|
|
22771
23694
|
try {
|
|
22772
|
-
|
|
22773
|
-
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
}
|
|
22777
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23695
|
+
ensureDataDirSync(dest);
|
|
23696
|
+
const moved = join3(dest, name);
|
|
23697
|
+
renameSync3(join3(base, name), moved);
|
|
23698
|
+
tightenFile(moved);
|
|
22778
23699
|
} catch {
|
|
22779
23700
|
}
|
|
22780
23701
|
}
|
|
22781
23702
|
}
|
|
22782
23703
|
|
|
22783
23704
|
// ../../packages/persistence/src/settings.ts
|
|
22784
|
-
import { readFileSync as readFileSync2
|
|
23705
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22785
23706
|
import { join as join4 } from "path";
|
|
22786
23707
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22787
23708
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22803,7 +23724,7 @@ function readJson(file2) {
|
|
|
22803
23724
|
}
|
|
22804
23725
|
|
|
22805
23726
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22806
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23727
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22807
23728
|
import { join as join5 } from "path";
|
|
22808
23729
|
var MARKER = "warn-era-capped";
|
|
22809
23730
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22811,7 +23732,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22811
23732
|
const marker = join5(dataDir2, MARKER);
|
|
22812
23733
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22813
23734
|
const capped = db.policies.capCategoryActions();
|
|
22814
|
-
|
|
23735
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22815
23736
|
`, { mode: DATA_FILE_MODE });
|
|
22816
23737
|
return { capped };
|
|
22817
23738
|
}
|
|
@@ -22866,6 +23787,12 @@ function resolveProvider() {
|
|
|
22866
23787
|
|
|
22867
23788
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22868
23789
|
function loadConfig(base = defaultDataDir()) {
|
|
23790
|
+
try {
|
|
23791
|
+
ensureLayoutDirSync(base);
|
|
23792
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23793
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23794
|
+
} catch {
|
|
23795
|
+
}
|
|
22869
23796
|
migrateLegacyLayout(base);
|
|
22870
23797
|
const settings = readWorkspaceSettings(base);
|
|
22871
23798
|
return {
|
|
@@ -22888,15 +23815,583 @@ function resolveProviderSafe() {
|
|
|
22888
23815
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22889
23816
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22890
23817
|
import { homedir as homedir2 } from "os";
|
|
22891
|
-
import { basename as basename2, join as
|
|
23818
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23819
|
+
|
|
23820
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23821
|
+
var EXTRACTOR_VERSION = "1";
|
|
23822
|
+
var PROVIDER_REGISTRY = [
|
|
23823
|
+
{
|
|
23824
|
+
id: "stripe",
|
|
23825
|
+
name: "Stripe",
|
|
23826
|
+
category: "Payments",
|
|
23827
|
+
hostSuffixes: ["stripe.com"],
|
|
23828
|
+
apiBase: "https://api.stripe.com",
|
|
23829
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23830
|
+
sdks: {
|
|
23831
|
+
npm: ["stripe"],
|
|
23832
|
+
pypi: ["stripe"],
|
|
23833
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23834
|
+
maven: ["com.stripe"],
|
|
23835
|
+
rubygems: ["stripe"],
|
|
23836
|
+
composer: ["stripe/stripe-php"],
|
|
23837
|
+
nuget: ["Stripe.net"]
|
|
23838
|
+
}
|
|
23839
|
+
},
|
|
23840
|
+
{
|
|
23841
|
+
id: "datadog",
|
|
23842
|
+
name: "Datadog",
|
|
23843
|
+
category: "Observability",
|
|
23844
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23845
|
+
apiBase: "https://api.datadoghq.com",
|
|
23846
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23847
|
+
sdks: {
|
|
23848
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23849
|
+
pypi: ["datadog", "ddtrace"],
|
|
23850
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23851
|
+
maven: ["com.datadoghq"],
|
|
23852
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23853
|
+
nuget: ["Datadog.Trace"]
|
|
23854
|
+
}
|
|
23855
|
+
},
|
|
23856
|
+
{
|
|
23857
|
+
id: "newrelic",
|
|
23858
|
+
name: "New Relic",
|
|
23859
|
+
category: "Observability",
|
|
23860
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23861
|
+
apiBase: "https://api.newrelic.com",
|
|
23862
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23863
|
+
sdks: {
|
|
23864
|
+
npm: ["newrelic"],
|
|
23865
|
+
pypi: ["newrelic"],
|
|
23866
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23867
|
+
maven: ["com.newrelic.agent.java"],
|
|
23868
|
+
rubygems: ["newrelic_rpm"],
|
|
23869
|
+
nuget: ["NewRelic.Agent"]
|
|
23870
|
+
}
|
|
23871
|
+
},
|
|
23872
|
+
{
|
|
23873
|
+
id: "sentry",
|
|
23874
|
+
name: "Sentry",
|
|
23875
|
+
category: "Error tracking",
|
|
23876
|
+
hostSuffixes: ["sentry.io"],
|
|
23877
|
+
apiBase: "https://sentry.io",
|
|
23878
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23879
|
+
sdks: {
|
|
23880
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23881
|
+
pypi: ["sentry-sdk"],
|
|
23882
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23883
|
+
maven: ["io.sentry"],
|
|
23884
|
+
rubygems: ["sentry-ruby"],
|
|
23885
|
+
cargo: ["sentry"],
|
|
23886
|
+
composer: ["sentry/sentry"],
|
|
23887
|
+
nuget: ["Sentry"]
|
|
23888
|
+
}
|
|
23889
|
+
},
|
|
23890
|
+
{
|
|
23891
|
+
id: "openai",
|
|
23892
|
+
name: "OpenAI",
|
|
23893
|
+
category: "LLM provider",
|
|
23894
|
+
hostSuffixes: ["openai.com"],
|
|
23895
|
+
apiBase: "https://api.openai.com",
|
|
23896
|
+
defaultDataClasses: ["pii", "source"],
|
|
23897
|
+
sdks: {
|
|
23898
|
+
npm: ["openai"],
|
|
23899
|
+
pypi: ["openai"],
|
|
23900
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23901
|
+
maven: ["com.openai"],
|
|
23902
|
+
rubygems: ["ruby-openai"],
|
|
23903
|
+
cargo: ["async-openai"],
|
|
23904
|
+
composer: ["openai-php/client"],
|
|
23905
|
+
nuget: ["OpenAI"]
|
|
23906
|
+
}
|
|
23907
|
+
},
|
|
23908
|
+
{
|
|
23909
|
+
id: "anthropic",
|
|
23910
|
+
name: "Anthropic",
|
|
23911
|
+
category: "LLM provider",
|
|
23912
|
+
hostSuffixes: ["anthropic.com"],
|
|
23913
|
+
apiBase: "https://api.anthropic.com",
|
|
23914
|
+
defaultDataClasses: ["pii", "source"],
|
|
23915
|
+
sdks: {
|
|
23916
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23917
|
+
pypi: ["anthropic"],
|
|
23918
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23919
|
+
nuget: ["Anthropic.SDK"]
|
|
23920
|
+
}
|
|
23921
|
+
},
|
|
23922
|
+
{
|
|
23923
|
+
id: "aws",
|
|
23924
|
+
name: "Amazon Web Services",
|
|
23925
|
+
category: "Cloud platform",
|
|
23926
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23927
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23928
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23929
|
+
sdks: {
|
|
23930
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23931
|
+
pypi: ["boto3"],
|
|
23932
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23933
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23934
|
+
rubygems: ["aws-sdk-s3"],
|
|
23935
|
+
cargo: ["aws-sdk-s3"],
|
|
23936
|
+
nuget: ["AWSSDK.S3"]
|
|
23937
|
+
}
|
|
23938
|
+
},
|
|
23939
|
+
{
|
|
23940
|
+
id: "gcp",
|
|
23941
|
+
name: "Google Cloud",
|
|
23942
|
+
category: "Cloud platform",
|
|
23943
|
+
hostSuffixes: ["googleapis.com"],
|
|
23944
|
+
apiBase: "https://storage.googleapis.com",
|
|
23945
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23946
|
+
sdks: {
|
|
23947
|
+
npm: ["@google-cloud/storage"],
|
|
23948
|
+
pypi: ["google-cloud-storage"],
|
|
23949
|
+
go: ["cloud.google.com/go"],
|
|
23950
|
+
maven: ["com.google.cloud"],
|
|
23951
|
+
rubygems: ["google-cloud-storage"],
|
|
23952
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
23953
|
+
}
|
|
23954
|
+
},
|
|
23955
|
+
{
|
|
23956
|
+
id: "azure",
|
|
23957
|
+
name: "Microsoft Azure",
|
|
23958
|
+
category: "Cloud platform",
|
|
23959
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
23960
|
+
apiBase: "https://management.azure.com",
|
|
23961
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23962
|
+
sdks: {
|
|
23963
|
+
npm: ["@azure/storage-blob"],
|
|
23964
|
+
pypi: ["azure-storage-blob"],
|
|
23965
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
23966
|
+
maven: ["com.azure"],
|
|
23967
|
+
rubygems: ["azure-storage-blob"],
|
|
23968
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
23969
|
+
}
|
|
23970
|
+
},
|
|
23971
|
+
{
|
|
23972
|
+
id: "slack",
|
|
23973
|
+
name: "Slack",
|
|
23974
|
+
category: "Notifications",
|
|
23975
|
+
hostSuffixes: ["slack.com"],
|
|
23976
|
+
apiBase: "https://slack.com/api",
|
|
23977
|
+
defaultDataClasses: ["logs"],
|
|
23978
|
+
sdks: {
|
|
23979
|
+
npm: ["@slack/web-api"],
|
|
23980
|
+
pypi: ["slack-sdk"],
|
|
23981
|
+
go: ["github.com/slack-go/slack"],
|
|
23982
|
+
maven: ["com.slack.api"],
|
|
23983
|
+
rubygems: ["slack-ruby-client"],
|
|
23984
|
+
composer: ["slack-php/slack-api"],
|
|
23985
|
+
nuget: ["SlackNet"]
|
|
23986
|
+
}
|
|
23987
|
+
},
|
|
23988
|
+
{
|
|
23989
|
+
id: "segment",
|
|
23990
|
+
name: "Segment",
|
|
23991
|
+
category: "Analytics",
|
|
23992
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
23993
|
+
apiBase: "https://api.segment.io",
|
|
23994
|
+
defaultDataClasses: ["customer"],
|
|
23995
|
+
sdks: {
|
|
23996
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
23997
|
+
pypi: ["segment-analytics-python"],
|
|
23998
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
23999
|
+
maven: ["com.segment.analytics.java"],
|
|
24000
|
+
rubygems: ["analytics-ruby"],
|
|
24001
|
+
nuget: ["Analytics"]
|
|
24002
|
+
}
|
|
24003
|
+
},
|
|
24004
|
+
{
|
|
24005
|
+
id: "twilio",
|
|
24006
|
+
name: "Twilio",
|
|
24007
|
+
category: "Communications",
|
|
24008
|
+
hostSuffixes: ["twilio.com"],
|
|
24009
|
+
apiBase: "https://api.twilio.com",
|
|
24010
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24011
|
+
sdks: {
|
|
24012
|
+
npm: ["twilio"],
|
|
24013
|
+
pypi: ["twilio"],
|
|
24014
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24015
|
+
maven: ["com.twilio.sdk"],
|
|
24016
|
+
rubygems: ["twilio-ruby"],
|
|
24017
|
+
composer: ["twilio/sdk"],
|
|
24018
|
+
nuget: ["Twilio"]
|
|
24019
|
+
}
|
|
24020
|
+
},
|
|
24021
|
+
{
|
|
24022
|
+
id: "sendgrid",
|
|
24023
|
+
name: "SendGrid",
|
|
24024
|
+
category: "Email",
|
|
24025
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24026
|
+
apiBase: "https://api.sendgrid.com",
|
|
24027
|
+
defaultDataClasses: ["pii"],
|
|
24028
|
+
sdks: {
|
|
24029
|
+
npm: ["@sendgrid/mail"],
|
|
24030
|
+
pypi: ["sendgrid"],
|
|
24031
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24032
|
+
maven: ["com.sendgrid"],
|
|
24033
|
+
rubygems: ["sendgrid-ruby"],
|
|
24034
|
+
composer: ["sendgrid/sendgrid"],
|
|
24035
|
+
nuget: ["SendGrid"]
|
|
24036
|
+
}
|
|
24037
|
+
},
|
|
24038
|
+
{
|
|
24039
|
+
id: "mailgun",
|
|
24040
|
+
name: "Mailgun",
|
|
24041
|
+
category: "Email",
|
|
24042
|
+
hostSuffixes: ["mailgun.net"],
|
|
24043
|
+
apiBase: "https://api.mailgun.net",
|
|
24044
|
+
defaultDataClasses: ["pii"],
|
|
24045
|
+
sdks: {
|
|
24046
|
+
npm: ["mailgun.js"],
|
|
24047
|
+
pypi: ["mailgun"],
|
|
24048
|
+
rubygems: ["mailgun-ruby"],
|
|
24049
|
+
composer: ["mailgun/mailgun-php"],
|
|
24050
|
+
nuget: ["Mailgun"]
|
|
24051
|
+
}
|
|
24052
|
+
},
|
|
24053
|
+
{
|
|
24054
|
+
id: "mixpanel",
|
|
24055
|
+
name: "Mixpanel",
|
|
24056
|
+
category: "Analytics",
|
|
24057
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24058
|
+
apiBase: "https://api.mixpanel.com",
|
|
24059
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24060
|
+
sdks: {
|
|
24061
|
+
npm: ["mixpanel"],
|
|
24062
|
+
pypi: ["mixpanel"],
|
|
24063
|
+
rubygems: ["mixpanel-ruby"],
|
|
24064
|
+
nuget: ["Mixpanel"]
|
|
24065
|
+
}
|
|
24066
|
+
},
|
|
24067
|
+
{
|
|
24068
|
+
id: "amplitude",
|
|
24069
|
+
name: "Amplitude",
|
|
24070
|
+
category: "Analytics",
|
|
24071
|
+
hostSuffixes: ["amplitude.com"],
|
|
24072
|
+
apiBase: "https://api2.amplitude.com",
|
|
24073
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24074
|
+
sdks: {
|
|
24075
|
+
npm: ["@amplitude/analytics-node"],
|
|
24076
|
+
pypi: ["amplitude-analytics"],
|
|
24077
|
+
nuget: ["Amplitude"]
|
|
24078
|
+
}
|
|
24079
|
+
},
|
|
24080
|
+
{
|
|
24081
|
+
id: "posthog",
|
|
24082
|
+
name: "PostHog",
|
|
24083
|
+
category: "Analytics",
|
|
24084
|
+
hostSuffixes: ["posthog.com"],
|
|
24085
|
+
apiBase: "https://us.i.posthog.com",
|
|
24086
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24087
|
+
sdks: {
|
|
24088
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24089
|
+
pypi: ["posthog"],
|
|
24090
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24091
|
+
rubygems: ["posthog-ruby"],
|
|
24092
|
+
composer: ["posthog/posthog-php"],
|
|
24093
|
+
nuget: ["PostHog"]
|
|
24094
|
+
}
|
|
24095
|
+
},
|
|
24096
|
+
{
|
|
24097
|
+
id: "honeycomb",
|
|
24098
|
+
name: "Honeycomb",
|
|
24099
|
+
category: "Observability",
|
|
24100
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24101
|
+
apiBase: "https://api.honeycomb.io",
|
|
24102
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24103
|
+
sdks: {
|
|
24104
|
+
npm: ["libhoney"],
|
|
24105
|
+
pypi: ["libhoney"],
|
|
24106
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24107
|
+
rubygems: ["libhoney"]
|
|
24108
|
+
}
|
|
24109
|
+
},
|
|
24110
|
+
{
|
|
24111
|
+
id: "grafana",
|
|
24112
|
+
name: "Grafana Cloud",
|
|
24113
|
+
category: "Observability",
|
|
24114
|
+
hostSuffixes: ["grafana.net"],
|
|
24115
|
+
apiBase: "https://grafana.net",
|
|
24116
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24117
|
+
sdks: {
|
|
24118
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24119
|
+
}
|
|
24120
|
+
},
|
|
24121
|
+
{
|
|
24122
|
+
id: "splunk",
|
|
24123
|
+
name: "Splunk",
|
|
24124
|
+
category: "Observability",
|
|
24125
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24126
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24127
|
+
defaultDataClasses: ["logs"],
|
|
24128
|
+
sdks: {
|
|
24129
|
+
npm: ["splunk-logging"],
|
|
24130
|
+
pypi: ["splunk-sdk"],
|
|
24131
|
+
maven: ["com.splunk"],
|
|
24132
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24133
|
+
}
|
|
24134
|
+
},
|
|
24135
|
+
{
|
|
24136
|
+
id: "pagerduty",
|
|
24137
|
+
name: "PagerDuty",
|
|
24138
|
+
category: "Incident response",
|
|
24139
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24140
|
+
apiBase: "https://api.pagerduty.com",
|
|
24141
|
+
defaultDataClasses: ["logs"],
|
|
24142
|
+
sdks: {
|
|
24143
|
+
npm: ["@pagerduty/pdjs"],
|
|
24144
|
+
pypi: ["pdpyras"],
|
|
24145
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24146
|
+
rubygems: ["pagerduty"]
|
|
24147
|
+
}
|
|
24148
|
+
},
|
|
24149
|
+
{
|
|
24150
|
+
id: "github",
|
|
24151
|
+
name: "GitHub",
|
|
24152
|
+
category: "Developer platform",
|
|
24153
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24154
|
+
apiBase: "https://api.github.com",
|
|
24155
|
+
defaultDataClasses: ["source"],
|
|
24156
|
+
sdks: {
|
|
24157
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24158
|
+
pypi: ["pygithub"],
|
|
24159
|
+
go: ["github.com/google/go-github"],
|
|
24160
|
+
maven: ["org.kohsuke.github-api"],
|
|
24161
|
+
rubygems: ["octokit"],
|
|
24162
|
+
cargo: ["octocrab"],
|
|
24163
|
+
composer: ["knplabs/github-api"],
|
|
24164
|
+
nuget: ["Octokit"]
|
|
24165
|
+
}
|
|
24166
|
+
},
|
|
24167
|
+
{
|
|
24168
|
+
id: "gitlab",
|
|
24169
|
+
name: "GitLab",
|
|
24170
|
+
category: "Developer platform",
|
|
24171
|
+
hostSuffixes: ["gitlab.com"],
|
|
24172
|
+
apiBase: "https://gitlab.com/api",
|
|
24173
|
+
defaultDataClasses: ["source"],
|
|
24174
|
+
sdks: {
|
|
24175
|
+
npm: ["@gitbeaker/rest"],
|
|
24176
|
+
pypi: ["python-gitlab"],
|
|
24177
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24178
|
+
rubygems: ["gitlab"],
|
|
24179
|
+
nuget: ["GitLabApiClient"]
|
|
24180
|
+
}
|
|
24181
|
+
},
|
|
24182
|
+
{
|
|
24183
|
+
id: "auth0",
|
|
24184
|
+
name: "Auth0",
|
|
24185
|
+
category: "Identity",
|
|
24186
|
+
hostSuffixes: ["auth0.com"],
|
|
24187
|
+
apiBase: "https://login.auth0.com",
|
|
24188
|
+
defaultDataClasses: ["pii"],
|
|
24189
|
+
sdks: {
|
|
24190
|
+
npm: ["auth0"],
|
|
24191
|
+
pypi: ["auth0-python"],
|
|
24192
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24193
|
+
maven: ["com.auth0"],
|
|
24194
|
+
rubygems: ["auth0"],
|
|
24195
|
+
composer: ["auth0/auth0-php"],
|
|
24196
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24197
|
+
}
|
|
24198
|
+
},
|
|
24199
|
+
{
|
|
24200
|
+
id: "okta",
|
|
24201
|
+
name: "Okta",
|
|
24202
|
+
category: "Identity",
|
|
24203
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24204
|
+
apiBase: "https://login.okta.com",
|
|
24205
|
+
defaultDataClasses: ["pii"],
|
|
24206
|
+
sdks: {
|
|
24207
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24208
|
+
pypi: ["okta"],
|
|
24209
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24210
|
+
maven: ["com.okta.sdk"],
|
|
24211
|
+
nuget: ["Okta.Sdk"]
|
|
24212
|
+
}
|
|
24213
|
+
},
|
|
24214
|
+
{
|
|
24215
|
+
id: "clerk",
|
|
24216
|
+
name: "Clerk",
|
|
24217
|
+
category: "Identity",
|
|
24218
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24219
|
+
apiBase: "https://api.clerk.com",
|
|
24220
|
+
defaultDataClasses: ["pii"],
|
|
24221
|
+
sdks: {
|
|
24222
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24223
|
+
pypi: ["clerk-backend-api"],
|
|
24224
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24225
|
+
}
|
|
24226
|
+
},
|
|
24227
|
+
{
|
|
24228
|
+
id: "supabase",
|
|
24229
|
+
name: "Supabase",
|
|
24230
|
+
category: "Backend platform",
|
|
24231
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24232
|
+
apiBase: "https://api.supabase.com",
|
|
24233
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24234
|
+
sdks: {
|
|
24235
|
+
npm: ["@supabase/supabase-js"],
|
|
24236
|
+
pypi: ["supabase"],
|
|
24237
|
+
cargo: ["postgrest"]
|
|
24238
|
+
}
|
|
24239
|
+
},
|
|
24240
|
+
{
|
|
24241
|
+
id: "firebase",
|
|
24242
|
+
name: "Firebase",
|
|
24243
|
+
category: "Backend platform",
|
|
24244
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24245
|
+
apiBase: "https://firebaseio.com",
|
|
24246
|
+
defaultDataClasses: ["customer"],
|
|
24247
|
+
sdks: {
|
|
24248
|
+
npm: ["firebase", "firebase-admin"],
|
|
24249
|
+
pypi: ["firebase-admin"],
|
|
24250
|
+
go: ["firebase.google.com/go"],
|
|
24251
|
+
maven: ["com.google.firebase"]
|
|
24252
|
+
}
|
|
24253
|
+
},
|
|
24254
|
+
{
|
|
24255
|
+
id: "mongodb-atlas",
|
|
24256
|
+
name: "MongoDB Atlas",
|
|
24257
|
+
category: "Database SaaS",
|
|
24258
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24259
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24260
|
+
defaultDataClasses: ["customer"],
|
|
24261
|
+
sdks: {
|
|
24262
|
+
npm: ["mongodb"],
|
|
24263
|
+
pypi: ["pymongo"],
|
|
24264
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24265
|
+
maven: ["org.mongodb"],
|
|
24266
|
+
rubygems: ["mongo"],
|
|
24267
|
+
cargo: ["mongodb"],
|
|
24268
|
+
nuget: ["MongoDB.Driver"]
|
|
24269
|
+
}
|
|
24270
|
+
},
|
|
24271
|
+
{
|
|
24272
|
+
id: "planetscale",
|
|
24273
|
+
name: "PlanetScale",
|
|
24274
|
+
category: "Database SaaS",
|
|
24275
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24276
|
+
apiBase: "https://api.planetscale.com",
|
|
24277
|
+
defaultDataClasses: ["customer"],
|
|
24278
|
+
sdks: {
|
|
24279
|
+
npm: ["@planetscale/database"],
|
|
24280
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24281
|
+
}
|
|
24282
|
+
},
|
|
24283
|
+
{
|
|
24284
|
+
id: "algolia",
|
|
24285
|
+
name: "Algolia",
|
|
24286
|
+
category: "Search SaaS",
|
|
24287
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24288
|
+
apiBase: "https://algolia.net",
|
|
24289
|
+
defaultDataClasses: ["customer"],
|
|
24290
|
+
sdks: {
|
|
24291
|
+
npm: ["algoliasearch"],
|
|
24292
|
+
pypi: ["algoliasearch"],
|
|
24293
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24294
|
+
maven: ["com.algolia"],
|
|
24295
|
+
rubygems: ["algolia"],
|
|
24296
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24297
|
+
nuget: ["Algolia.Search"]
|
|
24298
|
+
}
|
|
24299
|
+
},
|
|
24300
|
+
{
|
|
24301
|
+
id: "cloudflare",
|
|
24302
|
+
name: "Cloudflare",
|
|
24303
|
+
category: "CDN / edge",
|
|
24304
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24305
|
+
apiBase: "https://api.cloudflare.com",
|
|
24306
|
+
defaultDataClasses: ["logs"],
|
|
24307
|
+
sdks: {
|
|
24308
|
+
npm: ["cloudflare"],
|
|
24309
|
+
pypi: ["cloudflare"],
|
|
24310
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24311
|
+
nuget: ["CloudFlare.Client"]
|
|
24312
|
+
}
|
|
24313
|
+
},
|
|
24314
|
+
{
|
|
24315
|
+
id: "huggingface",
|
|
24316
|
+
name: "Hugging Face",
|
|
24317
|
+
category: "LLM provider",
|
|
24318
|
+
hostSuffixes: ["huggingface.co"],
|
|
24319
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24320
|
+
defaultDataClasses: ["source"],
|
|
24321
|
+
sdks: {
|
|
24322
|
+
npm: ["@huggingface/inference"],
|
|
24323
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24324
|
+
rubygems: ["hugging-face"]
|
|
24325
|
+
}
|
|
24326
|
+
},
|
|
24327
|
+
{
|
|
24328
|
+
id: "cohere",
|
|
24329
|
+
name: "Cohere",
|
|
24330
|
+
category: "LLM provider",
|
|
24331
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24332
|
+
apiBase: "https://api.cohere.com",
|
|
24333
|
+
defaultDataClasses: ["pii", "source"],
|
|
24334
|
+
sdks: {
|
|
24335
|
+
npm: ["cohere-ai"],
|
|
24336
|
+
pypi: ["cohere"],
|
|
24337
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24338
|
+
}
|
|
24339
|
+
},
|
|
24340
|
+
{
|
|
24341
|
+
id: "mistral",
|
|
24342
|
+
name: "Mistral AI",
|
|
24343
|
+
category: "LLM provider",
|
|
24344
|
+
hostSuffixes: ["mistral.ai"],
|
|
24345
|
+
apiBase: "https://api.mistral.ai",
|
|
24346
|
+
defaultDataClasses: ["pii", "source"],
|
|
24347
|
+
sdks: {
|
|
24348
|
+
npm: ["@mistralai/mistralai"],
|
|
24349
|
+
pypi: ["mistralai"],
|
|
24350
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24351
|
+
}
|
|
24352
|
+
}
|
|
24353
|
+
];
|
|
24354
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24355
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24356
|
+
|
|
24357
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24358
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24359
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24360
|
+
var SECRET_VALUE = new RegExp(
|
|
24361
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24362
|
+
"gi"
|
|
24363
|
+
);
|
|
24364
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24365
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24366
|
+
"gi"
|
|
24367
|
+
);
|
|
24368
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24369
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24370
|
+
{
|
|
24371
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24372
|
+
prefix: "/api/webhooks/"
|
|
24373
|
+
},
|
|
24374
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24375
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24376
|
+
];
|
|
24377
|
+
function escapeRegExp(literal2) {
|
|
24378
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24379
|
+
}
|
|
24380
|
+
var WEBHOOK_URL = new RegExp(
|
|
24381
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24382
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24383
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24384
|
+
"gi"
|
|
24385
|
+
);
|
|
22892
24386
|
|
|
22893
24387
|
// ../../packages/detections/src/escape-regexp.ts
|
|
22894
|
-
function
|
|
24388
|
+
function escapeRegExp2(value) {
|
|
22895
24389
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22896
24390
|
}
|
|
22897
24391
|
|
|
22898
24392
|
// ../../packages/detections/src/matchers/limits.ts
|
|
22899
24393
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24394
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
22900
24395
|
|
|
22901
24396
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22902
24397
|
var KeywordMatcher2 = class {
|
|
@@ -22907,7 +24402,7 @@ var KeywordMatcher2 = class {
|
|
|
22907
24402
|
for (const kw of keywords) {
|
|
22908
24403
|
if (kw.length === 0) continue;
|
|
22909
24404
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22910
|
-
const re = new RegExp(
|
|
24405
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
22911
24406
|
let m;
|
|
22912
24407
|
while ((m = re.exec(text)) !== null) {
|
|
22913
24408
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -22924,9 +24419,13 @@ var RegexMatcher2 = class {
|
|
|
22924
24419
|
if (rule.matcher.type !== "regex") return [];
|
|
22925
24420
|
const { pattern, flags, captureGroup } = rule.matcher;
|
|
22926
24421
|
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24422
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
22927
24423
|
const spans = [];
|
|
22928
24424
|
let m;
|
|
22929
|
-
|
|
24425
|
+
const maxIterations = scanText2.length + 1;
|
|
24426
|
+
let iterations = 0;
|
|
24427
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24428
|
+
if (++iterations > maxIterations) break;
|
|
22930
24429
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
22931
24430
|
if (m[0].length === 0) re.lastIndex++;
|
|
22932
24431
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -23001,6 +24500,31 @@ var CONFIG_POSTURE_RULES = [
|
|
|
23001
24500
|
}
|
|
23002
24501
|
];
|
|
23003
24502
|
|
|
24503
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24504
|
+
var EXPONENTIAL_UNITS = [
|
|
24505
|
+
"a",
|
|
24506
|
+
"0",
|
|
24507
|
+
" ",
|
|
24508
|
+
"x",
|
|
24509
|
+
"ab",
|
|
24510
|
+
"a.",
|
|
24511
|
+
"a-",
|
|
24512
|
+
"a_",
|
|
24513
|
+
"a@",
|
|
24514
|
+
"a/",
|
|
24515
|
+
"a:",
|
|
24516
|
+
"a=",
|
|
24517
|
+
"a;",
|
|
24518
|
+
"aA0",
|
|
24519
|
+
" "
|
|
24520
|
+
];
|
|
24521
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24522
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24523
|
+
);
|
|
24524
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24525
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24526
|
+
);
|
|
24527
|
+
|
|
23004
24528
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23005
24529
|
var auth_jwt_no_verify_default = {
|
|
23006
24530
|
specVersion: 1,
|
|
@@ -25027,26 +26551,27 @@ function bundledDetections() {
|
|
|
25027
26551
|
}
|
|
25028
26552
|
|
|
25029
26553
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25030
|
-
import { existsSync as
|
|
25031
|
-
import { basename, dirname, isAbsolute, join as
|
|
26554
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26555
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
25032
26556
|
|
|
25033
26557
|
// ../../packages/plugin-sdk/src/events.ts
|
|
25034
|
-
import { createHash as
|
|
25035
|
-
|
|
25036
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
25037
|
-
import { createHash as createHash4 } from "crypto";
|
|
26558
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
25038
26559
|
|
|
25039
26560
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25040
26561
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
25041
26562
|
|
|
25042
26563
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
25043
|
-
import { mkdirSync as
|
|
25044
|
-
import { join as
|
|
26564
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26565
|
+
import { join as join9 } from "path";
|
|
26566
|
+
|
|
26567
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
26568
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
26569
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
25045
26570
|
|
|
25046
26571
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25047
26572
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25048
|
-
import { existsSync as
|
|
25049
|
-
import { basename as
|
|
26573
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26574
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
25050
26575
|
|
|
25051
26576
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25052
26577
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -25055,8 +26580,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
25055
26580
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25056
26581
|
|
|
25057
26582
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25058
|
-
import { mkdirSync as
|
|
25059
|
-
import { join as
|
|
26583
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26584
|
+
import { join as join11 } from "path";
|
|
25060
26585
|
|
|
25061
26586
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25062
26587
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -25087,7 +26612,8 @@ var StandaloneDataGateway = class {
|
|
|
25087
26612
|
}
|
|
25088
26613
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
25089
26614
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
25090
|
-
// hands the natural key across.
|
|
26615
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
26616
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
25091
26617
|
recordLlmCall(input) {
|
|
25092
26618
|
this.db.auditEvents.insertLlmCall(input);
|
|
25093
26619
|
return Promise.resolve();
|
|
@@ -25129,7 +26655,9 @@ var StandaloneDataGateway = class {
|
|
|
25129
26655
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
25130
26656
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
25131
26657
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
25132
|
-
// content-addressed
|
|
26658
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
26659
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
26660
|
+
// than no-opping.
|
|
25133
26661
|
writeToolCall(input) {
|
|
25134
26662
|
this.db.auditEvents.insertToolCall(input);
|
|
25135
26663
|
if (input.inspections.length === 0) return;
|
|
@@ -25149,7 +26677,7 @@ var StandaloneDataGateway = class {
|
|
|
25149
26677
|
});
|
|
25150
26678
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
25151
26679
|
this.db.inspectionFindings.insertFinding({
|
|
25152
|
-
id: inspectionFindingId(auditEventId,
|
|
26680
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
25153
26681
|
auditEventId,
|
|
25154
26682
|
inspectionDefinitionId: definitionId,
|
|
25155
26683
|
classifiedDataId: classifiedDataId2,
|
|
@@ -25198,10 +26726,17 @@ var StandaloneDataGateway = class {
|
|
|
25198
26726
|
try {
|
|
25199
26727
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
25200
26728
|
if (snapshot.installedPacks === 0) return void 0;
|
|
25201
|
-
if (snapshot.enabledPacks === 0)
|
|
26729
|
+
if (snapshot.enabledPacks === 0) {
|
|
26730
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
26731
|
+
}
|
|
25202
26732
|
if (snapshot.invalidRules > 0) return void 0;
|
|
25203
26733
|
if (snapshot.rules.length === 0) return void 0;
|
|
25204
|
-
return {
|
|
26734
|
+
return {
|
|
26735
|
+
rules: snapshot.rules,
|
|
26736
|
+
ruleActions: snapshot.ruleActions,
|
|
26737
|
+
ruleVersions: snapshot.ruleVersions,
|
|
26738
|
+
complete: true
|
|
26739
|
+
};
|
|
25205
26740
|
} catch {
|
|
25206
26741
|
return void 0;
|
|
25207
26742
|
}
|
|
@@ -25229,6 +26764,7 @@ var StandaloneDataGateway = class {
|
|
|
25229
26764
|
policies: [...policies, ...rulePolicies],
|
|
25230
26765
|
rules: installed ? installed.rules : [],
|
|
25231
26766
|
...installed ? { rulesComplete: true } : {},
|
|
26767
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
25232
26768
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
25233
26769
|
customKeywords,
|
|
25234
26770
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -25327,6 +26863,13 @@ var StandaloneDataGateway = class {
|
|
|
25327
26863
|
this.db.scanLedger.upsertEntries(entries);
|
|
25328
26864
|
return Promise.resolve();
|
|
25329
26865
|
}
|
|
26866
|
+
getRuleProbeVerdict(ruleKey) {
|
|
26867
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
26868
|
+
}
|
|
26869
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
|
|
26870
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
|
|
26871
|
+
return Promise.resolve();
|
|
26872
|
+
}
|
|
25330
26873
|
openAtRestKeysForPath(path) {
|
|
25331
26874
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
25332
26875
|
}
|
|
@@ -25337,6 +26880,12 @@ var StandaloneDataGateway = class {
|
|
|
25337
26880
|
this.db.resolutions.insertResolution(input);
|
|
25338
26881
|
return Promise.resolve();
|
|
25339
26882
|
}
|
|
26883
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
26884
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
26885
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
26886
|
+
recordProjectEgress(input) {
|
|
26887
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
26888
|
+
}
|
|
25340
26889
|
close() {
|
|
25341
26890
|
this.db.close();
|
|
25342
26891
|
return Promise.resolve();
|
|
@@ -25357,18 +26906,28 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
|
25357
26906
|
async function readStdin() {
|
|
25358
26907
|
return new Promise((resolve) => {
|
|
25359
26908
|
let data = "";
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
26909
|
+
let settled = false;
|
|
26910
|
+
const finish = () => {
|
|
26911
|
+
if (settled) return;
|
|
26912
|
+
settled = true;
|
|
26913
|
+
clearTimeout(timer);
|
|
26914
|
+
process.stdin.removeListener("data", onData);
|
|
26915
|
+
process.stdin.removeListener("end", finish);
|
|
25365
26916
|
resolve(data);
|
|
25366
|
-
}
|
|
26917
|
+
};
|
|
26918
|
+
const onData = (chunk) => {
|
|
26919
|
+
data += chunk;
|
|
26920
|
+
};
|
|
26921
|
+
const timer = setTimeout(finish, 5e3);
|
|
26922
|
+
process.stdin.setEncoding("utf8");
|
|
26923
|
+
process.stdin.on("data", onData);
|
|
26924
|
+
process.stdin.on("end", finish);
|
|
26925
|
+
process.stdin.on("error", finish);
|
|
25367
26926
|
});
|
|
25368
26927
|
}
|
|
25369
26928
|
|
|
25370
26929
|
// src/command-registry.ts
|
|
25371
|
-
import { readdirSync as
|
|
26930
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
25372
26931
|
import { fileURLToPath } from "url";
|
|
25373
26932
|
var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
|
|
25374
26933
|
|
|
@@ -25423,14 +26982,14 @@ function renderStatusBar(s, opts = {}) {
|
|
|
25423
26982
|
const unreviewed = `unreviewed ${SHADE.full}${String(u.critical)} ${SHADE.dark}${String(u.high)} ${SHADE.medium}${String(u.medium)} ${SHADE.light}${String(u.low)}`;
|
|
25424
26983
|
return `\u25B8\u25B8 AKA health ${String(s.score)}/100 ${unreviewed} \u2691 ${String(s.openFindings)} open findings`;
|
|
25425
26984
|
}
|
|
25426
|
-
const
|
|
26985
|
+
const sep5 = ` ${paint.dim("\u2502")} `;
|
|
25427
26986
|
const sq = "\u25A0";
|
|
25428
26987
|
const dot = s.score >= 80 ? paint.ok("\u25CF") : s.score >= 50 ? paint.high("\u25CF") : paint.critical("\u25CF");
|
|
25429
26988
|
const score = `${dot} health ${paint.bold(String(s.score))}${paint.dim("/100")}`;
|
|
25430
26989
|
const tally = `${paint.dim("unreviewed")} ${paint.critical(sq)}${String(u.critical)} ${paint.high(sq)}${String(u.high)} ${paint.medium(sq)}${String(u.medium)} ${paint.low(sq)}${String(u.low)}`;
|
|
25431
26990
|
const flag = s.openFindings > 0 ? paint.critical("\u2691") : paint.dim("\u2691");
|
|
25432
26991
|
const open = `${flag} ${String(s.openFindings)} open findings`;
|
|
25433
|
-
return `${paint.brand("\u25B8\u25B8 AKA")}${
|
|
26992
|
+
return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open}`;
|
|
25434
26993
|
}
|
|
25435
26994
|
function renderStatusLine(summary) {
|
|
25436
26995
|
return renderStatusBar(findingStatus(summary), { color: true });
|