@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/query.js
CHANGED
|
@@ -493,7 +493,7 @@ var require_ignore = __commonJS({
|
|
|
493
493
|
|
|
494
494
|
// ../../packages/persistence/src/database.ts
|
|
495
495
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
496
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
497
497
|
import { join, sep } from "path";
|
|
498
498
|
import { DatabaseSync } from "node:sqlite";
|
|
499
499
|
|
|
@@ -542,6 +542,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
542
542
|
{
|
|
543
543
|
tag: "0010_events_session_expression_index",
|
|
544
544
|
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
tag: "0011_egress_writer",
|
|
548
|
+
sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tag: "0012_handy_the_captain",
|
|
552
|
+
sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
tag: "0013_legacy_history_backfill_support",
|
|
556
|
+
sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
tag: "0014_drop_legacy_events_findings",
|
|
560
|
+
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
545
561
|
}
|
|
546
562
|
];
|
|
547
563
|
|
|
@@ -15432,7 +15448,12 @@ var FindingFacets = external_exports.object({
|
|
|
15432
15448
|
severity: external_exports.array(FindingFacetItem),
|
|
15433
15449
|
subtype: external_exports.array(FindingFacetItem),
|
|
15434
15450
|
provider: external_exports.array(FindingFacetItem),
|
|
15435
|
-
action: external_exports.array(FindingFacetItem)
|
|
15451
|
+
action: external_exports.array(FindingFacetItem),
|
|
15452
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15453
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15454
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15455
|
+
// counted under no value.
|
|
15456
|
+
status: external_exports.array(FindingFacetItem)
|
|
15436
15457
|
}).meta({ id: "FindingFacets" });
|
|
15437
15458
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15438
15459
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15442,6 +15463,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15442
15463
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15443
15464
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15444
15465
|
action: external_exports.array(FindingAction).optional(),
|
|
15466
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15467
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15468
|
+
// one of the requested values.
|
|
15469
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15445
15470
|
q: external_exports.string().optional(),
|
|
15446
15471
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15447
15472
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15629,6 +15654,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15629
15654
|
parent_uuid: external_exports.string().optional(),
|
|
15630
15655
|
run_key: external_exports.string().optional()
|
|
15631
15656
|
}).catchall(external_exports.unknown());
|
|
15657
|
+
var CaptureAttributes = external_exports.object({
|
|
15658
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15659
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15660
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15661
|
+
source_tool: external_exports.string().optional(),
|
|
15662
|
+
file_path: external_exports.string().optional(),
|
|
15663
|
+
repo: external_exports.string().optional(),
|
|
15664
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15665
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15666
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15667
|
+
tool_name: external_exports.string().optional(),
|
|
15668
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15669
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15670
|
+
gitignored: external_exports.boolean().optional(),
|
|
15671
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15672
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15673
|
+
whole_file: external_exports.boolean().optional(),
|
|
15674
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15675
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15676
|
+
// trace id when telemetry is enabled.
|
|
15677
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15678
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15679
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15680
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15681
|
+
// authorized the bypass.
|
|
15682
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15683
|
+
}).catchall(external_exports.unknown());
|
|
15632
15684
|
var ToolCallInspection = external_exports.object({
|
|
15633
15685
|
ruleId: external_exports.string().min(1),
|
|
15634
15686
|
ruleName: external_exports.string(),
|
|
@@ -15715,7 +15767,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15715
15767
|
span: Span,
|
|
15716
15768
|
maskedMatch: external_exports.string(),
|
|
15717
15769
|
actionTaken: ActionTaken,
|
|
15718
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15770
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15771
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15772
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15773
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15774
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15775
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15776
|
+
findingKey: external_exports.string().optional(),
|
|
15777
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15778
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15779
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15780
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15781
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15719
15782
|
});
|
|
15720
15783
|
var InventoryContext = external_exports.object({
|
|
15721
15784
|
host: InventoryInput.optional(),
|
|
@@ -15917,6 +15980,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15917
15980
|
|
|
15918
15981
|
// ../../packages/schema/src/zod/event.ts
|
|
15919
15982
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15983
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15920
15984
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15921
15985
|
var EventMetadata = external_exports.object({
|
|
15922
15986
|
sessionId: external_exports.string().optional(),
|
|
@@ -16257,6 +16321,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16257
16321
|
|
|
16258
16322
|
// ../../packages/schema/src/zod/rule.ts
|
|
16259
16323
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16324
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16260
16325
|
var KeywordMatcher = external_exports.object({
|
|
16261
16326
|
type: external_exports.literal("keyword"),
|
|
16262
16327
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16281,9 +16346,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16281
16346
|
return false;
|
|
16282
16347
|
}
|
|
16283
16348
|
}
|
|
16349
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16284
16350
|
var RegexMatcher = external_exports.object({
|
|
16285
16351
|
type: external_exports.literal("regex"),
|
|
16286
|
-
pattern: external_exports.string(),
|
|
16352
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16287
16353
|
flags: external_exports.string().default("gi"),
|
|
16288
16354
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16289
16355
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16404,6 +16470,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16404
16470
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16405
16471
|
// `bundle.exceptions ?? []`.
|
|
16406
16472
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16473
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16474
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16475
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16476
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16477
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16478
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16407
16479
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16408
16480
|
fetchedAt: external_exports.iso.datetime()
|
|
16409
16481
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -16850,6 +16922,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16850
16922
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16851
16923
|
}
|
|
16852
16924
|
|
|
16925
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16926
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16927
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16928
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16929
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16930
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16931
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16932
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16933
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16934
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16935
|
+
var ReviewInfo = external_exports.object({
|
|
16936
|
+
needsReview: external_exports.boolean(),
|
|
16937
|
+
reasons: external_exports.array(ReviewReason)
|
|
16938
|
+
}).meta({ id: "ReviewInfo" });
|
|
16939
|
+
var DestinationNetwork = external_exports.object({
|
|
16940
|
+
port: external_exports.number().int().nullable(),
|
|
16941
|
+
geo: external_exports.string().nullable(),
|
|
16942
|
+
ptr: external_exports.string().nullable()
|
|
16943
|
+
}).meta({ id: "DestinationNetwork" });
|
|
16944
|
+
var EndpointSummary = external_exports.object({
|
|
16945
|
+
id: external_exports.string(),
|
|
16946
|
+
method: HttpMethod,
|
|
16947
|
+
transport: Transport,
|
|
16948
|
+
url: external_exports.string(),
|
|
16949
|
+
template: external_exports.boolean(),
|
|
16950
|
+
dataClass: DataClass,
|
|
16951
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16952
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
16953
|
+
}).meta({ id: "EndpointSummary" });
|
|
16954
|
+
var CallSite = external_exports.object({
|
|
16955
|
+
id: external_exports.string(),
|
|
16956
|
+
project: external_exports.string(),
|
|
16957
|
+
file: external_exports.string(),
|
|
16958
|
+
line: external_exports.number().int().nonnegative(),
|
|
16959
|
+
snippet: external_exports.string(),
|
|
16960
|
+
dynamic: external_exports.boolean(),
|
|
16961
|
+
vendored: external_exports.boolean(),
|
|
16962
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
16963
|
+
projectId: external_exports.string().nullable()
|
|
16964
|
+
}).meta({ id: "CallSite" });
|
|
16965
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
16966
|
+
sites: external_exports.array(CallSite)
|
|
16967
|
+
}).meta({ id: "EndpointWithSites" });
|
|
16968
|
+
var ShareDestinationSummary = external_exports.object({
|
|
16969
|
+
id: external_exports.string(),
|
|
16970
|
+
kind: DestinationKind,
|
|
16971
|
+
name: external_exports.string(),
|
|
16972
|
+
host: external_exports.string(),
|
|
16973
|
+
category: external_exports.string(),
|
|
16974
|
+
trust: ShareTrustLevel,
|
|
16975
|
+
/** Effective state (decision applied over the trust default). */
|
|
16976
|
+
status: EgressStatus,
|
|
16977
|
+
/** True when an egress decision override differs from the trust default. */
|
|
16978
|
+
isCustom: external_exports.boolean(),
|
|
16979
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16980
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
16981
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16982
|
+
transports: external_exports.array(Transport),
|
|
16983
|
+
/** Most-sensitive first. */
|
|
16984
|
+
dataClasses: external_exports.array(DataClass),
|
|
16985
|
+
review: ReviewInfo,
|
|
16986
|
+
/** Non-provider hosts only; null for providers. */
|
|
16987
|
+
network: DestinationNetwork.nullable(),
|
|
16988
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
16989
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
16990
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
16991
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
16992
|
+
endpointCount: true,
|
|
16993
|
+
callSiteCount: true,
|
|
16994
|
+
endpoints: true
|
|
16995
|
+
}).extend({
|
|
16996
|
+
/** Ownership/geo rationale; null for providers. */
|
|
16997
|
+
note: external_exports.string().nullable(),
|
|
16998
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
16999
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
17000
|
+
var ReviewDestination = external_exports.object({
|
|
17001
|
+
id: external_exports.string(),
|
|
17002
|
+
kind: DestinationKind,
|
|
17003
|
+
name: external_exports.string(),
|
|
17004
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17005
|
+
host: external_exports.string(),
|
|
17006
|
+
trust: ShareTrustLevel,
|
|
17007
|
+
status: EgressStatus,
|
|
17008
|
+
review: ReviewInfo,
|
|
17009
|
+
topDataClass: DataClass,
|
|
17010
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17011
|
+
lastSeen: external_exports.iso.datetime()
|
|
17012
|
+
}).meta({ id: "ReviewDestination" });
|
|
17013
|
+
var ShareDestinationGroup = external_exports.object({
|
|
17014
|
+
kind: DestinationKind,
|
|
17015
|
+
total: external_exports.number().int().nonnegative(),
|
|
17016
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
17017
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
17018
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17019
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17020
|
+
var SharesStats = external_exports.object({
|
|
17021
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17022
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17023
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17024
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
17025
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
17026
|
+
byKind: external_exports.object({
|
|
17027
|
+
provider: external_exports.number().int().nonnegative(),
|
|
17028
|
+
internal: external_exports.number().int().nonnegative(),
|
|
17029
|
+
external: external_exports.number().int().nonnegative(),
|
|
17030
|
+
ip: external_exports.number().int().nonnegative()
|
|
17031
|
+
}),
|
|
17032
|
+
byTrust: external_exports.object({
|
|
17033
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
17034
|
+
internal: external_exports.number().int().nonnegative(),
|
|
17035
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
17036
|
+
ip: external_exports.number().int().nonnegative()
|
|
17037
|
+
})
|
|
17038
|
+
}).meta({ id: "SharesStats" });
|
|
17039
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
17040
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17041
|
+
decision: EgressDecision.nullable()
|
|
17042
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
17043
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17044
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
17045
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17046
|
+
q: external_exports.string().optional(),
|
|
17047
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17048
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
17049
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17050
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17051
|
+
/**
|
|
17052
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17053
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17054
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17055
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17056
|
+
*/
|
|
17057
|
+
review: external_exports.stringbool().default(false)
|
|
17058
|
+
});
|
|
17059
|
+
var ExportSharesQuery = external_exports.object({
|
|
17060
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17061
|
+
q: external_exports.string().optional(),
|
|
17062
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
17063
|
+
});
|
|
17064
|
+
|
|
17065
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
17066
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
17067
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
17068
|
+
id: external_exports.string(),
|
|
17069
|
+
name: external_exports.string(),
|
|
17070
|
+
category: external_exports.string(),
|
|
17071
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
17072
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
17073
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
17074
|
+
apiBase: external_exports.string(),
|
|
17075
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
17076
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
17077
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
17078
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
17079
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
17080
|
+
var EgressCallSiteHit = external_exports.object({
|
|
17081
|
+
file: external_exports.string(),
|
|
17082
|
+
line: external_exports.number().int().positive(),
|
|
17083
|
+
snippet: external_exports.string(),
|
|
17084
|
+
dynamic: external_exports.boolean(),
|
|
17085
|
+
vendored: external_exports.boolean()
|
|
17086
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
17087
|
+
var ResolvedEgressHit = external_exports.object({
|
|
17088
|
+
host: external_exports.string(),
|
|
17089
|
+
kind: DestinationKind,
|
|
17090
|
+
name: external_exports.string(),
|
|
17091
|
+
category: external_exports.string(),
|
|
17092
|
+
trust: ShareTrustLevel,
|
|
17093
|
+
network: DestinationNetwork.nullable(),
|
|
17094
|
+
method: HttpMethod,
|
|
17095
|
+
transport: Transport,
|
|
17096
|
+
url: external_exports.string(),
|
|
17097
|
+
template: external_exports.boolean(),
|
|
17098
|
+
dataClass: DataClass,
|
|
17099
|
+
site: EgressCallSiteHit
|
|
17100
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
17101
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
17102
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
17103
|
+
external_exports.object({
|
|
17104
|
+
mode: external_exports.literal("ledger"),
|
|
17105
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
17106
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
17107
|
+
})
|
|
17108
|
+
]).meta({ id: "EgressReconcile" });
|
|
17109
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
17110
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
17111
|
+
projectKey: external_exports.string().min(1),
|
|
17112
|
+
/** Display name only — never keys reconciliation. */
|
|
17113
|
+
project: external_exports.string(),
|
|
17114
|
+
projectId: external_exports.string().nullable(),
|
|
17115
|
+
reconcile: EgressReconcile,
|
|
17116
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
17117
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
17118
|
+
var EgressWriteSummary = external_exports.object({
|
|
17119
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
17120
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
17121
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
17122
|
+
truncated: external_exports.boolean(),
|
|
17123
|
+
/**
|
|
17124
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
17125
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17126
|
+
* again next scan.
|
|
17127
|
+
*/
|
|
17128
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17129
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17130
|
+
|
|
16853
17131
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16854
17132
|
function toApiAction(dbVal) {
|
|
16855
17133
|
const map2 = {
|
|
@@ -16997,6 +17275,15 @@ function groupActions(g) {
|
|
|
16997
17275
|
actionsCache.set(g, actions);
|
|
16998
17276
|
return actions;
|
|
16999
17277
|
}
|
|
17278
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17279
|
+
const statusSet = new Set(statuses);
|
|
17280
|
+
let sum = 0;
|
|
17281
|
+
for (const input of statusInputs) {
|
|
17282
|
+
if (input.count === void 0) return null;
|
|
17283
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17284
|
+
}
|
|
17285
|
+
return sum;
|
|
17286
|
+
}
|
|
17000
17287
|
function applyFindingFilters(groups, opts) {
|
|
17001
17288
|
let filtered = groups;
|
|
17002
17289
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17015,6 +17302,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17015
17302
|
const subtypeSet = new Set(opts.subtype);
|
|
17016
17303
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17017
17304
|
}
|
|
17305
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17306
|
+
const statusSet = new Set(opts.statuses);
|
|
17307
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17308
|
+
}
|
|
17018
17309
|
if (opts.q) {
|
|
17019
17310
|
const q = opts.q.toLowerCase();
|
|
17020
17311
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17036,6 +17327,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17036
17327
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17037
17328
|
providers: opts.providers,
|
|
17038
17329
|
actions: opts.actions,
|
|
17330
|
+
statuses: opts.statuses,
|
|
17039
17331
|
q: opts.q,
|
|
17040
17332
|
subtype: opts.subtype
|
|
17041
17333
|
});
|
|
@@ -17045,6 +17337,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17045
17337
|
}
|
|
17046
17338
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17047
17339
|
actions: opts.actions,
|
|
17340
|
+
statuses: opts.statuses,
|
|
17048
17341
|
q: opts.q,
|
|
17049
17342
|
subtype: opts.subtype,
|
|
17050
17343
|
severity: opts.severity
|
|
@@ -17055,6 +17348,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17055
17348
|
}
|
|
17056
17349
|
const forAction = applyFindingFilters(allGroups, {
|
|
17057
17350
|
providers: opts.providers,
|
|
17351
|
+
statuses: opts.statuses,
|
|
17058
17352
|
q: opts.q,
|
|
17059
17353
|
subtype: opts.subtype,
|
|
17060
17354
|
severity: opts.severity
|
|
@@ -17066,17 +17360,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17066
17360
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17067
17361
|
providers: opts.providers,
|
|
17068
17362
|
actions: opts.actions,
|
|
17363
|
+
statuses: opts.statuses,
|
|
17069
17364
|
q: opts.q,
|
|
17070
17365
|
severity: opts.severity
|
|
17071
17366
|
});
|
|
17072
17367
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17073
17368
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17369
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17370
|
+
providers: opts.providers,
|
|
17371
|
+
actions: opts.actions,
|
|
17372
|
+
q: opts.q,
|
|
17373
|
+
subtype: opts.subtype,
|
|
17374
|
+
severity: opts.severity
|
|
17375
|
+
});
|
|
17376
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17377
|
+
for (const g of forStatus) {
|
|
17378
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17379
|
+
}
|
|
17074
17380
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17075
17381
|
return {
|
|
17076
17382
|
severity: toItems(severityMap),
|
|
17077
17383
|
provider: toItems(providerMap),
|
|
17078
17384
|
action: toItems(actionMap),
|
|
17079
|
-
subtype: toItems(subtypeMap)
|
|
17385
|
+
subtype: toItems(subtypeMap),
|
|
17386
|
+
status: toItems(statusMap)
|
|
17080
17387
|
};
|
|
17081
17388
|
}
|
|
17082
17389
|
|
|
@@ -17111,10 +17418,14 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17111
17418
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17112
17419
|
|
|
17113
17420
|
// ../../packages/schema/src/zod/local.ts
|
|
17114
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17421
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17115
17422
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17116
17423
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17117
17424
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17425
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17426
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17427
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17428
|
+
});
|
|
17118
17429
|
var WorkspaceSettings = external_exports.object({
|
|
17119
17430
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17120
17431
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17126,38 +17437,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17126
17437
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17127
17438
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17128
17439
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17440
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17441
|
+
// Shares writes.
|
|
17442
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17129
17443
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17130
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17444
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17445
|
+
// Records that the user consented to sending findings to the model API for
|
|
17446
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17447
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17448
|
+
// covers the current payload and must be re-granted.
|
|
17449
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17131
17450
|
});
|
|
17132
17451
|
function defaultWorkspaceSettings() {
|
|
17133
17452
|
return WorkspaceSettings.parse({});
|
|
17134
17453
|
}
|
|
17135
|
-
function toEventRow(event) {
|
|
17136
|
-
return {
|
|
17137
|
-
id: event.id,
|
|
17138
|
-
sourceTool: event.sourceTool,
|
|
17139
|
-
kind: event.kind,
|
|
17140
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17141
|
-
contentHash: event.contentHash,
|
|
17142
|
-
content: event.content,
|
|
17143
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17144
|
-
};
|
|
17145
|
-
}
|
|
17146
|
-
function toFindingRow(finding) {
|
|
17147
|
-
return {
|
|
17148
|
-
id: finding.id,
|
|
17149
|
-
eventId: finding.eventId,
|
|
17150
|
-
ruleId: finding.ruleId,
|
|
17151
|
-
category: finding.category,
|
|
17152
|
-
severity: finding.severity,
|
|
17153
|
-
spanStart: finding.span.start,
|
|
17154
|
-
spanEnd: finding.span.end,
|
|
17155
|
-
maskedMatch: finding.maskedMatch,
|
|
17156
|
-
actionTaken: finding.actionTaken,
|
|
17157
|
-
confidence: finding.confidence,
|
|
17158
|
-
findingKey: finding.findingKey ?? null
|
|
17159
|
-
};
|
|
17160
|
-
}
|
|
17161
17454
|
function toInventoryRow(input, id, now) {
|
|
17162
17455
|
return {
|
|
17163
17456
|
id,
|
|
@@ -17227,7 +17520,42 @@ function toInspectionFindingRow(input) {
|
|
|
17227
17520
|
spanEnd: input.span.end,
|
|
17228
17521
|
maskedMatch: input.maskedMatch,
|
|
17229
17522
|
actionTaken: input.actionTaken,
|
|
17230
|
-
confidence: input.confidence
|
|
17523
|
+
confidence: input.confidence,
|
|
17524
|
+
findingKey: input.findingKey ?? null,
|
|
17525
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17526
|
+
};
|
|
17527
|
+
}
|
|
17528
|
+
function toCaptureAttributes(event) {
|
|
17529
|
+
const metadata = event.metadata;
|
|
17530
|
+
return {
|
|
17531
|
+
source_tool: event.sourceTool,
|
|
17532
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17533
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17534
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17535
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17536
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17537
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17538
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17539
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17540
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17541
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17542
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17543
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17544
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17545
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17546
|
+
};
|
|
17547
|
+
}
|
|
17548
|
+
function captureDefinitionVersion(finding) {
|
|
17549
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17550
|
+
}
|
|
17551
|
+
function toCaptureDefinitionInput(finding) {
|
|
17552
|
+
return {
|
|
17553
|
+
ruleId: finding.ruleId,
|
|
17554
|
+
version: captureDefinitionVersion(finding),
|
|
17555
|
+
name: finding.ruleId,
|
|
17556
|
+
category: finding.category,
|
|
17557
|
+
severity: finding.severity,
|
|
17558
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17231
17559
|
};
|
|
17232
17560
|
}
|
|
17233
17561
|
|
|
@@ -17609,145 +17937,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17609
17937
|
path: ["liveKeys"]
|
|
17610
17938
|
});
|
|
17611
17939
|
|
|
17612
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17613
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17614
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17615
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17616
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17617
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17618
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17619
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17620
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17621
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17622
|
-
var ReviewInfo = external_exports.object({
|
|
17623
|
-
needsReview: external_exports.boolean(),
|
|
17624
|
-
reasons: external_exports.array(ReviewReason)
|
|
17625
|
-
}).meta({ id: "ReviewInfo" });
|
|
17626
|
-
var DestinationNetwork = external_exports.object({
|
|
17627
|
-
port: external_exports.number().int().nullable(),
|
|
17628
|
-
geo: external_exports.string().nullable(),
|
|
17629
|
-
ptr: external_exports.string().nullable()
|
|
17630
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17631
|
-
var EndpointSummary = external_exports.object({
|
|
17632
|
-
id: external_exports.string(),
|
|
17633
|
-
method: HttpMethod,
|
|
17634
|
-
transport: Transport,
|
|
17635
|
-
url: external_exports.string(),
|
|
17636
|
-
template: external_exports.boolean(),
|
|
17637
|
-
dataClass: DataClass,
|
|
17638
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17639
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17640
|
-
}).meta({ id: "EndpointSummary" });
|
|
17641
|
-
var CallSite = external_exports.object({
|
|
17642
|
-
id: external_exports.string(),
|
|
17643
|
-
project: external_exports.string(),
|
|
17644
|
-
file: external_exports.string(),
|
|
17645
|
-
line: external_exports.number().int().nonnegative(),
|
|
17646
|
-
snippet: external_exports.string(),
|
|
17647
|
-
dynamic: external_exports.boolean(),
|
|
17648
|
-
vendored: external_exports.boolean(),
|
|
17649
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17650
|
-
projectId: external_exports.string().nullable()
|
|
17651
|
-
}).meta({ id: "CallSite" });
|
|
17652
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17653
|
-
sites: external_exports.array(CallSite)
|
|
17654
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17655
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17656
|
-
id: external_exports.string(),
|
|
17657
|
-
kind: DestinationKind,
|
|
17658
|
-
name: external_exports.string(),
|
|
17659
|
-
host: external_exports.string(),
|
|
17660
|
-
category: external_exports.string(),
|
|
17661
|
-
trust: ShareTrustLevel,
|
|
17662
|
-
/** Effective state (decision applied over the trust default). */
|
|
17663
|
-
status: EgressStatus,
|
|
17664
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17665
|
-
isCustom: external_exports.boolean(),
|
|
17666
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17667
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17668
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17669
|
-
transports: external_exports.array(Transport),
|
|
17670
|
-
/** Most-sensitive first. */
|
|
17671
|
-
dataClasses: external_exports.array(DataClass),
|
|
17672
|
-
review: ReviewInfo,
|
|
17673
|
-
/** Non-provider hosts only; null for providers. */
|
|
17674
|
-
network: DestinationNetwork.nullable(),
|
|
17675
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17676
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17677
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17678
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17679
|
-
endpointCount: true,
|
|
17680
|
-
callSiteCount: true,
|
|
17681
|
-
endpoints: true
|
|
17682
|
-
}).extend({
|
|
17683
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17684
|
-
note: external_exports.string().nullable(),
|
|
17685
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17686
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17687
|
-
var ReviewDestination = external_exports.object({
|
|
17688
|
-
id: external_exports.string(),
|
|
17689
|
-
kind: DestinationKind,
|
|
17690
|
-
name: external_exports.string(),
|
|
17691
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17692
|
-
host: external_exports.string(),
|
|
17693
|
-
trust: ShareTrustLevel,
|
|
17694
|
-
status: EgressStatus,
|
|
17695
|
-
review: ReviewInfo,
|
|
17696
|
-
topDataClass: DataClass,
|
|
17697
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17698
|
-
lastSeen: external_exports.iso.datetime()
|
|
17699
|
-
}).meta({ id: "ReviewDestination" });
|
|
17700
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17701
|
-
kind: DestinationKind,
|
|
17702
|
-
total: external_exports.number().int().nonnegative(),
|
|
17703
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17704
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17705
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17706
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17707
|
-
var SharesStats = external_exports.object({
|
|
17708
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17709
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17710
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17711
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17712
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17713
|
-
byKind: external_exports.object({
|
|
17714
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17715
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17716
|
-
ip: external_exports.number().int().nonnegative()
|
|
17717
|
-
}),
|
|
17718
|
-
byTrust: external_exports.object({
|
|
17719
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17720
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17721
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17722
|
-
ip: external_exports.number().int().nonnegative()
|
|
17723
|
-
})
|
|
17724
|
-
}).meta({ id: "SharesStats" });
|
|
17725
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17726
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17727
|
-
decision: EgressDecision.nullable()
|
|
17728
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17729
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17730
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17731
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17732
|
-
q: external_exports.string().optional(),
|
|
17733
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17734
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17735
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17736
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17737
|
-
/**
|
|
17738
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17739
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17740
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17741
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17742
|
-
*/
|
|
17743
|
-
review: external_exports.stringbool().default(false)
|
|
17744
|
-
});
|
|
17745
|
-
var ExportSharesQuery = external_exports.object({
|
|
17746
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17747
|
-
q: external_exports.string().optional(),
|
|
17748
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17749
|
-
});
|
|
17750
|
-
|
|
17751
17940
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17752
17941
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17753
17942
|
function trustDefaultStatus(trust) {
|
|
@@ -17767,7 +17956,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17767
17956
|
const reasons = [];
|
|
17768
17957
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17769
17958
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17770
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17959
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17771
17960
|
return reasons;
|
|
17772
17961
|
}
|
|
17773
17962
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17794,6 +17983,48 @@ function reviewSeverityRank(reasons) {
|
|
|
17794
17983
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17795
17984
|
}
|
|
17796
17985
|
|
|
17986
|
+
// ../../packages/persistence/src/ids.ts
|
|
17987
|
+
import { createHash } from "crypto";
|
|
17988
|
+
function sha256Hex(input) {
|
|
17989
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17990
|
+
}
|
|
17991
|
+
function inventoryId(objectType, identityKey) {
|
|
17992
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17993
|
+
}
|
|
17994
|
+
function sourceProjectId(url2) {
|
|
17995
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17996
|
+
}
|
|
17997
|
+
function classifiedDataId(cls) {
|
|
17998
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17999
|
+
}
|
|
18000
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
18001
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
18002
|
+
}
|
|
18003
|
+
function llmCallId(sessionId, messageId) {
|
|
18004
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
18005
|
+
}
|
|
18006
|
+
function toolCallId(sessionId, toolUseId) {
|
|
18007
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
18008
|
+
}
|
|
18009
|
+
function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
|
|
18010
|
+
return sha256Hex(
|
|
18011
|
+
canonicalIdentity([
|
|
18012
|
+
"inspection_finding",
|
|
18013
|
+
auditEventId,
|
|
18014
|
+
ruleId,
|
|
18015
|
+
String(spanStart),
|
|
18016
|
+
String(spanEnd)
|
|
18017
|
+
])
|
|
18018
|
+
);
|
|
18019
|
+
}
|
|
18020
|
+
var NO_SESSION = "no_session";
|
|
18021
|
+
var NO_PATH = "no_path";
|
|
18022
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
18023
|
+
return sha256Hex(
|
|
18024
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
18025
|
+
);
|
|
18026
|
+
}
|
|
18027
|
+
|
|
17797
18028
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17798
18029
|
function escapeLikePattern(s) {
|
|
17799
18030
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17890,39 +18121,81 @@ function evidenceExists(db, object2) {
|
|
|
17890
18121
|
return schemaObjectExists(db, "table", object2.name);
|
|
17891
18122
|
}
|
|
17892
18123
|
|
|
17893
|
-
// ../../packages/persistence/src/
|
|
17894
|
-
|
|
17895
|
-
|
|
17896
|
-
|
|
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);
|
|
17897
18129
|
}
|
|
17898
|
-
function
|
|
17899
|
-
|
|
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);
|
|
17900
18134
|
}
|
|
17901
|
-
function
|
|
17902
|
-
return
|
|
18135
|
+
function intToBool(raw) {
|
|
18136
|
+
return raw === 1 || raw === true;
|
|
17903
18137
|
}
|
|
17904
|
-
function
|
|
17905
|
-
return
|
|
18138
|
+
function boolToInt(b) {
|
|
18139
|
+
return b ? 1 : 0;
|
|
17906
18140
|
}
|
|
17907
|
-
function
|
|
17908
|
-
|
|
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;
|
|
17909
18147
|
}
|
|
17910
|
-
function
|
|
17911
|
-
return
|
|
18148
|
+
function countScalar(db, sql, params) {
|
|
18149
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17912
18150
|
}
|
|
17913
|
-
function
|
|
17914
|
-
|
|
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;
|
|
17915
18157
|
}
|
|
17916
|
-
function
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
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
|
+
// ../../packages/persistence/src/paths.ts
|
|
18170
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18171
|
+
var DATA_DIR_MODE = 448;
|
|
18172
|
+
var DATA_FILE_MODE = 384;
|
|
18173
|
+
var DB_FILENAME = "aka.db";
|
|
18174
|
+
function chmodBestEffort(path, mode) {
|
|
18175
|
+
try {
|
|
18176
|
+
chmodSync(path, mode);
|
|
18177
|
+
} catch {
|
|
18178
|
+
}
|
|
18179
|
+
}
|
|
18180
|
+
function tightenDir(dir) {
|
|
18181
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18182
|
+
}
|
|
18183
|
+
function ensureDataDirSync(dir) {
|
|
18184
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18185
|
+
tightenDir(dir);
|
|
18186
|
+
}
|
|
18187
|
+
function dbSidecars(file2) {
|
|
18188
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18189
|
+
}
|
|
18190
|
+
function tightenFile(file2) {
|
|
18191
|
+
try {
|
|
18192
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18193
|
+
} catch {
|
|
18194
|
+
}
|
|
18195
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18196
|
+
}
|
|
18197
|
+
function tightenPerms(file2) {
|
|
18198
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
17926
18199
|
}
|
|
17927
18200
|
|
|
17928
18201
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17936,7 +18209,8 @@ function createdIndexName(statement) {
|
|
|
17936
18209
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17937
18210
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17938
18211
|
}
|
|
17939
|
-
|
|
18212
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18213
|
+
function applyMigrations(db, file2) {
|
|
17940
18214
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17941
18215
|
db.exec(
|
|
17942
18216
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17950,6 +18224,7 @@ function applyMigrations(db) {
|
|
|
17950
18224
|
);
|
|
17951
18225
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17952
18226
|
if (applied.has(migration.tag)) continue;
|
|
18227
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17953
18228
|
const evidence = evidenceObjects(migration.sql);
|
|
17954
18229
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17955
18230
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -17994,13 +18269,54 @@ function applyMigrations(db) {
|
|
|
17994
18269
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
17995
18270
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
17996
18271
|
}
|
|
17997
|
-
ensureSyncedAtColumn(db, "events");
|
|
17998
18272
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17999
18273
|
ensureScanLedgerTable(db);
|
|
18000
18274
|
ensureBlockedDetectionsTable(db);
|
|
18275
|
+
ensureRuleProbeCacheTable(db);
|
|
18001
18276
|
ensureWriteGateTrigger(db);
|
|
18002
18277
|
ensureTokenUsageColumns(db);
|
|
18003
18278
|
reconcileSourceProjectIds(db);
|
|
18279
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18280
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18281
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18282
|
+
}
|
|
18283
|
+
}
|
|
18284
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18285
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18286
|
+
if (!migration) return;
|
|
18287
|
+
if (file2) {
|
|
18288
|
+
try {
|
|
18289
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18290
|
+
} catch (error51) {
|
|
18291
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18292
|
+
return;
|
|
18293
|
+
}
|
|
18294
|
+
}
|
|
18295
|
+
try {
|
|
18296
|
+
withTransaction(
|
|
18297
|
+
db,
|
|
18298
|
+
() => {
|
|
18299
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18300
|
+
if (alreadyDropped) return;
|
|
18301
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18302
|
+
db.exec(statement);
|
|
18303
|
+
}
|
|
18304
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18305
|
+
migration.tag,
|
|
18306
|
+
Date.now()
|
|
18307
|
+
);
|
|
18308
|
+
},
|
|
18309
|
+
"IMMEDIATE"
|
|
18310
|
+
);
|
|
18311
|
+
} catch (error51) {
|
|
18312
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18313
|
+
}
|
|
18314
|
+
}
|
|
18315
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18316
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18317
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18318
|
+
tightenFile(backup);
|
|
18319
|
+
return backup;
|
|
18004
18320
|
}
|
|
18005
18321
|
var TOKEN_USAGE_COLUMNS = [
|
|
18006
18322
|
{
|
|
@@ -18029,6 +18345,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18029
18345
|
}
|
|
18030
18346
|
];
|
|
18031
18347
|
function ensureTokenUsageColumns(db) {
|
|
18348
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18032
18349
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18033
18350
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18034
18351
|
if (!existing.has(column.name)) {
|
|
@@ -18094,11 +18411,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18094
18411
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18095
18412
|
}
|
|
18096
18413
|
}
|
|
18414
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18415
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18416
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18417
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18418
|
+
return row?.lastRowid ?? 0;
|
|
18419
|
+
}
|
|
18420
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18421
|
+
db.prepare(
|
|
18422
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18423
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18424
|
+
).run(source, lastRowid);
|
|
18425
|
+
}
|
|
18426
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18427
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18428
|
+
let processed = 0;
|
|
18429
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18430
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18431
|
+
if (rows.length === 0) return true;
|
|
18432
|
+
withTransaction(
|
|
18433
|
+
db,
|
|
18434
|
+
() => {
|
|
18435
|
+
handleRows(rows);
|
|
18436
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18437
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18438
|
+
},
|
|
18439
|
+
"IMMEDIATE"
|
|
18440
|
+
);
|
|
18441
|
+
processed += rows.length;
|
|
18442
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18443
|
+
}
|
|
18444
|
+
return false;
|
|
18445
|
+
}
|
|
18446
|
+
function parseLegacyEventMetadata(raw) {
|
|
18447
|
+
if (raw === null) return void 0;
|
|
18448
|
+
try {
|
|
18449
|
+
return JSON.parse(raw);
|
|
18450
|
+
} catch {
|
|
18451
|
+
return void 0;
|
|
18452
|
+
}
|
|
18453
|
+
}
|
|
18454
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18455
|
+
return JSON.stringify(
|
|
18456
|
+
toCaptureAttributes({
|
|
18457
|
+
id: row.id,
|
|
18458
|
+
sourceTool: row.sourceTool,
|
|
18459
|
+
kind: row.kind,
|
|
18460
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18461
|
+
contentHash: row.contentHash,
|
|
18462
|
+
content: row.content,
|
|
18463
|
+
metadata: row.metadata
|
|
18464
|
+
})
|
|
18465
|
+
);
|
|
18466
|
+
}
|
|
18467
|
+
function copyLegacyEvents(db) {
|
|
18468
|
+
const selectStmt = db.prepare(
|
|
18469
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18470
|
+
content_hash AS contentHash, content, metadata
|
|
18471
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18472
|
+
);
|
|
18473
|
+
const insertStmt = db.prepare(
|
|
18474
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18475
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18476
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18477
|
+
);
|
|
18478
|
+
const stubRootStmt = db.prepare(
|
|
18479
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18480
|
+
);
|
|
18481
|
+
return drainLegacyTable(
|
|
18482
|
+
db,
|
|
18483
|
+
"events",
|
|
18484
|
+
selectStmt,
|
|
18485
|
+
(rows) => {
|
|
18486
|
+
for (const row of rows) {
|
|
18487
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18488
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18489
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18490
|
+
insertStmt.run(
|
|
18491
|
+
bindParams({
|
|
18492
|
+
id: row.id,
|
|
18493
|
+
parentId: sessionId,
|
|
18494
|
+
rootSessionId: sessionId,
|
|
18495
|
+
eventType: row.kind,
|
|
18496
|
+
startedAt: row.occurredAt,
|
|
18497
|
+
content: row.content,
|
|
18498
|
+
contentHash: row.contentHash,
|
|
18499
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18500
|
+
})
|
|
18501
|
+
);
|
|
18502
|
+
}
|
|
18503
|
+
}
|
|
18504
|
+
);
|
|
18505
|
+
}
|
|
18506
|
+
function copyLegacyFindings(db) {
|
|
18507
|
+
const selectStmt = db.prepare(
|
|
18508
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18509
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18510
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18511
|
+
first_detected_at AS firstDetectedAt
|
|
18512
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18513
|
+
);
|
|
18514
|
+
const definitionStmt = db.prepare(
|
|
18515
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18516
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18517
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18518
|
+
);
|
|
18519
|
+
const findingStmt = db.prepare(
|
|
18520
|
+
`INSERT INTO inspection_findings
|
|
18521
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18522
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18523
|
+
finding_key, first_detected_at)
|
|
18524
|
+
VALUES
|
|
18525
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18526
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18527
|
+
:findingKey, :firstDetectedAt)
|
|
18528
|
+
ON CONFLICT(id) DO NOTHING
|
|
18529
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18530
|
+
first_detected_at = CASE
|
|
18531
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18532
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18533
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18534
|
+
END`
|
|
18535
|
+
);
|
|
18536
|
+
return drainLegacyTable(
|
|
18537
|
+
db,
|
|
18538
|
+
"findings",
|
|
18539
|
+
selectStmt,
|
|
18540
|
+
(rows) => {
|
|
18541
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18542
|
+
for (const row of rows) {
|
|
18543
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18544
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18545
|
+
if (definitionId === void 0) {
|
|
18546
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18547
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18548
|
+
definitionStmt.run(
|
|
18549
|
+
bindParams({
|
|
18550
|
+
id: definitionId,
|
|
18551
|
+
ruleId: row.ruleId,
|
|
18552
|
+
name: row.ruleId,
|
|
18553
|
+
category: row.category,
|
|
18554
|
+
severity: row.severity,
|
|
18555
|
+
definition: "",
|
|
18556
|
+
version: version2
|
|
18557
|
+
})
|
|
18558
|
+
);
|
|
18559
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18560
|
+
}
|
|
18561
|
+
findingStmt.run(
|
|
18562
|
+
bindParams({
|
|
18563
|
+
id: row.id,
|
|
18564
|
+
auditEventId: row.eventId,
|
|
18565
|
+
inspectionDefinitionId: definitionId,
|
|
18566
|
+
spanStart: row.spanStart,
|
|
18567
|
+
spanEnd: row.spanEnd,
|
|
18568
|
+
maskedMatch: row.maskedMatch,
|
|
18569
|
+
actionTaken: row.actionTaken,
|
|
18570
|
+
confidence: row.confidence,
|
|
18571
|
+
findingKey: row.findingKey,
|
|
18572
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18573
|
+
})
|
|
18574
|
+
);
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18577
|
+
);
|
|
18578
|
+
}
|
|
18579
|
+
function runLegacyHistoryBackfill(db) {
|
|
18580
|
+
try {
|
|
18581
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18582
|
+
if (!eventsCaughtUp) return false;
|
|
18583
|
+
return copyLegacyFindings(db);
|
|
18584
|
+
} catch (error51) {
|
|
18585
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18586
|
+
return false;
|
|
18587
|
+
}
|
|
18588
|
+
}
|
|
18097
18589
|
function isForeignSqliteLineage(db) {
|
|
18098
18590
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18099
18591
|
return columnNames(db, "events").includes("tenant_id");
|
|
18100
18592
|
}
|
|
18101
18593
|
function ensureSyncedAtColumn(db, table2) {
|
|
18594
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18102
18595
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18103
18596
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18104
18597
|
}
|
|
@@ -18119,6 +18612,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18119
18612
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18120
18613
|
)`);
|
|
18121
18614
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18615
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18122
18616
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18123
18617
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18124
18618
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18137,29 +18631,13 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18137
18631
|
blocked_at INTEGER NOT NULL
|
|
18138
18632
|
)`);
|
|
18139
18633
|
}
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18148
|
-
try {
|
|
18149
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18150
|
-
} catch {
|
|
18151
|
-
}
|
|
18152
|
-
}
|
|
18153
|
-
function walSidecars(file2) {
|
|
18154
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18155
|
-
}
|
|
18156
|
-
function tightenPerms(file2) {
|
|
18157
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18158
|
-
try {
|
|
18159
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18160
|
-
} catch {
|
|
18161
|
-
}
|
|
18162
|
-
}
|
|
18634
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18635
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18636
|
+
rule_key TEXT PRIMARY KEY,
|
|
18637
|
+
verdict TEXT NOT NULL,
|
|
18638
|
+
worst_probe_ms REAL NOT NULL,
|
|
18639
|
+
checked_at INTEGER NOT NULL
|
|
18640
|
+
)`);
|
|
18163
18641
|
}
|
|
18164
18642
|
|
|
18165
18643
|
// ../../packages/persistence/src/internal/json.ts
|
|
@@ -18181,51 +18659,6 @@ function parseJsonObject(s) {
|
|
|
18181
18659
|
return void 0;
|
|
18182
18660
|
}
|
|
18183
18661
|
|
|
18184
|
-
// ../../packages/persistence/src/internal/rows.ts
|
|
18185
|
-
function allRows(stmt, params) {
|
|
18186
|
-
if (params === void 0) return stmt.all();
|
|
18187
|
-
if (Array.isArray(params)) return stmt.all(...params);
|
|
18188
|
-
return stmt.all(params);
|
|
18189
|
-
}
|
|
18190
|
-
function getRow(stmt, params) {
|
|
18191
|
-
if (params === void 0) return stmt.get();
|
|
18192
|
-
if (Array.isArray(params)) return stmt.get(...params);
|
|
18193
|
-
return stmt.get(params);
|
|
18194
|
-
}
|
|
18195
|
-
function intToBool(raw) {
|
|
18196
|
-
return raw === 1 || raw === true;
|
|
18197
|
-
}
|
|
18198
|
-
function boolToInt(b) {
|
|
18199
|
-
return b ? 1 : 0;
|
|
18200
|
-
}
|
|
18201
|
-
function bindParams(row) {
|
|
18202
|
-
const out = {};
|
|
18203
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18204
|
-
out[key] = value === void 0 ? null : value;
|
|
18205
|
-
}
|
|
18206
|
-
return out;
|
|
18207
|
-
}
|
|
18208
|
-
function countScalar(db, sql, params) {
|
|
18209
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18210
|
-
}
|
|
18211
|
-
function countBy(db, sql, params) {
|
|
18212
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18213
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18214
|
-
map2.set(row.k, row.n);
|
|
18215
|
-
}
|
|
18216
|
-
return map2;
|
|
18217
|
-
}
|
|
18218
|
-
function mapRowsTolerant(rows, map2) {
|
|
18219
|
-
const out = [];
|
|
18220
|
-
for (const row of rows) {
|
|
18221
|
-
try {
|
|
18222
|
-
out.push(map2(row));
|
|
18223
|
-
} catch {
|
|
18224
|
-
}
|
|
18225
|
-
}
|
|
18226
|
-
return out;
|
|
18227
|
-
}
|
|
18228
|
-
|
|
18229
18662
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18230
18663
|
var DAY_MS = 864e5;
|
|
18231
18664
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18842,6 +19275,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18842
19275
|
})
|
|
18843
19276
|
);
|
|
18844
19277
|
}
|
|
19278
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19279
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19280
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19281
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19282
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19283
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19284
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19285
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19286
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19287
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19288
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19289
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19290
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19291
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19292
|
+
}
|
|
18845
19293
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18846
19294
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18847
19295
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19303,8 +19751,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19303
19751
|
)
|
|
19304
19752
|
);
|
|
19305
19753
|
}
|
|
19306
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19307
|
-
//
|
|
19754
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19755
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19756
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19757
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19758
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19759
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19760
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19761
|
+
// to the old findings⋈events one.
|
|
19308
19762
|
countFindingsLast30d(ruleIds) {
|
|
19309
19763
|
if (ruleIds.length === 0) return 0;
|
|
19310
19764
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19312,8 +19766,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19312
19766
|
return countScalar(
|
|
19313
19767
|
this.db,
|
|
19314
19768
|
`SELECT count(*) AS n
|
|
19315
|
-
FROM
|
|
19316
|
-
|
|
19769
|
+
FROM inspection_findings f
|
|
19770
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19771
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19772
|
+
WHERE e.started_at >= ?
|
|
19773
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19774
|
+
AND d.rule_id IN (${inClause})`,
|
|
19317
19775
|
[since, ...ruleIds]
|
|
19318
19776
|
);
|
|
19319
19777
|
}
|
|
@@ -19323,35 +19781,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19323
19781
|
var SqliteEventsRepository = class {
|
|
19324
19782
|
constructor(db) {
|
|
19325
19783
|
this.db = db;
|
|
19326
|
-
this.insertStmt = db.prepare(
|
|
19327
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19328
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19329
|
-
);
|
|
19330
19784
|
}
|
|
19331
19785
|
db;
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
this.insertStmt.run(
|
|
19336
|
-
bindParams({
|
|
19337
|
-
id: row.id,
|
|
19338
|
-
sourceTool: row.sourceTool,
|
|
19339
|
-
kind: row.kind,
|
|
19340
|
-
occurredAt: row.occurredAt,
|
|
19341
|
-
contentHash: row.contentHash,
|
|
19342
|
-
content: row.content,
|
|
19343
|
-
metadata: row.metadata
|
|
19344
|
-
})
|
|
19345
|
-
);
|
|
19346
|
-
}
|
|
19347
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19348
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19349
|
-
// never duplicates findings.
|
|
19786
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19787
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19788
|
+
// scan never duplicates findings.
|
|
19350
19789
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19351
19790
|
// async EventsReadPort contract.
|
|
19791
|
+
//
|
|
19792
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19793
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19794
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19795
|
+
// index-friendly rather than walking rows that can never match.
|
|
19352
19796
|
contentHashes() {
|
|
19353
19797
|
const rows = allRows(
|
|
19354
|
-
this.db.prepare(
|
|
19798
|
+
this.db.prepare(
|
|
19799
|
+
`SELECT content_hash FROM audit_events
|
|
19800
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19801
|
+
)
|
|
19355
19802
|
);
|
|
19356
19803
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19357
19804
|
}
|
|
@@ -19687,17 +20134,20 @@ function parseExceptionRow(row) {
|
|
|
19687
20134
|
}
|
|
19688
20135
|
|
|
19689
20136
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19690
|
-
function
|
|
20137
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19691
20138
|
return `(
|
|
19692
|
-
SELECT fr
|
|
20139
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19693
20140
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19694
20141
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19695
20142
|
LIMIT 1
|
|
19696
20143
|
)`;
|
|
19697
20144
|
}
|
|
20145
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20146
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20147
|
+
}
|
|
19698
20148
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19699
|
-
SELECT finding_key, status FROM (
|
|
19700
|
-
SELECT fr.finding_key, fr.status,
|
|
20149
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20150
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19701
20151
|
ROW_NUMBER() OVER (
|
|
19702
20152
|
PARTITION BY fr.finding_key
|
|
19703
20153
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19724,68 +20174,21 @@ var DAY_MS3 = 864e5;
|
|
|
19724
20174
|
var SqliteFindingsRepository = class {
|
|
19725
20175
|
constructor(db) {
|
|
19726
20176
|
this.db = db;
|
|
19727
|
-
this.insertStmt = db.prepare(
|
|
19728
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19729
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19730
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19731
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19732
|
-
event_id = excluded.event_id,
|
|
19733
|
-
category = excluded.category,
|
|
19734
|
-
severity = excluded.severity,
|
|
19735
|
-
span_start = excluded.span_start,
|
|
19736
|
-
span_end = excluded.span_end,
|
|
19737
|
-
masked_match = excluded.masked_match,
|
|
19738
|
-
action_taken = excluded.action_taken,
|
|
19739
|
-
confidence = excluded.confidence`
|
|
19740
|
-
);
|
|
19741
|
-
this.sessionDupStmt = db.prepare(
|
|
19742
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19743
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19744
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19745
|
-
LIMIT 1`
|
|
19746
|
-
);
|
|
19747
20177
|
}
|
|
19748
20178
|
db;
|
|
19749
|
-
insertStmt;
|
|
19750
|
-
sessionDupStmt;
|
|
19751
|
-
insertFindings(findings, scope = {}) {
|
|
19752
|
-
for (const finding of findings) {
|
|
19753
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19754
|
-
const row = toFindingRow(finding);
|
|
19755
|
-
this.insertStmt.run({
|
|
19756
|
-
id: row.id,
|
|
19757
|
-
eventId: row.eventId,
|
|
19758
|
-
ruleId: row.ruleId,
|
|
19759
|
-
category: row.category,
|
|
19760
|
-
severity: row.severity,
|
|
19761
|
-
spanStart: row.spanStart,
|
|
19762
|
-
spanEnd: row.spanEnd,
|
|
19763
|
-
maskedMatch: row.maskedMatch,
|
|
19764
|
-
actionTaken: row.actionTaken,
|
|
19765
|
-
confidence: row.confidence,
|
|
19766
|
-
findingKey: row.findingKey ?? null
|
|
19767
|
-
});
|
|
19768
|
-
}
|
|
19769
|
-
}
|
|
19770
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19771
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19772
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19773
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19774
|
-
const hit = this.sessionDupStmt.get({
|
|
19775
|
-
ruleId: finding.ruleId,
|
|
19776
|
-
maskedMatch: finding.maskedMatch,
|
|
19777
|
-
sessionId
|
|
19778
|
-
});
|
|
19779
|
-
return hit !== void 0;
|
|
19780
|
-
}
|
|
19781
20179
|
recentFindings(opts) {
|
|
19782
20180
|
const limit = opts?.limit ?? 50;
|
|
19783
20181
|
const rows = allRows(
|
|
19784
20182
|
this.db.prepare(
|
|
19785
|
-
`SELECT f.id, f.event_id,
|
|
19786
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19787
|
-
|
|
19788
|
-
|
|
20183
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20184
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20185
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20186
|
+
e.event_type AS kind
|
|
20187
|
+
FROM inspection_findings f
|
|
20188
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20189
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20190
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20191
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19789
20192
|
LIMIT :limit`
|
|
19790
20193
|
),
|
|
19791
20194
|
{ limit }
|
|
@@ -19807,25 +20210,34 @@ var SqliteFindingsRepository = class {
|
|
|
19807
20210
|
);
|
|
19808
20211
|
}
|
|
19809
20212
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19810
|
-
* session-stamped
|
|
20213
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19811
20214
|
* page can label its findings link without the grouped pipeline. */
|
|
19812
20215
|
sessionFindingsCount(sessionId) {
|
|
19813
20216
|
if (!sessionId) return Promise.resolve(0);
|
|
19814
20217
|
return Promise.resolve(
|
|
19815
20218
|
countScalar(
|
|
19816
20219
|
this.db,
|
|
19817
|
-
`SELECT count(*) AS n FROM
|
|
19818
|
-
JOIN
|
|
19819
|
-
WHERE
|
|
20220
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20221
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20222
|
+
WHERE e.root_session_id = :sessionId
|
|
20223
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19820
20224
|
{ sessionId }
|
|
19821
20225
|
)
|
|
19822
20226
|
);
|
|
19823
20227
|
}
|
|
19824
|
-
/** Per-rule transcript firing tally for one session —
|
|
19825
|
-
*
|
|
19826
|
-
*
|
|
19827
|
-
*
|
|
19828
|
-
*
|
|
20228
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20229
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20230
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20231
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20232
|
+
* tally with the deduped groups it lists.
|
|
20233
|
+
*
|
|
20234
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20235
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20236
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20237
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20238
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20239
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20240
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19829
20241
|
sessionFirings(sessionId) {
|
|
19830
20242
|
return Object.fromEntries(
|
|
19831
20243
|
countBy(
|
|
@@ -19835,18 +20247,25 @@ var SqliteFindingsRepository = class {
|
|
|
19835
20247
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19836
20248
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19837
20249
|
WHERE e.root_session_id = :sessionId
|
|
20250
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19838
20251
|
GROUP BY d.rule_id`,
|
|
19839
20252
|
{ sessionId }
|
|
19840
20253
|
)
|
|
19841
20254
|
);
|
|
19842
20255
|
}
|
|
19843
20256
|
/**
|
|
19844
|
-
* Grouped findings for the dashboard — joins
|
|
19845
|
-
* toolName from
|
|
19846
|
-
*
|
|
20257
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20258
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20259
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20260
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20261
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20262
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20263
|
+
* severity then recency. Filtering
|
|
19847
20264
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19848
20265
|
* reflect the full filtered set; `items` is the requested
|
|
19849
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20266
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20267
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20268
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19850
20269
|
*
|
|
19851
20270
|
* Two reads, neither of which materializes a row per finding:
|
|
19852
20271
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19859,10 +20278,11 @@ var SqliteFindingsRepository = class {
|
|
|
19859
20278
|
* rule is ever restated in SQL.
|
|
19860
20279
|
*/
|
|
19861
20280
|
listGroupedFindings(query) {
|
|
19862
|
-
const sessionPredicate = query.sessionId ? `
|
|
20281
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20282
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19863
20283
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19864
20284
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19865
|
-
predicate
|
|
20285
|
+
predicate,
|
|
19866
20286
|
params: sessionParams
|
|
19867
20287
|
});
|
|
19868
20288
|
const rows = allRows(
|
|
@@ -19870,24 +20290,26 @@ var SqliteFindingsRepository = class {
|
|
|
19870
20290
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19871
20291
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19872
20292
|
FROM (
|
|
19873
|
-
SELECT f.id AS id,
|
|
19874
|
-
|
|
20293
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20294
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19875
20295
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19876
|
-
e.
|
|
19877
|
-
json_extract(e.
|
|
19878
|
-
json_extract(e.
|
|
19879
|
-
json_extract(e.
|
|
19880
|
-
e.
|
|
20296
|
+
e.started_at AS occurred_at,
|
|
20297
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20298
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20299
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20300
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20301
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19881
20302
|
latest.status AS latest_status,
|
|
19882
20303
|
ROW_NUMBER() OVER (
|
|
19883
|
-
PARTITION BY
|
|
19884
|
-
ORDER BY e.
|
|
20304
|
+
PARTITION BY d.rule_id
|
|
20305
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19885
20306
|
) AS rn
|
|
19886
|
-
FROM
|
|
19887
|
-
JOIN
|
|
20307
|
+
FROM inspection_findings f
|
|
20308
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20309
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19888
20310
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19889
20311
|
ON latest.finding_key = f.finding_key
|
|
19890
|
-
${
|
|
20312
|
+
${predicate}
|
|
19891
20313
|
)
|
|
19892
20314
|
WHERE rn <= :cap
|
|
19893
20315
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19914,17 +20336,29 @@ var SqliteFindingsRepository = class {
|
|
|
19914
20336
|
severity: query.severity,
|
|
19915
20337
|
providers: query.provider,
|
|
19916
20338
|
actions: query.action,
|
|
20339
|
+
statuses: query.status,
|
|
19917
20340
|
subtype: query.subtype,
|
|
19918
20341
|
q: query.q
|
|
19919
20342
|
};
|
|
19920
20343
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19921
20344
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20345
|
+
const statusFilter = query.status ?? [];
|
|
19922
20346
|
const totals = {
|
|
19923
|
-
findings: sorted.reduce((acc, g) =>
|
|
20347
|
+
findings: sorted.reduce((acc, g) => {
|
|
20348
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20349
|
+
const agg = aggregates.get(g.id);
|
|
20350
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20351
|
+
}, 0),
|
|
19924
20352
|
groups: sorted.length
|
|
19925
20353
|
};
|
|
19926
20354
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19927
|
-
const
|
|
20355
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20356
|
+
const items = sorted.slice(0, limit).map(
|
|
20357
|
+
(g) => statusSet ? {
|
|
20358
|
+
...g,
|
|
20359
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20360
|
+
} : g
|
|
20361
|
+
);
|
|
19928
20362
|
return Promise.resolve({
|
|
19929
20363
|
totals,
|
|
19930
20364
|
facets,
|
|
@@ -19938,45 +20372,62 @@ var SqliteFindingsRepository = class {
|
|
|
19938
20372
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19939
20373
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19940
20374
|
*
|
|
19941
|
-
*
|
|
19942
|
-
*
|
|
19943
|
-
*
|
|
19944
|
-
* status
|
|
19945
|
-
*
|
|
19946
|
-
*
|
|
20375
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20376
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20377
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20378
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20379
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20380
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20381
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20382
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20383
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20384
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20385
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19947
20386
|
* a group's row stays small however many findings it holds.
|
|
19948
20387
|
*
|
|
19949
20388
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19950
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19951
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19952
|
-
* that is a string proportional to the store (~8MB over
|
|
19953
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19954
|
-
* match an instance outside the preview, which searching
|
|
19955
|
-
* would silently lose, so it is fetched only when the
|
|
19956
|
-
* carries a `q`.
|
|
20389
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20390
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20391
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20392
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20393
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20394
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20395
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20396
|
+
* path repeating across tuples.)
|
|
19957
20397
|
*/
|
|
19958
20398
|
groupAggregates(withSearchText, scope) {
|
|
19959
|
-
const
|
|
19960
|
-
group_concat(DISTINCT json_extract(e.
|
|
19961
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20399
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20400
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20401
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19962
20402
|
const rows = this.db.prepare(
|
|
19963
|
-
`SELECT
|
|
19964
|
-
|
|
19965
|
-
max(
|
|
19966
|
-
group_concat(
|
|
19967
|
-
group_concat(
|
|
19968
|
-
group_concat(
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
20403
|
+
`SELECT rule_id,
|
|
20404
|
+
sum(tuple_count) AS instance_count,
|
|
20405
|
+
max(latest_at) AS latest_at,
|
|
20406
|
+
group_concat(source_tools) AS source_tools,
|
|
20407
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20408
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20409
|
+
group_concat(repos) AS repos,
|
|
20410
|
+
group_concat(files) AS files,
|
|
20411
|
+
group_concat(tool_names) AS tool_names
|
|
20412
|
+
FROM (
|
|
20413
|
+
SELECT d.rule_id AS rule_id,
|
|
20414
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20415
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20416
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20417
|
+
count(*) AS tuple_count,
|
|
20418
|
+
max(e.started_at) AS latest_at,
|
|
20419
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20420
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20421
|
+
${innerSearchColumns}
|
|
20422
|
+
FROM inspection_findings f
|
|
20423
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20424
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20425
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20426
|
+
ON latest.finding_key = f.finding_key
|
|
20427
|
+
${scope.predicate}
|
|
20428
|
+
GROUP BY d.rule_id, status_tuple
|
|
20429
|
+
)
|
|
20430
|
+
GROUP BY rule_id`
|
|
19980
20431
|
).all(scope.params);
|
|
19981
20432
|
return new Map(
|
|
19982
20433
|
rows.map((r) => [
|
|
@@ -19986,13 +20437,14 @@ var SqliteFindingsRepository = class {
|
|
|
19986
20437
|
sourceTools: splitConcat(r.source_tools),
|
|
19987
20438
|
actionsTaken: splitConcat(r.actions_taken),
|
|
19988
20439
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19989
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20440
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
19990
20441
|
return {
|
|
19991
20442
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19992
20443
|
// so the marker stands in for the key itself (never rendered).
|
|
19993
20444
|
kind,
|
|
19994
20445
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19995
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20446
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20447
|
+
count: Number(count)
|
|
19996
20448
|
};
|
|
19997
20449
|
}),
|
|
19998
20450
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20009,10 +20461,21 @@ var SqliteFindingsRepository = class {
|
|
|
20009
20461
|
);
|
|
20010
20462
|
}
|
|
20011
20463
|
healthSummary() {
|
|
20012
|
-
const total = countScalar(
|
|
20464
|
+
const total = countScalar(
|
|
20465
|
+
this.db,
|
|
20466
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20467
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20468
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20469
|
+
);
|
|
20013
20470
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20014
20471
|
const grouped = allRows(
|
|
20015
|
-
this.db.prepare(
|
|
20472
|
+
this.db.prepare(
|
|
20473
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20474
|
+
FROM inspection_findings f
|
|
20475
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20476
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20477
|
+
GROUP BY f.action_taken`
|
|
20478
|
+
)
|
|
20016
20479
|
);
|
|
20017
20480
|
for (const row of grouped) {
|
|
20018
20481
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20020,12 +20483,15 @@ var SqliteFindingsRepository = class {
|
|
|
20020
20483
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20021
20484
|
const sevRows = allRows(
|
|
20022
20485
|
this.db.prepare(
|
|
20023
|
-
`SELECT
|
|
20024
|
-
FROM
|
|
20486
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20487
|
+
FROM inspection_findings f
|
|
20488
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20489
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20025
20490
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20026
20491
|
ON latest.finding_key = f.finding_key
|
|
20027
|
-
WHERE
|
|
20028
|
-
|
|
20492
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20493
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20494
|
+
GROUP BY d.severity`
|
|
20029
20495
|
)
|
|
20030
20496
|
);
|
|
20031
20497
|
for (const row of sevRows) {
|
|
@@ -20046,9 +20512,11 @@ var SqliteFindingsRepository = class {
|
|
|
20046
20512
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20047
20513
|
const rows = allRows(
|
|
20048
20514
|
this.db.prepare(
|
|
20049
|
-
`SELECT date(e.
|
|
20050
|
-
FROM
|
|
20051
|
-
|
|
20515
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20516
|
+
FROM inspection_findings f
|
|
20517
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20518
|
+
WHERE e.started_at >= :since
|
|
20519
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20052
20520
|
GROUP BY day, f.action_taken`
|
|
20053
20521
|
),
|
|
20054
20522
|
{ since }
|
|
@@ -20113,15 +20581,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20113
20581
|
this.insertStmt = db.prepare(
|
|
20114
20582
|
`INSERT INTO inspection_findings
|
|
20115
20583
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20116
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20584
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20585
|
+
finding_key, first_detected_at)
|
|
20117
20586
|
VALUES
|
|
20118
20587
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20119
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20120
|
-
|
|
20588
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20589
|
+
:findingKey,
|
|
20590
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20591
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20592
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20593
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20594
|
+
audit_event_id = excluded.audit_event_id,
|
|
20595
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20596
|
+
classified_data_id = excluded.classified_data_id,
|
|
20597
|
+
span_start = excluded.span_start,
|
|
20598
|
+
span_end = excluded.span_end,
|
|
20599
|
+
masked_match = excluded.masked_match,
|
|
20600
|
+
action_taken = excluded.action_taken,
|
|
20601
|
+
confidence = excluded.confidence`
|
|
20602
|
+
);
|
|
20603
|
+
this.sessionDupStmt = db.prepare(
|
|
20604
|
+
`SELECT 1 FROM inspection_findings f
|
|
20605
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20606
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20607
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20608
|
+
AND e.root_session_id = :sessionId
|
|
20609
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20610
|
+
LIMIT 1`
|
|
20611
|
+
);
|
|
20612
|
+
this.eventDupStmt = db.prepare(
|
|
20613
|
+
`SELECT 1 FROM inspection_findings f
|
|
20614
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20615
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20616
|
+
AND f.masked_match = :maskedMatch
|
|
20617
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20618
|
+
LIMIT 1`
|
|
20121
20619
|
);
|
|
20122
20620
|
}
|
|
20123
20621
|
db;
|
|
20124
20622
|
insertStmt;
|
|
20623
|
+
sessionDupStmt;
|
|
20624
|
+
eventDupStmt;
|
|
20625
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20626
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20627
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20628
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20629
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20630
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20631
|
+
}
|
|
20632
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20633
|
+
// recorded against the given audit event.
|
|
20634
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20635
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20636
|
+
}
|
|
20125
20637
|
insertFinding(input) {
|
|
20126
20638
|
const row = toInspectionFindingRow(input);
|
|
20127
20639
|
this.insertStmt.run(
|
|
@@ -20134,7 +20646,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20134
20646
|
spanEnd: row.spanEnd,
|
|
20135
20647
|
maskedMatch: row.maskedMatch,
|
|
20136
20648
|
actionTaken: row.actionTaken,
|
|
20137
|
-
confidence: row.confidence
|
|
20649
|
+
confidence: row.confidence,
|
|
20650
|
+
findingKey: row.findingKey,
|
|
20651
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20138
20652
|
})
|
|
20139
20653
|
);
|
|
20140
20654
|
}
|
|
@@ -20406,7 +20920,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20406
20920
|
installedRuleset() {
|
|
20407
20921
|
const rows = allRows(
|
|
20408
20922
|
this.db.prepare(
|
|
20409
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20923
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20410
20924
|
)
|
|
20411
20925
|
);
|
|
20412
20926
|
const out = {
|
|
@@ -20414,7 +20928,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20414
20928
|
enabledPacks: 0,
|
|
20415
20929
|
rules: [],
|
|
20416
20930
|
invalidRules: 0,
|
|
20417
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20931
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20932
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20418
20933
|
};
|
|
20419
20934
|
for (const row of rows) {
|
|
20420
20935
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20436,6 +20951,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20436
20951
|
if (parsed.success) {
|
|
20437
20952
|
out.rules.push(parsed.data);
|
|
20438
20953
|
out.ruleActions.set(parsed.data.id, action);
|
|
20954
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20439
20955
|
} else out.invalidRules += 1;
|
|
20440
20956
|
}
|
|
20441
20957
|
}
|
|
@@ -21610,19 +22126,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21610
22126
|
);
|
|
21611
22127
|
this.openAtRestStmt = db.prepare(
|
|
21612
22128
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21613
|
-
FROM
|
|
21614
|
-
JOIN
|
|
21615
|
-
WHERE e.
|
|
21616
|
-
AND json_extract(e.
|
|
22129
|
+
FROM inspection_findings f
|
|
22130
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22131
|
+
WHERE e.event_type = 'code_change'
|
|
22132
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21617
22133
|
AND f.finding_key IS NOT NULL
|
|
21618
22134
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21619
22135
|
);
|
|
21620
22136
|
this.resolvedAtRestStmt = db.prepare(
|
|
21621
22137
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21622
|
-
FROM
|
|
21623
|
-
JOIN
|
|
21624
|
-
WHERE e.
|
|
21625
|
-
AND json_extract(e.
|
|
22138
|
+
FROM inspection_findings f
|
|
22139
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22140
|
+
WHERE e.event_type = 'code_change'
|
|
22141
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21626
22142
|
AND f.finding_key IS NOT NULL
|
|
21627
22143
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21628
22144
|
);
|
|
@@ -21690,6 +22206,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21690
22206
|
}
|
|
21691
22207
|
};
|
|
21692
22208
|
|
|
22209
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
22210
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
22211
|
+
constructor(db) {
|
|
22212
|
+
this.db = db;
|
|
22213
|
+
this.upsertStmt = db.prepare(
|
|
22214
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
22215
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
22216
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
22217
|
+
verdict = excluded.verdict,
|
|
22218
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
22219
|
+
checked_at = excluded.checked_at`
|
|
22220
|
+
);
|
|
22221
|
+
this.readStmt = db.prepare(
|
|
22222
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22223
|
+
);
|
|
22224
|
+
}
|
|
22225
|
+
db;
|
|
22226
|
+
upsertStmt;
|
|
22227
|
+
readStmt;
|
|
22228
|
+
getVerdict(ruleKey) {
|
|
22229
|
+
return getRow(this.readStmt, { ruleKey });
|
|
22230
|
+
}
|
|
22231
|
+
setVerdict(ruleKey, verdict, worstProbeMs) {
|
|
22232
|
+
failOpenTransaction(this.db, () => {
|
|
22233
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
|
|
22234
|
+
});
|
|
22235
|
+
}
|
|
22236
|
+
};
|
|
22237
|
+
|
|
21693
22238
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21694
22239
|
var SqliteScanLedgerRepository = class {
|
|
21695
22240
|
constructor(db) {
|
|
@@ -21816,25 +22361,27 @@ var SqliteSecurityRepository = class {
|
|
|
21816
22361
|
severitySummary() {
|
|
21817
22362
|
const rows = allRows(
|
|
21818
22363
|
this.db.prepare(
|
|
21819
|
-
`SELECT
|
|
22364
|
+
`SELECT d.severity AS severity,
|
|
21820
22365
|
COUNT(*) AS count,
|
|
21821
22366
|
SUM(CASE
|
|
21822
|
-
WHEN e.
|
|
22367
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21823
22368
|
WHEN f.finding_key IS NULL THEN 0
|
|
21824
22369
|
WHEN latest.status = 'resolved' THEN 1
|
|
21825
22370
|
ELSE 0
|
|
21826
22371
|
END) AS caught,
|
|
21827
22372
|
SUM(CASE
|
|
21828
|
-
WHEN e.
|
|
22373
|
+
WHEN e.event_type = 'code_change'
|
|
21829
22374
|
AND f.finding_key IS NOT NULL
|
|
21830
22375
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21831
22376
|
ELSE 0
|
|
21832
22377
|
END) AS open_at_rest
|
|
21833
|
-
FROM
|
|
21834
|
-
JOIN
|
|
22378
|
+
FROM inspection_findings f
|
|
22379
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22380
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21835
22381
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21836
22382
|
ON latest.finding_key = f.finding_key
|
|
21837
|
-
|
|
22383
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22384
|
+
GROUP BY d.severity`
|
|
21838
22385
|
)
|
|
21839
22386
|
);
|
|
21840
22387
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21900,7 +22447,7 @@ var SqliteSecurityRepository = class {
|
|
|
21900
22447
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21901
22448
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21902
22449
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21903
|
-
// (
|
|
22450
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21904
22451
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21905
22452
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21906
22453
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21925,30 +22472,20 @@ var SqliteSecurityRepository = class {
|
|
|
21925
22472
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21926
22473
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21927
22474
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21928
|
-
//
|
|
21929
|
-
// the parent event's
|
|
21930
|
-
// backfill left null.
|
|
21931
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
WHERE fr.finding_key = f.finding_key
|
|
21941
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21942
|
-
LIMIT 1
|
|
21943
|
-
) AS latest_method,
|
|
21944
|
-
(
|
|
21945
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
21946
|
-
WHERE fr.finding_key = f.finding_key
|
|
21947
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21948
|
-
LIMIT 1
|
|
21949
|
-
) AS latest_resolved_at
|
|
21950
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22475
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22476
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22477
|
+
// legacy/edge row the backfill left null.
|
|
22478
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22479
|
+
latest.status AS latest_status,
|
|
22480
|
+
latest.method AS latest_method,
|
|
22481
|
+
latest.resolved_at AS latest_resolved_at
|
|
22482
|
+
FROM inspection_findings f
|
|
22483
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22484
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22485
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22486
|
+
ON latest.finding_key = f.finding_key
|
|
21951
22487
|
WHERE f.finding_key IS NOT NULL
|
|
22488
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
21952
22489
|
AND EXISTS (
|
|
21953
22490
|
SELECT 1 FROM finding_resolution fr
|
|
21954
22491
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21995,11 +22532,13 @@ var SqliteSecurityRepository = class {
|
|
|
21995
22532
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21996
22533
|
const rows = allRows(
|
|
21997
22534
|
this.db.prepare(
|
|
21998
|
-
`SELECT json_extract(e.
|
|
21999
|
-
FROM
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
AND
|
|
22535
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22536
|
+
FROM inspection_findings f
|
|
22537
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22538
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22539
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22540
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22541
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22003
22542
|
GROUP BY repo
|
|
22004
22543
|
ORDER BY c DESC, repo
|
|
22005
22544
|
LIMIT :limit`
|
|
@@ -22023,44 +22562,28 @@ var SqliteSecurityRepository = class {
|
|
|
22023
22562
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22024
22563
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22025
22564
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22026
|
-
// finding's parent event (
|
|
22027
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22028
|
-
// capped at `limit`.
|
|
22565
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22566
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22567
|
+
// DESC, capped at `limit`.
|
|
22029
22568
|
recentlyResolved(limit = 20) {
|
|
22030
22569
|
const rows = allRows(
|
|
22031
22570
|
this.db.prepare(
|
|
22032
22571
|
`SELECT f.finding_key AS finding_key,
|
|
22033
|
-
|
|
22034
|
-
|
|
22035
|
-
json_extract(e.
|
|
22036
|
-
COALESCE(f.first_detected_at, e.
|
|
22037
|
-
|
|
22038
|
-
|
|
22039
|
-
|
|
22040
|
-
|
|
22041
|
-
|
|
22042
|
-
|
|
22043
|
-
|
|
22044
|
-
WHERE e.kind = 'code_change'
|
|
22572
|
+
d.rule_id AS rule_id,
|
|
22573
|
+
d.severity AS severity,
|
|
22574
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22575
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22576
|
+
latest.resolved_at AS latest_resolved_at
|
|
22577
|
+
FROM inspection_findings f
|
|
22578
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22579
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22580
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22581
|
+
ON latest.finding_key = f.finding_key
|
|
22582
|
+
WHERE e.event_type = 'code_change'
|
|
22045
22583
|
AND f.finding_key IS NOT NULL
|
|
22046
|
-
AND
|
|
22047
|
-
|
|
22048
|
-
|
|
22049
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22050
|
-
LIMIT 1
|
|
22051
|
-
) = 'resolved'
|
|
22052
|
-
AND (
|
|
22053
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22054
|
-
WHERE fr.finding_key = f.finding_key
|
|
22055
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22056
|
-
LIMIT 1
|
|
22057
|
-
) = 'fixed-at-source'
|
|
22058
|
-
AND (
|
|
22059
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22060
|
-
WHERE fr.finding_key = f.finding_key
|
|
22061
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22062
|
-
LIMIT 1
|
|
22063
|
-
) IS NOT NULL
|
|
22584
|
+
AND latest.status = 'resolved'
|
|
22585
|
+
AND latest.method = 'fixed-at-source'
|
|
22586
|
+
AND latest.resolved_at IS NOT NULL
|
|
22064
22587
|
ORDER BY latest_resolved_at DESC
|
|
22065
22588
|
LIMIT :limit`
|
|
22066
22589
|
),
|
|
@@ -22079,15 +22602,18 @@ var SqliteSecurityRepository = class {
|
|
|
22079
22602
|
return Promise.resolve({ items });
|
|
22080
22603
|
}
|
|
22081
22604
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22082
|
-
// epoch-millis timestamp.
|
|
22605
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22083
22606
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22084
22607
|
findingsInRange(fromMs, toMs) {
|
|
22085
22608
|
const rows = allRows(
|
|
22086
22609
|
this.db.prepare(
|
|
22087
|
-
`SELECT e.
|
|
22088
|
-
FROM
|
|
22089
|
-
|
|
22090
|
-
|
|
22610
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22611
|
+
FROM inspection_findings f
|
|
22612
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22613
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22614
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22615
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22616
|
+
ORDER BY e.started_at`
|
|
22091
22617
|
),
|
|
22092
22618
|
{ from: fromMs, to: toMs }
|
|
22093
22619
|
);
|
|
@@ -22101,11 +22627,50 @@ var SqliteSecurityRepository = class {
|
|
|
22101
22627
|
|
|
22102
22628
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22103
22629
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22104
|
-
var
|
|
22630
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22631
|
+
var IN_CHUNK = 500;
|
|
22632
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22633
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22634
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22635
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22105
22636
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22106
22637
|
function parseNetwork(networkJson) {
|
|
22107
22638
|
return safeJson(networkJson, null);
|
|
22108
22639
|
}
|
|
22640
|
+
function capHits(all, mode) {
|
|
22641
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22642
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22643
|
+
}
|
|
22644
|
+
if (mode === "walk") {
|
|
22645
|
+
return {
|
|
22646
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22647
|
+
droppedFiles: [],
|
|
22648
|
+
truncated: true
|
|
22649
|
+
};
|
|
22650
|
+
}
|
|
22651
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22652
|
+
for (const hit of all) {
|
|
22653
|
+
const bucket = byFile.get(hit.site.file);
|
|
22654
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22655
|
+
else bucket.push(hit);
|
|
22656
|
+
}
|
|
22657
|
+
const hits = [];
|
|
22658
|
+
const droppedFiles = [];
|
|
22659
|
+
for (const [file2, bucket] of byFile) {
|
|
22660
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22661
|
+
else hits.push(...bucket);
|
|
22662
|
+
}
|
|
22663
|
+
return { hits, droppedFiles, truncated: true };
|
|
22664
|
+
}
|
|
22665
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22666
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22667
|
+
const dropped = new Set(droppedFiles);
|
|
22668
|
+
return {
|
|
22669
|
+
mode: "ledger",
|
|
22670
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22671
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22672
|
+
};
|
|
22673
|
+
}
|
|
22109
22674
|
function toEndpointSummary(row) {
|
|
22110
22675
|
return {
|
|
22111
22676
|
id: row.id,
|
|
@@ -22196,13 +22761,15 @@ var SqliteSharesRepository = class {
|
|
|
22196
22761
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22197
22762
|
const insecure = countScalar(
|
|
22198
22763
|
this.db,
|
|
22199
|
-
|
|
22764
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22765
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22200
22766
|
);
|
|
22201
22767
|
const needsReview = countScalar(
|
|
22202
22768
|
this.db,
|
|
22203
22769
|
`SELECT count(DISTINCT d.id) AS n
|
|
22204
22770
|
FROM share_destination d
|
|
22205
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22771
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22772
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22206
22773
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22207
22774
|
);
|
|
22208
22775
|
const kindCounts = countBy(
|
|
@@ -22212,6 +22779,7 @@ var SqliteSharesRepository = class {
|
|
|
22212
22779
|
const byKind = {
|
|
22213
22780
|
provider: kindCounts.get("provider") ?? 0,
|
|
22214
22781
|
internal: kindCounts.get("internal") ?? 0,
|
|
22782
|
+
external: kindCounts.get("external") ?? 0,
|
|
22215
22783
|
ip: kindCounts.get("ip") ?? 0
|
|
22216
22784
|
};
|
|
22217
22785
|
const trustCounts = countBy(
|
|
@@ -22287,23 +22855,316 @@ var SqliteSharesRepository = class {
|
|
|
22287
22855
|
// real edit from a no-such-destination.
|
|
22288
22856
|
/**
|
|
22289
22857
|
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22290
|
-
* `null` deletes the override
|
|
22858
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22859
|
+
*
|
|
22860
|
+
* The written row carries both the destination id and its host, so the
|
|
22861
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22862
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22863
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22864
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22865
|
+
* would otherwise race a concurrent prune.
|
|
22291
22866
|
*/
|
|
22292
22867
|
setEgressDecision(destinationId, decision) {
|
|
22293
|
-
|
|
22294
|
-
|
|
22295
|
-
|
|
22296
|
-
|
|
22297
|
-
|
|
22868
|
+
let existed = false;
|
|
22869
|
+
withTransaction(
|
|
22870
|
+
this.db,
|
|
22871
|
+
() => {
|
|
22872
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22873
|
+
if (dest === void 0) return;
|
|
22874
|
+
existed = true;
|
|
22875
|
+
this.db.prepare(
|
|
22876
|
+
`DELETE FROM egress_decision_override
|
|
22877
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22878
|
+
).run({ host: dest.host, destinationId });
|
|
22879
|
+
if (decision === null) return;
|
|
22880
|
+
this.db.prepare(
|
|
22881
|
+
`INSERT INTO egress_decision_override
|
|
22882
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22883
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22884
|
+
).run({
|
|
22885
|
+
id: randomUUID7(),
|
|
22886
|
+
destinationId,
|
|
22887
|
+
host: dest.host,
|
|
22888
|
+
decision,
|
|
22889
|
+
now: Date.now()
|
|
22890
|
+
});
|
|
22891
|
+
},
|
|
22892
|
+
"IMMEDIATE"
|
|
22893
|
+
);
|
|
22894
|
+
return existed;
|
|
22895
|
+
}
|
|
22896
|
+
/**
|
|
22897
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22898
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22899
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22900
|
+
* references, and drop what no longer has evidence.
|
|
22901
|
+
*
|
|
22902
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22903
|
+
* display payload and never scope a delete. The whole write is one
|
|
22904
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22905
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22906
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22907
|
+
* ledger commit so the next scan retries.
|
|
22908
|
+
*
|
|
22909
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22910
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22911
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22912
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22913
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22914
|
+
*/
|
|
22915
|
+
recordProjectEgress(input) {
|
|
22916
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22917
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22918
|
+
const now = Date.now();
|
|
22919
|
+
let summary = {
|
|
22920
|
+
destinations: 0,
|
|
22921
|
+
endpoints: 0,
|
|
22922
|
+
callSites: 0,
|
|
22923
|
+
truncated,
|
|
22924
|
+
droppedFiles
|
|
22925
|
+
};
|
|
22926
|
+
withTransaction(
|
|
22927
|
+
this.db,
|
|
22928
|
+
() => {
|
|
22929
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22930
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22931
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22932
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22933
|
+
this.pruneOrphans();
|
|
22934
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22935
|
+
},
|
|
22936
|
+
"IMMEDIATE"
|
|
22937
|
+
);
|
|
22938
|
+
return summary;
|
|
22939
|
+
}
|
|
22940
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22941
|
+
/**
|
|
22942
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22943
|
+
*
|
|
22944
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22945
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22946
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22947
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22948
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22949
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22950
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22951
|
+
*/
|
|
22952
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22953
|
+
if (reconcile.mode === "walk") {
|
|
22954
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22955
|
+
this.db.prepare(
|
|
22956
|
+
`DELETE FROM share_call_site
|
|
22957
|
+
WHERE project_key = :key
|
|
22958
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22959
|
+
AND file NOT LIKE '.%'
|
|
22960
|
+
AND file NOT LIKE '%/.%'`
|
|
22961
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22962
|
+
return;
|
|
22963
|
+
}
|
|
22964
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22965
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22966
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22967
|
+
this.db.prepare(
|
|
22968
|
+
`DELETE FROM share_call_site
|
|
22969
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22970
|
+
).run(projectKey, ...chunk);
|
|
22971
|
+
}
|
|
22972
|
+
}
|
|
22973
|
+
/**
|
|
22974
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22975
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22976
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22977
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22978
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22979
|
+
* classification for this batch.
|
|
22980
|
+
*/
|
|
22981
|
+
upsertHits(input, hits, projectId, now) {
|
|
22982
|
+
if (hits.length === 0) return;
|
|
22983
|
+
const destStmt = this.db.prepare(
|
|
22984
|
+
`INSERT INTO share_destination
|
|
22985
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22986
|
+
created_at, updated_at)
|
|
22987
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22988
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22989
|
+
kind = excluded.kind,
|
|
22990
|
+
name = excluded.name,
|
|
22991
|
+
category = excluded.category,
|
|
22992
|
+
trust = excluded.trust,
|
|
22993
|
+
network_json = excluded.network_json,
|
|
22994
|
+
last_seen = excluded.last_seen,
|
|
22995
|
+
updated_at = excluded.updated_at`
|
|
22996
|
+
);
|
|
22997
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22998
|
+
const endpointStmt = this.db.prepare(
|
|
22999
|
+
`INSERT INTO share_endpoint
|
|
23000
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
23001
|
+
created_at, updated_at)
|
|
23002
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
23003
|
+
:now, :now)
|
|
23004
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
23005
|
+
transport = excluded.transport,
|
|
23006
|
+
template = excluded.template,
|
|
23007
|
+
data_class = excluded.data_class,
|
|
23008
|
+
last_seen = excluded.last_seen,
|
|
23009
|
+
updated_at = excluded.updated_at`
|
|
23010
|
+
);
|
|
23011
|
+
const endpointIdStmt = this.db.prepare(
|
|
23012
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
23013
|
+
);
|
|
23014
|
+
const siteStmt = this.db.prepare(
|
|
23015
|
+
`INSERT INTO share_call_site
|
|
23016
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
23017
|
+
project_id, created_at, updated_at)
|
|
23018
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
23019
|
+
:vendored, :projectId, :now, :now)
|
|
23020
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
23021
|
+
snippet = excluded.snippet,
|
|
23022
|
+
dynamic = excluded.dynamic,
|
|
23023
|
+
vendored = excluded.vendored,
|
|
23024
|
+
project = excluded.project,
|
|
23025
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
23026
|
+
updated_at = excluded.updated_at`
|
|
23027
|
+
);
|
|
23028
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
23029
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
23030
|
+
for (const hit of hits) {
|
|
23031
|
+
let destinationId = destIds.get(hit.host);
|
|
23032
|
+
if (destinationId === void 0) {
|
|
23033
|
+
destStmt.run({
|
|
23034
|
+
id: randomUUID7(),
|
|
23035
|
+
kind: hit.kind,
|
|
23036
|
+
name: hit.name,
|
|
23037
|
+
host: hit.host,
|
|
23038
|
+
category: hit.category,
|
|
23039
|
+
trust: hit.trust,
|
|
23040
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
23041
|
+
now
|
|
23042
|
+
});
|
|
23043
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
23044
|
+
destIds.set(hit.host, destinationId);
|
|
23045
|
+
}
|
|
23046
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
23047
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
23048
|
+
if (endpointId === void 0) {
|
|
23049
|
+
endpointStmt.run({
|
|
23050
|
+
id: randomUUID7(),
|
|
23051
|
+
destinationId,
|
|
23052
|
+
method: hit.method,
|
|
23053
|
+
transport: hit.transport,
|
|
23054
|
+
url: hit.url,
|
|
23055
|
+
template: boolToInt(hit.template),
|
|
23056
|
+
dataClass: hit.dataClass,
|
|
23057
|
+
now
|
|
23058
|
+
});
|
|
23059
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
23060
|
+
endpointIds.set(endpointKey, endpointId);
|
|
23061
|
+
}
|
|
23062
|
+
siteStmt.run({
|
|
23063
|
+
id: randomUUID7(),
|
|
23064
|
+
endpointId,
|
|
23065
|
+
project: input.project,
|
|
23066
|
+
projectKey: input.projectKey,
|
|
23067
|
+
file: hit.site.file,
|
|
23068
|
+
line: hit.site.line,
|
|
23069
|
+
snippet: hit.site.snippet,
|
|
23070
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
23071
|
+
vendored: boolToInt(hit.site.vendored),
|
|
23072
|
+
projectId,
|
|
23073
|
+
now
|
|
23074
|
+
});
|
|
22298
23075
|
}
|
|
23076
|
+
}
|
|
23077
|
+
/**
|
|
23078
|
+
* The source-project id this project's stored call sites already carry, if
|
|
23079
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
23080
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
23081
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
23082
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
23083
|
+
* representative.
|
|
23084
|
+
*/
|
|
23085
|
+
knownProjectId(projectKey) {
|
|
23086
|
+
return getRow(
|
|
23087
|
+
this.db.prepare(
|
|
23088
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
23089
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
23090
|
+
),
|
|
23091
|
+
[projectKey]
|
|
23092
|
+
)?.projectId ?? null;
|
|
23093
|
+
}
|
|
23094
|
+
/**
|
|
23095
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
23096
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
23097
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
23098
|
+
*/
|
|
23099
|
+
confirmLastSeen(projectKey, now) {
|
|
22299
23100
|
this.db.prepare(
|
|
22300
|
-
`
|
|
22301
|
-
|
|
22302
|
-
|
|
22303
|
-
|
|
22304
|
-
|
|
22305
|
-
|
|
22306
|
-
|
|
23101
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
23102
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
23103
|
+
).run({ now, key: projectKey });
|
|
23104
|
+
this.db.prepare(
|
|
23105
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
23106
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
23107
|
+
FROM share_endpoint e
|
|
23108
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23109
|
+
WHERE c.project_key = :key)`
|
|
23110
|
+
).run({ now, key: projectKey });
|
|
23111
|
+
}
|
|
23112
|
+
/**
|
|
23113
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
23114
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
23115
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
23116
|
+
*
|
|
23117
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
23118
|
+
* before the host column existed. Those match a destination by id alone;
|
|
23119
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
23120
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
23121
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
23122
|
+
* re-attaches a user's decision when the destination comes back.
|
|
23123
|
+
*/
|
|
23124
|
+
pruneOrphans() {
|
|
23125
|
+
this.db.exec(
|
|
23126
|
+
`DELETE FROM share_endpoint
|
|
23127
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
23128
|
+
);
|
|
23129
|
+
this.db.exec(
|
|
23130
|
+
`DELETE FROM egress_decision_override
|
|
23131
|
+
WHERE host IS NULL
|
|
23132
|
+
AND destination_id IN (
|
|
23133
|
+
SELECT d.id FROM share_destination d
|
|
23134
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
23135
|
+
);
|
|
23136
|
+
this.db.exec(
|
|
23137
|
+
`DELETE FROM share_destination
|
|
23138
|
+
WHERE NOT EXISTS (
|
|
23139
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
23140
|
+
);
|
|
23141
|
+
}
|
|
23142
|
+
/**
|
|
23143
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
23144
|
+
* projects and carry no project column, so both are counted through the call
|
|
23145
|
+
* sites that reference them.
|
|
23146
|
+
*/
|
|
23147
|
+
projectTotals(projectKey) {
|
|
23148
|
+
return {
|
|
23149
|
+
destinations: countScalar(
|
|
23150
|
+
this.db,
|
|
23151
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
23152
|
+
FROM share_endpoint e
|
|
23153
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
23154
|
+
WHERE c.project_key = ?`,
|
|
23155
|
+
[projectKey]
|
|
23156
|
+
),
|
|
23157
|
+
endpoints: countScalar(
|
|
23158
|
+
this.db,
|
|
23159
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
23160
|
+
[projectKey]
|
|
23161
|
+
),
|
|
23162
|
+
callSites: countScalar(
|
|
23163
|
+
this.db,
|
|
23164
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
23165
|
+
[projectKey]
|
|
23166
|
+
)
|
|
23167
|
+
};
|
|
22307
23168
|
}
|
|
22308
23169
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22309
23170
|
mapDestRow(r) {
|
|
@@ -22323,7 +23184,8 @@ var SqliteSharesRepository = class {
|
|
|
22323
23184
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22324
23185
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22325
23186
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22326
|
-
d.created_at AS createdAt,
|
|
23187
|
+
d.created_at AS createdAt,
|
|
23188
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22327
23189
|
const conditions = [];
|
|
22328
23190
|
const params = [];
|
|
22329
23191
|
if (kinds && kinds.length > 0) {
|
|
@@ -22334,7 +23196,8 @@ var SqliteSharesRepository = class {
|
|
|
22334
23196
|
conditions.push(
|
|
22335
23197
|
`(d.trust IN ('unverified', 'ip')
|
|
22336
23198
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22337
|
-
WHERE re.destination_id = d.id
|
|
23199
|
+
WHERE re.destination_id = d.id
|
|
23200
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22338
23201
|
);
|
|
22339
23202
|
}
|
|
22340
23203
|
let sql;
|
|
@@ -22347,7 +23210,7 @@ var SqliteSharesRepository = class {
|
|
|
22347
23210
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22348
23211
|
sql = `SELECT DISTINCT ${cols}
|
|
22349
23212
|
FROM share_destination d
|
|
22350
|
-
|
|
23213
|
+
${OVERRIDE_JOIN}
|
|
22351
23214
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22352
23215
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22353
23216
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22355,7 +23218,7 @@ var SqliteSharesRepository = class {
|
|
|
22355
23218
|
} else {
|
|
22356
23219
|
sql = `SELECT ${cols}
|
|
22357
23220
|
FROM share_destination d
|
|
22358
|
-
|
|
23221
|
+
${OVERRIDE_JOIN}
|
|
22359
23222
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22360
23223
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22361
23224
|
}
|
|
@@ -22370,9 +23233,9 @@ var SqliteSharesRepository = class {
|
|
|
22370
23233
|
this.db.prepare(
|
|
22371
23234
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22372
23235
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22373
|
-
|
|
23236
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22374
23237
|
FROM share_destination d
|
|
22375
|
-
|
|
23238
|
+
${OVERRIDE_JOIN}
|
|
22376
23239
|
WHERE d.id = ?`
|
|
22377
23240
|
),
|
|
22378
23241
|
[destinationId]
|
|
@@ -22574,9 +23437,10 @@ function openWithPragmas(file2) {
|
|
|
22574
23437
|
}
|
|
22575
23438
|
function backupLegacyStore(file2) {
|
|
22576
23439
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22577
|
-
|
|
22578
|
-
|
|
22579
|
-
|
|
23440
|
+
renameSync2(file2, backup);
|
|
23441
|
+
tightenFile(backup);
|
|
23442
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23443
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22580
23444
|
}
|
|
22581
23445
|
return backup;
|
|
22582
23446
|
}
|
|
@@ -22592,7 +23456,7 @@ function openLocalDatabase(dir) {
|
|
|
22592
23456
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
22593
23457
|
);
|
|
22594
23458
|
}
|
|
22595
|
-
applyMigrations(db);
|
|
23459
|
+
applyMigrations(db, file2);
|
|
22596
23460
|
tightenPerms(file2);
|
|
22597
23461
|
const events = new SqliteEventsRepository(db);
|
|
22598
23462
|
const findings = new SqliteFindingsRepository(db);
|
|
@@ -22601,6 +23465,7 @@ function openLocalDatabase(dir) {
|
|
|
22601
23465
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22602
23466
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22603
23467
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
23468
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22604
23469
|
const security = new SqliteSecurityRepository(db);
|
|
22605
23470
|
const detections = new SqliteDetectionsRepository(db);
|
|
22606
23471
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22618,9 +23483,56 @@ function openLocalDatabase(dir) {
|
|
|
22618
23483
|
policies.seedDefaults();
|
|
22619
23484
|
function recordCapture(event, detected) {
|
|
22620
23485
|
failOpenTransaction(db, () => {
|
|
22621
|
-
events.insertEvent(event);
|
|
22622
23486
|
const sessionId = event.metadata?.sessionId;
|
|
22623
|
-
|
|
23487
|
+
if (sessionId) {
|
|
23488
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23489
|
+
}
|
|
23490
|
+
const auditEventId = captureId(
|
|
23491
|
+
sessionId ?? null,
|
|
23492
|
+
event.contentHash,
|
|
23493
|
+
event.metadata?.filePath ?? null
|
|
23494
|
+
);
|
|
23495
|
+
auditEvents.insertAuditEvent({
|
|
23496
|
+
id: auditEventId,
|
|
23497
|
+
eventType: event.kind,
|
|
23498
|
+
startedAt: event.occurredAt,
|
|
23499
|
+
parentId: sessionId,
|
|
23500
|
+
rootSessionId: sessionId,
|
|
23501
|
+
content: event.content,
|
|
23502
|
+
contentHash: event.contentHash,
|
|
23503
|
+
attributes: toCaptureAttributes(event)
|
|
23504
|
+
});
|
|
23505
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23506
|
+
for (const finding of detected) {
|
|
23507
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23508
|
+
continue;
|
|
23509
|
+
}
|
|
23510
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23511
|
+
auditEventId,
|
|
23512
|
+
finding.ruleId,
|
|
23513
|
+
finding.maskedMatch,
|
|
23514
|
+
finding.span.start,
|
|
23515
|
+
finding.span.end
|
|
23516
|
+
)) {
|
|
23517
|
+
continue;
|
|
23518
|
+
}
|
|
23519
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23520
|
+
let definitionId = definitionIds.get(key);
|
|
23521
|
+
if (!definitionId) {
|
|
23522
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23523
|
+
definitionIds.set(key, definitionId);
|
|
23524
|
+
}
|
|
23525
|
+
inspectionFindings.insertFinding({
|
|
23526
|
+
id: finding.id,
|
|
23527
|
+
auditEventId,
|
|
23528
|
+
inspectionDefinitionId: definitionId,
|
|
23529
|
+
span: finding.span,
|
|
23530
|
+
maskedMatch: finding.maskedMatch,
|
|
23531
|
+
actionTaken: finding.actionTaken,
|
|
23532
|
+
confidence: finding.confidence,
|
|
23533
|
+
findingKey: finding.findingKey ?? void 0
|
|
23534
|
+
});
|
|
23535
|
+
}
|
|
22624
23536
|
});
|
|
22625
23537
|
}
|
|
22626
23538
|
function ensureInventory(ctx) {
|
|
@@ -22738,6 +23650,7 @@ function openLocalDatabase(dir) {
|
|
|
22738
23650
|
scanLedger,
|
|
22739
23651
|
exceptions,
|
|
22740
23652
|
resolutions,
|
|
23653
|
+
ruleProbeCache,
|
|
22741
23654
|
security,
|
|
22742
23655
|
detections,
|
|
22743
23656
|
shares,
|
|
@@ -22767,9 +23680,12 @@ function openLocalDatabase(dir) {
|
|
|
22767
23680
|
};
|
|
22768
23681
|
}
|
|
22769
23682
|
|
|
23683
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23684
|
+
import { createHash as createHash3 } from "crypto";
|
|
23685
|
+
|
|
22770
23686
|
// ../../packages/persistence/src/fingerprint.ts
|
|
22771
23687
|
import { createHmac, randomBytes } from "crypto";
|
|
22772
|
-
import {
|
|
23688
|
+
import { readFileSync } from "fs";
|
|
22773
23689
|
import { join as join2 } from "path";
|
|
22774
23690
|
var KEY_FILENAME = "exception.key";
|
|
22775
23691
|
var KEY_MATERIAL_BYTES = 32;
|
|
@@ -22806,8 +23722,8 @@ function readFingerprintKey(dataDir2) {
|
|
|
22806
23722
|
}
|
|
22807
23723
|
|
|
22808
23724
|
// ../../packages/persistence/src/local-layout.ts
|
|
22809
|
-
import {
|
|
22810
|
-
import {
|
|
23725
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23726
|
+
import { mkdir } from "fs/promises";
|
|
22811
23727
|
import { homedir } from "os";
|
|
22812
23728
|
import { join as join3 } from "path";
|
|
22813
23729
|
function defaultDataDir() {
|
|
@@ -22822,6 +23738,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
22822
23738
|
function dbPath(base = defaultDataDir()) {
|
|
22823
23739
|
return join3(dataDir(base), "aka.db");
|
|
22824
23740
|
}
|
|
23741
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23742
|
+
ensureDataDirSync(dir);
|
|
23743
|
+
}
|
|
22825
23744
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
22826
23745
|
const moves = [
|
|
22827
23746
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -22829,19 +23748,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
22829
23748
|
];
|
|
22830
23749
|
for (const { name, dest } of moves) {
|
|
22831
23750
|
try {
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22836
|
-
}
|
|
22837
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23751
|
+
ensureDataDirSync(dest);
|
|
23752
|
+
const moved = join3(dest, name);
|
|
23753
|
+
renameSync3(join3(base, name), moved);
|
|
23754
|
+
tightenFile(moved);
|
|
22838
23755
|
} catch {
|
|
22839
23756
|
}
|
|
22840
23757
|
}
|
|
22841
23758
|
}
|
|
22842
23759
|
|
|
22843
23760
|
// ../../packages/persistence/src/settings.ts
|
|
22844
|
-
import { readFileSync as readFileSync2
|
|
23761
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
22845
23762
|
import { join as join4 } from "path";
|
|
22846
23763
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
22847
23764
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -22863,7 +23780,7 @@ function readJson(file2) {
|
|
|
22863
23780
|
}
|
|
22864
23781
|
|
|
22865
23782
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22866
|
-
import { existsSync as existsSync2, writeFileSync as
|
|
23783
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22867
23784
|
import { join as join5 } from "path";
|
|
22868
23785
|
var MARKER = "warn-era-capped";
|
|
22869
23786
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
@@ -22871,11 +23788,15 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
|
22871
23788
|
const marker = join5(dataDir2, MARKER);
|
|
22872
23789
|
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22873
23790
|
const capped = db.policies.capCategoryActions();
|
|
22874
|
-
|
|
23791
|
+
writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
|
|
22875
23792
|
`, { mode: DATA_FILE_MODE });
|
|
22876
23793
|
return { capped };
|
|
22877
23794
|
}
|
|
22878
23795
|
|
|
23796
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23797
|
+
import { existsSync as existsSync3 } from "fs";
|
|
23798
|
+
import { join as join6 } from "path";
|
|
23799
|
+
|
|
22879
23800
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
22880
23801
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
22881
23802
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -22926,6 +23847,12 @@ function resolveProvider() {
|
|
|
22926
23847
|
|
|
22927
23848
|
// ../../packages/plugin-sdk/src/config.ts
|
|
22928
23849
|
function loadConfig(base = defaultDataDir()) {
|
|
23850
|
+
try {
|
|
23851
|
+
ensureLayoutDirSync(base);
|
|
23852
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23853
|
+
if (existsSync3(settingsFile)) tightenFile(settingsFile);
|
|
23854
|
+
} catch {
|
|
23855
|
+
}
|
|
22929
23856
|
migrateLegacyLayout(base);
|
|
22930
23857
|
const settings = readWorkspaceSettings(base);
|
|
22931
23858
|
return {
|
|
@@ -22948,15 +23875,583 @@ function resolveProviderSafe() {
|
|
|
22948
23875
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22949
23876
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22950
23877
|
import { homedir as homedir2 } from "os";
|
|
22951
|
-
import { basename as basename2, join as
|
|
23878
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23879
|
+
|
|
23880
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23881
|
+
var EXTRACTOR_VERSION = "1";
|
|
23882
|
+
var PROVIDER_REGISTRY = [
|
|
23883
|
+
{
|
|
23884
|
+
id: "stripe",
|
|
23885
|
+
name: "Stripe",
|
|
23886
|
+
category: "Payments",
|
|
23887
|
+
hostSuffixes: ["stripe.com"],
|
|
23888
|
+
apiBase: "https://api.stripe.com",
|
|
23889
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23890
|
+
sdks: {
|
|
23891
|
+
npm: ["stripe"],
|
|
23892
|
+
pypi: ["stripe"],
|
|
23893
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23894
|
+
maven: ["com.stripe"],
|
|
23895
|
+
rubygems: ["stripe"],
|
|
23896
|
+
composer: ["stripe/stripe-php"],
|
|
23897
|
+
nuget: ["Stripe.net"]
|
|
23898
|
+
}
|
|
23899
|
+
},
|
|
23900
|
+
{
|
|
23901
|
+
id: "datadog",
|
|
23902
|
+
name: "Datadog",
|
|
23903
|
+
category: "Observability",
|
|
23904
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23905
|
+
apiBase: "https://api.datadoghq.com",
|
|
23906
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23907
|
+
sdks: {
|
|
23908
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23909
|
+
pypi: ["datadog", "ddtrace"],
|
|
23910
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23911
|
+
maven: ["com.datadoghq"],
|
|
23912
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23913
|
+
nuget: ["Datadog.Trace"]
|
|
23914
|
+
}
|
|
23915
|
+
},
|
|
23916
|
+
{
|
|
23917
|
+
id: "newrelic",
|
|
23918
|
+
name: "New Relic",
|
|
23919
|
+
category: "Observability",
|
|
23920
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23921
|
+
apiBase: "https://api.newrelic.com",
|
|
23922
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23923
|
+
sdks: {
|
|
23924
|
+
npm: ["newrelic"],
|
|
23925
|
+
pypi: ["newrelic"],
|
|
23926
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23927
|
+
maven: ["com.newrelic.agent.java"],
|
|
23928
|
+
rubygems: ["newrelic_rpm"],
|
|
23929
|
+
nuget: ["NewRelic.Agent"]
|
|
23930
|
+
}
|
|
23931
|
+
},
|
|
23932
|
+
{
|
|
23933
|
+
id: "sentry",
|
|
23934
|
+
name: "Sentry",
|
|
23935
|
+
category: "Error tracking",
|
|
23936
|
+
hostSuffixes: ["sentry.io"],
|
|
23937
|
+
apiBase: "https://sentry.io",
|
|
23938
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23939
|
+
sdks: {
|
|
23940
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23941
|
+
pypi: ["sentry-sdk"],
|
|
23942
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23943
|
+
maven: ["io.sentry"],
|
|
23944
|
+
rubygems: ["sentry-ruby"],
|
|
23945
|
+
cargo: ["sentry"],
|
|
23946
|
+
composer: ["sentry/sentry"],
|
|
23947
|
+
nuget: ["Sentry"]
|
|
23948
|
+
}
|
|
23949
|
+
},
|
|
23950
|
+
{
|
|
23951
|
+
id: "openai",
|
|
23952
|
+
name: "OpenAI",
|
|
23953
|
+
category: "LLM provider",
|
|
23954
|
+
hostSuffixes: ["openai.com"],
|
|
23955
|
+
apiBase: "https://api.openai.com",
|
|
23956
|
+
defaultDataClasses: ["pii", "source"],
|
|
23957
|
+
sdks: {
|
|
23958
|
+
npm: ["openai"],
|
|
23959
|
+
pypi: ["openai"],
|
|
23960
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23961
|
+
maven: ["com.openai"],
|
|
23962
|
+
rubygems: ["ruby-openai"],
|
|
23963
|
+
cargo: ["async-openai"],
|
|
23964
|
+
composer: ["openai-php/client"],
|
|
23965
|
+
nuget: ["OpenAI"]
|
|
23966
|
+
}
|
|
23967
|
+
},
|
|
23968
|
+
{
|
|
23969
|
+
id: "anthropic",
|
|
23970
|
+
name: "Anthropic",
|
|
23971
|
+
category: "LLM provider",
|
|
23972
|
+
hostSuffixes: ["anthropic.com"],
|
|
23973
|
+
apiBase: "https://api.anthropic.com",
|
|
23974
|
+
defaultDataClasses: ["pii", "source"],
|
|
23975
|
+
sdks: {
|
|
23976
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23977
|
+
pypi: ["anthropic"],
|
|
23978
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23979
|
+
nuget: ["Anthropic.SDK"]
|
|
23980
|
+
}
|
|
23981
|
+
},
|
|
23982
|
+
{
|
|
23983
|
+
id: "aws",
|
|
23984
|
+
name: "Amazon Web Services",
|
|
23985
|
+
category: "Cloud platform",
|
|
23986
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23987
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23988
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23989
|
+
sdks: {
|
|
23990
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23991
|
+
pypi: ["boto3"],
|
|
23992
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23993
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23994
|
+
rubygems: ["aws-sdk-s3"],
|
|
23995
|
+
cargo: ["aws-sdk-s3"],
|
|
23996
|
+
nuget: ["AWSSDK.S3"]
|
|
23997
|
+
}
|
|
23998
|
+
},
|
|
23999
|
+
{
|
|
24000
|
+
id: "gcp",
|
|
24001
|
+
name: "Google Cloud",
|
|
24002
|
+
category: "Cloud platform",
|
|
24003
|
+
hostSuffixes: ["googleapis.com"],
|
|
24004
|
+
apiBase: "https://storage.googleapis.com",
|
|
24005
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24006
|
+
sdks: {
|
|
24007
|
+
npm: ["@google-cloud/storage"],
|
|
24008
|
+
pypi: ["google-cloud-storage"],
|
|
24009
|
+
go: ["cloud.google.com/go"],
|
|
24010
|
+
maven: ["com.google.cloud"],
|
|
24011
|
+
rubygems: ["google-cloud-storage"],
|
|
24012
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
24013
|
+
}
|
|
24014
|
+
},
|
|
24015
|
+
{
|
|
24016
|
+
id: "azure",
|
|
24017
|
+
name: "Microsoft Azure",
|
|
24018
|
+
category: "Cloud platform",
|
|
24019
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
24020
|
+
apiBase: "https://management.azure.com",
|
|
24021
|
+
defaultDataClasses: ["customer", "logs"],
|
|
24022
|
+
sdks: {
|
|
24023
|
+
npm: ["@azure/storage-blob"],
|
|
24024
|
+
pypi: ["azure-storage-blob"],
|
|
24025
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
24026
|
+
maven: ["com.azure"],
|
|
24027
|
+
rubygems: ["azure-storage-blob"],
|
|
24028
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
24029
|
+
}
|
|
24030
|
+
},
|
|
24031
|
+
{
|
|
24032
|
+
id: "slack",
|
|
24033
|
+
name: "Slack",
|
|
24034
|
+
category: "Notifications",
|
|
24035
|
+
hostSuffixes: ["slack.com"],
|
|
24036
|
+
apiBase: "https://slack.com/api",
|
|
24037
|
+
defaultDataClasses: ["logs"],
|
|
24038
|
+
sdks: {
|
|
24039
|
+
npm: ["@slack/web-api"],
|
|
24040
|
+
pypi: ["slack-sdk"],
|
|
24041
|
+
go: ["github.com/slack-go/slack"],
|
|
24042
|
+
maven: ["com.slack.api"],
|
|
24043
|
+
rubygems: ["slack-ruby-client"],
|
|
24044
|
+
composer: ["slack-php/slack-api"],
|
|
24045
|
+
nuget: ["SlackNet"]
|
|
24046
|
+
}
|
|
24047
|
+
},
|
|
24048
|
+
{
|
|
24049
|
+
id: "segment",
|
|
24050
|
+
name: "Segment",
|
|
24051
|
+
category: "Analytics",
|
|
24052
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
24053
|
+
apiBase: "https://api.segment.io",
|
|
24054
|
+
defaultDataClasses: ["customer"],
|
|
24055
|
+
sdks: {
|
|
24056
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
24057
|
+
pypi: ["segment-analytics-python"],
|
|
24058
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
24059
|
+
maven: ["com.segment.analytics.java"],
|
|
24060
|
+
rubygems: ["analytics-ruby"],
|
|
24061
|
+
nuget: ["Analytics"]
|
|
24062
|
+
}
|
|
24063
|
+
},
|
|
24064
|
+
{
|
|
24065
|
+
id: "twilio",
|
|
24066
|
+
name: "Twilio",
|
|
24067
|
+
category: "Communications",
|
|
24068
|
+
hostSuffixes: ["twilio.com"],
|
|
24069
|
+
apiBase: "https://api.twilio.com",
|
|
24070
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24071
|
+
sdks: {
|
|
24072
|
+
npm: ["twilio"],
|
|
24073
|
+
pypi: ["twilio"],
|
|
24074
|
+
go: ["github.com/twilio/twilio-go"],
|
|
24075
|
+
maven: ["com.twilio.sdk"],
|
|
24076
|
+
rubygems: ["twilio-ruby"],
|
|
24077
|
+
composer: ["twilio/sdk"],
|
|
24078
|
+
nuget: ["Twilio"]
|
|
24079
|
+
}
|
|
24080
|
+
},
|
|
24081
|
+
{
|
|
24082
|
+
id: "sendgrid",
|
|
24083
|
+
name: "SendGrid",
|
|
24084
|
+
category: "Email",
|
|
24085
|
+
hostSuffixes: ["sendgrid.com"],
|
|
24086
|
+
apiBase: "https://api.sendgrid.com",
|
|
24087
|
+
defaultDataClasses: ["pii"],
|
|
24088
|
+
sdks: {
|
|
24089
|
+
npm: ["@sendgrid/mail"],
|
|
24090
|
+
pypi: ["sendgrid"],
|
|
24091
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
24092
|
+
maven: ["com.sendgrid"],
|
|
24093
|
+
rubygems: ["sendgrid-ruby"],
|
|
24094
|
+
composer: ["sendgrid/sendgrid"],
|
|
24095
|
+
nuget: ["SendGrid"]
|
|
24096
|
+
}
|
|
24097
|
+
},
|
|
24098
|
+
{
|
|
24099
|
+
id: "mailgun",
|
|
24100
|
+
name: "Mailgun",
|
|
24101
|
+
category: "Email",
|
|
24102
|
+
hostSuffixes: ["mailgun.net"],
|
|
24103
|
+
apiBase: "https://api.mailgun.net",
|
|
24104
|
+
defaultDataClasses: ["pii"],
|
|
24105
|
+
sdks: {
|
|
24106
|
+
npm: ["mailgun.js"],
|
|
24107
|
+
pypi: ["mailgun"],
|
|
24108
|
+
rubygems: ["mailgun-ruby"],
|
|
24109
|
+
composer: ["mailgun/mailgun-php"],
|
|
24110
|
+
nuget: ["Mailgun"]
|
|
24111
|
+
}
|
|
24112
|
+
},
|
|
24113
|
+
{
|
|
24114
|
+
id: "mixpanel",
|
|
24115
|
+
name: "Mixpanel",
|
|
24116
|
+
category: "Analytics",
|
|
24117
|
+
hostSuffixes: ["mixpanel.com"],
|
|
24118
|
+
apiBase: "https://api.mixpanel.com",
|
|
24119
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24120
|
+
sdks: {
|
|
24121
|
+
npm: ["mixpanel"],
|
|
24122
|
+
pypi: ["mixpanel"],
|
|
24123
|
+
rubygems: ["mixpanel-ruby"],
|
|
24124
|
+
nuget: ["Mixpanel"]
|
|
24125
|
+
}
|
|
24126
|
+
},
|
|
24127
|
+
{
|
|
24128
|
+
id: "amplitude",
|
|
24129
|
+
name: "Amplitude",
|
|
24130
|
+
category: "Analytics",
|
|
24131
|
+
hostSuffixes: ["amplitude.com"],
|
|
24132
|
+
apiBase: "https://api2.amplitude.com",
|
|
24133
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24134
|
+
sdks: {
|
|
24135
|
+
npm: ["@amplitude/analytics-node"],
|
|
24136
|
+
pypi: ["amplitude-analytics"],
|
|
24137
|
+
nuget: ["Amplitude"]
|
|
24138
|
+
}
|
|
24139
|
+
},
|
|
24140
|
+
{
|
|
24141
|
+
id: "posthog",
|
|
24142
|
+
name: "PostHog",
|
|
24143
|
+
category: "Analytics",
|
|
24144
|
+
hostSuffixes: ["posthog.com"],
|
|
24145
|
+
apiBase: "https://us.i.posthog.com",
|
|
24146
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
24147
|
+
sdks: {
|
|
24148
|
+
npm: ["posthog-node", "posthog-js"],
|
|
24149
|
+
pypi: ["posthog"],
|
|
24150
|
+
go: ["github.com/posthog/posthog-go"],
|
|
24151
|
+
rubygems: ["posthog-ruby"],
|
|
24152
|
+
composer: ["posthog/posthog-php"],
|
|
24153
|
+
nuget: ["PostHog"]
|
|
24154
|
+
}
|
|
24155
|
+
},
|
|
24156
|
+
{
|
|
24157
|
+
id: "honeycomb",
|
|
24158
|
+
name: "Honeycomb",
|
|
24159
|
+
category: "Observability",
|
|
24160
|
+
hostSuffixes: ["honeycomb.io"],
|
|
24161
|
+
apiBase: "https://api.honeycomb.io",
|
|
24162
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
24163
|
+
sdks: {
|
|
24164
|
+
npm: ["libhoney"],
|
|
24165
|
+
pypi: ["libhoney"],
|
|
24166
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
24167
|
+
rubygems: ["libhoney"]
|
|
24168
|
+
}
|
|
24169
|
+
},
|
|
24170
|
+
{
|
|
24171
|
+
id: "grafana",
|
|
24172
|
+
name: "Grafana Cloud",
|
|
24173
|
+
category: "Observability",
|
|
24174
|
+
hostSuffixes: ["grafana.net"],
|
|
24175
|
+
apiBase: "https://grafana.net",
|
|
24176
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
24177
|
+
sdks: {
|
|
24178
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
24179
|
+
}
|
|
24180
|
+
},
|
|
24181
|
+
{
|
|
24182
|
+
id: "splunk",
|
|
24183
|
+
name: "Splunk",
|
|
24184
|
+
category: "Observability",
|
|
24185
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
24186
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
24187
|
+
defaultDataClasses: ["logs"],
|
|
24188
|
+
sdks: {
|
|
24189
|
+
npm: ["splunk-logging"],
|
|
24190
|
+
pypi: ["splunk-sdk"],
|
|
24191
|
+
maven: ["com.splunk"],
|
|
24192
|
+
nuget: ["Splunk.Logging.Common"]
|
|
24193
|
+
}
|
|
24194
|
+
},
|
|
24195
|
+
{
|
|
24196
|
+
id: "pagerduty",
|
|
24197
|
+
name: "PagerDuty",
|
|
24198
|
+
category: "Incident response",
|
|
24199
|
+
hostSuffixes: ["pagerduty.com"],
|
|
24200
|
+
apiBase: "https://api.pagerduty.com",
|
|
24201
|
+
defaultDataClasses: ["logs"],
|
|
24202
|
+
sdks: {
|
|
24203
|
+
npm: ["@pagerduty/pdjs"],
|
|
24204
|
+
pypi: ["pdpyras"],
|
|
24205
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
24206
|
+
rubygems: ["pagerduty"]
|
|
24207
|
+
}
|
|
24208
|
+
},
|
|
24209
|
+
{
|
|
24210
|
+
id: "github",
|
|
24211
|
+
name: "GitHub",
|
|
24212
|
+
category: "Developer platform",
|
|
24213
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
24214
|
+
apiBase: "https://api.github.com",
|
|
24215
|
+
defaultDataClasses: ["source"],
|
|
24216
|
+
sdks: {
|
|
24217
|
+
npm: ["@octokit/rest", "octokit"],
|
|
24218
|
+
pypi: ["pygithub"],
|
|
24219
|
+
go: ["github.com/google/go-github"],
|
|
24220
|
+
maven: ["org.kohsuke.github-api"],
|
|
24221
|
+
rubygems: ["octokit"],
|
|
24222
|
+
cargo: ["octocrab"],
|
|
24223
|
+
composer: ["knplabs/github-api"],
|
|
24224
|
+
nuget: ["Octokit"]
|
|
24225
|
+
}
|
|
24226
|
+
},
|
|
24227
|
+
{
|
|
24228
|
+
id: "gitlab",
|
|
24229
|
+
name: "GitLab",
|
|
24230
|
+
category: "Developer platform",
|
|
24231
|
+
hostSuffixes: ["gitlab.com"],
|
|
24232
|
+
apiBase: "https://gitlab.com/api",
|
|
24233
|
+
defaultDataClasses: ["source"],
|
|
24234
|
+
sdks: {
|
|
24235
|
+
npm: ["@gitbeaker/rest"],
|
|
24236
|
+
pypi: ["python-gitlab"],
|
|
24237
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
24238
|
+
rubygems: ["gitlab"],
|
|
24239
|
+
nuget: ["GitLabApiClient"]
|
|
24240
|
+
}
|
|
24241
|
+
},
|
|
24242
|
+
{
|
|
24243
|
+
id: "auth0",
|
|
24244
|
+
name: "Auth0",
|
|
24245
|
+
category: "Identity",
|
|
24246
|
+
hostSuffixes: ["auth0.com"],
|
|
24247
|
+
apiBase: "https://login.auth0.com",
|
|
24248
|
+
defaultDataClasses: ["pii"],
|
|
24249
|
+
sdks: {
|
|
24250
|
+
npm: ["auth0"],
|
|
24251
|
+
pypi: ["auth0-python"],
|
|
24252
|
+
go: ["github.com/auth0/go-auth0"],
|
|
24253
|
+
maven: ["com.auth0"],
|
|
24254
|
+
rubygems: ["auth0"],
|
|
24255
|
+
composer: ["auth0/auth0-php"],
|
|
24256
|
+
nuget: ["Auth0.ManagementApi"]
|
|
24257
|
+
}
|
|
24258
|
+
},
|
|
24259
|
+
{
|
|
24260
|
+
id: "okta",
|
|
24261
|
+
name: "Okta",
|
|
24262
|
+
category: "Identity",
|
|
24263
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
24264
|
+
apiBase: "https://login.okta.com",
|
|
24265
|
+
defaultDataClasses: ["pii"],
|
|
24266
|
+
sdks: {
|
|
24267
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
24268
|
+
pypi: ["okta"],
|
|
24269
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
24270
|
+
maven: ["com.okta.sdk"],
|
|
24271
|
+
nuget: ["Okta.Sdk"]
|
|
24272
|
+
}
|
|
24273
|
+
},
|
|
24274
|
+
{
|
|
24275
|
+
id: "clerk",
|
|
24276
|
+
name: "Clerk",
|
|
24277
|
+
category: "Identity",
|
|
24278
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
24279
|
+
apiBase: "https://api.clerk.com",
|
|
24280
|
+
defaultDataClasses: ["pii"],
|
|
24281
|
+
sdks: {
|
|
24282
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
24283
|
+
pypi: ["clerk-backend-api"],
|
|
24284
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
24285
|
+
}
|
|
24286
|
+
},
|
|
24287
|
+
{
|
|
24288
|
+
id: "supabase",
|
|
24289
|
+
name: "Supabase",
|
|
24290
|
+
category: "Backend platform",
|
|
24291
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
24292
|
+
apiBase: "https://api.supabase.com",
|
|
24293
|
+
defaultDataClasses: ["pii", "customer"],
|
|
24294
|
+
sdks: {
|
|
24295
|
+
npm: ["@supabase/supabase-js"],
|
|
24296
|
+
pypi: ["supabase"],
|
|
24297
|
+
cargo: ["postgrest"]
|
|
24298
|
+
}
|
|
24299
|
+
},
|
|
24300
|
+
{
|
|
24301
|
+
id: "firebase",
|
|
24302
|
+
name: "Firebase",
|
|
24303
|
+
category: "Backend platform",
|
|
24304
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
24305
|
+
apiBase: "https://firebaseio.com",
|
|
24306
|
+
defaultDataClasses: ["customer"],
|
|
24307
|
+
sdks: {
|
|
24308
|
+
npm: ["firebase", "firebase-admin"],
|
|
24309
|
+
pypi: ["firebase-admin"],
|
|
24310
|
+
go: ["firebase.google.com/go"],
|
|
24311
|
+
maven: ["com.google.firebase"]
|
|
24312
|
+
}
|
|
24313
|
+
},
|
|
24314
|
+
{
|
|
24315
|
+
id: "mongodb-atlas",
|
|
24316
|
+
name: "MongoDB Atlas",
|
|
24317
|
+
category: "Database SaaS",
|
|
24318
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
24319
|
+
apiBase: "https://cloud.mongodb.com",
|
|
24320
|
+
defaultDataClasses: ["customer"],
|
|
24321
|
+
sdks: {
|
|
24322
|
+
npm: ["mongodb"],
|
|
24323
|
+
pypi: ["pymongo"],
|
|
24324
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
24325
|
+
maven: ["org.mongodb"],
|
|
24326
|
+
rubygems: ["mongo"],
|
|
24327
|
+
cargo: ["mongodb"],
|
|
24328
|
+
nuget: ["MongoDB.Driver"]
|
|
24329
|
+
}
|
|
24330
|
+
},
|
|
24331
|
+
{
|
|
24332
|
+
id: "planetscale",
|
|
24333
|
+
name: "PlanetScale",
|
|
24334
|
+
category: "Database SaaS",
|
|
24335
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
24336
|
+
apiBase: "https://api.planetscale.com",
|
|
24337
|
+
defaultDataClasses: ["customer"],
|
|
24338
|
+
sdks: {
|
|
24339
|
+
npm: ["@planetscale/database"],
|
|
24340
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
24341
|
+
}
|
|
24342
|
+
},
|
|
24343
|
+
{
|
|
24344
|
+
id: "algolia",
|
|
24345
|
+
name: "Algolia",
|
|
24346
|
+
category: "Search SaaS",
|
|
24347
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
24348
|
+
apiBase: "https://algolia.net",
|
|
24349
|
+
defaultDataClasses: ["customer"],
|
|
24350
|
+
sdks: {
|
|
24351
|
+
npm: ["algoliasearch"],
|
|
24352
|
+
pypi: ["algoliasearch"],
|
|
24353
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
24354
|
+
maven: ["com.algolia"],
|
|
24355
|
+
rubygems: ["algolia"],
|
|
24356
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
24357
|
+
nuget: ["Algolia.Search"]
|
|
24358
|
+
}
|
|
24359
|
+
},
|
|
24360
|
+
{
|
|
24361
|
+
id: "cloudflare",
|
|
24362
|
+
name: "Cloudflare",
|
|
24363
|
+
category: "CDN / edge",
|
|
24364
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
24365
|
+
apiBase: "https://api.cloudflare.com",
|
|
24366
|
+
defaultDataClasses: ["logs"],
|
|
24367
|
+
sdks: {
|
|
24368
|
+
npm: ["cloudflare"],
|
|
24369
|
+
pypi: ["cloudflare"],
|
|
24370
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
24371
|
+
nuget: ["CloudFlare.Client"]
|
|
24372
|
+
}
|
|
24373
|
+
},
|
|
24374
|
+
{
|
|
24375
|
+
id: "huggingface",
|
|
24376
|
+
name: "Hugging Face",
|
|
24377
|
+
category: "LLM provider",
|
|
24378
|
+
hostSuffixes: ["huggingface.co"],
|
|
24379
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
24380
|
+
defaultDataClasses: ["source"],
|
|
24381
|
+
sdks: {
|
|
24382
|
+
npm: ["@huggingface/inference"],
|
|
24383
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
24384
|
+
rubygems: ["hugging-face"]
|
|
24385
|
+
}
|
|
24386
|
+
},
|
|
24387
|
+
{
|
|
24388
|
+
id: "cohere",
|
|
24389
|
+
name: "Cohere",
|
|
24390
|
+
category: "LLM provider",
|
|
24391
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
24392
|
+
apiBase: "https://api.cohere.com",
|
|
24393
|
+
defaultDataClasses: ["pii", "source"],
|
|
24394
|
+
sdks: {
|
|
24395
|
+
npm: ["cohere-ai"],
|
|
24396
|
+
pypi: ["cohere"],
|
|
24397
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
24398
|
+
}
|
|
24399
|
+
},
|
|
24400
|
+
{
|
|
24401
|
+
id: "mistral",
|
|
24402
|
+
name: "Mistral AI",
|
|
24403
|
+
category: "LLM provider",
|
|
24404
|
+
hostSuffixes: ["mistral.ai"],
|
|
24405
|
+
apiBase: "https://api.mistral.ai",
|
|
24406
|
+
defaultDataClasses: ["pii", "source"],
|
|
24407
|
+
sdks: {
|
|
24408
|
+
npm: ["@mistralai/mistralai"],
|
|
24409
|
+
pypi: ["mistralai"],
|
|
24410
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
24411
|
+
}
|
|
24412
|
+
}
|
|
24413
|
+
];
|
|
24414
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
24415
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
24416
|
+
|
|
24417
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24418
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24419
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24420
|
+
var SECRET_VALUE = new RegExp(
|
|
24421
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24422
|
+
"gi"
|
|
24423
|
+
);
|
|
24424
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24425
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24426
|
+
"gi"
|
|
24427
|
+
);
|
|
24428
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24429
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24430
|
+
{
|
|
24431
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24432
|
+
prefix: "/api/webhooks/"
|
|
24433
|
+
},
|
|
24434
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24435
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24436
|
+
];
|
|
24437
|
+
function escapeRegExp(literal2) {
|
|
24438
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24439
|
+
}
|
|
24440
|
+
var WEBHOOK_URL = new RegExp(
|
|
24441
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24442
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24443
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24444
|
+
"gi"
|
|
24445
|
+
);
|
|
22952
24446
|
|
|
22953
24447
|
// ../../packages/detections/src/escape-regexp.ts
|
|
22954
|
-
function
|
|
24448
|
+
function escapeRegExp2(value) {
|
|
22955
24449
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22956
24450
|
}
|
|
22957
24451
|
|
|
22958
24452
|
// ../../packages/detections/src/matchers/limits.ts
|
|
22959
24453
|
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24454
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
22960
24455
|
|
|
22961
24456
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22962
24457
|
var KeywordMatcher2 = class {
|
|
@@ -22967,7 +24462,7 @@ var KeywordMatcher2 = class {
|
|
|
22967
24462
|
for (const kw of keywords) {
|
|
22968
24463
|
if (kw.length === 0) continue;
|
|
22969
24464
|
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22970
|
-
const re = new RegExp(
|
|
24465
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
22971
24466
|
let m;
|
|
22972
24467
|
while ((m = re.exec(text)) !== null) {
|
|
22973
24468
|
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -22984,9 +24479,13 @@ var RegexMatcher2 = class {
|
|
|
22984
24479
|
if (rule.matcher.type !== "regex") return [];
|
|
22985
24480
|
const { pattern, flags, captureGroup } = rule.matcher;
|
|
22986
24481
|
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24482
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
22987
24483
|
const spans = [];
|
|
22988
24484
|
let m;
|
|
22989
|
-
|
|
24485
|
+
const maxIterations = scanText2.length + 1;
|
|
24486
|
+
let iterations = 0;
|
|
24487
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24488
|
+
if (++iterations > maxIterations) break;
|
|
22990
24489
|
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
22991
24490
|
if (m[0].length === 0) re.lastIndex++;
|
|
22992
24491
|
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
@@ -23061,6 +24560,31 @@ var CONFIG_POSTURE_RULES = [
|
|
|
23061
24560
|
}
|
|
23062
24561
|
];
|
|
23063
24562
|
|
|
24563
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
24564
|
+
var EXPONENTIAL_UNITS = [
|
|
24565
|
+
"a",
|
|
24566
|
+
"0",
|
|
24567
|
+
" ",
|
|
24568
|
+
"x",
|
|
24569
|
+
"ab",
|
|
24570
|
+
"a.",
|
|
24571
|
+
"a-",
|
|
24572
|
+
"a_",
|
|
24573
|
+
"a@",
|
|
24574
|
+
"a/",
|
|
24575
|
+
"a:",
|
|
24576
|
+
"a=",
|
|
24577
|
+
"a;",
|
|
24578
|
+
"aA0",
|
|
24579
|
+
" "
|
|
24580
|
+
];
|
|
24581
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
24582
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
24583
|
+
);
|
|
24584
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
24585
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24586
|
+
);
|
|
24587
|
+
|
|
23064
24588
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23065
24589
|
var auth_jwt_no_verify_default = {
|
|
23066
24590
|
specVersion: 1,
|
|
@@ -25087,26 +26611,27 @@ function bundledDetections() {
|
|
|
25087
26611
|
}
|
|
25088
26612
|
|
|
25089
26613
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25090
|
-
import { existsSync as
|
|
25091
|
-
import { basename, dirname, isAbsolute, join as
|
|
26614
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
26615
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
25092
26616
|
|
|
25093
26617
|
// ../../packages/plugin-sdk/src/events.ts
|
|
25094
|
-
import { createHash as
|
|
25095
|
-
|
|
25096
|
-
// ../../packages/plugin-sdk/src/finding-key.ts
|
|
25097
|
-
import { createHash as createHash4 } from "crypto";
|
|
26618
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
25098
26619
|
|
|
25099
26620
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
25100
26621
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
25101
26622
|
|
|
25102
26623
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
25103
|
-
import { mkdirSync as
|
|
25104
|
-
import { join as
|
|
26624
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26625
|
+
import { join as join9 } from "path";
|
|
26626
|
+
|
|
26627
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
26628
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
26629
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
25105
26630
|
|
|
25106
26631
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25107
26632
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25108
|
-
import { existsSync as
|
|
25109
|
-
import { basename as
|
|
26633
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26634
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
25110
26635
|
|
|
25111
26636
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25112
26637
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -25115,8 +26640,8 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
25115
26640
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25116
26641
|
|
|
25117
26642
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25118
|
-
import { mkdirSync as
|
|
25119
|
-
import { join as
|
|
26643
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26644
|
+
import { join as join11 } from "path";
|
|
25120
26645
|
|
|
25121
26646
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25122
26647
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -25147,7 +26672,8 @@ var StandaloneDataGateway = class {
|
|
|
25147
26672
|
}
|
|
25148
26673
|
// The id is minted inside the repository from the natural key — the plugin can't
|
|
25149
26674
|
// import @akasecurity/persistence to compute it, so the gateway is the boundary that
|
|
25150
|
-
// hands the natural key across.
|
|
26675
|
+
// hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
|
|
26676
|
+
// converge a streaming partial/final split (see insertLlmCall).
|
|
25151
26677
|
recordLlmCall(input) {
|
|
25152
26678
|
this.db.auditEvents.insertLlmCall(input);
|
|
25153
26679
|
return Promise.resolve();
|
|
@@ -25189,7 +26715,9 @@ var StandaloneDataGateway = class {
|
|
|
25189
26715
|
// caller's transaction (Layer 2b). The audit-event id the findings FK into is the
|
|
25190
26716
|
// SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
|
|
25191
26717
|
// idempotently. Definitions/classified-data are idempotent upserts; findings are
|
|
25192
|
-
// content-addressed
|
|
26718
|
+
// content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
|
|
26719
|
+
// so a re-detection under a bumped rule version repoints the definition FK rather
|
|
26720
|
+
// than no-opping.
|
|
25193
26721
|
writeToolCall(input) {
|
|
25194
26722
|
this.db.auditEvents.insertToolCall(input);
|
|
25195
26723
|
if (input.inspections.length === 0) return;
|
|
@@ -25209,7 +26737,7 @@ var StandaloneDataGateway = class {
|
|
|
25209
26737
|
});
|
|
25210
26738
|
const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
|
|
25211
26739
|
this.db.inspectionFindings.insertFinding({
|
|
25212
|
-
id: inspectionFindingId(auditEventId,
|
|
26740
|
+
id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
|
|
25213
26741
|
auditEventId,
|
|
25214
26742
|
inspectionDefinitionId: definitionId,
|
|
25215
26743
|
classifiedDataId: classifiedDataId2,
|
|
@@ -25258,10 +26786,17 @@ var StandaloneDataGateway = class {
|
|
|
25258
26786
|
try {
|
|
25259
26787
|
const snapshot = this.db.installedPacks.installedRuleset();
|
|
25260
26788
|
if (snapshot.installedPacks === 0) return void 0;
|
|
25261
|
-
if (snapshot.enabledPacks === 0)
|
|
26789
|
+
if (snapshot.enabledPacks === 0) {
|
|
26790
|
+
return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
|
|
26791
|
+
}
|
|
25262
26792
|
if (snapshot.invalidRules > 0) return void 0;
|
|
25263
26793
|
if (snapshot.rules.length === 0) return void 0;
|
|
25264
|
-
return {
|
|
26794
|
+
return {
|
|
26795
|
+
rules: snapshot.rules,
|
|
26796
|
+
ruleActions: snapshot.ruleActions,
|
|
26797
|
+
ruleVersions: snapshot.ruleVersions,
|
|
26798
|
+
complete: true
|
|
26799
|
+
};
|
|
25265
26800
|
} catch {
|
|
25266
26801
|
return void 0;
|
|
25267
26802
|
}
|
|
@@ -25289,6 +26824,7 @@ var StandaloneDataGateway = class {
|
|
|
25289
26824
|
policies: [...policies, ...rulePolicies],
|
|
25290
26825
|
rules: installed ? installed.rules : [],
|
|
25291
26826
|
...installed ? { rulesComplete: true } : {},
|
|
26827
|
+
...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
|
|
25292
26828
|
...exceptions !== void 0 ? { exceptions } : {},
|
|
25293
26829
|
customKeywords,
|
|
25294
26830
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -25387,6 +26923,13 @@ var StandaloneDataGateway = class {
|
|
|
25387
26923
|
this.db.scanLedger.upsertEntries(entries);
|
|
25388
26924
|
return Promise.resolve();
|
|
25389
26925
|
}
|
|
26926
|
+
getRuleProbeVerdict(ruleKey) {
|
|
26927
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
26928
|
+
}
|
|
26929
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
|
|
26930
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
|
|
26931
|
+
return Promise.resolve();
|
|
26932
|
+
}
|
|
25390
26933
|
openAtRestKeysForPath(path) {
|
|
25391
26934
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
25392
26935
|
}
|
|
@@ -25397,6 +26940,12 @@ var StandaloneDataGateway = class {
|
|
|
25397
26940
|
this.db.resolutions.insertResolution(input);
|
|
25398
26941
|
return Promise.resolve();
|
|
25399
26942
|
}
|
|
26943
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
26944
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
26945
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
26946
|
+
recordProjectEgress(input) {
|
|
26947
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
26948
|
+
}
|
|
25400
26949
|
close() {
|
|
25401
26950
|
this.db.close();
|
|
25402
26951
|
return Promise.resolve();
|
|
@@ -25507,8 +27056,8 @@ function table(headers, rows, opts = {}) {
|
|
|
25507
27056
|
const widths = headers.map(
|
|
25508
27057
|
(h, i) => Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i] ?? "")))
|
|
25509
27058
|
);
|
|
25510
|
-
const
|
|
25511
|
-
const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(
|
|
27059
|
+
const sep5 = " ".repeat(gap);
|
|
27060
|
+
const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep5);
|
|
25512
27061
|
const headerLine = fmt(headers.map((h) => h.toUpperCase()));
|
|
25513
27062
|
if (opts.rowSep === true) {
|
|
25514
27063
|
const fullWidth = widths.reduce((n, w) => n + w, 0) + gap * Math.max(0, widths.length - 1);
|
|
@@ -25520,7 +27069,7 @@ function table(headers, rows, opts = {}) {
|
|
|
25520
27069
|
});
|
|
25521
27070
|
return [headerLine, rule, ...body].join("\n");
|
|
25522
27071
|
}
|
|
25523
|
-
const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(
|
|
27072
|
+
const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep5);
|
|
25524
27073
|
return [headerLine, ruleLine, ...rows.map(fmt)].join("\n");
|
|
25525
27074
|
}
|
|
25526
27075
|
function fenced(body) {
|
|
@@ -25530,7 +27079,7 @@ function fenced(body) {
|
|
|
25530
27079
|
}
|
|
25531
27080
|
|
|
25532
27081
|
// src/command-registry.ts
|
|
25533
|
-
import { readdirSync as
|
|
27082
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
25534
27083
|
import { fileURLToPath } from "url";
|
|
25535
27084
|
var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
|
|
25536
27085
|
|
|
@@ -25613,14 +27162,14 @@ function renderStatusBar(s, opts = {}) {
|
|
|
25613
27162
|
const unreviewed = `unreviewed ${SHADE.full}${String(u.critical)} ${SHADE.dark}${String(u.high)} ${SHADE.medium}${String(u.medium)} ${SHADE.light}${String(u.low)}`;
|
|
25614
27163
|
return `\u25B8\u25B8 AKA health ${String(s.score)}/100 ${unreviewed} \u2691 ${String(s.openFindings)} open findings`;
|
|
25615
27164
|
}
|
|
25616
|
-
const
|
|
27165
|
+
const sep5 = ` ${paint.dim("\u2502")} `;
|
|
25617
27166
|
const sq = "\u25A0";
|
|
25618
27167
|
const dot = s.score >= 80 ? paint.ok("\u25CF") : s.score >= 50 ? paint.high("\u25CF") : paint.critical("\u25CF");
|
|
25619
27168
|
const score = `${dot} health ${paint.bold(String(s.score))}${paint.dim("/100")}`;
|
|
25620
27169
|
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)}`;
|
|
25621
27170
|
const flag = s.openFindings > 0 ? paint.critical("\u2691") : paint.dim("\u2691");
|
|
25622
27171
|
const open = `${flag} ${String(s.openFindings)} open findings`;
|
|
25623
|
-
return `${paint.brand("\u25B8\u25B8 AKA")}${
|
|
27172
|
+
return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open}`;
|
|
25624
27173
|
}
|
|
25625
27174
|
function findingStatus(summary) {
|
|
25626
27175
|
return {
|