@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.3
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 +101 -19
- package/package.json +3 -3
- package/scripts/apply-suppressions.js +3251 -497
- package/scripts/backfill.js +1081 -493
- package/scripts/filescan.js +1069 -487
- package/scripts/firstrun.js +981 -450
- package/scripts/intro.js +92 -24
- package/scripts/onboard.js +1006 -453
- package/scripts/post-tool-use.js +1062 -480
- package/scripts/pre-tool-use.js +1067 -485
- package/scripts/query.js +981 -450
- package/scripts/reconcile.js +1010 -473
- package/scripts/remediate.js +1072 -490
- package/scripts/session-start.js +1043 -512
- package/scripts/start-light.js +92 -24
- package/scripts/statusline.js +981 -450
- package/scripts/stop.js +133 -38
- package/scripts/triage-rubric.md +4 -3
- package/scripts/user-prompt-submit.js +1070 -488
|
@@ -492,14 +492,14 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// src/apply-suppressions.ts
|
|
495
|
-
import { existsSync as
|
|
495
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
|
|
496
496
|
import { userInfo } from "os";
|
|
497
|
-
import { dirname as dirname5, join as
|
|
497
|
+
import { dirname as dirname5, join as join14 } from "path";
|
|
498
498
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
499
499
|
|
|
500
500
|
// ../../packages/persistence/src/database.ts
|
|
501
501
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
502
|
-
import { existsSync, renameSync, rmSync } from "fs";
|
|
502
|
+
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
503
503
|
import { join, sep } from "path";
|
|
504
504
|
import { DatabaseSync } from "node:sqlite";
|
|
505
505
|
|
|
@@ -552,6 +552,18 @@ var SQLITE_MIGRATIONS = [
|
|
|
552
552
|
{
|
|
553
553
|
tag: "0011_egress_writer",
|
|
554
554
|
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'
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
tag: "0012_handy_the_captain",
|
|
558
|
+
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`);"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
tag: "0013_legacy_history_backfill_support",
|
|
562
|
+
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"
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
tag: "0014_drop_legacy_events_findings",
|
|
566
|
+
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"
|
|
555
567
|
}
|
|
556
568
|
];
|
|
557
569
|
|
|
@@ -15382,7 +15394,12 @@ var FindingFacets = external_exports.object({
|
|
|
15382
15394
|
severity: external_exports.array(FindingFacetItem),
|
|
15383
15395
|
subtype: external_exports.array(FindingFacetItem),
|
|
15384
15396
|
provider: external_exports.array(FindingFacetItem),
|
|
15385
|
-
action: external_exports.array(FindingFacetItem)
|
|
15397
|
+
action: external_exports.array(FindingFacetItem),
|
|
15398
|
+
// Counts by the group's derived status. The SQLite store derives a status
|
|
15399
|
+
// for every instance, so every group lands in a bucket; a status-less
|
|
15400
|
+
// group (possible only for callers whose rows carry no statuses) is
|
|
15401
|
+
// counted under no value.
|
|
15402
|
+
status: external_exports.array(FindingFacetItem)
|
|
15386
15403
|
}).meta({ id: "FindingFacets" });
|
|
15387
15404
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15388
15405
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15392,6 +15409,10 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15392
15409
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15393
15410
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15394
15411
|
action: external_exports.array(FindingAction).optional(),
|
|
15412
|
+
// Matches a group's DERIVED status (see FindingGroup.status), not its
|
|
15413
|
+
// individual instances' — so a filtered group's Status column always reads
|
|
15414
|
+
// one of the requested values.
|
|
15415
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15395
15416
|
q: external_exports.string().optional(),
|
|
15396
15417
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15397
15418
|
// session → findings drilldown). Findings without a session never match.
|
|
@@ -15579,6 +15600,33 @@ var ToolCallAttributes = external_exports.object({
|
|
|
15579
15600
|
parent_uuid: external_exports.string().optional(),
|
|
15580
15601
|
run_key: external_exports.string().optional()
|
|
15581
15602
|
}).catchall(external_exports.unknown());
|
|
15603
|
+
var CaptureAttributes = external_exports.object({
|
|
15604
|
+
// The harness/tool that produced the capture (`claude-code`, `cli`, …). A
|
|
15605
|
+
// column on the legacy `events` table; here it rides the bag because a
|
|
15606
|
+
// capture-typed audit row has no equivalent column of its own.
|
|
15607
|
+
source_tool: external_exports.string().optional(),
|
|
15608
|
+
file_path: external_exports.string().optional(),
|
|
15609
|
+
repo: external_exports.string().optional(),
|
|
15610
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
|
|
15611
|
+
// gives a non-file capture a display location ("via Bash") when file_path
|
|
15612
|
+
// is absent. The tool NAME only, never its arguments/output.
|
|
15613
|
+
tool_name: external_exports.string().optional(),
|
|
15614
|
+
// Presence-only provenance flag: set when the file is excluded by the
|
|
15615
|
+
// repo's .gitignore. Omitted (not false) for tracked files.
|
|
15616
|
+
gitignored: external_exports.boolean().optional(),
|
|
15617
|
+
// Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
|
|
15618
|
+
// reading from disk), never a partial fragment (a hook-captured edit).
|
|
15619
|
+
whole_file: external_exports.boolean().optional(),
|
|
15620
|
+
// Distributed-tracing correlation: `correlation_id` ties the capture back to
|
|
15621
|
+
// the request that produced it; `trace_id` is the originating span's W3C
|
|
15622
|
+
// trace id when telemetry is enabled.
|
|
15623
|
+
correlation_id: external_exports.uuid().optional(),
|
|
15624
|
+
trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
15625
|
+
// Ids of the detection exceptions that downgraded findings in this capture
|
|
15626
|
+
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
15627
|
+
// authorized the bypass.
|
|
15628
|
+
exception_ids: external_exports.array(external_exports.guid()).optional()
|
|
15629
|
+
}).catchall(external_exports.unknown());
|
|
15582
15630
|
var ToolCallInspection = external_exports.object({
|
|
15583
15631
|
ruleId: external_exports.string().min(1),
|
|
15584
15632
|
ruleName: external_exports.string(),
|
|
@@ -15665,7 +15713,18 @@ var InspectionFindingInput = external_exports.object({
|
|
|
15665
15713
|
span: Span,
|
|
15666
15714
|
maskedMatch: external_exports.string(),
|
|
15667
15715
|
actionTaken: ActionTaken,
|
|
15668
|
-
confidence: external_exports.number().min(0).max(1)
|
|
15716
|
+
confidence: external_exports.number().min(0).max(1),
|
|
15717
|
+
// Stable, content-addressed key correlating this finding across re-detections
|
|
15718
|
+
// — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
|
|
15719
|
+
// its unique index). Optional: only an at-rest/re-scannable finding carries
|
|
15720
|
+
// one; an in-flight capture (prompt/response) has nothing to re-detect
|
|
15721
|
+
// against and leaves it unset, so every insert is a fresh row.
|
|
15722
|
+
findingKey: external_exports.string().optional(),
|
|
15723
|
+
// The ORIGINAL detection time, preserved across a later re-detection of the
|
|
15724
|
+
// same findingKey — mirrors the legacy `findings.first_detected_at`.
|
|
15725
|
+
// Optional: when omitted, the writer derives it from the referenced audit
|
|
15726
|
+
// event's startedAt on first insert (see SqliteInspectionFindingsRepository).
|
|
15727
|
+
firstDetectedAt: external_exports.iso.datetime().optional()
|
|
15669
15728
|
});
|
|
15670
15729
|
var InventoryContext = external_exports.object({
|
|
15671
15730
|
host: InventoryInput.optional(),
|
|
@@ -15867,6 +15926,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15867
15926
|
|
|
15868
15927
|
// ../../packages/schema/src/zod/event.ts
|
|
15869
15928
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15929
|
+
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15870
15930
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15871
15931
|
var EventMetadata = external_exports.object({
|
|
15872
15932
|
sessionId: external_exports.string().optional(),
|
|
@@ -16356,6 +16416,12 @@ var PolicyBundle = external_exports.object({
|
|
|
16356
16416
|
// on-disk caches — that omit the field still parse; consumers read
|
|
16357
16417
|
// `bundle.exceptions ?? []`.
|
|
16358
16418
|
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
16419
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
16420
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
16421
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
16422
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
16423
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
16424
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
16359
16425
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16360
16426
|
fetchedAt: external_exports.iso.datetime()
|
|
16361
16427
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -17160,6 +17226,15 @@ function groupActions(g) {
|
|
|
17160
17226
|
actionsCache.set(g, actions);
|
|
17161
17227
|
return actions;
|
|
17162
17228
|
}
|
|
17229
|
+
function countInstancesByStatus(statusInputs, statuses) {
|
|
17230
|
+
const statusSet = new Set(statuses);
|
|
17231
|
+
let sum = 0;
|
|
17232
|
+
for (const input of statusInputs) {
|
|
17233
|
+
if (input.count === void 0) return null;
|
|
17234
|
+
if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
|
|
17235
|
+
}
|
|
17236
|
+
return sum;
|
|
17237
|
+
}
|
|
17163
17238
|
function applyFindingFilters(groups, opts) {
|
|
17164
17239
|
let filtered = groups;
|
|
17165
17240
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -17178,6 +17253,10 @@ function applyFindingFilters(groups, opts) {
|
|
|
17178
17253
|
const subtypeSet = new Set(opts.subtype);
|
|
17179
17254
|
filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
|
|
17180
17255
|
}
|
|
17256
|
+
if (opts.statuses && opts.statuses.length > 0) {
|
|
17257
|
+
const statusSet = new Set(opts.statuses);
|
|
17258
|
+
filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
|
|
17259
|
+
}
|
|
17181
17260
|
if (opts.q) {
|
|
17182
17261
|
const q = opts.q.toLowerCase();
|
|
17183
17262
|
filtered = filtered.filter((g) => groupHaystack(g).includes(q));
|
|
@@ -17199,6 +17278,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17199
17278
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
17200
17279
|
providers: opts.providers,
|
|
17201
17280
|
actions: opts.actions,
|
|
17281
|
+
statuses: opts.statuses,
|
|
17202
17282
|
q: opts.q,
|
|
17203
17283
|
subtype: opts.subtype
|
|
17204
17284
|
});
|
|
@@ -17208,6 +17288,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17208
17288
|
}
|
|
17209
17289
|
const forProvider = applyFindingFilters(allGroups, {
|
|
17210
17290
|
actions: opts.actions,
|
|
17291
|
+
statuses: opts.statuses,
|
|
17211
17292
|
q: opts.q,
|
|
17212
17293
|
subtype: opts.subtype,
|
|
17213
17294
|
severity: opts.severity
|
|
@@ -17218,6 +17299,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17218
17299
|
}
|
|
17219
17300
|
const forAction = applyFindingFilters(allGroups, {
|
|
17220
17301
|
providers: opts.providers,
|
|
17302
|
+
statuses: opts.statuses,
|
|
17221
17303
|
q: opts.q,
|
|
17222
17304
|
subtype: opts.subtype,
|
|
17223
17305
|
severity: opts.severity
|
|
@@ -17229,17 +17311,30 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17229
17311
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17230
17312
|
providers: opts.providers,
|
|
17231
17313
|
actions: opts.actions,
|
|
17314
|
+
statuses: opts.statuses,
|
|
17232
17315
|
q: opts.q,
|
|
17233
17316
|
severity: opts.severity
|
|
17234
17317
|
});
|
|
17235
17318
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
17236
17319
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
17320
|
+
const forStatus = applyFindingFilters(allGroups, {
|
|
17321
|
+
providers: opts.providers,
|
|
17322
|
+
actions: opts.actions,
|
|
17323
|
+
q: opts.q,
|
|
17324
|
+
subtype: opts.subtype,
|
|
17325
|
+
severity: opts.severity
|
|
17326
|
+
});
|
|
17327
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
17328
|
+
for (const g of forStatus) {
|
|
17329
|
+
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17330
|
+
}
|
|
17237
17331
|
const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17238
17332
|
return {
|
|
17239
17333
|
severity: toItems(severityMap),
|
|
17240
17334
|
provider: toItems(providerMap),
|
|
17241
17335
|
action: toItems(actionMap),
|
|
17242
|
-
subtype: toItems(subtypeMap)
|
|
17336
|
+
subtype: toItems(subtypeMap),
|
|
17337
|
+
status: toItems(statusMap)
|
|
17243
17338
|
};
|
|
17244
17339
|
}
|
|
17245
17340
|
|
|
@@ -17274,10 +17369,18 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17274
17369
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17275
17370
|
|
|
17276
17371
|
// ../../packages/schema/src/zod/local.ts
|
|
17277
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17372
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
|
|
17373
|
+
var MODEL_JUDGE_PAYLOAD_VERSION = 1;
|
|
17278
17374
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17279
17375
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17280
17376
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
17377
|
+
var ModelJudgeConsent = external_exports.object({
|
|
17378
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17379
|
+
payloadVersion: external_exports.number().int().positive()
|
|
17380
|
+
});
|
|
17381
|
+
function isModelJudgeConsentValid(consent) {
|
|
17382
|
+
return consent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION;
|
|
17383
|
+
}
|
|
17281
17384
|
var WorkspaceSettings = external_exports.object({
|
|
17282
17385
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
17283
17386
|
// Settings files written by earlier releases may carry the retired 'attached'
|
|
@@ -17293,37 +17396,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17293
17396
|
// Shares writes.
|
|
17294
17397
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17295
17398
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17296
|
-
onboardedAt: external_exports.iso.datetime().optional()
|
|
17399
|
+
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17400
|
+
// Records that the user consented to sending findings to the model API for
|
|
17401
|
+
// the /aka:setup judge, along with the payload-shape version they agreed to.
|
|
17402
|
+
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
17403
|
+
// covers the current payload and must be re-granted.
|
|
17404
|
+
modelJudgeConsent: ModelJudgeConsent.optional()
|
|
17297
17405
|
});
|
|
17298
17406
|
function defaultWorkspaceSettings() {
|
|
17299
17407
|
return WorkspaceSettings.parse({});
|
|
17300
17408
|
}
|
|
17301
|
-
function toEventRow(event) {
|
|
17302
|
-
return {
|
|
17303
|
-
id: event.id,
|
|
17304
|
-
sourceTool: event.sourceTool,
|
|
17305
|
-
kind: event.kind,
|
|
17306
|
-
occurredAt: isoToEpochMillis(event.occurredAt),
|
|
17307
|
-
contentHash: event.contentHash,
|
|
17308
|
-
content: event.content,
|
|
17309
|
-
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
17310
|
-
};
|
|
17311
|
-
}
|
|
17312
|
-
function toFindingRow(finding) {
|
|
17313
|
-
return {
|
|
17314
|
-
id: finding.id,
|
|
17315
|
-
eventId: finding.eventId,
|
|
17316
|
-
ruleId: finding.ruleId,
|
|
17317
|
-
category: finding.category,
|
|
17318
|
-
severity: finding.severity,
|
|
17319
|
-
spanStart: finding.span.start,
|
|
17320
|
-
spanEnd: finding.span.end,
|
|
17321
|
-
maskedMatch: finding.maskedMatch,
|
|
17322
|
-
actionTaken: finding.actionTaken,
|
|
17323
|
-
confidence: finding.confidence,
|
|
17324
|
-
findingKey: finding.findingKey ?? null
|
|
17325
|
-
};
|
|
17326
|
-
}
|
|
17327
17409
|
function toInventoryRow(input, id, now) {
|
|
17328
17410
|
return {
|
|
17329
17411
|
id,
|
|
@@ -17393,7 +17475,42 @@ function toInspectionFindingRow(input) {
|
|
|
17393
17475
|
spanEnd: input.span.end,
|
|
17394
17476
|
maskedMatch: input.maskedMatch,
|
|
17395
17477
|
actionTaken: input.actionTaken,
|
|
17396
|
-
confidence: input.confidence
|
|
17478
|
+
confidence: input.confidence,
|
|
17479
|
+
findingKey: input.findingKey ?? null,
|
|
17480
|
+
firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
|
|
17481
|
+
};
|
|
17482
|
+
}
|
|
17483
|
+
function toCaptureAttributes(event) {
|
|
17484
|
+
const metadata = event.metadata;
|
|
17485
|
+
return {
|
|
17486
|
+
source_tool: event.sourceTool,
|
|
17487
|
+
...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
|
|
17488
|
+
...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
|
|
17489
|
+
...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
|
|
17490
|
+
...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
|
|
17491
|
+
...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
|
|
17492
|
+
...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
|
|
17493
|
+
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
17494
|
+
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
17495
|
+
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
17496
|
+
// has ever populated either), but every legacy metadata key still rides
|
|
17497
|
+
// the bag rather than being silently dropped — CaptureAttributes'
|
|
17498
|
+
// `.catchall(z.unknown())` carries the long tail.
|
|
17499
|
+
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
17500
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
17501
|
+
};
|
|
17502
|
+
}
|
|
17503
|
+
function captureDefinitionVersion(finding) {
|
|
17504
|
+
return `capture/${finding.category}/${finding.severity}`;
|
|
17505
|
+
}
|
|
17506
|
+
function toCaptureDefinitionInput(finding) {
|
|
17507
|
+
return {
|
|
17508
|
+
ruleId: finding.ruleId,
|
|
17509
|
+
version: captureDefinitionVersion(finding),
|
|
17510
|
+
name: finding.ruleId,
|
|
17511
|
+
category: finding.category,
|
|
17512
|
+
severity: finding.severity,
|
|
17513
|
+
definition: JSON.stringify({ ruleId: finding.ruleId })
|
|
17397
17514
|
};
|
|
17398
17515
|
}
|
|
17399
17516
|
|
|
@@ -17821,6 +17938,37 @@ function reviewSeverityRank(reasons) {
|
|
|
17821
17938
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17822
17939
|
}
|
|
17823
17940
|
|
|
17941
|
+
// ../../packages/persistence/src/ids.ts
|
|
17942
|
+
import { createHash } from "crypto";
|
|
17943
|
+
function sha256Hex(input) {
|
|
17944
|
+
return createHash("sha256").update(input).digest("hex");
|
|
17945
|
+
}
|
|
17946
|
+
function inventoryId(objectType, identityKey) {
|
|
17947
|
+
return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
|
|
17948
|
+
}
|
|
17949
|
+
function sourceProjectId(url2) {
|
|
17950
|
+
return sha256Hex(canonicalIdentity(["source_project", url2]));
|
|
17951
|
+
}
|
|
17952
|
+
function classifiedDataId(cls) {
|
|
17953
|
+
return sha256Hex(canonicalIdentity(["classified_data", cls]));
|
|
17954
|
+
}
|
|
17955
|
+
function inspectionDefinitionId(ruleId, version2) {
|
|
17956
|
+
return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
|
|
17957
|
+
}
|
|
17958
|
+
function llmCallId(sessionId, messageId) {
|
|
17959
|
+
return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
|
|
17960
|
+
}
|
|
17961
|
+
function toolCallId(sessionId, toolUseId) {
|
|
17962
|
+
return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
|
|
17963
|
+
}
|
|
17964
|
+
var NO_SESSION = "no_session";
|
|
17965
|
+
var NO_PATH = "no_path";
|
|
17966
|
+
function captureId(sessionId, contentHash, filePath = null) {
|
|
17967
|
+
return sha256Hex(
|
|
17968
|
+
canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
|
|
17969
|
+
);
|
|
17970
|
+
}
|
|
17971
|
+
|
|
17824
17972
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17825
17973
|
function escapeLikePattern(s) {
|
|
17826
17974
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -17917,28 +18065,81 @@ function evidenceExists(db, object2) {
|
|
|
17917
18065
|
return schemaObjectExists(db, "table", object2.name);
|
|
17918
18066
|
}
|
|
17919
18067
|
|
|
17920
|
-
// ../../packages/persistence/src/
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
18068
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18069
|
+
function allRows(stmt, params) {
|
|
18070
|
+
if (params === void 0) return stmt.all();
|
|
18071
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18072
|
+
return stmt.all(params);
|
|
17924
18073
|
}
|
|
17925
|
-
function
|
|
17926
|
-
|
|
18074
|
+
function getRow(stmt, params) {
|
|
18075
|
+
if (params === void 0) return stmt.get();
|
|
18076
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18077
|
+
return stmt.get(params);
|
|
17927
18078
|
}
|
|
17928
|
-
function
|
|
17929
|
-
return
|
|
18079
|
+
function intToBool(raw) {
|
|
18080
|
+
return raw === 1 || raw === true;
|
|
17930
18081
|
}
|
|
17931
|
-
function
|
|
17932
|
-
return
|
|
18082
|
+
function boolToInt(b) {
|
|
18083
|
+
return b ? 1 : 0;
|
|
17933
18084
|
}
|
|
17934
|
-
function
|
|
17935
|
-
|
|
18085
|
+
function bindParams(row) {
|
|
18086
|
+
const out = {};
|
|
18087
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18088
|
+
out[key] = value === void 0 ? null : value;
|
|
18089
|
+
}
|
|
18090
|
+
return out;
|
|
17936
18091
|
}
|
|
17937
|
-
function
|
|
17938
|
-
return
|
|
18092
|
+
function countScalar(db, sql, params) {
|
|
18093
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17939
18094
|
}
|
|
17940
|
-
function
|
|
17941
|
-
|
|
18095
|
+
function countBy(db, sql, params) {
|
|
18096
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18097
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18098
|
+
map2.set(row.k, row.n);
|
|
18099
|
+
}
|
|
18100
|
+
return map2;
|
|
18101
|
+
}
|
|
18102
|
+
function mapRowsTolerant(rows, map2) {
|
|
18103
|
+
const out = [];
|
|
18104
|
+
for (const row of rows) {
|
|
18105
|
+
try {
|
|
18106
|
+
out.push(map2(row));
|
|
18107
|
+
} catch {
|
|
18108
|
+
}
|
|
18109
|
+
}
|
|
18110
|
+
return out;
|
|
18111
|
+
}
|
|
18112
|
+
|
|
18113
|
+
// ../../packages/persistence/src/paths.ts
|
|
18114
|
+
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
18115
|
+
var DATA_DIR_MODE = 448;
|
|
18116
|
+
var DATA_FILE_MODE = 384;
|
|
18117
|
+
var DB_FILENAME = "aka.db";
|
|
18118
|
+
function chmodBestEffort(path, mode) {
|
|
18119
|
+
try {
|
|
18120
|
+
chmodSync(path, mode);
|
|
18121
|
+
} catch {
|
|
18122
|
+
}
|
|
18123
|
+
}
|
|
18124
|
+
function tightenDir(dir) {
|
|
18125
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18126
|
+
}
|
|
18127
|
+
function ensureDataDirSync(dir) {
|
|
18128
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18129
|
+
tightenDir(dir);
|
|
18130
|
+
}
|
|
18131
|
+
function dbSidecars(file2) {
|
|
18132
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18133
|
+
}
|
|
18134
|
+
function tightenFile(file2) {
|
|
18135
|
+
try {
|
|
18136
|
+
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18137
|
+
} catch {
|
|
18138
|
+
}
|
|
18139
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18140
|
+
}
|
|
18141
|
+
function tightenPerms(file2) {
|
|
18142
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
17942
18143
|
}
|
|
17943
18144
|
|
|
17944
18145
|
// ../../packages/persistence/src/migrations.ts
|
|
@@ -17952,7 +18153,8 @@ function createdIndexName(statement) {
|
|
|
17952
18153
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17953
18154
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17954
18155
|
}
|
|
17955
|
-
|
|
18156
|
+
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
18157
|
+
function applyMigrations(db, file2) {
|
|
17956
18158
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17957
18159
|
db.exec(
|
|
17958
18160
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -17966,6 +18168,7 @@ function applyMigrations(db) {
|
|
|
17966
18168
|
);
|
|
17967
18169
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
17968
18170
|
if (applied.has(migration.tag)) continue;
|
|
18171
|
+
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
17969
18172
|
const evidence = evidenceObjects(migration.sql);
|
|
17970
18173
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17971
18174
|
if (present.length > 0 && present.length < evidence.length) {
|
|
@@ -18010,7 +18213,6 @@ function applyMigrations(db) {
|
|
|
18010
18213
|
if (legacyCount < SQLITE_MIGRATIONS.length) {
|
|
18011
18214
|
db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
|
|
18012
18215
|
}
|
|
18013
|
-
ensureSyncedAtColumn(db, "events");
|
|
18014
18216
|
ensureSyncedAtColumn(db, "audit_events");
|
|
18015
18217
|
ensureScanLedgerTable(db);
|
|
18016
18218
|
ensureBlockedDetectionsTable(db);
|
|
@@ -18018,6 +18220,47 @@ function applyMigrations(db) {
|
|
|
18018
18220
|
ensureWriteGateTrigger(db);
|
|
18019
18221
|
ensureTokenUsageColumns(db);
|
|
18020
18222
|
reconcileSourceProjectIds(db);
|
|
18223
|
+
if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
|
|
18224
|
+
const drained = runLegacyHistoryBackfill(db);
|
|
18225
|
+
if (drained) applyLegacyDropMigration(db, file2);
|
|
18226
|
+
}
|
|
18227
|
+
}
|
|
18228
|
+
function applyLegacyDropMigration(db, file2) {
|
|
18229
|
+
const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
|
|
18230
|
+
if (!migration) return;
|
|
18231
|
+
if (file2) {
|
|
18232
|
+
try {
|
|
18233
|
+
backupBeforeLegacyDrop(db, file2);
|
|
18234
|
+
} catch (error51) {
|
|
18235
|
+
akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
|
|
18236
|
+
return;
|
|
18237
|
+
}
|
|
18238
|
+
}
|
|
18239
|
+
try {
|
|
18240
|
+
withTransaction(
|
|
18241
|
+
db,
|
|
18242
|
+
() => {
|
|
18243
|
+
const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
|
|
18244
|
+
if (alreadyDropped) return;
|
|
18245
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
18246
|
+
db.exec(statement);
|
|
18247
|
+
}
|
|
18248
|
+
db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
|
|
18249
|
+
migration.tag,
|
|
18250
|
+
Date.now()
|
|
18251
|
+
);
|
|
18252
|
+
},
|
|
18253
|
+
"IMMEDIATE"
|
|
18254
|
+
);
|
|
18255
|
+
} catch (error51) {
|
|
18256
|
+
akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
|
|
18257
|
+
}
|
|
18258
|
+
}
|
|
18259
|
+
function backupBeforeLegacyDrop(db, file2) {
|
|
18260
|
+
const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
|
|
18261
|
+
db.prepare("VACUUM INTO ?").run(backup);
|
|
18262
|
+
tightenFile(backup);
|
|
18263
|
+
return backup;
|
|
18021
18264
|
}
|
|
18022
18265
|
var TOKEN_USAGE_COLUMNS = [
|
|
18023
18266
|
{
|
|
@@ -18046,6 +18289,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
18046
18289
|
}
|
|
18047
18290
|
];
|
|
18048
18291
|
function ensureTokenUsageColumns(db) {
|
|
18292
|
+
if (!schemaObjectExists(db, "table", "audit_events")) return;
|
|
18049
18293
|
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
18050
18294
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
18051
18295
|
if (!existing.has(column.name)) {
|
|
@@ -18111,11 +18355,187 @@ function reconcileSourceProjectIds(db) {
|
|
|
18111
18355
|
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
18112
18356
|
}
|
|
18113
18357
|
}
|
|
18358
|
+
var LEGACY_BACKFILL_BATCH_SIZE = 200;
|
|
18359
|
+
var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
|
|
18360
|
+
function getLegacyCopyWatermark(db, source) {
|
|
18361
|
+
const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
|
|
18362
|
+
return row?.lastRowid ?? 0;
|
|
18363
|
+
}
|
|
18364
|
+
function setLegacyCopyWatermark(db, source, lastRowid) {
|
|
18365
|
+
db.prepare(
|
|
18366
|
+
`INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
|
|
18367
|
+
ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
|
|
18368
|
+
).run(source, lastRowid);
|
|
18369
|
+
}
|
|
18370
|
+
function drainLegacyTable(db, source, selectStmt, handleRows) {
|
|
18371
|
+
let watermark = getLegacyCopyWatermark(db, source);
|
|
18372
|
+
let processed = 0;
|
|
18373
|
+
while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
|
|
18374
|
+
const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
|
|
18375
|
+
if (rows.length === 0) return true;
|
|
18376
|
+
withTransaction(
|
|
18377
|
+
db,
|
|
18378
|
+
() => {
|
|
18379
|
+
handleRows(rows);
|
|
18380
|
+
watermark = rows[rows.length - 1]?.rowid ?? watermark;
|
|
18381
|
+
setLegacyCopyWatermark(db, source, watermark);
|
|
18382
|
+
},
|
|
18383
|
+
"IMMEDIATE"
|
|
18384
|
+
);
|
|
18385
|
+
processed += rows.length;
|
|
18386
|
+
if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
|
|
18387
|
+
}
|
|
18388
|
+
return false;
|
|
18389
|
+
}
|
|
18390
|
+
function parseLegacyEventMetadata(raw) {
|
|
18391
|
+
if (raw === null) return void 0;
|
|
18392
|
+
try {
|
|
18393
|
+
return JSON.parse(raw);
|
|
18394
|
+
} catch {
|
|
18395
|
+
return void 0;
|
|
18396
|
+
}
|
|
18397
|
+
}
|
|
18398
|
+
function toLegacyAuditAttributesJson(row) {
|
|
18399
|
+
return JSON.stringify(
|
|
18400
|
+
toCaptureAttributes({
|
|
18401
|
+
id: row.id,
|
|
18402
|
+
sourceTool: row.sourceTool,
|
|
18403
|
+
kind: row.kind,
|
|
18404
|
+
occurredAt: new Date(row.occurredAt).toISOString(),
|
|
18405
|
+
contentHash: row.contentHash,
|
|
18406
|
+
content: row.content,
|
|
18407
|
+
metadata: row.metadata
|
|
18408
|
+
})
|
|
18409
|
+
);
|
|
18410
|
+
}
|
|
18411
|
+
function copyLegacyEvents(db) {
|
|
18412
|
+
const selectStmt = db.prepare(
|
|
18413
|
+
`SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
|
|
18414
|
+
content_hash AS contentHash, content, metadata
|
|
18415
|
+
FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18416
|
+
);
|
|
18417
|
+
const insertStmt = db.prepare(
|
|
18418
|
+
`INSERT OR IGNORE INTO audit_events
|
|
18419
|
+
(id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
|
|
18420
|
+
VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
|
|
18421
|
+
);
|
|
18422
|
+
const stubRootStmt = db.prepare(
|
|
18423
|
+
`INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
|
|
18424
|
+
);
|
|
18425
|
+
return drainLegacyTable(
|
|
18426
|
+
db,
|
|
18427
|
+
"events",
|
|
18428
|
+
selectStmt,
|
|
18429
|
+
(rows) => {
|
|
18430
|
+
for (const row of rows) {
|
|
18431
|
+
const metadata = parseLegacyEventMetadata(row.metadata);
|
|
18432
|
+
const sessionId = metadata?.sessionId ?? null;
|
|
18433
|
+
if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
|
|
18434
|
+
insertStmt.run(
|
|
18435
|
+
bindParams({
|
|
18436
|
+
id: row.id,
|
|
18437
|
+
parentId: sessionId,
|
|
18438
|
+
rootSessionId: sessionId,
|
|
18439
|
+
eventType: row.kind,
|
|
18440
|
+
startedAt: row.occurredAt,
|
|
18441
|
+
content: row.content,
|
|
18442
|
+
contentHash: row.contentHash,
|
|
18443
|
+
attributes: toLegacyAuditAttributesJson({ ...row, metadata })
|
|
18444
|
+
})
|
|
18445
|
+
);
|
|
18446
|
+
}
|
|
18447
|
+
}
|
|
18448
|
+
);
|
|
18449
|
+
}
|
|
18450
|
+
function copyLegacyFindings(db) {
|
|
18451
|
+
const selectStmt = db.prepare(
|
|
18452
|
+
`SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
|
|
18453
|
+
span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
|
|
18454
|
+
action_taken AS actionTaken, confidence, finding_key AS findingKey,
|
|
18455
|
+
first_detected_at AS firstDetectedAt
|
|
18456
|
+
FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
18457
|
+
);
|
|
18458
|
+
const definitionStmt = db.prepare(
|
|
18459
|
+
`INSERT OR IGNORE INTO inspection_definitions
|
|
18460
|
+
(id, rule_id, name, category, severity, definition, version)
|
|
18461
|
+
VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
|
|
18462
|
+
);
|
|
18463
|
+
const findingStmt = db.prepare(
|
|
18464
|
+
`INSERT INTO inspection_findings
|
|
18465
|
+
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
18466
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
18467
|
+
finding_key, first_detected_at)
|
|
18468
|
+
VALUES
|
|
18469
|
+
(:id, :auditEventId, :inspectionDefinitionId, NULL,
|
|
18470
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
18471
|
+
:findingKey, :firstDetectedAt)
|
|
18472
|
+
ON CONFLICT(id) DO NOTHING
|
|
18473
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
18474
|
+
first_detected_at = CASE
|
|
18475
|
+
WHEN first_detected_at IS NULL THEN excluded.first_detected_at
|
|
18476
|
+
WHEN excluded.first_detected_at IS NULL THEN first_detected_at
|
|
18477
|
+
ELSE min(first_detected_at, excluded.first_detected_at)
|
|
18478
|
+
END`
|
|
18479
|
+
);
|
|
18480
|
+
return drainLegacyTable(
|
|
18481
|
+
db,
|
|
18482
|
+
"findings",
|
|
18483
|
+
selectStmt,
|
|
18484
|
+
(rows) => {
|
|
18485
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
18486
|
+
for (const row of rows) {
|
|
18487
|
+
const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
|
|
18488
|
+
let definitionId = definitionIds.get(tupleKey);
|
|
18489
|
+
if (definitionId === void 0) {
|
|
18490
|
+
const version2 = `unmigrated/${row.category}/${row.severity}`;
|
|
18491
|
+
definitionId = inspectionDefinitionId(row.ruleId, version2);
|
|
18492
|
+
definitionStmt.run(
|
|
18493
|
+
bindParams({
|
|
18494
|
+
id: definitionId,
|
|
18495
|
+
ruleId: row.ruleId,
|
|
18496
|
+
name: row.ruleId,
|
|
18497
|
+
category: row.category,
|
|
18498
|
+
severity: row.severity,
|
|
18499
|
+
definition: "",
|
|
18500
|
+
version: version2
|
|
18501
|
+
})
|
|
18502
|
+
);
|
|
18503
|
+
definitionIds.set(tupleKey, definitionId);
|
|
18504
|
+
}
|
|
18505
|
+
findingStmt.run(
|
|
18506
|
+
bindParams({
|
|
18507
|
+
id: row.id,
|
|
18508
|
+
auditEventId: row.eventId,
|
|
18509
|
+
inspectionDefinitionId: definitionId,
|
|
18510
|
+
spanStart: row.spanStart,
|
|
18511
|
+
spanEnd: row.spanEnd,
|
|
18512
|
+
maskedMatch: row.maskedMatch,
|
|
18513
|
+
actionTaken: row.actionTaken,
|
|
18514
|
+
confidence: row.confidence,
|
|
18515
|
+
findingKey: row.findingKey,
|
|
18516
|
+
firstDetectedAt: row.firstDetectedAt
|
|
18517
|
+
})
|
|
18518
|
+
);
|
|
18519
|
+
}
|
|
18520
|
+
}
|
|
18521
|
+
);
|
|
18522
|
+
}
|
|
18523
|
+
function runLegacyHistoryBackfill(db) {
|
|
18524
|
+
try {
|
|
18525
|
+
const eventsCaughtUp = copyLegacyEvents(db);
|
|
18526
|
+
if (!eventsCaughtUp) return false;
|
|
18527
|
+
return copyLegacyFindings(db);
|
|
18528
|
+
} catch (error51) {
|
|
18529
|
+
akaWarn(`legacy history backfill failed: ${String(error51)}`);
|
|
18530
|
+
return false;
|
|
18531
|
+
}
|
|
18532
|
+
}
|
|
18114
18533
|
function isForeignSqliteLineage(db) {
|
|
18115
18534
|
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18116
18535
|
return columnNames(db, "events").includes("tenant_id");
|
|
18117
18536
|
}
|
|
18118
18537
|
function ensureSyncedAtColumn(db, table2) {
|
|
18538
|
+
if (!schemaObjectExists(db, "table", table2)) return;
|
|
18119
18539
|
if (!columnNames(db, table2).includes("synced_at")) {
|
|
18120
18540
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
18121
18541
|
}
|
|
@@ -18136,6 +18556,7 @@ function ensureWriteGateTrigger(db) {
|
|
|
18136
18556
|
CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
|
|
18137
18557
|
)`);
|
|
18138
18558
|
db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
|
|
18559
|
+
if (!schemaObjectExists(db, "table", "installed_packs")) return;
|
|
18139
18560
|
db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
|
|
18140
18561
|
BEFORE UPDATE OF version, name, rules_json ON installed_packs
|
|
18141
18562
|
WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
|
|
@@ -18163,30 +18584,6 @@ function ensureRuleProbeCacheTable(db) {
|
|
|
18163
18584
|
)`);
|
|
18164
18585
|
}
|
|
18165
18586
|
|
|
18166
|
-
// ../../packages/persistence/src/paths.ts
|
|
18167
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
18168
|
-
var DATA_DIR_MODE = 448;
|
|
18169
|
-
var DATA_FILE_MODE = 384;
|
|
18170
|
-
var DB_FILENAME = "aka.db";
|
|
18171
|
-
function ensureDataDirSync(dir) {
|
|
18172
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18173
|
-
try {
|
|
18174
|
-
chmodSync(dir, DATA_DIR_MODE);
|
|
18175
|
-
} catch {
|
|
18176
|
-
}
|
|
18177
|
-
}
|
|
18178
|
-
function walSidecars(file2) {
|
|
18179
|
-
return [`${file2}-wal`, `${file2}-shm`];
|
|
18180
|
-
}
|
|
18181
|
-
function tightenPerms(file2) {
|
|
18182
|
-
for (const path of [file2, ...walSidecars(file2)]) {
|
|
18183
|
-
try {
|
|
18184
|
-
chmodSync(path, DATA_FILE_MODE);
|
|
18185
|
-
} catch {
|
|
18186
|
-
}
|
|
18187
|
-
}
|
|
18188
|
-
}
|
|
18189
|
-
|
|
18190
18587
|
// ../../packages/persistence/src/internal/json.ts
|
|
18191
18588
|
function safeJson(s, fallback) {
|
|
18192
18589
|
if (s == null) return fallback;
|
|
@@ -18206,60 +18603,15 @@ function parseJsonObject(s) {
|
|
|
18206
18603
|
return void 0;
|
|
18207
18604
|
}
|
|
18208
18605
|
|
|
18209
|
-
// ../../packages/persistence/src/
|
|
18210
|
-
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
return stmt.get(params);
|
|
18219
|
-
}
|
|
18220
|
-
function intToBool(raw) {
|
|
18221
|
-
return raw === 1 || raw === true;
|
|
18222
|
-
}
|
|
18223
|
-
function boolToInt(b) {
|
|
18224
|
-
return b ? 1 : 0;
|
|
18225
|
-
}
|
|
18226
|
-
function bindParams(row) {
|
|
18227
|
-
const out = {};
|
|
18228
|
-
for (const [key, value] of Object.entries(row)) {
|
|
18229
|
-
out[key] = value === void 0 ? null : value;
|
|
18230
|
-
}
|
|
18231
|
-
return out;
|
|
18232
|
-
}
|
|
18233
|
-
function countScalar(db, sql, params) {
|
|
18234
|
-
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18235
|
-
}
|
|
18236
|
-
function countBy(db, sql, params) {
|
|
18237
|
-
const map2 = /* @__PURE__ */ new Map();
|
|
18238
|
-
for (const row of allRows(db.prepare(sql), params)) {
|
|
18239
|
-
map2.set(row.k, row.n);
|
|
18240
|
-
}
|
|
18241
|
-
return map2;
|
|
18242
|
-
}
|
|
18243
|
-
function mapRowsTolerant(rows, map2) {
|
|
18244
|
-
const out = [];
|
|
18245
|
-
for (const row of rows) {
|
|
18246
|
-
try {
|
|
18247
|
-
out.push(map2(row));
|
|
18248
|
-
} catch {
|
|
18249
|
-
}
|
|
18250
|
-
}
|
|
18251
|
-
return out;
|
|
18252
|
-
}
|
|
18253
|
-
|
|
18254
|
-
// ../../packages/persistence/src/repositories/activity.ts
|
|
18255
|
-
var DAY_MS = 864e5;
|
|
18256
|
-
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
18257
|
-
var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
|
|
18258
|
-
function defaultTimeZone() {
|
|
18259
|
-
try {
|
|
18260
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
18261
|
-
} catch {
|
|
18262
|
-
return "UTC";
|
|
18606
|
+
// ../../packages/persistence/src/repositories/activity.ts
|
|
18607
|
+
var DAY_MS = 864e5;
|
|
18608
|
+
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
18609
|
+
var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
|
|
18610
|
+
function defaultTimeZone() {
|
|
18611
|
+
try {
|
|
18612
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
18613
|
+
} catch {
|
|
18614
|
+
return "UTC";
|
|
18263
18615
|
}
|
|
18264
18616
|
}
|
|
18265
18617
|
function todayWindow(timeZone, nowMs) {
|
|
@@ -18867,6 +19219,21 @@ var SqliteAuditEventsRepository = class {
|
|
|
18867
19219
|
})
|
|
18868
19220
|
);
|
|
18869
19221
|
}
|
|
19222
|
+
// Idempotent stub of a session's structural root. Session-scoped leaves
|
|
19223
|
+
// (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
|
|
19224
|
+
// INSERT OR IGNORE does NOT suppress a foreign-key violation (only
|
|
19225
|
+
// UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
|
|
19226
|
+
// raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
|
|
19227
|
+
// dropping the write under failOpenTransaction. SessionStart's own root write
|
|
19228
|
+
// is itself fail-open and marks "attempted", not "succeeded", so a session
|
|
19229
|
+
// with no root row yet is a real, permanent condition, not a transient race.
|
|
19230
|
+
// The stub carries no dimensions/attributes; an authoritative root
|
|
19231
|
+
// (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
|
|
19232
|
+
// on the id PK, so the stub never shadows real data. This is the single named
|
|
19233
|
+
// home for that FK invariant — call it before writing any session-scoped row.
|
|
19234
|
+
ensureSessionRoot(sessionId, startedAt) {
|
|
19235
|
+
this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
|
|
19236
|
+
}
|
|
18870
19237
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18871
19238
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
18872
19239
|
// from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
|
|
@@ -19328,8 +19695,14 @@ var SqliteDetectionsRepository = class {
|
|
|
19328
19695
|
)
|
|
19329
19696
|
);
|
|
19330
19697
|
}
|
|
19331
|
-
// Findings whose parent event occurred in the last 30 days
|
|
19332
|
-
//
|
|
19698
|
+
// Findings whose parent audit event occurred in the last 30 days, is one of
|
|
19699
|
+
// the four capture kinds, and whose definition's rule_id is in the given set.
|
|
19700
|
+
// Mirrors the security repo's inspection_findings⋈audit_events window join.
|
|
19701
|
+
// rule_id lives on inspection_definitions, not the finding row, so the join
|
|
19702
|
+
// chains through it. audit_events also holds structural rows (session, run,
|
|
19703
|
+
// tool_call, llm_call, source_lookup, config_scan) that never had a legacy
|
|
19704
|
+
// events counterpart, so the event_type predicate keeps this count identical
|
|
19705
|
+
// to the old findings⋈events one.
|
|
19333
19706
|
countFindingsLast30d(ruleIds) {
|
|
19334
19707
|
if (ruleIds.length === 0) return 0;
|
|
19335
19708
|
const since = this.now() - 30 * DAY_MS2;
|
|
@@ -19337,8 +19710,12 @@ var SqliteDetectionsRepository = class {
|
|
|
19337
19710
|
return countScalar(
|
|
19338
19711
|
this.db,
|
|
19339
19712
|
`SELECT count(*) AS n
|
|
19340
|
-
FROM
|
|
19341
|
-
|
|
19713
|
+
FROM inspection_findings f
|
|
19714
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19715
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19716
|
+
WHERE e.started_at >= ?
|
|
19717
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19718
|
+
AND d.rule_id IN (${inClause})`,
|
|
19342
19719
|
[since, ...ruleIds]
|
|
19343
19720
|
);
|
|
19344
19721
|
}
|
|
@@ -19348,35 +19725,24 @@ var SqliteDetectionsRepository = class {
|
|
|
19348
19725
|
var SqliteEventsRepository = class {
|
|
19349
19726
|
constructor(db) {
|
|
19350
19727
|
this.db = db;
|
|
19351
|
-
this.insertStmt = db.prepare(
|
|
19352
|
-
`INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
|
|
19353
|
-
VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
|
|
19354
|
-
);
|
|
19355
19728
|
}
|
|
19356
19729
|
db;
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
this.insertStmt.run(
|
|
19361
|
-
bindParams({
|
|
19362
|
-
id: row.id,
|
|
19363
|
-
sourceTool: row.sourceTool,
|
|
19364
|
-
kind: row.kind,
|
|
19365
|
-
occurredAt: row.occurredAt,
|
|
19366
|
-
contentHash: row.contentHash,
|
|
19367
|
-
content: row.content,
|
|
19368
|
-
metadata: row.metadata
|
|
19369
|
-
})
|
|
19370
|
-
);
|
|
19371
|
-
}
|
|
19372
|
-
// Every recorded event's content hash — the historical backfill loads this once
|
|
19373
|
-
// to skip transcript messages it has already stored, so re-running the scan
|
|
19374
|
-
// never duplicates findings.
|
|
19730
|
+
// Every recorded capture's content hash — the historical backfill loads this
|
|
19731
|
+
// once to skip transcript messages it has already stored, so re-running the
|
|
19732
|
+
// scan never duplicates findings.
|
|
19375
19733
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
19376
19734
|
// async EventsReadPort contract.
|
|
19735
|
+
//
|
|
19736
|
+
// audit_events also holds structural rows (session, run, tool_call, llm_call,
|
|
19737
|
+
// source_lookup, config_scan) with a NULL content_hash, so the capture-kind
|
|
19738
|
+
// predicate isn't load-bearing here — it documents intent and keeps the scan
|
|
19739
|
+
// index-friendly rather than walking rows that can never match.
|
|
19377
19740
|
contentHashes() {
|
|
19378
19741
|
const rows = allRows(
|
|
19379
|
-
this.db.prepare(
|
|
19742
|
+
this.db.prepare(
|
|
19743
|
+
`SELECT content_hash FROM audit_events
|
|
19744
|
+
WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
19745
|
+
)
|
|
19380
19746
|
);
|
|
19381
19747
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
19382
19748
|
}
|
|
@@ -19712,17 +20078,20 @@ function parseExceptionRow(row) {
|
|
|
19712
20078
|
}
|
|
19713
20079
|
|
|
19714
20080
|
// ../../packages/persistence/src/repositories/resolution-sql.ts
|
|
19715
|
-
function
|
|
20081
|
+
function latestResolutionColumnSql(column, findingsAlias) {
|
|
19716
20082
|
return `(
|
|
19717
|
-
SELECT fr
|
|
20083
|
+
SELECT fr.${column} FROM finding_resolution fr
|
|
19718
20084
|
WHERE fr.finding_key = ${findingsAlias}.finding_key
|
|
19719
20085
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
19720
20086
|
LIMIT 1
|
|
19721
20087
|
)`;
|
|
19722
20088
|
}
|
|
20089
|
+
function latestResolutionStatusSql(findingsAlias) {
|
|
20090
|
+
return latestResolutionColumnSql("status", findingsAlias);
|
|
20091
|
+
}
|
|
19723
20092
|
var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
19724
|
-
SELECT finding_key, status FROM (
|
|
19725
|
-
SELECT fr.finding_key, fr.status,
|
|
20093
|
+
SELECT finding_key, status, method, resolved_at FROM (
|
|
20094
|
+
SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
|
|
19726
20095
|
ROW_NUMBER() OVER (
|
|
19727
20096
|
PARTITION BY fr.finding_key
|
|
19728
20097
|
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
@@ -19749,68 +20118,21 @@ var DAY_MS3 = 864e5;
|
|
|
19749
20118
|
var SqliteFindingsRepository = class {
|
|
19750
20119
|
constructor(db) {
|
|
19751
20120
|
this.db = db;
|
|
19752
|
-
this.insertStmt = db.prepare(
|
|
19753
|
-
`INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
|
|
19754
|
-
VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
|
|
19755
|
-
(SELECT occurred_at FROM events WHERE id = :eventId))
|
|
19756
|
-
ON CONFLICT (finding_key) DO UPDATE SET
|
|
19757
|
-
event_id = excluded.event_id,
|
|
19758
|
-
category = excluded.category,
|
|
19759
|
-
severity = excluded.severity,
|
|
19760
|
-
span_start = excluded.span_start,
|
|
19761
|
-
span_end = excluded.span_end,
|
|
19762
|
-
masked_match = excluded.masked_match,
|
|
19763
|
-
action_taken = excluded.action_taken,
|
|
19764
|
-
confidence = excluded.confidence`
|
|
19765
|
-
);
|
|
19766
|
-
this.sessionDupStmt = db.prepare(
|
|
19767
|
-
`SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
|
|
19768
|
-
WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
19769
|
-
AND json_extract(e.metadata, '$.sessionId') = :sessionId
|
|
19770
|
-
LIMIT 1`
|
|
19771
|
-
);
|
|
19772
20121
|
}
|
|
19773
20122
|
db;
|
|
19774
|
-
insertStmt;
|
|
19775
|
-
sessionDupStmt;
|
|
19776
|
-
insertFindings(findings, scope = {}) {
|
|
19777
|
-
for (const finding of findings) {
|
|
19778
|
-
if (scope.sessionId && this.isSessionDuplicate(finding, scope.sessionId)) continue;
|
|
19779
|
-
const row = toFindingRow(finding);
|
|
19780
|
-
this.insertStmt.run({
|
|
19781
|
-
id: row.id,
|
|
19782
|
-
eventId: row.eventId,
|
|
19783
|
-
ruleId: row.ruleId,
|
|
19784
|
-
category: row.category,
|
|
19785
|
-
severity: row.severity,
|
|
19786
|
-
spanStart: row.spanStart,
|
|
19787
|
-
spanEnd: row.spanEnd,
|
|
19788
|
-
maskedMatch: row.maskedMatch,
|
|
19789
|
-
actionTaken: row.actionTaken,
|
|
19790
|
-
confidence: row.confidence,
|
|
19791
|
-
findingKey: row.findingKey ?? null
|
|
19792
|
-
});
|
|
19793
|
-
}
|
|
19794
|
-
}
|
|
19795
|
-
// True when an earlier event in the same session already recorded a finding
|
|
19796
|
-
// with the same rule and masked value. The current event is inserted before
|
|
19797
|
-
// its findings, but carries no findings yet, so this never self-matches.
|
|
19798
|
-
isSessionDuplicate(finding, sessionId) {
|
|
19799
|
-
const hit = this.sessionDupStmt.get({
|
|
19800
|
-
ruleId: finding.ruleId,
|
|
19801
|
-
maskedMatch: finding.maskedMatch,
|
|
19802
|
-
sessionId
|
|
19803
|
-
});
|
|
19804
|
-
return hit !== void 0;
|
|
19805
|
-
}
|
|
19806
20123
|
recentFindings(opts) {
|
|
19807
20124
|
const limit = opts?.limit ?? 50;
|
|
19808
20125
|
const rows = allRows(
|
|
19809
20126
|
this.db.prepare(
|
|
19810
|
-
`SELECT f.id, f.event_id,
|
|
19811
|
-
f.action_taken, f.confidence, e.occurred_at,
|
|
19812
|
-
|
|
19813
|
-
|
|
20127
|
+
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
20128
|
+
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
20129
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20130
|
+
e.event_type AS kind
|
|
20131
|
+
FROM inspection_findings f
|
|
20132
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20133
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20134
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20135
|
+
ORDER BY e.started_at DESC, f.rowid DESC
|
|
19814
20136
|
LIMIT :limit`
|
|
19815
20137
|
),
|
|
19816
20138
|
{ limit }
|
|
@@ -19832,25 +20154,34 @@ var SqliteFindingsRepository = class {
|
|
|
19832
20154
|
);
|
|
19833
20155
|
}
|
|
19834
20156
|
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19835
|
-
* session-stamped
|
|
20157
|
+
* session-stamped audit_events (served by idx_audit_session), so the Activity
|
|
19836
20158
|
* page can label its findings link without the grouped pipeline. */
|
|
19837
20159
|
sessionFindingsCount(sessionId) {
|
|
19838
20160
|
if (!sessionId) return Promise.resolve(0);
|
|
19839
20161
|
return Promise.resolve(
|
|
19840
20162
|
countScalar(
|
|
19841
20163
|
this.db,
|
|
19842
|
-
`SELECT count(*) AS n FROM
|
|
19843
|
-
JOIN
|
|
19844
|
-
WHERE
|
|
20164
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20165
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20166
|
+
WHERE e.root_session_id = :sessionId
|
|
20167
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
|
|
19845
20168
|
{ sessionId }
|
|
19846
20169
|
)
|
|
19847
20170
|
);
|
|
19848
20171
|
}
|
|
19849
|
-
/** Per-rule transcript firing tally for one session —
|
|
19850
|
-
*
|
|
19851
|
-
*
|
|
19852
|
-
*
|
|
19853
|
-
*
|
|
20172
|
+
/** Per-rule transcript firing tally for one session — every detection the
|
|
20173
|
+
* transcript-reconciler pass recorded against the session's `tool_call` rows,
|
|
20174
|
+
* counted per firing rather than per unique value. Rides on session-scoped
|
|
20175
|
+
* grouped responses so the findings view can reconcile the Activity page's
|
|
20176
|
+
* tally with the deduped groups it lists.
|
|
20177
|
+
*
|
|
20178
|
+
* `inspection_findings`/`audit_events` are now the SAME physical tables the
|
|
20179
|
+
* rest of this class reads for the live-capture list above (they used to be
|
|
20180
|
+
* a separate store), so this excludes the four capture kinds those rows
|
|
20181
|
+
* already carry — without that exclusion, every live-capture finding in the
|
|
20182
|
+
* session would be tallied here too, double-counting against the grouped
|
|
20183
|
+
* list this response rides alongside. The reconciler attaches its findings
|
|
20184
|
+
* only to `tool_call` rows, which the exclusion leaves untouched. */
|
|
19854
20185
|
sessionFirings(sessionId) {
|
|
19855
20186
|
return Object.fromEntries(
|
|
19856
20187
|
countBy(
|
|
@@ -19860,18 +20191,25 @@ var SqliteFindingsRepository = class {
|
|
|
19860
20191
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19861
20192
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19862
20193
|
WHERE e.root_session_id = :sessionId
|
|
20194
|
+
AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
19863
20195
|
GROUP BY d.rule_id`,
|
|
19864
20196
|
{ sessionId }
|
|
19865
20197
|
)
|
|
19866
20198
|
);
|
|
19867
20199
|
}
|
|
19868
20200
|
/**
|
|
19869
|
-
* Grouped findings for the dashboard — joins
|
|
19870
|
-
* toolName from
|
|
19871
|
-
*
|
|
20201
|
+
* Grouped findings for the dashboard — joins inspection_findings⋈audit_events
|
|
20202
|
+
* ⋈inspection_definitions (repo/file/toolName from the audit event's
|
|
20203
|
+
* attributes bag, rule_id/category/severity from the definition), scoped to
|
|
20204
|
+
* the four capture kinds (audit_events also holds structural/reconciler/scan
|
|
20205
|
+
* rows this list must never surface), groups by ruleId, computes
|
|
20206
|
+
* per-filter-excluded facets, applies the requested filters, and sorts by
|
|
20207
|
+
* severity then recency. Filtering
|
|
19872
20208
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19873
20209
|
* reflect the full filtered set; `items` is the requested
|
|
19874
|
-
* page (default 50); no cursor (nextCursor is always null).
|
|
20210
|
+
* page (default 50); no cursor (nextCursor is always null). Under a `status`
|
|
20211
|
+
* filter, `totals.findings` counts only instances whose derived status was
|
|
20212
|
+
* requested, and each item's instance preview is narrowed the same way.
|
|
19875
20213
|
*
|
|
19876
20214
|
* Two reads, neither of which materializes a row per finding:
|
|
19877
20215
|
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
@@ -19884,10 +20222,11 @@ var SqliteFindingsRepository = class {
|
|
|
19884
20222
|
* rule is ever restated in SQL.
|
|
19885
20223
|
*/
|
|
19886
20224
|
listGroupedFindings(query) {
|
|
19887
|
-
const sessionPredicate = query.sessionId ? `
|
|
20225
|
+
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20226
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
|
|
19888
20227
|
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19889
20228
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19890
|
-
predicate
|
|
20229
|
+
predicate,
|
|
19891
20230
|
params: sessionParams
|
|
19892
20231
|
});
|
|
19893
20232
|
const rows = allRows(
|
|
@@ -19895,24 +20234,26 @@ var SqliteFindingsRepository = class {
|
|
|
19895
20234
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19896
20235
|
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19897
20236
|
FROM (
|
|
19898
|
-
SELECT f.id AS id,
|
|
19899
|
-
|
|
20237
|
+
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20238
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
19900
20239
|
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19901
|
-
e.
|
|
19902
|
-
json_extract(e.
|
|
19903
|
-
json_extract(e.
|
|
19904
|
-
json_extract(e.
|
|
19905
|
-
e.
|
|
20240
|
+
e.started_at AS occurred_at,
|
|
20241
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
20242
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
20243
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
20244
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
20245
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
19906
20246
|
latest.status AS latest_status,
|
|
19907
20247
|
ROW_NUMBER() OVER (
|
|
19908
|
-
PARTITION BY
|
|
19909
|
-
ORDER BY e.
|
|
20248
|
+
PARTITION BY d.rule_id
|
|
20249
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
19910
20250
|
) AS rn
|
|
19911
|
-
FROM
|
|
19912
|
-
JOIN
|
|
20251
|
+
FROM inspection_findings f
|
|
20252
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20253
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19913
20254
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19914
20255
|
ON latest.finding_key = f.finding_key
|
|
19915
|
-
${
|
|
20256
|
+
${predicate}
|
|
19916
20257
|
)
|
|
19917
20258
|
WHERE rn <= :cap
|
|
19918
20259
|
ORDER BY occurred_at DESC, id DESC`
|
|
@@ -19939,17 +20280,29 @@ var SqliteFindingsRepository = class {
|
|
|
19939
20280
|
severity: query.severity,
|
|
19940
20281
|
providers: query.provider,
|
|
19941
20282
|
actions: query.action,
|
|
20283
|
+
statuses: query.status,
|
|
19942
20284
|
subtype: query.subtype,
|
|
19943
20285
|
q: query.q
|
|
19944
20286
|
};
|
|
19945
20287
|
const facets = computeFindingFacets(allGroups, filterOpts);
|
|
19946
20288
|
const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
|
|
20289
|
+
const statusFilter = query.status ?? [];
|
|
19947
20290
|
const totals = {
|
|
19948
|
-
findings: sorted.reduce((acc, g) =>
|
|
20291
|
+
findings: sorted.reduce((acc, g) => {
|
|
20292
|
+
if (statusFilter.length === 0) return acc + g.instanceCount;
|
|
20293
|
+
const agg = aggregates.get(g.id);
|
|
20294
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
|
|
20295
|
+
}, 0),
|
|
19949
20296
|
groups: sorted.length
|
|
19950
20297
|
};
|
|
19951
20298
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19952
|
-
const
|
|
20299
|
+
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20300
|
+
const items = sorted.slice(0, limit).map(
|
|
20301
|
+
(g) => statusSet ? {
|
|
20302
|
+
...g,
|
|
20303
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
20304
|
+
} : g
|
|
20305
|
+
);
|
|
19953
20306
|
return Promise.resolve({
|
|
19954
20307
|
totals,
|
|
19955
20308
|
facets,
|
|
@@ -19963,45 +20316,62 @@ var SqliteFindingsRepository = class {
|
|
|
19963
20316
|
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19964
20317
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19965
20318
|
*
|
|
19966
|
-
*
|
|
19967
|
-
*
|
|
19968
|
-
*
|
|
19969
|
-
* status
|
|
19970
|
-
*
|
|
19971
|
-
*
|
|
20319
|
+
* A single scan, folded in two levels: the inner SELECT groups by
|
|
20320
|
+
* (rule_id, status tuple) so each (kind, has-key, latest-status) combination
|
|
20321
|
+
* carries its instance count — countInstancesByStatus needs those counts for
|
|
20322
|
+
* status-scoped totals — and the outer SELECT folds the tuples back to one
|
|
20323
|
+
* row per rule. The per-instance sets ride back as group_concat lists of RAW
|
|
20324
|
+
* DB values — source_tool, action_taken, and the tuples deriveFindingStatus
|
|
20325
|
+
* consumes. Aggregating the status INPUTS rather than a status keeps the
|
|
20326
|
+
* classifier itself in @akasecurity/schema, where severitySummary's SQL and
|
|
20327
|
+
* this query can't drift apart on what 'resolved' means (see
|
|
20328
|
+
* resolution-sql.ts). The concat-of-concats can repeat a value across
|
|
20329
|
+
* tuples; the schema mappers dedupe, and each set is bounded by an enum, so
|
|
19972
20330
|
* a group's row stays small however many findings it holds.
|
|
19973
20331
|
*
|
|
19974
20332
|
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19975
|
-
* stay small: the group's distinct repos/filePaths, whose size
|
|
19976
|
-
* distinct paths a rule fired across — for a rule hitting
|
|
19977
|
-
* that is a string proportional to the store (~8MB over
|
|
19978
|
-
* and buildHaystack lowercases a second copy). It buys
|
|
19979
|
-
* match an instance outside the preview, which searching
|
|
19980
|
-
* would silently lose, so it is fetched only when the
|
|
19981
|
-
* carries a `q`.
|
|
20333
|
+
* stay small: the group's per-tuple-distinct repos/filePaths, whose size
|
|
20334
|
+
* tracks how many distinct paths a rule fired across — for a rule hitting
|
|
20335
|
+
* mostly-unique paths that is a string proportional to the store (~8MB over
|
|
20336
|
+
* 200k distinct paths, and buildHaystack lowercases a second copy). It buys
|
|
20337
|
+
* `q` the ability to match an instance outside the preview, which searching
|
|
20338
|
+
* the preview alone would silently lose, so it is fetched only when the
|
|
20339
|
+
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20340
|
+
* path repeating across tuples.)
|
|
19982
20341
|
*/
|
|
19983
20342
|
groupAggregates(withSearchText, scope) {
|
|
19984
|
-
const
|
|
19985
|
-
group_concat(DISTINCT json_extract(e.
|
|
19986
|
-
group_concat(DISTINCT 'via ' || json_extract(e.
|
|
20343
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20344
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
20345
|
+
group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19987
20346
|
const rows = this.db.prepare(
|
|
19988
|
-
`SELECT
|
|
19989
|
-
|
|
19990
|
-
max(
|
|
19991
|
-
group_concat(
|
|
19992
|
-
group_concat(
|
|
19993
|
-
group_concat(
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20347
|
+
`SELECT rule_id,
|
|
20348
|
+
sum(tuple_count) AS instance_count,
|
|
20349
|
+
max(latest_at) AS latest_at,
|
|
20350
|
+
group_concat(source_tools) AS source_tools,
|
|
20351
|
+
group_concat(actions_taken) AS actions_taken,
|
|
20352
|
+
group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
|
|
20353
|
+
group_concat(repos) AS repos,
|
|
20354
|
+
group_concat(files) AS files,
|
|
20355
|
+
group_concat(tool_names) AS tool_names
|
|
20356
|
+
FROM (
|
|
20357
|
+
SELECT d.rule_id AS rule_id,
|
|
20358
|
+
e.event_type || '${TUPLE_SEP}' ||
|
|
20359
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
20360
|
+
coalesce(latest.status, '') AS status_tuple,
|
|
20361
|
+
count(*) AS tuple_count,
|
|
20362
|
+
max(e.started_at) AS latest_at,
|
|
20363
|
+
group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
|
|
20364
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
20365
|
+
${innerSearchColumns}
|
|
20366
|
+
FROM inspection_findings f
|
|
20367
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20368
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20369
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20370
|
+
ON latest.finding_key = f.finding_key
|
|
20371
|
+
${scope.predicate}
|
|
20372
|
+
GROUP BY d.rule_id, status_tuple
|
|
20373
|
+
)
|
|
20374
|
+
GROUP BY rule_id`
|
|
20005
20375
|
).all(scope.params);
|
|
20006
20376
|
return new Map(
|
|
20007
20377
|
rows.map((r) => [
|
|
@@ -20011,13 +20381,14 @@ var SqliteFindingsRepository = class {
|
|
|
20011
20381
|
sourceTools: splitConcat(r.source_tools),
|
|
20012
20382
|
actionsTaken: splitConcat(r.actions_taken),
|
|
20013
20383
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
20014
|
-
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
20384
|
+
const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
|
|
20015
20385
|
return {
|
|
20016
20386
|
// deriveFindingStatus only distinguishes null from non-null here,
|
|
20017
20387
|
// so the marker stands in for the key itself (never rendered).
|
|
20018
20388
|
kind,
|
|
20019
20389
|
findingKey: keyMarker === "" ? null : keyMarker,
|
|
20020
|
-
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
20390
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus,
|
|
20391
|
+
count: Number(count)
|
|
20021
20392
|
};
|
|
20022
20393
|
}),
|
|
20023
20394
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
@@ -20034,10 +20405,21 @@ var SqliteFindingsRepository = class {
|
|
|
20034
20405
|
);
|
|
20035
20406
|
}
|
|
20036
20407
|
healthSummary() {
|
|
20037
|
-
const total = countScalar(
|
|
20408
|
+
const total = countScalar(
|
|
20409
|
+
this.db,
|
|
20410
|
+
`SELECT count(*) AS n FROM inspection_findings f
|
|
20411
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20412
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
|
|
20413
|
+
);
|
|
20038
20414
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
20039
20415
|
const grouped = allRows(
|
|
20040
|
-
this.db.prepare(
|
|
20416
|
+
this.db.prepare(
|
|
20417
|
+
`SELECT f.action_taken AS action_taken, count(*) AS c
|
|
20418
|
+
FROM inspection_findings f
|
|
20419
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20420
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20421
|
+
GROUP BY f.action_taken`
|
|
20422
|
+
)
|
|
20041
20423
|
);
|
|
20042
20424
|
for (const row of grouped) {
|
|
20043
20425
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
@@ -20045,12 +20427,15 @@ var SqliteFindingsRepository = class {
|
|
|
20045
20427
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20046
20428
|
const sevRows = allRows(
|
|
20047
20429
|
this.db.prepare(
|
|
20048
|
-
`SELECT
|
|
20049
|
-
FROM
|
|
20430
|
+
`SELECT d.severity AS severity, count(*) AS c
|
|
20431
|
+
FROM inspection_findings f
|
|
20432
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20433
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20050
20434
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
20051
20435
|
ON latest.finding_key = f.finding_key
|
|
20052
|
-
WHERE
|
|
20053
|
-
|
|
20436
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20437
|
+
AND (latest.status IS NULL OR latest.status != 'resolved')
|
|
20438
|
+
GROUP BY d.severity`
|
|
20054
20439
|
)
|
|
20055
20440
|
);
|
|
20056
20441
|
for (const row of sevRows) {
|
|
@@ -20071,9 +20456,11 @@ var SqliteFindingsRepository = class {
|
|
|
20071
20456
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
20072
20457
|
const rows = allRows(
|
|
20073
20458
|
this.db.prepare(
|
|
20074
|
-
`SELECT date(e.
|
|
20075
|
-
FROM
|
|
20076
|
-
|
|
20459
|
+
`SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
20460
|
+
FROM inspection_findings f
|
|
20461
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20462
|
+
WHERE e.started_at >= :since
|
|
20463
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20077
20464
|
GROUP BY day, f.action_taken`
|
|
20078
20465
|
),
|
|
20079
20466
|
{ since }
|
|
@@ -20138,15 +20525,59 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20138
20525
|
this.insertStmt = db.prepare(
|
|
20139
20526
|
`INSERT INTO inspection_findings
|
|
20140
20527
|
(id, audit_event_id, inspection_definition_id, classified_data_id,
|
|
20141
|
-
span_start, span_end, masked_match, action_taken, confidence
|
|
20528
|
+
span_start, span_end, masked_match, action_taken, confidence,
|
|
20529
|
+
finding_key, first_detected_at)
|
|
20142
20530
|
VALUES
|
|
20143
20531
|
(:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
|
|
20144
|
-
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence
|
|
20145
|
-
|
|
20532
|
+
:spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
|
|
20533
|
+
:findingKey,
|
|
20534
|
+
COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
|
|
20535
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20536
|
+
inspection_definition_id = excluded.inspection_definition_id
|
|
20537
|
+
ON CONFLICT (finding_key) DO UPDATE SET
|
|
20538
|
+
audit_event_id = excluded.audit_event_id,
|
|
20539
|
+
inspection_definition_id = excluded.inspection_definition_id,
|
|
20540
|
+
classified_data_id = excluded.classified_data_id,
|
|
20541
|
+
span_start = excluded.span_start,
|
|
20542
|
+
span_end = excluded.span_end,
|
|
20543
|
+
masked_match = excluded.masked_match,
|
|
20544
|
+
action_taken = excluded.action_taken,
|
|
20545
|
+
confidence = excluded.confidence`
|
|
20546
|
+
);
|
|
20547
|
+
this.sessionDupStmt = db.prepare(
|
|
20548
|
+
`SELECT 1 FROM inspection_findings f
|
|
20549
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
20550
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20551
|
+
WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
|
|
20552
|
+
AND e.root_session_id = :sessionId
|
|
20553
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
20554
|
+
LIMIT 1`
|
|
20555
|
+
);
|
|
20556
|
+
this.eventDupStmt = db.prepare(
|
|
20557
|
+
`SELECT 1 FROM inspection_findings f
|
|
20558
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
20559
|
+
WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
|
|
20560
|
+
AND f.masked_match = :maskedMatch
|
|
20561
|
+
AND f.span_start = :spanStart AND f.span_end = :spanEnd
|
|
20562
|
+
LIMIT 1`
|
|
20146
20563
|
);
|
|
20147
20564
|
}
|
|
20148
20565
|
db;
|
|
20149
20566
|
insertStmt;
|
|
20567
|
+
sessionDupStmt;
|
|
20568
|
+
eventDupStmt;
|
|
20569
|
+
// True when an earlier event in the same session already recorded a finding
|
|
20570
|
+
// with the same rule and masked value. The current event's own findings are
|
|
20571
|
+
// inserted one at a time in caller order, so an earlier finding in the SAME
|
|
20572
|
+
// recordCapture call is visible to a later duplicate check within it too.
|
|
20573
|
+
isSessionDuplicate(ruleId, maskedMatch, sessionId) {
|
|
20574
|
+
return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
|
|
20575
|
+
}
|
|
20576
|
+
// True when this exact detection (rule + masked value + span) is already
|
|
20577
|
+
// recorded against the given audit event.
|
|
20578
|
+
isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
|
|
20579
|
+
return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
|
|
20580
|
+
}
|
|
20150
20581
|
insertFinding(input) {
|
|
20151
20582
|
const row = toInspectionFindingRow(input);
|
|
20152
20583
|
this.insertStmt.run(
|
|
@@ -20159,7 +20590,9 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20159
20590
|
spanEnd: row.spanEnd,
|
|
20160
20591
|
maskedMatch: row.maskedMatch,
|
|
20161
20592
|
actionTaken: row.actionTaken,
|
|
20162
|
-
confidence: row.confidence
|
|
20593
|
+
confidence: row.confidence,
|
|
20594
|
+
findingKey: row.findingKey,
|
|
20595
|
+
firstDetectedAt: row.firstDetectedAt
|
|
20163
20596
|
})
|
|
20164
20597
|
);
|
|
20165
20598
|
}
|
|
@@ -20212,8 +20645,8 @@ function isParseableBinaryVersion(version2) {
|
|
|
20212
20645
|
|
|
20213
20646
|
// ../../packages/persistence/src/repositories/installed-packs.ts
|
|
20214
20647
|
var DEFAULT_POLICY_ID = DEFAULT_PACK_POLICY_ID;
|
|
20215
|
-
function inventorySignature(
|
|
20216
|
-
return
|
|
20648
|
+
function inventorySignature(packs2) {
|
|
20649
|
+
return packs2.map((p) => `${p.namespace}/${p.packId}@${p.version}#${hashRules(p.rulesJson)}`).sort().join(",");
|
|
20217
20650
|
}
|
|
20218
20651
|
function hashRules(rulesJson) {
|
|
20219
20652
|
return createHash2("sha1").update(rulesJson).digest("hex");
|
|
@@ -20297,10 +20730,10 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20297
20730
|
* Optional — callers that can't know their version (old writers, most plugin
|
|
20298
20731
|
* hooks) simply leave it null.
|
|
20299
20732
|
*/
|
|
20300
|
-
recordInventory(
|
|
20301
|
-
if (
|
|
20733
|
+
recordInventory(packs2, meta3) {
|
|
20734
|
+
if (packs2.length === 0) return;
|
|
20302
20735
|
try {
|
|
20303
|
-
const rows =
|
|
20736
|
+
const rows = packs2.map((pack) => ({
|
|
20304
20737
|
namespace: pack.namespace,
|
|
20305
20738
|
packId: pack.packId,
|
|
20306
20739
|
version: pack.version,
|
|
@@ -20431,7 +20864,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20431
20864
|
installedRuleset() {
|
|
20432
20865
|
const rows = allRows(
|
|
20433
20866
|
this.db.prepare(
|
|
20434
|
-
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20867
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
|
|
20435
20868
|
)
|
|
20436
20869
|
);
|
|
20437
20870
|
const out = {
|
|
@@ -20439,7 +20872,8 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20439
20872
|
enabledPacks: 0,
|
|
20440
20873
|
rules: [],
|
|
20441
20874
|
invalidRules: 0,
|
|
20442
|
-
ruleActions: /* @__PURE__ */ new Map()
|
|
20875
|
+
ruleActions: /* @__PURE__ */ new Map(),
|
|
20876
|
+
ruleVersions: /* @__PURE__ */ new Map()
|
|
20443
20877
|
};
|
|
20444
20878
|
for (const row of rows) {
|
|
20445
20879
|
if (!intToBool(row.enabled)) continue;
|
|
@@ -20461,6 +20895,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20461
20895
|
if (parsed.success) {
|
|
20462
20896
|
out.rules.push(parsed.data);
|
|
20463
20897
|
out.ruleActions.set(parsed.data.id, action);
|
|
20898
|
+
out.ruleVersions.set(parsed.data.id, row.version);
|
|
20464
20899
|
} else out.invalidRules += 1;
|
|
20465
20900
|
}
|
|
20466
20901
|
}
|
|
@@ -21529,8 +21964,8 @@ var SqlitePoliciesRepository = class {
|
|
|
21529
21964
|
|
|
21530
21965
|
// ../../packages/persistence/src/repositories/policy-catalog.ts
|
|
21531
21966
|
var SqlitePolicyCatalogRepository = class {
|
|
21532
|
-
constructor(
|
|
21533
|
-
this.packs =
|
|
21967
|
+
constructor(packs2) {
|
|
21968
|
+
this.packs = packs2;
|
|
21534
21969
|
}
|
|
21535
21970
|
packs;
|
|
21536
21971
|
getPolicyList(kind) {
|
|
@@ -21635,19 +22070,19 @@ var SqliteResolutionsRepository = class {
|
|
|
21635
22070
|
);
|
|
21636
22071
|
this.openAtRestStmt = db.prepare(
|
|
21637
22072
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21638
|
-
FROM
|
|
21639
|
-
JOIN
|
|
21640
|
-
WHERE e.
|
|
21641
|
-
AND json_extract(e.
|
|
22073
|
+
FROM inspection_findings f
|
|
22074
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22075
|
+
WHERE e.event_type = 'code_change'
|
|
22076
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21642
22077
|
AND f.finding_key IS NOT NULL
|
|
21643
22078
|
AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
|
|
21644
22079
|
);
|
|
21645
22080
|
this.resolvedAtRestStmt = db.prepare(
|
|
21646
22081
|
`SELECT DISTINCT f.finding_key AS finding_key
|
|
21647
|
-
FROM
|
|
21648
|
-
JOIN
|
|
21649
|
-
WHERE e.
|
|
21650
|
-
AND json_extract(e.
|
|
22082
|
+
FROM inspection_findings f
|
|
22083
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22084
|
+
WHERE e.event_type = 'code_change'
|
|
22085
|
+
AND json_extract(e.attributes, '$.file_path') = :path
|
|
21651
22086
|
AND f.finding_key IS NOT NULL
|
|
21652
22087
|
AND ${latestResolutionStatusSql("f")} = 'resolved'`
|
|
21653
22088
|
);
|
|
@@ -21870,25 +22305,27 @@ var SqliteSecurityRepository = class {
|
|
|
21870
22305
|
severitySummary() {
|
|
21871
22306
|
const rows = allRows(
|
|
21872
22307
|
this.db.prepare(
|
|
21873
|
-
`SELECT
|
|
22308
|
+
`SELECT d.severity AS severity,
|
|
21874
22309
|
COUNT(*) AS count,
|
|
21875
22310
|
SUM(CASE
|
|
21876
|
-
WHEN e.
|
|
22311
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
21877
22312
|
WHEN f.finding_key IS NULL THEN 0
|
|
21878
22313
|
WHEN latest.status = 'resolved' THEN 1
|
|
21879
22314
|
ELSE 0
|
|
21880
22315
|
END) AS caught,
|
|
21881
22316
|
SUM(CASE
|
|
21882
|
-
WHEN e.
|
|
22317
|
+
WHEN e.event_type = 'code_change'
|
|
21883
22318
|
AND f.finding_key IS NOT NULL
|
|
21884
22319
|
AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
|
|
21885
22320
|
ELSE 0
|
|
21886
22321
|
END) AS open_at_rest
|
|
21887
|
-
FROM
|
|
21888
|
-
JOIN
|
|
22322
|
+
FROM inspection_findings f
|
|
22323
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22324
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21889
22325
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21890
22326
|
ON latest.finding_key = f.finding_key
|
|
21891
|
-
|
|
22327
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22328
|
+
GROUP BY d.severity`
|
|
21892
22329
|
)
|
|
21893
22330
|
);
|
|
21894
22331
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
@@ -21954,7 +22391,7 @@ var SqliteSecurityRepository = class {
|
|
|
21954
22391
|
// Mean time-to-remediate per bucket, split by severity — a sibling of
|
|
21955
22392
|
// findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
|
|
21956
22393
|
// on a different timestamp: findingsTimeseries buckets by first-detection
|
|
21957
|
-
// (
|
|
22394
|
+
// (audit_events.started_at), this buckets by resolution time (the latest
|
|
21958
22395
|
// finding_resolution row's resolved_at) — it's a "resolved in this bucket"
|
|
21959
22396
|
// trend, not a "detected in this bucket" one. Only findings whose LATEST
|
|
21960
22397
|
// resolution row (latest-resolution-wins, same correlated subquery as
|
|
@@ -21979,30 +22416,20 @@ var SqliteSecurityRepository = class {
|
|
|
21979
22416
|
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21980
22417
|
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21981
22418
|
// measures from first sighting — not the latest re-scan's event, whose
|
|
21982
|
-
//
|
|
21983
|
-
// the parent event's
|
|
21984
|
-
// backfill left null.
|
|
21985
|
-
`SELECT COALESCE(f.first_detected_at, e.
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
WHERE fr.finding_key = f.finding_key
|
|
21995
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
21996
|
-
LIMIT 1
|
|
21997
|
-
) AS latest_method,
|
|
21998
|
-
(
|
|
21999
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22000
|
-
WHERE fr.finding_key = f.finding_key
|
|
22001
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22002
|
-
LIMIT 1
|
|
22003
|
-
) AS latest_resolved_at
|
|
22004
|
-
FROM findings f JOIN events e ON e.id = f.event_id
|
|
22419
|
+
// started_at the upsert overwrites onto inspection_findings.audit_event_id.
|
|
22420
|
+
// COALESCE onto the parent event's started_at defends against any
|
|
22421
|
+
// legacy/edge row the backfill left null.
|
|
22422
|
+
`SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
|
|
22423
|
+
latest.status AS latest_status,
|
|
22424
|
+
latest.method AS latest_method,
|
|
22425
|
+
latest.resolved_at AS latest_resolved_at
|
|
22426
|
+
FROM inspection_findings f
|
|
22427
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22428
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22429
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22430
|
+
ON latest.finding_key = f.finding_key
|
|
22005
22431
|
WHERE f.finding_key IS NOT NULL
|
|
22432
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22006
22433
|
AND EXISTS (
|
|
22007
22434
|
SELECT 1 FROM finding_resolution fr
|
|
22008
22435
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -22049,11 +22476,13 @@ var SqliteSecurityRepository = class {
|
|
|
22049
22476
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
22050
22477
|
const rows = allRows(
|
|
22051
22478
|
this.db.prepare(
|
|
22052
|
-
`SELECT json_extract(e.
|
|
22053
|
-
FROM
|
|
22054
|
-
|
|
22055
|
-
|
|
22056
|
-
AND
|
|
22479
|
+
`SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
|
|
22480
|
+
FROM inspection_findings f
|
|
22481
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22482
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22483
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22484
|
+
AND json_extract(e.attributes, '$.repo') IS NOT NULL
|
|
22485
|
+
AND json_extract(e.attributes, '$.repo') != ''
|
|
22057
22486
|
GROUP BY repo
|
|
22058
22487
|
ORDER BY c DESC, repo
|
|
22059
22488
|
LIMIT :limit`
|
|
@@ -22077,44 +22506,28 @@ var SqliteSecurityRepository = class {
|
|
|
22077
22506
|
// secret came back) is excluded — it is not currently resolved. Legacy
|
|
22078
22507
|
// at-rest findings with finding_key IS NULL are excluded outright (the
|
|
22079
22508
|
// resolution lifecycle can never attach to them). Path comes from the
|
|
22080
|
-
// finding's parent event (
|
|
22081
|
-
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22082
|
-
// capped at `limit`.
|
|
22509
|
+
// finding's parent event (event_type 'code_change', attributes.file_path) —
|
|
22510
|
+
// mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
|
|
22511
|
+
// DESC, capped at `limit`.
|
|
22083
22512
|
recentlyResolved(limit = 20) {
|
|
22084
22513
|
const rows = allRows(
|
|
22085
22514
|
this.db.prepare(
|
|
22086
22515
|
`SELECT f.finding_key AS finding_key,
|
|
22087
|
-
|
|
22088
|
-
|
|
22089
|
-
json_extract(e.
|
|
22090
|
-
COALESCE(f.first_detected_at, e.
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22098
|
-
WHERE e.kind = 'code_change'
|
|
22516
|
+
d.rule_id AS rule_id,
|
|
22517
|
+
d.severity AS severity,
|
|
22518
|
+
json_extract(e.attributes, '$.file_path') AS path,
|
|
22519
|
+
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
22520
|
+
latest.resolved_at AS latest_resolved_at
|
|
22521
|
+
FROM inspection_findings f
|
|
22522
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22523
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22524
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
22525
|
+
ON latest.finding_key = f.finding_key
|
|
22526
|
+
WHERE e.event_type = 'code_change'
|
|
22099
22527
|
AND f.finding_key IS NOT NULL
|
|
22100
|
-
AND
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22104
|
-
LIMIT 1
|
|
22105
|
-
) = 'resolved'
|
|
22106
|
-
AND (
|
|
22107
|
-
SELECT fr.method FROM finding_resolution fr
|
|
22108
|
-
WHERE fr.finding_key = f.finding_key
|
|
22109
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22110
|
-
LIMIT 1
|
|
22111
|
-
) = 'fixed-at-source'
|
|
22112
|
-
AND (
|
|
22113
|
-
SELECT fr.resolved_at FROM finding_resolution fr
|
|
22114
|
-
WHERE fr.finding_key = f.finding_key
|
|
22115
|
-
ORDER BY fr.created_at DESC, fr.rowid DESC
|
|
22116
|
-
LIMIT 1
|
|
22117
|
-
) IS NOT NULL
|
|
22528
|
+
AND latest.status = 'resolved'
|
|
22529
|
+
AND latest.method = 'fixed-at-source'
|
|
22530
|
+
AND latest.resolved_at IS NOT NULL
|
|
22118
22531
|
ORDER BY latest_resolved_at DESC
|
|
22119
22532
|
LIMIT :limit`
|
|
22120
22533
|
),
|
|
@@ -22133,15 +22546,18 @@ var SqliteSecurityRepository = class {
|
|
|
22133
22546
|
return Promise.resolve({ items });
|
|
22134
22547
|
}
|
|
22135
22548
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
22136
|
-
// epoch-millis timestamp.
|
|
22549
|
+
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
22137
22550
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
22138
22551
|
findingsInRange(fromMs, toMs) {
|
|
22139
22552
|
const rows = allRows(
|
|
22140
22553
|
this.db.prepare(
|
|
22141
|
-
`SELECT e.
|
|
22142
|
-
FROM
|
|
22143
|
-
|
|
22144
|
-
|
|
22554
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
|
|
22555
|
+
FROM inspection_findings f
|
|
22556
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
22557
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
22558
|
+
WHERE e.started_at >= :from AND e.started_at < :to
|
|
22559
|
+
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
22560
|
+
ORDER BY e.started_at`
|
|
22145
22561
|
),
|
|
22146
22562
|
{ from: fromMs, to: toMs }
|
|
22147
22563
|
);
|
|
@@ -22956,63 +23372,159 @@ function purgeSampleData(db) {
|
|
|
22956
23372
|
function linkHost(input, hostId) {
|
|
22957
23373
|
return hostId ? { ...input, hostId } : input;
|
|
22958
23374
|
}
|
|
23375
|
+
function closeQuietly(db) {
|
|
23376
|
+
try {
|
|
23377
|
+
db.close();
|
|
23378
|
+
} catch {
|
|
23379
|
+
}
|
|
23380
|
+
}
|
|
22959
23381
|
function openWithPragmas(file2) {
|
|
22960
23382
|
const db = new DatabaseSync(file2);
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
23383
|
+
try {
|
|
23384
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
23385
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
23386
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
23387
|
+
} catch (err) {
|
|
23388
|
+
closeQuietly(db);
|
|
23389
|
+
throw err;
|
|
23390
|
+
}
|
|
22964
23391
|
return db;
|
|
22965
23392
|
}
|
|
22966
23393
|
function backupLegacyStore(file2) {
|
|
22967
23394
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
23395
|
+
renameSync2(file2, backup);
|
|
23396
|
+
tightenFile(backup);
|
|
23397
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
23398
|
+
if (existsSync(sidecar)) rmSync2(sidecar);
|
|
22971
23399
|
}
|
|
22972
23400
|
return backup;
|
|
22973
23401
|
}
|
|
23402
|
+
function openAndInitialize(file2) {
|
|
23403
|
+
let db = openWithPragmas(file2);
|
|
23404
|
+
try {
|
|
23405
|
+
if (isForeignSqliteLineage(db)) {
|
|
23406
|
+
db.close();
|
|
23407
|
+
const backup = backupLegacyStore(file2);
|
|
23408
|
+
db = openWithPragmas(file2);
|
|
23409
|
+
akaWarn(
|
|
23410
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
23411
|
+
);
|
|
23412
|
+
}
|
|
23413
|
+
applyMigrations(db, file2);
|
|
23414
|
+
tightenPerms(file2);
|
|
23415
|
+
const policies = new SqlitePoliciesRepository(db);
|
|
23416
|
+
const installedPacks = new SqliteInstalledPacksRepository(db);
|
|
23417
|
+
const repositories = {
|
|
23418
|
+
events: new SqliteEventsRepository(db),
|
|
23419
|
+
findings: new SqliteFindingsRepository(db),
|
|
23420
|
+
policies,
|
|
23421
|
+
installedPacks,
|
|
23422
|
+
scanLedger: new SqliteScanLedgerRepository(db),
|
|
23423
|
+
exceptions: new SqliteExceptionsRepository(db),
|
|
23424
|
+
resolutions: new SqliteResolutionsRepository(db),
|
|
23425
|
+
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
23426
|
+
security: new SqliteSecurityRepository(db),
|
|
23427
|
+
detections: new SqliteDetectionsRepository(db),
|
|
23428
|
+
shares: new SqliteSharesRepository(db),
|
|
23429
|
+
policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
|
|
23430
|
+
inventory: new SqliteInventoryRepository(db),
|
|
23431
|
+
inventoryAssets: new SqliteInventoryAssetsRepository(db),
|
|
23432
|
+
projectFiles: new SqliteProjectFilesRepository(db),
|
|
23433
|
+
activity: new SqliteActivityRepository(db),
|
|
23434
|
+
sourceProject: new SqliteSourceProjectRepository(db),
|
|
23435
|
+
auditEvents: new SqliteAuditEventsRepository(db),
|
|
23436
|
+
classifiedData: new SqliteClassifiedDataRepository(db),
|
|
23437
|
+
inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
|
|
23438
|
+
inspectionFindings: new SqliteInspectionFindingsRepository(db),
|
|
23439
|
+
configInventory: new SqliteConfigInventoryRepository(db)
|
|
23440
|
+
};
|
|
23441
|
+
policies.seedDefaults();
|
|
23442
|
+
return { db, ...repositories };
|
|
23443
|
+
} catch (err) {
|
|
23444
|
+
closeQuietly(db);
|
|
23445
|
+
throw err;
|
|
23446
|
+
}
|
|
23447
|
+
}
|
|
22974
23448
|
function openLocalDatabase(dir) {
|
|
22975
23449
|
ensureDataDirSync(dir);
|
|
22976
23450
|
const file2 = join(dir, DB_FILENAME);
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
const projectFiles = new SqliteProjectFilesRepository(db);
|
|
23003
|
-
const activity = new SqliteActivityRepository(db);
|
|
23004
|
-
const sourceProject = new SqliteSourceProjectRepository(db);
|
|
23005
|
-
const auditEvents = new SqliteAuditEventsRepository(db);
|
|
23006
|
-
const classifiedData = new SqliteClassifiedDataRepository(db);
|
|
23007
|
-
const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
|
|
23008
|
-
const inspectionFindings = new SqliteInspectionFindingsRepository(db);
|
|
23009
|
-
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
23010
|
-
policies.seedDefaults();
|
|
23451
|
+
const {
|
|
23452
|
+
db,
|
|
23453
|
+
events,
|
|
23454
|
+
findings,
|
|
23455
|
+
policies,
|
|
23456
|
+
installedPacks,
|
|
23457
|
+
scanLedger,
|
|
23458
|
+
exceptions,
|
|
23459
|
+
resolutions,
|
|
23460
|
+
ruleProbeCache,
|
|
23461
|
+
security,
|
|
23462
|
+
detections,
|
|
23463
|
+
shares,
|
|
23464
|
+
policyCatalog,
|
|
23465
|
+
inventory,
|
|
23466
|
+
inventoryAssets,
|
|
23467
|
+
projectFiles,
|
|
23468
|
+
activity,
|
|
23469
|
+
sourceProject,
|
|
23470
|
+
auditEvents,
|
|
23471
|
+
classifiedData,
|
|
23472
|
+
inspectionDefinitions,
|
|
23473
|
+
inspectionFindings,
|
|
23474
|
+
configInventory
|
|
23475
|
+
} = openAndInitialize(file2);
|
|
23011
23476
|
function recordCapture(event, detected) {
|
|
23012
23477
|
failOpenTransaction(db, () => {
|
|
23013
|
-
events.insertEvent(event);
|
|
23014
23478
|
const sessionId = event.metadata?.sessionId;
|
|
23015
|
-
|
|
23479
|
+
if (sessionId) {
|
|
23480
|
+
auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
|
|
23481
|
+
}
|
|
23482
|
+
const auditEventId = captureId(
|
|
23483
|
+
sessionId ?? null,
|
|
23484
|
+
event.contentHash,
|
|
23485
|
+
event.metadata?.filePath ?? null
|
|
23486
|
+
);
|
|
23487
|
+
auditEvents.insertAuditEvent({
|
|
23488
|
+
id: auditEventId,
|
|
23489
|
+
eventType: event.kind,
|
|
23490
|
+
startedAt: event.occurredAt,
|
|
23491
|
+
parentId: sessionId,
|
|
23492
|
+
rootSessionId: sessionId,
|
|
23493
|
+
content: event.content,
|
|
23494
|
+
contentHash: event.contentHash,
|
|
23495
|
+
attributes: toCaptureAttributes(event)
|
|
23496
|
+
});
|
|
23497
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
23498
|
+
for (const finding of detected) {
|
|
23499
|
+
if (sessionId && inspectionFindings.isSessionDuplicate(finding.ruleId, finding.maskedMatch, sessionId)) {
|
|
23500
|
+
continue;
|
|
23501
|
+
}
|
|
23502
|
+
if (inspectionFindings.isEventDuplicate(
|
|
23503
|
+
auditEventId,
|
|
23504
|
+
finding.ruleId,
|
|
23505
|
+
finding.maskedMatch,
|
|
23506
|
+
finding.span.start,
|
|
23507
|
+
finding.span.end
|
|
23508
|
+
)) {
|
|
23509
|
+
continue;
|
|
23510
|
+
}
|
|
23511
|
+
const key = `${finding.ruleId}@${captureDefinitionVersion(finding)}`;
|
|
23512
|
+
let definitionId = definitionIds.get(key);
|
|
23513
|
+
if (!definitionId) {
|
|
23514
|
+
definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding));
|
|
23515
|
+
definitionIds.set(key, definitionId);
|
|
23516
|
+
}
|
|
23517
|
+
inspectionFindings.insertFinding({
|
|
23518
|
+
id: finding.id,
|
|
23519
|
+
auditEventId,
|
|
23520
|
+
inspectionDefinitionId: definitionId,
|
|
23521
|
+
span: finding.span,
|
|
23522
|
+
maskedMatch: finding.maskedMatch,
|
|
23523
|
+
actionTaken: finding.actionTaken,
|
|
23524
|
+
confidence: finding.confidence,
|
|
23525
|
+
findingKey: finding.findingKey ?? void 0
|
|
23526
|
+
});
|
|
23527
|
+
}
|
|
23016
23528
|
});
|
|
23017
23529
|
}
|
|
23018
23530
|
function ensureInventory(ctx) {
|
|
@@ -23160,14 +23672,18 @@ function openLocalDatabase(dir) {
|
|
|
23160
23672
|
};
|
|
23161
23673
|
}
|
|
23162
23674
|
|
|
23675
|
+
// ../../packages/persistence/src/finding-key.ts
|
|
23676
|
+
import { createHash as createHash3 } from "crypto";
|
|
23677
|
+
|
|
23163
23678
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23164
23679
|
import { createHmac, randomBytes } from "crypto";
|
|
23165
|
-
import {
|
|
23680
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
23166
23681
|
import { join as join2 } from "path";
|
|
23682
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
23167
23683
|
|
|
23168
23684
|
// ../../packages/persistence/src/local-layout.ts
|
|
23169
|
-
import {
|
|
23170
|
-
import {
|
|
23685
|
+
import { renameSync as renameSync3 } from "fs";
|
|
23686
|
+
import { mkdir } from "fs/promises";
|
|
23171
23687
|
import { homedir } from "os";
|
|
23172
23688
|
import { join as join3 } from "path";
|
|
23173
23689
|
function defaultDataDir() {
|
|
@@ -23182,6 +23698,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23182
23698
|
function dbPath(base = defaultDataDir()) {
|
|
23183
23699
|
return join3(dataDir(base), "aka.db");
|
|
23184
23700
|
}
|
|
23701
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23702
|
+
ensureDataDirSync(dir);
|
|
23703
|
+
}
|
|
23185
23704
|
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
23186
23705
|
const moves = [
|
|
23187
23706
|
{ name: "config.json", dest: settingsDir(base) },
|
|
@@ -23189,19 +23708,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23189
23708
|
];
|
|
23190
23709
|
for (const { name, dest } of moves) {
|
|
23191
23710
|
try {
|
|
23192
|
-
|
|
23193
|
-
|
|
23194
|
-
|
|
23195
|
-
|
|
23196
|
-
}
|
|
23197
|
-
renameSync3(join3(base, name), join3(dest, name));
|
|
23711
|
+
ensureDataDirSync(dest);
|
|
23712
|
+
const moved = join3(dest, name);
|
|
23713
|
+
renameSync3(join3(base, name), moved);
|
|
23714
|
+
tightenFile(moved);
|
|
23198
23715
|
} catch {
|
|
23199
23716
|
}
|
|
23200
23717
|
}
|
|
23201
23718
|
}
|
|
23202
23719
|
|
|
23203
23720
|
// ../../packages/persistence/src/settings.ts
|
|
23204
|
-
import { readFileSync as readFileSync2
|
|
23721
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
23205
23722
|
import { join as join4 } from "path";
|
|
23206
23723
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23207
23724
|
const record2 = readJson(join4(settingsDir(base), "settings.json"));
|
|
@@ -23223,9 +23740,13 @@ function readJson(file2) {
|
|
|
23223
23740
|
}
|
|
23224
23741
|
|
|
23225
23742
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23226
|
-
import { existsSync as
|
|
23743
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
23227
23744
|
import { join as join5 } from "path";
|
|
23228
23745
|
|
|
23746
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
23747
|
+
import { existsSync as existsSync4 } from "fs";
|
|
23748
|
+
import { join as join6 } from "path";
|
|
23749
|
+
|
|
23229
23750
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
23230
23751
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
23231
23752
|
var booleanish = external_exports.string().optional().transform((v) => {
|
|
@@ -23276,6 +23797,12 @@ function resolveProvider() {
|
|
|
23276
23797
|
|
|
23277
23798
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23278
23799
|
function loadConfig(base = defaultDataDir()) {
|
|
23800
|
+
try {
|
|
23801
|
+
ensureLayoutDirSync(base);
|
|
23802
|
+
const settingsFile = join6(settingsDir(base), "settings.json");
|
|
23803
|
+
if (existsSync4(settingsFile)) tightenFile(settingsFile);
|
|
23804
|
+
} catch {
|
|
23805
|
+
}
|
|
23279
23806
|
migrateLegacyLayout(base);
|
|
23280
23807
|
const settings = readWorkspaceSettings(base);
|
|
23281
23808
|
return {
|
|
@@ -23298,7 +23825,7 @@ function resolveProviderSafe() {
|
|
|
23298
23825
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23299
23826
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
23300
23827
|
import { homedir as homedir2 } from "os";
|
|
23301
|
-
import { basename as basename2, join as
|
|
23828
|
+
import { basename as basename2, join as join8 } from "path";
|
|
23302
23829
|
|
|
23303
23830
|
// ../../packages/detections/src/egress/registry.ts
|
|
23304
23831
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -23922,9 +24449,156 @@ var RegexMatcher2 = class {
|
|
|
23922
24449
|
}
|
|
23923
24450
|
};
|
|
23924
24451
|
|
|
24452
|
+
// ../../packages/detections/src/validators/entropy.ts
|
|
24453
|
+
function shannonEntropy(str2) {
|
|
24454
|
+
const freq = {};
|
|
24455
|
+
for (const ch of str2) {
|
|
24456
|
+
freq[ch] = (freq[ch] ?? 0) + 1;
|
|
24457
|
+
}
|
|
24458
|
+
let entropy = 0;
|
|
24459
|
+
for (const count of Object.values(freq)) {
|
|
24460
|
+
const p = count / str2.length;
|
|
24461
|
+
entropy -= p * Math.log2(p);
|
|
24462
|
+
}
|
|
24463
|
+
return entropy;
|
|
24464
|
+
}
|
|
24465
|
+
function isHighEntropy(value, threshold = 3.5, minLength = 20) {
|
|
24466
|
+
return value.length >= minLength && shannonEntropy(value) >= threshold;
|
|
24467
|
+
}
|
|
24468
|
+
|
|
24469
|
+
// ../../packages/detections/src/validators/luhn.ts
|
|
24470
|
+
function luhnCheck(digits) {
|
|
24471
|
+
const nums = digits.replace(/\D/g, "");
|
|
24472
|
+
if (nums.length < 13) return false;
|
|
24473
|
+
let sum = 0;
|
|
24474
|
+
let isOdd = true;
|
|
24475
|
+
for (let i = nums.length - 1; i >= 0; i--) {
|
|
24476
|
+
let digit = parseInt(nums[i] ?? "0", 10);
|
|
24477
|
+
if (!isOdd) {
|
|
24478
|
+
digit *= 2;
|
|
24479
|
+
if (digit > 9) digit -= 9;
|
|
24480
|
+
}
|
|
24481
|
+
sum += digit;
|
|
24482
|
+
isOdd = !isOdd;
|
|
24483
|
+
}
|
|
24484
|
+
return sum % 10 === 0;
|
|
24485
|
+
}
|
|
24486
|
+
|
|
23925
24487
|
// ../../packages/detections/src/engine.ts
|
|
23926
24488
|
var keywordMatcher = new KeywordMatcher2();
|
|
23927
24489
|
var regexMatcher = new RegexMatcher2();
|
|
24490
|
+
var packs = /* @__PURE__ */ new Map();
|
|
24491
|
+
var POST_VALIDATORS = {
|
|
24492
|
+
entropy: (value, config2) => isHighEntropy(value, numberOption(config2, "threshold"), numberOption(config2, "minLength")),
|
|
24493
|
+
luhn: (value) => luhnCheck(value)
|
|
24494
|
+
};
|
|
24495
|
+
function numberOption(config2, key) {
|
|
24496
|
+
const value = config2?.[key];
|
|
24497
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
24498
|
+
}
|
|
24499
|
+
function passesPostValidators(rule, value) {
|
|
24500
|
+
const validators = rule.postValidators;
|
|
24501
|
+
if (!validators || validators.length === 0) return true;
|
|
24502
|
+
for (const ref of validators) {
|
|
24503
|
+
const name = typeof ref === "string" ? ref : ref.name;
|
|
24504
|
+
const config2 = typeof ref === "string" ? void 0 : ref.config;
|
|
24505
|
+
const validate = POST_VALIDATORS[name];
|
|
24506
|
+
if (validate && !validate(value, config2)) return false;
|
|
24507
|
+
}
|
|
24508
|
+
return true;
|
|
24509
|
+
}
|
|
24510
|
+
function registerPack(pack) {
|
|
24511
|
+
packs.set(pack.id, pack);
|
|
24512
|
+
}
|
|
24513
|
+
function getLoadedRules() {
|
|
24514
|
+
return [...packs.values()].flatMap((p) => p.rules);
|
|
24515
|
+
}
|
|
24516
|
+
function isCorroborated(candidate, candidates, text) {
|
|
24517
|
+
const req = candidate.rule.requiresNearby;
|
|
24518
|
+
if (!req) return true;
|
|
24519
|
+
const halfWindow = req.windowChars;
|
|
24520
|
+
const { start, end } = candidate.match.span;
|
|
24521
|
+
const winStart = start - halfWindow;
|
|
24522
|
+
const winEnd = end + halfWindow;
|
|
24523
|
+
const categories = req.categories;
|
|
24524
|
+
const ruleIds = req.ruleIds;
|
|
24525
|
+
if (categories?.length || ruleIds?.length) {
|
|
24526
|
+
for (const other of candidates) {
|
|
24527
|
+
if (other === candidate) continue;
|
|
24528
|
+
const os = other.match.span;
|
|
24529
|
+
if (os.end < winStart || os.start > winEnd) continue;
|
|
24530
|
+
if (other.match.ruleId !== candidate.match.ruleId && categories?.includes(other.match.category)) {
|
|
24531
|
+
return true;
|
|
24532
|
+
}
|
|
24533
|
+
if (ruleIds?.includes(other.match.ruleId)) return true;
|
|
24534
|
+
}
|
|
24535
|
+
}
|
|
24536
|
+
const labels = req.labels;
|
|
24537
|
+
if (labels && labels.length > 0) {
|
|
24538
|
+
const haystack = text.slice(Math.max(0, winStart), winEnd);
|
|
24539
|
+
for (const label of labels) {
|
|
24540
|
+
const trimmed = label.trim();
|
|
24541
|
+
if (trimmed.length === 0) continue;
|
|
24542
|
+
const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
|
|
24543
|
+
if (re.test(haystack)) return true;
|
|
24544
|
+
}
|
|
24545
|
+
}
|
|
24546
|
+
return false;
|
|
24547
|
+
}
|
|
24548
|
+
function extensionOf(filePath) {
|
|
24549
|
+
const base = filePath.slice(Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\")) + 1);
|
|
24550
|
+
const dot = base.lastIndexOf(".");
|
|
24551
|
+
return dot > 0 ? base.slice(dot).toLowerCase() : void 0;
|
|
24552
|
+
}
|
|
24553
|
+
function ruleApplies(rule, extension) {
|
|
24554
|
+
if (!rule.appliesTo || extension === void 0) return true;
|
|
24555
|
+
return rule.appliesTo.extensions.some((e) => e.toLowerCase() === extension);
|
|
24556
|
+
}
|
|
24557
|
+
function scan(text, rules, context) {
|
|
24558
|
+
const ruleset = rules ?? getLoadedRules();
|
|
24559
|
+
const extension = context?.filePath ? extensionOf(context.filePath) : void 0;
|
|
24560
|
+
const candidates = [];
|
|
24561
|
+
for (const rule of ruleset) {
|
|
24562
|
+
if (!ruleApplies(rule, extension)) continue;
|
|
24563
|
+
let spans;
|
|
24564
|
+
if (rule.matcher.type === "keyword") {
|
|
24565
|
+
spans = keywordMatcher.match(text, rule);
|
|
24566
|
+
} else if (rule.matcher.type === "regex") {
|
|
24567
|
+
spans = regexMatcher.match(text, rule);
|
|
24568
|
+
} else {
|
|
24569
|
+
continue;
|
|
24570
|
+
}
|
|
24571
|
+
for (const span of spans) {
|
|
24572
|
+
const rawMatch = text.slice(span.start, span.end);
|
|
24573
|
+
if (!passesPostValidators(rule, rawMatch)) continue;
|
|
24574
|
+
candidates.push({
|
|
24575
|
+
rule,
|
|
24576
|
+
match: {
|
|
24577
|
+
ruleId: rule.id,
|
|
24578
|
+
category: rule.category,
|
|
24579
|
+
severity: rule.severity,
|
|
24580
|
+
span,
|
|
24581
|
+
rawMatch,
|
|
24582
|
+
confidence: 0.9
|
|
24583
|
+
}
|
|
24584
|
+
});
|
|
24585
|
+
}
|
|
24586
|
+
}
|
|
24587
|
+
const findings = [];
|
|
24588
|
+
for (const candidate of candidates) {
|
|
24589
|
+
const req = candidate.rule.requiresNearby;
|
|
24590
|
+
if (!req) {
|
|
24591
|
+
findings.push(candidate.match);
|
|
24592
|
+
continue;
|
|
24593
|
+
}
|
|
24594
|
+
if (!isCorroborated(candidate, candidates, text)) continue;
|
|
24595
|
+
const boost = req.confidenceBoost;
|
|
24596
|
+
findings.push(
|
|
24597
|
+
boost ? { ...candidate.match, confidence: Math.min(0.99, candidate.match.confidence + boost) } : candidate.match
|
|
24598
|
+
);
|
|
24599
|
+
}
|
|
24600
|
+
return findings;
|
|
24601
|
+
}
|
|
23928
24602
|
var SEVERITY_RANK2 = {
|
|
23929
24603
|
critical: 3,
|
|
23930
24604
|
high: 2,
|
|
@@ -24055,41 +24729,2102 @@ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].m
|
|
|
24055
24729
|
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
24056
24730
|
);
|
|
24057
24731
|
|
|
24058
|
-
// ../../
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
|
|
24063
|
-
|
|
24064
|
-
|
|
24065
|
-
|
|
24066
|
-
|
|
24732
|
+
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
24733
|
+
var auth_jwt_no_verify_default = {
|
|
24734
|
+
specVersion: 1,
|
|
24735
|
+
id: "code-flaws/auth-jwt-no-verify",
|
|
24736
|
+
name: "JWT decoded without signature verification",
|
|
24737
|
+
category: "code_flaw",
|
|
24738
|
+
severity: "critical",
|
|
24739
|
+
matcher: {
|
|
24740
|
+
type: "regex",
|
|
24741
|
+
pattern: "(verify\\s*=\\s*false|verify_signature.*false|algorithms\\s*=\\s*\\[\\])",
|
|
24742
|
+
flags: "gi"
|
|
24743
|
+
},
|
|
24744
|
+
requiresNearby: {
|
|
24745
|
+
labels: ["jwt", "decode", "token"],
|
|
24746
|
+
windowChars: 160
|
|
24747
|
+
},
|
|
24748
|
+
examples: [
|
|
24749
|
+
'jwt.decode(token, options={"verify_signature": False})',
|
|
24750
|
+
"jwt.decode(token, algorithms=[])"
|
|
24751
|
+
]
|
|
24752
|
+
};
|
|
24067
24753
|
|
|
24068
|
-
// ../../
|
|
24069
|
-
|
|
24754
|
+
// ../../rules/code-flaws/auth-ssl-verify-false.json
|
|
24755
|
+
var auth_ssl_verify_false_default = {
|
|
24756
|
+
specVersion: 1,
|
|
24757
|
+
id: "code-flaws/auth-ssl-verify-false",
|
|
24758
|
+
name: "SSL certificate verification disabled",
|
|
24759
|
+
category: "code_flaw",
|
|
24760
|
+
severity: "high",
|
|
24761
|
+
matcher: {
|
|
24762
|
+
type: "regex",
|
|
24763
|
+
pattern: "\\brequests\\.(get|post|put|delete|patch|request)\\s*\\([^)]*verify\\s*=\\s*False",
|
|
24764
|
+
flags: "gi"
|
|
24765
|
+
},
|
|
24766
|
+
examples: [
|
|
24767
|
+
"requests.get(url, verify=False)",
|
|
24768
|
+
"requests.post(endpoint, data=payload, verify=False)"
|
|
24769
|
+
],
|
|
24770
|
+
appliesTo: {
|
|
24771
|
+
extensions: [".py"]
|
|
24772
|
+
}
|
|
24773
|
+
};
|
|
24070
24774
|
|
|
24071
|
-
// ../../
|
|
24072
|
-
|
|
24073
|
-
|
|
24775
|
+
// ../../rules/code-flaws/cmd-inject-exec.json
|
|
24776
|
+
var cmd_inject_exec_default = {
|
|
24777
|
+
specVersion: 1,
|
|
24778
|
+
id: "code-flaws/cmd-inject-exec",
|
|
24779
|
+
name: "Command injection via Java Runtime.exec",
|
|
24780
|
+
category: "code_flaw",
|
|
24781
|
+
severity: "critical",
|
|
24782
|
+
matcher: {
|
|
24783
|
+
type: "regex",
|
|
24784
|
+
pattern: "Runtime\\.getRuntime\\(\\)\\.exec\\(",
|
|
24785
|
+
flags: "g"
|
|
24786
|
+
},
|
|
24787
|
+
requiresNearby: {
|
|
24788
|
+
labels: ["input", "request", "param", "user"],
|
|
24789
|
+
windowChars: 200
|
|
24790
|
+
},
|
|
24791
|
+
examples: ["Runtime.getRuntime().exec(userInput)"],
|
|
24792
|
+
appliesTo: {
|
|
24793
|
+
extensions: [".java"]
|
|
24794
|
+
}
|
|
24795
|
+
};
|
|
24074
24796
|
|
|
24075
|
-
// ../../
|
|
24076
|
-
|
|
24077
|
-
|
|
24797
|
+
// ../../rules/code-flaws/cmd-inject-node-exec.json
|
|
24798
|
+
var cmd_inject_node_exec_default = {
|
|
24799
|
+
specVersion: 1,
|
|
24800
|
+
id: "code-flaws/cmd-inject-node-exec",
|
|
24801
|
+
name: "Command injection via Node.js exec with variable argument",
|
|
24802
|
+
category: "code_flaw",
|
|
24803
|
+
severity: "critical",
|
|
24804
|
+
matcher: {
|
|
24805
|
+
type: "regex",
|
|
24806
|
+
pattern: "\\bexec(?:Sync)?\\s*\\(\\s*(?!['\"`])",
|
|
24807
|
+
flags: "g"
|
|
24808
|
+
},
|
|
24809
|
+
requiresNearby: {
|
|
24810
|
+
labels: ["input", "request", "param", "argv", "user", "query", "body"],
|
|
24811
|
+
windowChars: 200
|
|
24812
|
+
},
|
|
24813
|
+
examples: ["exec(req.query.cmd)", "execSync(userInput)"],
|
|
24814
|
+
appliesTo: {
|
|
24815
|
+
extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]
|
|
24816
|
+
}
|
|
24817
|
+
};
|
|
24078
24818
|
|
|
24079
|
-
// ../../
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
|
|
24083
|
-
|
|
24084
|
-
|
|
24085
|
-
|
|
24819
|
+
// ../../rules/code-flaws/cmd-inject-shell.json
|
|
24820
|
+
var cmd_inject_shell_default = {
|
|
24821
|
+
specVersion: 1,
|
|
24822
|
+
id: "code-flaws/cmd-inject-shell",
|
|
24823
|
+
name: "Command injection via subprocess shell=True",
|
|
24824
|
+
category: "code_flaw",
|
|
24825
|
+
severity: "critical",
|
|
24826
|
+
matcher: {
|
|
24827
|
+
type: "regex",
|
|
24828
|
+
pattern: "subprocess\\.(run|Popen|call|check_output)\\s*\\([^)]*shell\\s*=\\s*True",
|
|
24829
|
+
flags: "gi"
|
|
24830
|
+
},
|
|
24831
|
+
requiresNearby: {
|
|
24832
|
+
labels: ["input", "request", "param", "argv", "user"],
|
|
24833
|
+
windowChars: 300
|
|
24834
|
+
},
|
|
24835
|
+
examples: ["subprocess.run(user_input, shell=True)"],
|
|
24836
|
+
appliesTo: {
|
|
24837
|
+
extensions: [".py"]
|
|
24086
24838
|
}
|
|
24087
|
-
}
|
|
24839
|
+
};
|
|
24840
|
+
|
|
24841
|
+
// ../../rules/code-flaws/crypto-insecure-random.json
|
|
24842
|
+
var crypto_insecure_random_default = {
|
|
24843
|
+
specVersion: 1,
|
|
24844
|
+
id: "code-flaws/crypto-insecure-random",
|
|
24845
|
+
name: "Insecure random number generator used for security-sensitive value",
|
|
24846
|
+
category: "code_flaw",
|
|
24847
|
+
severity: "medium",
|
|
24848
|
+
matcher: {
|
|
24849
|
+
type: "regex",
|
|
24850
|
+
pattern: "\\b(Math\\.random\\(\\)|random\\.random\\(\\)|random\\.randint\\()",
|
|
24851
|
+
flags: "gi"
|
|
24852
|
+
},
|
|
24853
|
+
requiresNearby: {
|
|
24854
|
+
labels: ["token", "session", "key", "password", "secret", "nonce", "salt"],
|
|
24855
|
+
windowChars: 160
|
|
24856
|
+
},
|
|
24857
|
+
examples: ["const token = Math.random().toString(36)", "session_key = str(random.random())"],
|
|
24858
|
+
appliesTo: {
|
|
24859
|
+
extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py"]
|
|
24860
|
+
}
|
|
24861
|
+
};
|
|
24862
|
+
|
|
24863
|
+
// ../../rules/code-flaws/crypto-weak-hash-md5.json
|
|
24864
|
+
var crypto_weak_hash_md5_default = {
|
|
24865
|
+
specVersion: 1,
|
|
24866
|
+
id: "code-flaws/crypto-weak-hash-md5",
|
|
24867
|
+
name: "Weak cryptographic hash: MD5",
|
|
24868
|
+
category: "code_flaw",
|
|
24869
|
+
severity: "medium",
|
|
24870
|
+
matcher: {
|
|
24871
|
+
type: "regex",
|
|
24872
|
+
pattern: `(hashlib\\.md5\\s*\\(|MD5\\.(?:digest|hash)\\b|Digest::MD5|MessageDigest\\.getInstance\\s*\\(\\s*["']MD5["'])`,
|
|
24873
|
+
flags: "gi"
|
|
24874
|
+
},
|
|
24875
|
+
examples: ["hashlib.md5(password.encode())", 'MessageDigest.getInstance("MD5")'],
|
|
24876
|
+
appliesTo: {
|
|
24877
|
+
extensions: [".py", ".rb", ".java"]
|
|
24878
|
+
}
|
|
24879
|
+
};
|
|
24880
|
+
|
|
24881
|
+
// ../../rules/code-flaws/crypto-weak-hash-sha1.json
|
|
24882
|
+
var crypto_weak_hash_sha1_default = {
|
|
24883
|
+
specVersion: 1,
|
|
24884
|
+
id: "code-flaws/crypto-weak-hash-sha1",
|
|
24885
|
+
name: "Weak cryptographic hash: SHA-1 in security context",
|
|
24886
|
+
category: "code_flaw",
|
|
24887
|
+
severity: "medium",
|
|
24888
|
+
matcher: {
|
|
24889
|
+
type: "regex",
|
|
24890
|
+
pattern: `(hashlib\\.sha1\\s*\\(|SHA1\\.(?:digest|hash)\\b|Digest::SHA1|MessageDigest\\.getInstance\\s*\\(\\s*["']SHA-?1["'])`,
|
|
24891
|
+
flags: "gi"
|
|
24892
|
+
},
|
|
24893
|
+
requiresNearby: {
|
|
24894
|
+
labels: ["password", "credential", "hash", "token", "secret"],
|
|
24895
|
+
windowChars: 160
|
|
24896
|
+
},
|
|
24897
|
+
examples: [
|
|
24898
|
+
"password_hash = hashlib.sha1(password.encode()).hexdigest()",
|
|
24899
|
+
'MessageDigest.getInstance("SHA1")'
|
|
24900
|
+
],
|
|
24901
|
+
appliesTo: {
|
|
24902
|
+
extensions: [".py", ".rb", ".java"]
|
|
24903
|
+
}
|
|
24904
|
+
};
|
|
24905
|
+
|
|
24906
|
+
// ../../rules/code-flaws/deser-java-ois.json
|
|
24907
|
+
var deser_java_ois_default = {
|
|
24908
|
+
specVersion: 1,
|
|
24909
|
+
id: "code-flaws/deser-java-ois",
|
|
24910
|
+
name: "Insecure deserialization via Java ObjectInputStream",
|
|
24911
|
+
category: "code_flaw",
|
|
24912
|
+
severity: "critical",
|
|
24913
|
+
matcher: {
|
|
24914
|
+
type: "regex",
|
|
24915
|
+
pattern: "\\bObjectInputStream\\b",
|
|
24916
|
+
flags: "g"
|
|
24917
|
+
},
|
|
24918
|
+
requiresNearby: {
|
|
24919
|
+
labels: ["readObject"],
|
|
24920
|
+
windowChars: 200
|
|
24921
|
+
},
|
|
24922
|
+
examples: [
|
|
24923
|
+
"ObjectInputStream ois = new ObjectInputStream(inputStream); Object obj = ois.readObject();"
|
|
24924
|
+
],
|
|
24925
|
+
appliesTo: {
|
|
24926
|
+
extensions: [".java"]
|
|
24927
|
+
}
|
|
24928
|
+
};
|
|
24929
|
+
|
|
24930
|
+
// ../../rules/code-flaws/deser-pickle.json
|
|
24931
|
+
var deser_pickle_default = {
|
|
24932
|
+
specVersion: 1,
|
|
24933
|
+
id: "code-flaws/deser-pickle",
|
|
24934
|
+
name: "Insecure deserialization via Python pickle",
|
|
24935
|
+
category: "code_flaw",
|
|
24936
|
+
severity: "critical",
|
|
24937
|
+
matcher: {
|
|
24938
|
+
type: "regex",
|
|
24939
|
+
pattern: "\\bpickle\\.loads?\\s*\\(",
|
|
24940
|
+
flags: "gi"
|
|
24941
|
+
},
|
|
24942
|
+
examples: ["data = pickle.loads(user_data)", "obj = pickle.load(request.stream)"],
|
|
24943
|
+
appliesTo: {
|
|
24944
|
+
extensions: [".py"]
|
|
24945
|
+
}
|
|
24946
|
+
};
|
|
24947
|
+
|
|
24948
|
+
// ../../rules/code-flaws/deser-yaml-unsafe.json
|
|
24949
|
+
var deser_yaml_unsafe_default = {
|
|
24950
|
+
specVersion: 1,
|
|
24951
|
+
id: "code-flaws/deser-yaml-unsafe",
|
|
24952
|
+
name: "Insecure deserialization via yaml.load without SafeLoader",
|
|
24953
|
+
category: "code_flaw",
|
|
24954
|
+
severity: "high",
|
|
24955
|
+
matcher: {
|
|
24956
|
+
type: "regex",
|
|
24957
|
+
pattern: "\\byaml\\.load\\s*\\((?![^)]*SafeLoader|[^)]*safe_load)",
|
|
24958
|
+
flags: "gi"
|
|
24959
|
+
},
|
|
24960
|
+
examples: ["data = yaml.load(user_input)", "config = yaml.load(f)"],
|
|
24961
|
+
appliesTo: {
|
|
24962
|
+
extensions: [".py"]
|
|
24963
|
+
}
|
|
24964
|
+
};
|
|
24965
|
+
|
|
24966
|
+
// ../../rules/code-flaws/dev-debug-enabled.json
|
|
24967
|
+
var dev_debug_enabled_default = {
|
|
24968
|
+
specVersion: 1,
|
|
24969
|
+
id: "code-flaws/dev-debug-enabled",
|
|
24970
|
+
name: "Debug mode enabled in configuration",
|
|
24971
|
+
category: "code_flaw",
|
|
24972
|
+
severity: "medium",
|
|
24973
|
+
matcher: {
|
|
24974
|
+
type: "regex",
|
|
24975
|
+
pattern: "\\bdebug\\s*[=:]\\s*(true|1|yes)",
|
|
24976
|
+
flags: "gi"
|
|
24977
|
+
},
|
|
24978
|
+
examples: ["DEBUG = True", "debug: true", "app.debug = True"]
|
|
24979
|
+
};
|
|
24980
|
+
|
|
24981
|
+
// ../../rules/code-flaws/dev-placeholder-secret.json
|
|
24982
|
+
var dev_placeholder_secret_default = {
|
|
24983
|
+
specVersion: 1,
|
|
24984
|
+
id: "code-flaws/dev-placeholder-secret",
|
|
24985
|
+
name: "Placeholder or default secret left in code",
|
|
24986
|
+
category: "code_flaw",
|
|
24987
|
+
severity: "high",
|
|
24988
|
+
matcher: {
|
|
24989
|
+
type: "keyword",
|
|
24990
|
+
keywords: [
|
|
24991
|
+
"changeme",
|
|
24992
|
+
"placeholder",
|
|
24993
|
+
"your-secret",
|
|
24994
|
+
"dev-secret",
|
|
24995
|
+
"replace-me",
|
|
24996
|
+
"todo-secret"
|
|
24997
|
+
],
|
|
24998
|
+
caseSensitive: false
|
|
24999
|
+
},
|
|
25000
|
+
requiresNearby: {
|
|
25001
|
+
labels: ["key", "secret", "password", "token"],
|
|
25002
|
+
windowChars: 80
|
|
25003
|
+
},
|
|
25004
|
+
examples: ["SECRET_KEY = 'changeme'", "api_token = 'your-secret'"]
|
|
25005
|
+
};
|
|
25006
|
+
|
|
25007
|
+
// ../../rules/code-flaws/dev-wildcard-cors.json
|
|
25008
|
+
var dev_wildcard_cors_default = {
|
|
25009
|
+
specVersion: 1,
|
|
25010
|
+
id: "code-flaws/dev-wildcard-cors",
|
|
25011
|
+
name: "CORS wildcard allowing all origins",
|
|
25012
|
+
category: "code_flaw",
|
|
25013
|
+
severity: "medium",
|
|
25014
|
+
matcher: {
|
|
25015
|
+
type: "regex",
|
|
25016
|
+
pattern: `(Access-Control-Allow-Origin:\\s*\\*|origin:\\s*["']\\*["']|allow_all_origins\\s*=\\s*True|CORS_ALLOW_ALL_ORIGINS\\s*=\\s*True)`,
|
|
25017
|
+
flags: "gi"
|
|
25018
|
+
},
|
|
25019
|
+
examples: [
|
|
25020
|
+
"Access-Control-Allow-Origin: *",
|
|
25021
|
+
"cors({ origin: '*' })",
|
|
25022
|
+
"CORS_ALLOW_ALL_ORIGINS = True"
|
|
25023
|
+
]
|
|
25024
|
+
};
|
|
25025
|
+
|
|
25026
|
+
// ../../rules/code-flaws/eval-dynamic-exec.json
|
|
25027
|
+
var eval_dynamic_exec_default = {
|
|
25028
|
+
specVersion: 1,
|
|
25029
|
+
id: "code-flaws/eval-dynamic-exec",
|
|
25030
|
+
name: "Dynamic code execution with user-controlled input",
|
|
25031
|
+
category: "code_flaw",
|
|
25032
|
+
severity: "critical",
|
|
25033
|
+
matcher: {
|
|
25034
|
+
type: "regex",
|
|
25035
|
+
pattern: "\\b(eval|exec)\\s*\\(\\s*(?!['\"`])",
|
|
25036
|
+
flags: "g"
|
|
25037
|
+
},
|
|
25038
|
+
requiresNearby: {
|
|
25039
|
+
labels: ["input", "request", "param", "user", "query", "body"],
|
|
25040
|
+
windowChars: 200
|
|
25041
|
+
},
|
|
25042
|
+
examples: ["eval(request.body.code)", "exec(user_input)"]
|
|
25043
|
+
};
|
|
25044
|
+
|
|
25045
|
+
// ../../rules/code-flaws/hardcoded-password.json
|
|
25046
|
+
var hardcoded_password_default = {
|
|
25047
|
+
specVersion: 1,
|
|
25048
|
+
id: "code-flaws/hardcoded-password",
|
|
25049
|
+
name: "Hardcoded password in source code",
|
|
25050
|
+
category: "code_flaw",
|
|
25051
|
+
severity: "high",
|
|
25052
|
+
matcher: {
|
|
25053
|
+
type: "regex",
|
|
25054
|
+
pattern: `(?:password|passwd|pwd)\\s*=\\s*["']([^"']{3,})["']`,
|
|
25055
|
+
flags: "gi",
|
|
25056
|
+
captureGroup: 1
|
|
25057
|
+
},
|
|
25058
|
+
postValidators: [
|
|
25059
|
+
{
|
|
25060
|
+
name: "entropy",
|
|
25061
|
+
config: {
|
|
25062
|
+
minLength: 8,
|
|
25063
|
+
threshold: 3
|
|
25064
|
+
}
|
|
25065
|
+
}
|
|
25066
|
+
],
|
|
25067
|
+
examples: ['password = "Tr0ub4dor&3"', "DB_PASSWORD = 'c0rr3ctH0rseBatt3ry'"]
|
|
25068
|
+
};
|
|
25069
|
+
|
|
25070
|
+
// ../../rules/code-flaws/hardcoded-secret-key.json
|
|
25071
|
+
var hardcoded_secret_key_default = {
|
|
25072
|
+
specVersion: 1,
|
|
25073
|
+
id: "code-flaws/hardcoded-secret-key",
|
|
25074
|
+
name: "Hardcoded API key or secret key in source code",
|
|
25075
|
+
category: "code_flaw",
|
|
25076
|
+
severity: "high",
|
|
25077
|
+
matcher: {
|
|
25078
|
+
type: "regex",
|
|
25079
|
+
pattern: `\\b(secret[_-]?key|api[_-]?key|auth[_-]?token)\\s*=\\s*["'][^"']{8,}["']`,
|
|
25080
|
+
flags: "gi"
|
|
25081
|
+
},
|
|
25082
|
+
postValidators: ["entropy"],
|
|
25083
|
+
examples: [
|
|
25084
|
+
'SECRET_KEY = "a9f2c8e1b4d7f3a6c9e2b5d8f1a4c7e0"',
|
|
25085
|
+
"api_key = 'sk-proj-abc123XYZ789def456'"
|
|
25086
|
+
]
|
|
25087
|
+
};
|
|
25088
|
+
|
|
25089
|
+
// ../../rules/code-flaws/path-traversal-join.json
|
|
25090
|
+
var path_traversal_join_default = {
|
|
25091
|
+
specVersion: 1,
|
|
25092
|
+
id: "code-flaws/path-traversal-join",
|
|
25093
|
+
name: "Path traversal via os.path.join with user-controlled component",
|
|
25094
|
+
category: "code_flaw",
|
|
25095
|
+
severity: "high",
|
|
25096
|
+
matcher: {
|
|
25097
|
+
type: "regex",
|
|
25098
|
+
pattern: "os\\.path\\.join\\s*\\([^,)]+,\\s*(request|req|params|user)",
|
|
25099
|
+
flags: "gi"
|
|
25100
|
+
},
|
|
25101
|
+
examples: [
|
|
25102
|
+
"os.path.join(base_dir, request.args['file'])",
|
|
25103
|
+
"os.path.join(upload_dir, user_filename)"
|
|
25104
|
+
],
|
|
25105
|
+
appliesTo: {
|
|
25106
|
+
extensions: [".py"]
|
|
25107
|
+
}
|
|
25108
|
+
};
|
|
25109
|
+
|
|
25110
|
+
// ../../rules/code-flaws/path-traversal-open.json
|
|
25111
|
+
var path_traversal_open_default = {
|
|
25112
|
+
specVersion: 1,
|
|
25113
|
+
id: "code-flaws/path-traversal-open",
|
|
25114
|
+
name: "Path traversal via open() with user-controlled path",
|
|
25115
|
+
category: "code_flaw",
|
|
25116
|
+
severity: "high",
|
|
25117
|
+
matcher: {
|
|
25118
|
+
type: "regex",
|
|
25119
|
+
pattern: "\\bopen\\s*\\(\\s*(request|req|params|argv|input)",
|
|
25120
|
+
flags: "gi"
|
|
25121
|
+
},
|
|
25122
|
+
requiresNearby: {
|
|
25123
|
+
labels: ["request", "param", "input", "argv", "user"],
|
|
25124
|
+
windowChars: 200
|
|
25125
|
+
},
|
|
25126
|
+
examples: ["open(request.args['filename'])", "open(params['path'])"],
|
|
25127
|
+
appliesTo: {
|
|
25128
|
+
extensions: [".py", ".rb"]
|
|
25129
|
+
}
|
|
25130
|
+
};
|
|
25131
|
+
|
|
25132
|
+
// ../../rules/code-flaws/prototype-pollution-merge.json
|
|
25133
|
+
var prototype_pollution_merge_default = {
|
|
25134
|
+
specVersion: 1,
|
|
25135
|
+
id: "code-flaws/prototype-pollution-merge",
|
|
25136
|
+
name: "Prototype pollution via merge with user-controlled input",
|
|
25137
|
+
category: "code_flaw",
|
|
25138
|
+
severity: "high",
|
|
25139
|
+
matcher: {
|
|
25140
|
+
type: "regex",
|
|
25141
|
+
pattern: "(Object\\.assign\\s*\\(\\s*\\w+\\s*,\\s*(req|request|body|params|query)|_\\.merge\\s*\\(\\s*\\w+\\s*,\\s*(req|request))",
|
|
25142
|
+
flags: "gi"
|
|
25143
|
+
},
|
|
25144
|
+
examples: ["Object.assign(config, req.body)", "_.merge(target, request.body)"],
|
|
25145
|
+
appliesTo: {
|
|
25146
|
+
extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]
|
|
25147
|
+
}
|
|
25148
|
+
};
|
|
25149
|
+
|
|
25150
|
+
// ../../rules/code-flaws/regex-redos-backtrack.json
|
|
25151
|
+
var regex_redos_backtrack_default = {
|
|
25152
|
+
specVersion: 1,
|
|
25153
|
+
id: "code-flaws/regex-redos-backtrack",
|
|
25154
|
+
name: "ReDoS via catastrophic backtracking pattern",
|
|
25155
|
+
category: "code_flaw",
|
|
25156
|
+
severity: "medium",
|
|
25157
|
+
matcher: {
|
|
25158
|
+
type: "regex",
|
|
25159
|
+
pattern: "\\((?:\\?:)?(?:\\\\.|\\[[^\\]]*\\]|[^\\\\()\\[\\]+*?|])[+*](?:(?:\\\\.|\\[[^\\]]*\\]|[^\\\\()\\[\\]+*?|])\\*)?\\)[+*]|\\((?:\\?:)?([^()|]+)\\|\\1\\)[+*]",
|
|
25160
|
+
flags: "g"
|
|
25161
|
+
},
|
|
25162
|
+
examples: ["/(a+)+/", "/(\\w+\\s*)+/"]
|
|
25163
|
+
};
|
|
25164
|
+
|
|
25165
|
+
// ../../rules/code-flaws/sql-inject-concat-dot.json
|
|
25166
|
+
var sql_inject_concat_dot_default = {
|
|
25167
|
+
specVersion: 1,
|
|
25168
|
+
id: "code-flaws/sql-inject-concat-dot",
|
|
25169
|
+
name: "SQL injection via string concatenation (PHP dot operator)",
|
|
25170
|
+
category: "code_flaw",
|
|
25171
|
+
severity: "high",
|
|
25172
|
+
matcher: {
|
|
25173
|
+
type: "regex",
|
|
25174
|
+
pattern: `["'][^\\n]*?(select|insert|update|delete|drop|where)[^\\n]*?["']\\s*\\.`,
|
|
25175
|
+
flags: "gi"
|
|
25176
|
+
},
|
|
25177
|
+
appliesTo: {
|
|
25178
|
+
extensions: [".php"]
|
|
25179
|
+
},
|
|
25180
|
+
examples: ['$query = "SELECT * FROM orders WHERE user = " . $username;']
|
|
25181
|
+
};
|
|
25182
|
+
|
|
25183
|
+
// ../../rules/code-flaws/sql-inject-concat.json
|
|
25184
|
+
var sql_inject_concat_default = {
|
|
25185
|
+
specVersion: 1,
|
|
25186
|
+
id: "code-flaws/sql-inject-concat",
|
|
25187
|
+
name: "SQL injection via string concatenation",
|
|
25188
|
+
category: "code_flaw",
|
|
25189
|
+
severity: "high",
|
|
25190
|
+
matcher: {
|
|
25191
|
+
type: "regex",
|
|
25192
|
+
pattern: `["']\\s*(?:select\\b[^\\n]*?\\bfrom\\b|insert\\s+into\\b|update\\s+\\S+\\s+set\\b|delete\\s+from\\b|drop\\s+(?:table|database)\\b|where\\b[^\\n]*?=)[^\\n]*?["']\\s*\\+`,
|
|
25193
|
+
flags: "gi"
|
|
25194
|
+
},
|
|
25195
|
+
examples: ['"SELECT * FROM users WHERE id = " + userId']
|
|
25196
|
+
};
|
|
25197
|
+
|
|
25198
|
+
// ../../rules/code-flaws/sql-inject-format.json
|
|
25199
|
+
var sql_inject_format_default = {
|
|
25200
|
+
specVersion: 1,
|
|
25201
|
+
id: "code-flaws/sql-inject-format",
|
|
25202
|
+
name: "SQL injection via format string or f-string",
|
|
25203
|
+
category: "code_flaw",
|
|
25204
|
+
severity: "high",
|
|
25205
|
+
matcher: {
|
|
25206
|
+
type: "regex",
|
|
25207
|
+
pattern: "\\b(?:select\\b[^\\n]*?\\bfrom\\b|insert\\s+into\\b|update\\s+\\S+\\s+set\\b|delete\\s+from\\b|drop\\s+(?:table|database)\\b|where\\b[^\\n]*?=)[^\\n]*?(?:[%][sd]|\\{[^}]+\\})",
|
|
25208
|
+
flags: "gi"
|
|
25209
|
+
},
|
|
25210
|
+
examples: [
|
|
25211
|
+
'f"SELECT * FROM users WHERE id = {user_id}"',
|
|
25212
|
+
`"SELECT * FROM users WHERE name = '%s'" % name`
|
|
25213
|
+
]
|
|
25214
|
+
};
|
|
25215
|
+
|
|
25216
|
+
// ../../rules/code-flaws/sql-inject-interp.json
|
|
25217
|
+
var sql_inject_interp_default = {
|
|
25218
|
+
specVersion: 1,
|
|
25219
|
+
id: "code-flaws/sql-inject-interp",
|
|
25220
|
+
name: "SQL injection via Ruby string interpolation or PHP variable",
|
|
25221
|
+
category: "code_flaw",
|
|
25222
|
+
severity: "high",
|
|
25223
|
+
matcher: {
|
|
25224
|
+
type: "regex",
|
|
25225
|
+
pattern: "(select|insert|update|delete|where)[^\\n]*?(#\\{|\\$[a-z_])",
|
|
25226
|
+
flags: "gi"
|
|
25227
|
+
},
|
|
25228
|
+
examples: [
|
|
25229
|
+
'"SELECT * FROM users WHERE id = #{params[:id]}"',
|
|
25230
|
+
'"SELECT * FROM users WHERE id = $userId"'
|
|
25231
|
+
],
|
|
25232
|
+
appliesTo: {
|
|
25233
|
+
extensions: [".rb", ".php"]
|
|
25234
|
+
}
|
|
25235
|
+
};
|
|
25236
|
+
|
|
25237
|
+
// ../../rules/code-flaws/ssrf-user-url.json
|
|
25238
|
+
var ssrf_user_url_default = {
|
|
25239
|
+
specVersion: 1,
|
|
25240
|
+
id: "code-flaws/ssrf-user-url",
|
|
25241
|
+
name: "Server-side request forgery via user-controlled URL",
|
|
25242
|
+
category: "code_flaw",
|
|
25243
|
+
severity: "high",
|
|
25244
|
+
matcher: {
|
|
25245
|
+
type: "regex",
|
|
25246
|
+
pattern: "(fetch|axios\\.\\w+|requests\\.get|urllib\\.request\\.urlopen)\\s*\\(\\s*(req|request|params|query|body)",
|
|
25247
|
+
flags: "gi"
|
|
25248
|
+
},
|
|
25249
|
+
examples: ["fetch(req.query.url)", "requests.get(request.form['url'])"],
|
|
25250
|
+
appliesTo: {
|
|
25251
|
+
extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py"]
|
|
25252
|
+
}
|
|
25253
|
+
};
|
|
25254
|
+
|
|
25255
|
+
// ../../rules/code-flaws/xss-dangerously-set.json
|
|
25256
|
+
var xss_dangerously_set_default = {
|
|
25257
|
+
specVersion: 1,
|
|
25258
|
+
id: "code-flaws/xss-dangerously-set",
|
|
25259
|
+
name: "XSS via React dangerouslySetInnerHTML",
|
|
25260
|
+
category: "code_flaw",
|
|
25261
|
+
severity: "high",
|
|
25262
|
+
matcher: {
|
|
25263
|
+
type: "keyword",
|
|
25264
|
+
keywords: ["dangerouslySetInnerHTML"],
|
|
25265
|
+
caseSensitive: true
|
|
25266
|
+
},
|
|
25267
|
+
examples: ["<div dangerouslySetInnerHTML={{ __html: userContent }} />"],
|
|
25268
|
+
appliesTo: {
|
|
25269
|
+
extensions: [".js", ".jsx", ".ts", ".tsx"]
|
|
25270
|
+
}
|
|
25271
|
+
};
|
|
25272
|
+
|
|
25273
|
+
// ../../rules/code-flaws/xss-inner-html.json
|
|
25274
|
+
var xss_inner_html_default = {
|
|
25275
|
+
specVersion: 1,
|
|
25276
|
+
id: "code-flaws/xss-inner-html",
|
|
25277
|
+
name: "XSS via innerHTML assignment",
|
|
25278
|
+
category: "code_flaw",
|
|
25279
|
+
severity: "high",
|
|
25280
|
+
matcher: {
|
|
25281
|
+
type: "keyword",
|
|
25282
|
+
keywords: ["innerHTML =", "innerHTML="],
|
|
25283
|
+
caseSensitive: true
|
|
25284
|
+
},
|
|
25285
|
+
examples: ["element.innerHTML = userInput"],
|
|
25286
|
+
appliesTo: {
|
|
25287
|
+
extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]
|
|
25288
|
+
}
|
|
25289
|
+
};
|
|
25290
|
+
|
|
25291
|
+
// ../../rules/code-flaws/xss-unescaped-render.json
|
|
25292
|
+
var xss_unescaped_render_default = {
|
|
25293
|
+
specVersion: 1,
|
|
25294
|
+
id: "code-flaws/xss-unescaped-render",
|
|
25295
|
+
name: "XSS via unescaped template rendering",
|
|
25296
|
+
category: "code_flaw",
|
|
25297
|
+
severity: "high",
|
|
25298
|
+
matcher: {
|
|
25299
|
+
type: "regex",
|
|
25300
|
+
pattern: "(\\braw\\s*\\(|\\bhtml_safe\\b|\\|\\s*safe\\b|\\bmark_safe\\s*\\()",
|
|
25301
|
+
flags: "gi"
|
|
25302
|
+
},
|
|
25303
|
+
examples: ["<%= raw(user_comment) %>", "{{ user_input | safe }}", "mark_safe(user_html)"],
|
|
25304
|
+
appliesTo: {
|
|
25305
|
+
extensions: [".rb", ".py"]
|
|
25306
|
+
}
|
|
25307
|
+
};
|
|
25308
|
+
|
|
25309
|
+
// ../../rules/core-code-context/db-table-name.json
|
|
25310
|
+
var db_table_name_default = {
|
|
25311
|
+
specVersion: 1,
|
|
25312
|
+
id: "core-code-context/db-table-name",
|
|
25313
|
+
name: "Internal database table/column name",
|
|
25314
|
+
category: "code_context",
|
|
25315
|
+
severity: "low",
|
|
25316
|
+
matcher: {
|
|
25317
|
+
type: "keyword",
|
|
25318
|
+
keywords: [
|
|
25319
|
+
"SELECT * FROM ",
|
|
25320
|
+
"SELECT COUNT(*) FROM ",
|
|
25321
|
+
"INSERT INTO ",
|
|
25322
|
+
"DELETE FROM ",
|
|
25323
|
+
"CREATE TABLE ",
|
|
25324
|
+
"ALTER TABLE ",
|
|
25325
|
+
"DROP TABLE ",
|
|
25326
|
+
"FROM users",
|
|
25327
|
+
"FROM orders",
|
|
25328
|
+
"FROM employees",
|
|
25329
|
+
"FROM customers",
|
|
25330
|
+
"FROM accounts",
|
|
25331
|
+
"FROM transactions",
|
|
25332
|
+
"FROM products",
|
|
25333
|
+
"FROM payments"
|
|
25334
|
+
],
|
|
25335
|
+
caseSensitive: false
|
|
25336
|
+
},
|
|
25337
|
+
examples: ["SELECT * FROM users WHERE id = 1"]
|
|
25338
|
+
};
|
|
25339
|
+
|
|
25340
|
+
// ../../rules/core-code-context/feature-flag.json
|
|
25341
|
+
var feature_flag_default = {
|
|
25342
|
+
specVersion: 1,
|
|
25343
|
+
id: "core-code-context/feature-flag",
|
|
25344
|
+
name: "Feature flag / toggle name",
|
|
25345
|
+
category: "code_context",
|
|
25346
|
+
severity: "low",
|
|
25347
|
+
matcher: {
|
|
25348
|
+
type: "keyword",
|
|
25349
|
+
keywords: [
|
|
25350
|
+
"feature_flag:",
|
|
25351
|
+
"feature_toggle:",
|
|
25352
|
+
"feature-flag:",
|
|
25353
|
+
"feature-toggle:",
|
|
25354
|
+
"FF_",
|
|
25355
|
+
"FEATURE_FLAG_",
|
|
25356
|
+
"FEATURE_TOGGLE_",
|
|
25357
|
+
"unleash:",
|
|
25358
|
+
"launchdarkly:",
|
|
25359
|
+
"split.io:"
|
|
25360
|
+
],
|
|
25361
|
+
caseSensitive: false
|
|
25362
|
+
},
|
|
25363
|
+
examples: ["feature_flag: new-checkout-flow", "FF_ENABLE_DARK_MODE"]
|
|
25364
|
+
};
|
|
25365
|
+
|
|
25366
|
+
// ../../rules/core-code-context/file-path.json
|
|
25367
|
+
var file_path_default = {
|
|
25368
|
+
specVersion: 1,
|
|
25369
|
+
id: "core-code-context/file-path",
|
|
25370
|
+
name: "User home / identity-bearing file path",
|
|
25371
|
+
category: "code_context",
|
|
25372
|
+
severity: "low",
|
|
25373
|
+
matcher: {
|
|
25374
|
+
type: "regex",
|
|
25375
|
+
pattern: "(?:/home|/Users)/[A-Za-z0-9_.@-]+(?:/[A-Za-z0-9_.@+-]+)*|/root(?:/[A-Za-z0-9_.@+-]+)+|[A-Za-z]:\\\\[Uu]sers\\\\[A-Za-z0-9_.-]+(?:\\\\[A-Za-z0-9_.-]+)*",
|
|
25376
|
+
flags: "g"
|
|
25377
|
+
},
|
|
25378
|
+
examples: [
|
|
25379
|
+
"/home/user/.ssh/id_rsa",
|
|
25380
|
+
"/Users/jane/projects/acme/secrets.env",
|
|
25381
|
+
"C:\\Users\\admin\\config.yml"
|
|
25382
|
+
]
|
|
25383
|
+
};
|
|
25384
|
+
|
|
25385
|
+
// ../../rules/core-code-context/internal-domain.json
|
|
25386
|
+
var internal_domain_default = {
|
|
25387
|
+
specVersion: 1,
|
|
25388
|
+
id: "core-code-context/internal-domain",
|
|
25389
|
+
name: "Internal domain / hostname",
|
|
25390
|
+
category: "code_context",
|
|
25391
|
+
severity: "low",
|
|
25392
|
+
matcher: {
|
|
25393
|
+
type: "regex",
|
|
25394
|
+
pattern: "(?<![A-Za-z0-9.-])[A-Za-z0-9][A-Za-z0-9.-]{0,251}\\.(?:internal|corp|local|lan|intranet|private|dev|staging|test|qa)(?:\\.(?:com|net|org|io|co|app))?\\b",
|
|
25395
|
+
flags: "gi"
|
|
25396
|
+
},
|
|
25397
|
+
examples: ["db.internal.com", "app.corp.local", "api.staging.example.com"]
|
|
25398
|
+
};
|
|
25399
|
+
|
|
25400
|
+
// ../../rules/core-code-context/internal-ip.json
|
|
25401
|
+
var internal_ip_default = {
|
|
25402
|
+
specVersion: 1,
|
|
25403
|
+
id: "core-code-context/internal-ip",
|
|
25404
|
+
name: "Internal (RFC 1918) IP address",
|
|
25405
|
+
category: "code_context",
|
|
25406
|
+
severity: "low",
|
|
25407
|
+
matcher: {
|
|
25408
|
+
type: "regex",
|
|
25409
|
+
pattern: "(?<![\\d.])(?:10\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)|172\\.(?:1[6-9]|2\\d|3[01])\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)|192\\.168\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?))(?![\\d])",
|
|
25410
|
+
flags: "g"
|
|
25411
|
+
},
|
|
25412
|
+
examples: ["10.0.0.1", "192.168.1.1", "172.16.0.1"]
|
|
25413
|
+
};
|
|
25414
|
+
|
|
25415
|
+
// ../../rules/core-code-context/internal-url.json
|
|
25416
|
+
var internal_url_default = {
|
|
25417
|
+
specVersion: 1,
|
|
25418
|
+
id: "core-code-context/internal-url",
|
|
25419
|
+
name: "Internal environment URL",
|
|
25420
|
+
category: "code_context",
|
|
25421
|
+
severity: "low",
|
|
25422
|
+
matcher: {
|
|
25423
|
+
type: "keyword",
|
|
25424
|
+
keywords: [
|
|
25425
|
+
"https://staging.",
|
|
25426
|
+
"https://dev.",
|
|
25427
|
+
"https://qa.",
|
|
25428
|
+
"https://test.",
|
|
25429
|
+
"http://staging.",
|
|
25430
|
+
"http://dev.",
|
|
25431
|
+
"http://qa.",
|
|
25432
|
+
"http://test.",
|
|
25433
|
+
".staging.com",
|
|
25434
|
+
".dev.com",
|
|
25435
|
+
".qa.com",
|
|
25436
|
+
"staging.internal",
|
|
25437
|
+
"dev.internal"
|
|
25438
|
+
],
|
|
25439
|
+
caseSensitive: false
|
|
25440
|
+
},
|
|
25441
|
+
examples: ["https://staging.example.com"]
|
|
25442
|
+
};
|
|
25443
|
+
|
|
25444
|
+
// ../../rules/core-code-context/localhost-ref.json
|
|
25445
|
+
var localhost_ref_default = {
|
|
25446
|
+
specVersion: 1,
|
|
25447
|
+
id: "core-code-context/localhost-ref",
|
|
25448
|
+
name: "Localhost reference",
|
|
25449
|
+
category: "code_context",
|
|
25450
|
+
severity: "low",
|
|
25451
|
+
matcher: {
|
|
25452
|
+
type: "regex",
|
|
25453
|
+
pattern: "\\b(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|::1)\\b",
|
|
25454
|
+
flags: "g"
|
|
25455
|
+
},
|
|
25456
|
+
examples: ["localhost", "127.0.0.1"]
|
|
25457
|
+
};
|
|
25458
|
+
|
|
25459
|
+
// ../../rules/core-code-context/stack-trace.json
|
|
25460
|
+
var stack_trace_default = {
|
|
25461
|
+
specVersion: 1,
|
|
25462
|
+
id: "core-code-context/stack-trace",
|
|
25463
|
+
name: "Stack trace (internal code structure)",
|
|
25464
|
+
category: "code_context",
|
|
25465
|
+
severity: "low",
|
|
25466
|
+
matcher: {
|
|
25467
|
+
type: "regex",
|
|
25468
|
+
pattern: "(?:^|\\s+)at\\s+[A-Za-z0-9._$<>]+\\.[A-Za-z0-9_$]+\\s*\\([A-Za-z0-9._\\-/:]+:\\d+:\\d+\\)",
|
|
25469
|
+
flags: "g"
|
|
25470
|
+
},
|
|
25471
|
+
examples: ["at com.example.service.UserService.findUser(UserService.java:42:13)"]
|
|
25472
|
+
};
|
|
25473
|
+
|
|
25474
|
+
// ../../rules/core-financial/credit-card.json
|
|
25475
|
+
var credit_card_default = {
|
|
25476
|
+
specVersion: 1,
|
|
25477
|
+
id: "core-financial/credit-card",
|
|
25478
|
+
name: "Credit/debit card number",
|
|
25479
|
+
category: "financial",
|
|
25480
|
+
severity: "critical",
|
|
25481
|
+
matcher: {
|
|
25482
|
+
type: "regex",
|
|
25483
|
+
pattern: "\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\\b",
|
|
25484
|
+
flags: "g"
|
|
25485
|
+
},
|
|
25486
|
+
postValidators: ["luhn"],
|
|
25487
|
+
examples: ["4111111111111111", "5500000000000004"]
|
|
25488
|
+
};
|
|
25489
|
+
|
|
25490
|
+
// ../../rules/core-financial/cusip.json
|
|
25491
|
+
var cusip_default = {
|
|
25492
|
+
specVersion: 1,
|
|
25493
|
+
id: "core-financial/cusip",
|
|
25494
|
+
name: "CUSIP security identifier",
|
|
25495
|
+
category: "financial",
|
|
25496
|
+
severity: "medium",
|
|
25497
|
+
matcher: {
|
|
25498
|
+
type: "regex",
|
|
25499
|
+
pattern: "\\bcusip\\b[\\s#:=-]{0,8}(\\d{3}[A-Z0-9]{2}[A-Z0-9*#@]\\d{2}[0-9])\\b",
|
|
25500
|
+
flags: "gi",
|
|
25501
|
+
captureGroup: 1
|
|
25502
|
+
},
|
|
25503
|
+
examples: ["CUSIP: 037833100", "CUSIP 931142103"]
|
|
25504
|
+
};
|
|
25505
|
+
|
|
25506
|
+
// ../../rules/core-financial/cvv.json
|
|
25507
|
+
var cvv_default = {
|
|
25508
|
+
specVersion: 1,
|
|
25509
|
+
id: "core-financial/cvv",
|
|
25510
|
+
name: "Card security code (CVV/CVC)",
|
|
25511
|
+
category: "financial",
|
|
25512
|
+
severity: "high",
|
|
25513
|
+
matcher: {
|
|
25514
|
+
type: "regex",
|
|
25515
|
+
pattern: "\\b(?:CVV|CVC|CID|security\\s+code|card\\s+code)[:\\s]*\\d{3,4}\\b",
|
|
25516
|
+
flags: "gi"
|
|
25517
|
+
},
|
|
25518
|
+
examples: ["CVV: 123", "CVC: 456"]
|
|
25519
|
+
};
|
|
25520
|
+
|
|
25521
|
+
// ../../rules/core-financial/iban.json
|
|
25522
|
+
var iban_default = {
|
|
25523
|
+
specVersion: 1,
|
|
25524
|
+
id: "core-financial/iban",
|
|
25525
|
+
name: "International Bank Account Number",
|
|
25526
|
+
category: "financial",
|
|
25527
|
+
severity: "high",
|
|
25528
|
+
matcher: {
|
|
25529
|
+
type: "regex",
|
|
25530
|
+
pattern: "\\b[A-Z]{2}\\d{2}[A-Z0-9 ]{10,30}\\b",
|
|
25531
|
+
flags: "g"
|
|
25532
|
+
},
|
|
25533
|
+
requiresNearby: {
|
|
25534
|
+
labels: ["iban", "bank", "account", "bic", "swift"],
|
|
25535
|
+
windowChars: 160
|
|
25536
|
+
},
|
|
25537
|
+
examples: ["IBAN: GB82 WEST 1234 5698 7654 32", "IBAN: DE89 3704 0044 0532 0130 00"]
|
|
25538
|
+
};
|
|
25539
|
+
|
|
25540
|
+
// ../../rules/core-financial/paypal.json
|
|
25541
|
+
var paypal_default = {
|
|
25542
|
+
specVersion: 1,
|
|
25543
|
+
id: "core-financial/paypal",
|
|
25544
|
+
name: "PayPal transaction/merchant ID",
|
|
25545
|
+
category: "financial",
|
|
25546
|
+
severity: "medium",
|
|
25547
|
+
matcher: {
|
|
25548
|
+
type: "regex",
|
|
25549
|
+
pattern: "paypal.{0,30}?\\b([A-Z0-9]{17})\\b",
|
|
25550
|
+
flags: "gi",
|
|
25551
|
+
captureGroup: 1
|
|
25552
|
+
},
|
|
25553
|
+
examples: ["PayPal transaction ID: 9HW7382910ABCDEF1"]
|
|
25554
|
+
};
|
|
25555
|
+
|
|
25556
|
+
// ../../rules/core-financial/routing-number.json
|
|
25557
|
+
var routing_number_default = {
|
|
25558
|
+
specVersion: 1,
|
|
25559
|
+
id: "core-financial/routing-number",
|
|
25560
|
+
name: "US bank routing number (ABA)",
|
|
25561
|
+
category: "financial",
|
|
25562
|
+
severity: "high",
|
|
25563
|
+
matcher: {
|
|
25564
|
+
type: "regex",
|
|
25565
|
+
pattern: "\\b(?:routing|aba|rtn)\\b[\\s#:=-]{0,8}(\\d{9})\\b",
|
|
25566
|
+
flags: "gi",
|
|
25567
|
+
captureGroup: 1
|
|
25568
|
+
},
|
|
25569
|
+
examples: ["Routing: 021000021", "ABA 111000025"]
|
|
25570
|
+
};
|
|
25571
|
+
|
|
25572
|
+
// ../../rules/core-financial/salary.json
|
|
25573
|
+
var salary_default = {
|
|
25574
|
+
specVersion: 1,
|
|
25575
|
+
id: "core-financial/salary",
|
|
25576
|
+
name: "Salary/compensation (contextual)",
|
|
25577
|
+
category: "financial",
|
|
25578
|
+
severity: "low",
|
|
25579
|
+
matcher: {
|
|
25580
|
+
type: "keyword",
|
|
25581
|
+
keywords: [
|
|
25582
|
+
"my salary is",
|
|
25583
|
+
"my base salary",
|
|
25584
|
+
"annual compensation",
|
|
25585
|
+
"total comp",
|
|
25586
|
+
"my tc is",
|
|
25587
|
+
"my package is",
|
|
25588
|
+
"i make $",
|
|
25589
|
+
"i earn $",
|
|
25590
|
+
"offered $"
|
|
25591
|
+
],
|
|
25592
|
+
caseSensitive: false
|
|
25593
|
+
},
|
|
25594
|
+
examples: ["My salary is $150,000"]
|
|
25595
|
+
};
|
|
25596
|
+
|
|
25597
|
+
// ../../rules/core-financial/swift.json
|
|
25598
|
+
var swift_default = {
|
|
25599
|
+
specVersion: 1,
|
|
25600
|
+
id: "core-financial/swift",
|
|
25601
|
+
name: "SWIFT/BIC code",
|
|
25602
|
+
category: "financial",
|
|
25603
|
+
severity: "high",
|
|
25604
|
+
matcher: {
|
|
25605
|
+
type: "regex",
|
|
25606
|
+
pattern: "\\b(?:swift|bic)\\b[\\s#:=-]{0,8}([A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?)\\b",
|
|
25607
|
+
flags: "gi",
|
|
25608
|
+
captureGroup: 1
|
|
25609
|
+
},
|
|
25610
|
+
examples: ["SWIFT: BOFAUS3N", "BIC: CHASGB2LXXX"]
|
|
25611
|
+
};
|
|
25612
|
+
|
|
25613
|
+
// ../../rules/core-phi/biometric-ref.json
|
|
25614
|
+
var biometric_ref_default = {
|
|
25615
|
+
specVersion: 1,
|
|
25616
|
+
id: "core-phi/biometric-ref",
|
|
25617
|
+
name: "Biometric data reference",
|
|
25618
|
+
category: "phi",
|
|
25619
|
+
severity: "high",
|
|
25620
|
+
matcher: {
|
|
25621
|
+
type: "keyword",
|
|
25622
|
+
keywords: [
|
|
25623
|
+
"fingerprint data",
|
|
25624
|
+
"fingerprint scan",
|
|
25625
|
+
"my iris scan",
|
|
25626
|
+
"my retina scan",
|
|
25627
|
+
"my biometric",
|
|
25628
|
+
"my facial recognition",
|
|
25629
|
+
"my voiceprint",
|
|
25630
|
+
"my palm print",
|
|
25631
|
+
"my hand geometry"
|
|
25632
|
+
],
|
|
25633
|
+
caseSensitive: false
|
|
25634
|
+
},
|
|
25635
|
+
examples: ["My fingerprint data is stored in the biometric database"]
|
|
25636
|
+
};
|
|
25637
|
+
|
|
25638
|
+
// ../../rules/core-phi/genetic-data.json
|
|
25639
|
+
var genetic_data_default = {
|
|
25640
|
+
specVersion: 1,
|
|
25641
|
+
id: "core-phi/genetic-data",
|
|
25642
|
+
name: "Genetic/DNA sequence reference",
|
|
25643
|
+
category: "phi",
|
|
25644
|
+
severity: "high",
|
|
25645
|
+
matcher: {
|
|
25646
|
+
type: "keyword",
|
|
25647
|
+
keywords: [
|
|
25648
|
+
"my dna sequence",
|
|
25649
|
+
"my genome",
|
|
25650
|
+
"my genetic",
|
|
25651
|
+
"dna test results",
|
|
25652
|
+
"genetic test results",
|
|
25653
|
+
"my 23andme",
|
|
25654
|
+
"my ancestrydna",
|
|
25655
|
+
"gene variant",
|
|
25656
|
+
"my genotype",
|
|
25657
|
+
"my haplogroup"
|
|
25658
|
+
],
|
|
25659
|
+
caseSensitive: false
|
|
25660
|
+
},
|
|
25661
|
+
examples: ["My DNA sequence shows a variant in the BRCA1 gene"]
|
|
25662
|
+
};
|
|
25663
|
+
|
|
25664
|
+
// ../../rules/core-phi/group-id.json
|
|
25665
|
+
var group_id_default = {
|
|
25666
|
+
specVersion: 1,
|
|
25667
|
+
id: "core-phi/group-id",
|
|
25668
|
+
name: "Health plan group ID",
|
|
25669
|
+
category: "phi",
|
|
25670
|
+
severity: "high",
|
|
25671
|
+
matcher: {
|
|
25672
|
+
type: "regex",
|
|
25673
|
+
pattern: "\\bgroup\\s*(?:id|number|no|#)\\s*[:#]?\\s*[A-Za-z0-9]{6,20}\\b",
|
|
25674
|
+
flags: "gi"
|
|
25675
|
+
},
|
|
25676
|
+
requiresNearby: {
|
|
25677
|
+
ruleIds: ["core-phi/member-id"],
|
|
25678
|
+
labels: ["member id", "subscriber", "health plan"],
|
|
25679
|
+
windowChars: 200,
|
|
25680
|
+
confidenceBoost: 0.05
|
|
25681
|
+
},
|
|
25682
|
+
examples: ["Member ID: ABC987654321, Group ID: GRP12345678"]
|
|
25683
|
+
};
|
|
25684
|
+
|
|
25685
|
+
// ../../rules/core-phi/hipaa-identifier.json
|
|
25686
|
+
var hipaa_identifier_default = {
|
|
25687
|
+
specVersion: 1,
|
|
25688
|
+
id: "core-phi/hipaa-identifier",
|
|
25689
|
+
name: "HIPAA standard identifier (health plan / beneficiary / claim number)",
|
|
25690
|
+
category: "phi",
|
|
25691
|
+
severity: "high",
|
|
25692
|
+
matcher: {
|
|
25693
|
+
type: "regex",
|
|
25694
|
+
pattern: "\\b(?:health\\s+plan|beneficiary|claim\\s+number|subscriber)[:\\s]*[A-Za-z0-9]{8,20}\\b",
|
|
25695
|
+
flags: "gi"
|
|
25696
|
+
},
|
|
25697
|
+
examples: ["Health Plan: XYZ123456789", "Beneficiary: ABC987654321"]
|
|
25698
|
+
};
|
|
25699
|
+
|
|
25700
|
+
// ../../rules/core-phi/icd10-code.json
|
|
25701
|
+
var icd10_code_default = {
|
|
25702
|
+
specVersion: 1,
|
|
25703
|
+
id: "core-phi/icd10-code",
|
|
25704
|
+
name: "ICD-10 diagnosis code",
|
|
25705
|
+
category: "phi",
|
|
25706
|
+
severity: "medium",
|
|
25707
|
+
matcher: {
|
|
25708
|
+
type: "regex",
|
|
25709
|
+
pattern: "(?:icd[- ]?10|diagnosis|\\bdx\\b).{0,12}?([A-Z][0-9]{2}(?:\\.[0-9]{1,4})?)\\b",
|
|
25710
|
+
flags: "gi",
|
|
25711
|
+
captureGroup: 1
|
|
25712
|
+
},
|
|
25713
|
+
examples: ["ICD-10: I10", "Diagnosis code E11.9", "dx: J45.909"]
|
|
25714
|
+
};
|
|
25715
|
+
|
|
25716
|
+
// ../../rules/core-phi/member-id.json
|
|
25717
|
+
var member_id_default = {
|
|
25718
|
+
specVersion: 1,
|
|
25719
|
+
id: "core-phi/member-id",
|
|
25720
|
+
name: "Health plan member ID",
|
|
25721
|
+
category: "phi",
|
|
25722
|
+
severity: "high",
|
|
25723
|
+
matcher: {
|
|
25724
|
+
type: "regex",
|
|
25725
|
+
pattern: "\\bmember\\s*(?:id|number|no|#)\\s*[:#]?\\s*[A-Za-z0-9]{6,20}\\b",
|
|
25726
|
+
flags: "gi"
|
|
25727
|
+
},
|
|
25728
|
+
examples: ["Member ID: ABC987654321"]
|
|
25729
|
+
};
|
|
25730
|
+
|
|
25731
|
+
// ../../rules/core-phi/mrn.json
|
|
25732
|
+
var mrn_default = {
|
|
25733
|
+
specVersion: 1,
|
|
25734
|
+
id: "core-phi/mrn",
|
|
25735
|
+
name: "Medical Record Number",
|
|
25736
|
+
category: "phi",
|
|
25737
|
+
severity: "high",
|
|
25738
|
+
matcher: {
|
|
25739
|
+
type: "regex",
|
|
25740
|
+
pattern: "\\b(?:MRN|MR#|medical\\s+record)[:\\s]*[A-Za-z0-9]{6,16}\\b",
|
|
25741
|
+
flags: "gi"
|
|
25742
|
+
},
|
|
25743
|
+
examples: ["MRN: 1234567890", "Medical Record: ABCD1234"]
|
|
25744
|
+
};
|
|
25745
|
+
|
|
25746
|
+
// ../../rules/core-phi/ndc-code.json
|
|
25747
|
+
var ndc_code_default = {
|
|
25748
|
+
specVersion: 1,
|
|
25749
|
+
id: "core-phi/ndc-code",
|
|
25750
|
+
name: "National Drug Code (NDC)",
|
|
25751
|
+
category: "phi",
|
|
25752
|
+
severity: "medium",
|
|
25753
|
+
matcher: {
|
|
25754
|
+
type: "regex",
|
|
25755
|
+
pattern: "\\b\\d{4,5}-\\d{3,4}-\\d{1,2}\\b",
|
|
25756
|
+
flags: "g"
|
|
25757
|
+
},
|
|
25758
|
+
examples: ["0002-1427-01", "68788-6965-1"]
|
|
25759
|
+
};
|
|
25760
|
+
|
|
25761
|
+
// ../../rules/core-pii/dob.json
|
|
25762
|
+
var dob_default = {
|
|
25763
|
+
specVersion: 1,
|
|
25764
|
+
id: "core-pii/dob",
|
|
25765
|
+
name: "Date of birth",
|
|
25766
|
+
category: "pii",
|
|
25767
|
+
severity: "high",
|
|
25768
|
+
matcher: {
|
|
25769
|
+
type: "regex",
|
|
25770
|
+
pattern: "\\b(?:(?:19[4-9]\\d|20[0-1]\\d)[/-]\\d{1,2}[/-]\\d{1,2}|\\d{1,2}[/-]\\d{1,2}[/-](?:19[4-9]\\d|20[0-1]\\d))\\b",
|
|
25771
|
+
flags: "g"
|
|
25772
|
+
},
|
|
25773
|
+
requiresNearby: {
|
|
25774
|
+
categories: ["pii"],
|
|
25775
|
+
labels: ["dob", "date of birth", "birth", "d.o.b"],
|
|
25776
|
+
windowChars: 160
|
|
25777
|
+
},
|
|
25778
|
+
examples: ["DOB: 1985-03-22", "Date of birth: 03/22/1985"]
|
|
25779
|
+
};
|
|
25780
|
+
|
|
25781
|
+
// ../../rules/core-pii/drivers-license-us.json
|
|
25782
|
+
var drivers_license_us_default = {
|
|
25783
|
+
specVersion: 1,
|
|
25784
|
+
id: "core-pii/drivers-license-us",
|
|
25785
|
+
name: "US driver's license number",
|
|
25786
|
+
category: "pii",
|
|
25787
|
+
severity: "high",
|
|
25788
|
+
matcher: {
|
|
25789
|
+
type: "regex",
|
|
25790
|
+
pattern: "(?:driver'?s?\\s*licen[sc]e|licen[sc]e|\\bDL\\b).{0,10}?([A-Z]\\d{6,8}|\\d{8,9})\\b",
|
|
25791
|
+
flags: "gi",
|
|
25792
|
+
captureGroup: 1
|
|
25793
|
+
},
|
|
25794
|
+
examples: ["Driver's License: D1234567", "DL: 123456789"]
|
|
25795
|
+
};
|
|
25796
|
+
|
|
25797
|
+
// ../../rules/core-pii/email.json
|
|
25798
|
+
var email_default = {
|
|
25799
|
+
specVersion: 1,
|
|
25800
|
+
id: "core-pii/email",
|
|
25801
|
+
name: "Email address",
|
|
25802
|
+
category: "pii",
|
|
25803
|
+
severity: "medium",
|
|
25804
|
+
matcher: {
|
|
25805
|
+
type: "regex",
|
|
25806
|
+
pattern: "(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\\.[A-Za-z]{2,24}\\b",
|
|
25807
|
+
flags: "g"
|
|
25808
|
+
},
|
|
25809
|
+
examples: ["user@example.com"]
|
|
25810
|
+
};
|
|
25811
|
+
|
|
25812
|
+
// ../../rules/core-pii/home-address.json
|
|
25813
|
+
var home_address_default = {
|
|
25814
|
+
specVersion: 1,
|
|
25815
|
+
id: "core-pii/home-address",
|
|
25816
|
+
name: "Home address (contextual)",
|
|
25817
|
+
category: "pii",
|
|
25818
|
+
severity: "medium",
|
|
25819
|
+
matcher: {
|
|
25820
|
+
type: "keyword",
|
|
25821
|
+
keywords: [
|
|
25822
|
+
"my address is",
|
|
25823
|
+
"home address",
|
|
25824
|
+
"shipping address",
|
|
25825
|
+
"billing address",
|
|
25826
|
+
"mailing address",
|
|
25827
|
+
"my home is at",
|
|
25828
|
+
"i reside at"
|
|
25829
|
+
],
|
|
25830
|
+
caseSensitive: false
|
|
25831
|
+
},
|
|
25832
|
+
examples: ["My address is 123 Main St, Springfield, IL 62701"]
|
|
25833
|
+
};
|
|
25834
|
+
|
|
25835
|
+
// ../../rules/core-pii/ip-address.json
|
|
25836
|
+
var ip_address_default = {
|
|
25837
|
+
specVersion: 1,
|
|
25838
|
+
id: "core-pii/ip-address",
|
|
25839
|
+
name: "IP address",
|
|
25840
|
+
category: "pii",
|
|
25841
|
+
severity: "low",
|
|
25842
|
+
matcher: {
|
|
25843
|
+
type: "regex",
|
|
25844
|
+
pattern: "\\b(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\b",
|
|
25845
|
+
flags: "g"
|
|
25846
|
+
},
|
|
25847
|
+
examples: ["192.168.1.1", "10.0.0.1"]
|
|
25848
|
+
};
|
|
25849
|
+
|
|
25850
|
+
// ../../rules/core-pii/mac-address.json
|
|
25851
|
+
var mac_address_default = {
|
|
25852
|
+
specVersion: 1,
|
|
25853
|
+
id: "core-pii/mac-address",
|
|
25854
|
+
name: "MAC address",
|
|
25855
|
+
category: "pii",
|
|
25856
|
+
severity: "low",
|
|
25857
|
+
matcher: {
|
|
25858
|
+
type: "regex",
|
|
25859
|
+
pattern: "\\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b",
|
|
25860
|
+
flags: "g"
|
|
25861
|
+
},
|
|
25862
|
+
examples: ["AA:BB:CC:DD:EE:FF"]
|
|
25863
|
+
};
|
|
25864
|
+
|
|
25865
|
+
// ../../rules/core-pii/name.json
|
|
25866
|
+
var name_default = {
|
|
25867
|
+
specVersion: 1,
|
|
25868
|
+
id: "core-pii/name",
|
|
25869
|
+
name: "Personal name (contextual)",
|
|
25870
|
+
category: "pii",
|
|
25871
|
+
severity: "medium",
|
|
25872
|
+
matcher: {
|
|
25873
|
+
type: "keyword",
|
|
25874
|
+
keywords: [
|
|
25875
|
+
"my name is",
|
|
25876
|
+
"my full name",
|
|
25877
|
+
"i am called",
|
|
25878
|
+
"please call me",
|
|
25879
|
+
"name:\n",
|
|
25880
|
+
'"full_name": "',
|
|
25881
|
+
'"fullName": "'
|
|
25882
|
+
],
|
|
25883
|
+
caseSensitive: false
|
|
25884
|
+
},
|
|
25885
|
+
examples: ["My name is John Doe"]
|
|
25886
|
+
};
|
|
25887
|
+
|
|
25888
|
+
// ../../rules/core-pii/national-id.json
|
|
25889
|
+
var national_id_default = {
|
|
25890
|
+
specVersion: 1,
|
|
25891
|
+
id: "core-pii/national-id",
|
|
25892
|
+
name: "National ID number (multi-country)",
|
|
25893
|
+
category: "pii",
|
|
25894
|
+
severity: "high",
|
|
25895
|
+
matcher: {
|
|
25896
|
+
type: "regex",
|
|
25897
|
+
pattern: "\\b[A-Z]{2}\\d{6}[A-D]\\b|\\b[2-9]\\d{11}\\b|(?:national\\s*id|aadhaar|\\bSIN\\b).{0,10}?\\d{3}[\\s-]?\\d{3}[\\s-]?\\d{3}\\b",
|
|
25898
|
+
flags: "gi"
|
|
25899
|
+
},
|
|
25900
|
+
requiresNearby: {
|
|
25901
|
+
labels: [
|
|
25902
|
+
"national id",
|
|
25903
|
+
"national identity",
|
|
25904
|
+
"national insurance",
|
|
25905
|
+
"nino",
|
|
25906
|
+
"ni",
|
|
25907
|
+
"aadhaar",
|
|
25908
|
+
"sin",
|
|
25909
|
+
"identity number",
|
|
25910
|
+
"id number"
|
|
25911
|
+
],
|
|
25912
|
+
windowChars: 160
|
|
25913
|
+
},
|
|
25914
|
+
examples: ["Aadhaar: 234567890123", "NI: AB123456C", "SIN: 123-456-789"]
|
|
25915
|
+
};
|
|
25916
|
+
|
|
25917
|
+
// ../../rules/core-pii/passport-us.json
|
|
25918
|
+
var passport_us_default = {
|
|
25919
|
+
specVersion: 1,
|
|
25920
|
+
id: "core-pii/passport-us",
|
|
25921
|
+
name: "US passport number",
|
|
25922
|
+
category: "pii",
|
|
25923
|
+
severity: "high",
|
|
25924
|
+
matcher: {
|
|
25925
|
+
type: "regex",
|
|
25926
|
+
pattern: "\\b[A-Z]\\d{8}\\b",
|
|
25927
|
+
flags: "g"
|
|
25928
|
+
},
|
|
25929
|
+
requiresNearby: {
|
|
25930
|
+
labels: ["passport"],
|
|
25931
|
+
windowChars: 160
|
|
25932
|
+
},
|
|
25933
|
+
examples: ["Passport: A12345678"]
|
|
25934
|
+
};
|
|
25935
|
+
|
|
25936
|
+
// ../../rules/core-pii/phone-intl.json
|
|
25937
|
+
var phone_intl_default = {
|
|
25938
|
+
specVersion: 1,
|
|
25939
|
+
id: "core-pii/phone-intl",
|
|
25940
|
+
name: "International phone number",
|
|
25941
|
+
category: "pii",
|
|
25942
|
+
severity: "high",
|
|
25943
|
+
matcher: {
|
|
25944
|
+
type: "regex",
|
|
25945
|
+
pattern: "\\+\\d{1,3}(?:[-.\\s]\\d{1,14}){1,5}\\b",
|
|
25946
|
+
flags: "g"
|
|
25947
|
+
},
|
|
25948
|
+
examples: ["+44 20 7946 0958", "+86 138 0013 8000"]
|
|
25949
|
+
};
|
|
25950
|
+
|
|
25951
|
+
// ../../rules/core-pii/phone-us.json
|
|
25952
|
+
var phone_us_default = {
|
|
25953
|
+
specVersion: 1,
|
|
25954
|
+
id: "core-pii/phone-us",
|
|
25955
|
+
name: "US phone number",
|
|
25956
|
+
category: "pii",
|
|
25957
|
+
severity: "high",
|
|
25958
|
+
matcher: {
|
|
25959
|
+
type: "regex",
|
|
25960
|
+
pattern: "(?<!\\d)(?:\\+?1[-.\\s]?)?(?:\\([2-9]\\d{2}\\)[-.\\s]?|[2-9]\\d{2}[-.\\s])\\d{3}[-.\\s]\\d{4}(?!\\d)",
|
|
25961
|
+
flags: "g"
|
|
25962
|
+
},
|
|
25963
|
+
requiresNearby: {
|
|
25964
|
+
labels: ["phone", "call", "mobile", "cell", "tel", "telephone", "contact", "fax", "dial"],
|
|
25965
|
+
windowChars: 160
|
|
25966
|
+
},
|
|
25967
|
+
examples: ["(555) 123-4567", "+1-555-123-4567"]
|
|
25968
|
+
};
|
|
25969
|
+
|
|
25970
|
+
// ../../rules/core-pii/ssn.json
|
|
25971
|
+
var ssn_default = {
|
|
25972
|
+
specVersion: 1,
|
|
25973
|
+
id: "core-pii/ssn",
|
|
25974
|
+
name: "US Social Security Number",
|
|
25975
|
+
category: "pii",
|
|
25976
|
+
severity: "high",
|
|
25977
|
+
matcher: {
|
|
25978
|
+
type: "regex",
|
|
25979
|
+
pattern: "\\b(?!000|666|9\\d{2})\\d{3}[\\s\\-](?!00)\\d{2}[\\s\\-](?!0000)\\d{4}\\b",
|
|
25980
|
+
flags: "g"
|
|
25981
|
+
},
|
|
25982
|
+
examples: ["123-45-6789"]
|
|
25983
|
+
};
|
|
25984
|
+
|
|
25985
|
+
// ../../rules/core-pii/vin.json
|
|
25986
|
+
var vin_default = {
|
|
25987
|
+
specVersion: 1,
|
|
25988
|
+
id: "core-pii/vin",
|
|
25989
|
+
name: "Vehicle Identification Number",
|
|
25990
|
+
category: "pii",
|
|
25991
|
+
severity: "medium",
|
|
25992
|
+
matcher: {
|
|
25993
|
+
type: "regex",
|
|
25994
|
+
pattern: "\\b[A-HJ-NPR-Z0-9]{17}\\b",
|
|
25995
|
+
flags: "g"
|
|
25996
|
+
},
|
|
25997
|
+
examples: ["1HGCM82633A004352"]
|
|
25998
|
+
};
|
|
25999
|
+
|
|
26000
|
+
// ../../rules/core-pii/zip.json
|
|
26001
|
+
var zip_default = {
|
|
26002
|
+
specVersion: 1,
|
|
26003
|
+
id: "core-pii/zip",
|
|
26004
|
+
name: "US ZIP / ZIP+4 postal code",
|
|
26005
|
+
category: "pii",
|
|
26006
|
+
severity: "low",
|
|
26007
|
+
matcher: {
|
|
26008
|
+
type: "regex",
|
|
26009
|
+
pattern: "\\b\\d{5}(?:-\\d{4})?\\b",
|
|
26010
|
+
flags: "g"
|
|
26011
|
+
},
|
|
26012
|
+
requiresNearby: {
|
|
26013
|
+
ruleIds: ["core-pii/home-address"],
|
|
26014
|
+
labels: [
|
|
26015
|
+
"address",
|
|
26016
|
+
"street",
|
|
26017
|
+
"avenue",
|
|
26018
|
+
"boulevard",
|
|
26019
|
+
"mailing",
|
|
26020
|
+
"shipping",
|
|
26021
|
+
"billing",
|
|
26022
|
+
"suite",
|
|
26023
|
+
"p.o. box",
|
|
26024
|
+
"po box",
|
|
26025
|
+
"postal"
|
|
26026
|
+
],
|
|
26027
|
+
windowChars: 120
|
|
26028
|
+
},
|
|
26029
|
+
examples: ["Shipping address: 123 Main Street, Springfield, IL 94107-1234"]
|
|
26030
|
+
};
|
|
26031
|
+
|
|
26032
|
+
// ../../rules/secrets-infra/api-key-header.json
|
|
26033
|
+
var api_key_header_default = {
|
|
26034
|
+
specVersion: 1,
|
|
26035
|
+
id: "secrets-infra/api-key-header",
|
|
26036
|
+
name: "API key in a custom HTTP header",
|
|
26037
|
+
category: "secret",
|
|
26038
|
+
severity: "high",
|
|
26039
|
+
matcher: {
|
|
26040
|
+
type: "regex",
|
|
26041
|
+
pattern: `["']?\\bapi-?key\\b["']?\\s*:\\s*["']?([A-Za-z0-9+/_\\-.]{16,500})["']?`,
|
|
26042
|
+
flags: "gi",
|
|
26043
|
+
captureGroup: 1
|
|
26044
|
+
},
|
|
26045
|
+
postValidators: ["entropy"],
|
|
26046
|
+
examples: ["X-Api-Key: aBc123XyZ789kLmNoPqRsTuVwXyZ1234567890"]
|
|
26047
|
+
};
|
|
26048
|
+
|
|
26049
|
+
// ../../rules/secrets-infra/basic-auth-header.json
|
|
26050
|
+
var basic_auth_header_default = {
|
|
26051
|
+
specVersion: 1,
|
|
26052
|
+
id: "secrets-infra/basic-auth-header",
|
|
26053
|
+
name: "HTTP Basic auth Authorization header",
|
|
26054
|
+
category: "secret",
|
|
26055
|
+
severity: "critical",
|
|
26056
|
+
matcher: {
|
|
26057
|
+
type: "regex",
|
|
26058
|
+
pattern: "authorization\\s*:\\s*basic\\s+[A-Za-z0-9+/=]{16,}",
|
|
26059
|
+
flags: "gi"
|
|
26060
|
+
},
|
|
26061
|
+
examples: ["Authorization: Basic dXNlcjpwYXNzd29yZDEyMzQ="]
|
|
26062
|
+
};
|
|
26063
|
+
|
|
26064
|
+
// ../../rules/secrets-infra/basic-auth-url.json
|
|
26065
|
+
var basic_auth_url_default = {
|
|
26066
|
+
specVersion: 1,
|
|
26067
|
+
id: "secrets-infra/basic-auth-url",
|
|
26068
|
+
name: "URL with embedded basic auth credentials",
|
|
26069
|
+
category: "secret",
|
|
26070
|
+
severity: "critical",
|
|
26071
|
+
matcher: {
|
|
26072
|
+
type: "regex",
|
|
26073
|
+
pattern: "https?://[A-Za-z0-9_%\\-]+:[A-Za-z0-9_%\\-]+@[A-Za-z0-9.\\-]+",
|
|
26074
|
+
flags: "g"
|
|
26075
|
+
},
|
|
26076
|
+
examples: ["https://user:password@example.com/path"]
|
|
26077
|
+
};
|
|
26078
|
+
|
|
26079
|
+
// ../../rules/secrets-infra/bearer-token.json
|
|
26080
|
+
var bearer_token_default = {
|
|
26081
|
+
specVersion: 1,
|
|
26082
|
+
id: "secrets-infra/bearer-token",
|
|
26083
|
+
name: "Generic Authorization Bearer token",
|
|
26084
|
+
category: "secret",
|
|
26085
|
+
severity: "high",
|
|
26086
|
+
matcher: {
|
|
26087
|
+
type: "regex",
|
|
26088
|
+
pattern: "\\b(?:Bearer|bearer)\\s+([A-Za-z0-9_\\-.]{20,500})",
|
|
26089
|
+
flags: "g",
|
|
26090
|
+
captureGroup: 1
|
|
26091
|
+
},
|
|
26092
|
+
postValidators: ["entropy"],
|
|
26093
|
+
examples: [
|
|
26094
|
+
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
|
26095
|
+
]
|
|
26096
|
+
};
|
|
26097
|
+
|
|
26098
|
+
// ../../rules/secrets-infra/db-connection-string.json
|
|
26099
|
+
var db_connection_string_default = {
|
|
26100
|
+
specVersion: 1,
|
|
26101
|
+
id: "secrets-infra/db-connection-string",
|
|
26102
|
+
name: "Database connection string with credentials",
|
|
26103
|
+
category: "secret",
|
|
26104
|
+
severity: "critical",
|
|
26105
|
+
matcher: {
|
|
26106
|
+
type: "regex",
|
|
26107
|
+
pattern: "\\b(?:postgres(?:ql)?|mysql|mongodb(?:\\+srv)?|redis|rediss)://[A-Za-z0-9_%\\-]+:[A-Za-z0-9_%\\-]+@[A-Za-z0-9.\\-]+(?:\\:\\d+)?",
|
|
26108
|
+
flags: "g"
|
|
26109
|
+
},
|
|
26110
|
+
examples: ["postgresql://user:password@localhost:5432/mydb"]
|
|
26111
|
+
};
|
|
26112
|
+
|
|
26113
|
+
// ../../rules/secrets-infra/docker-config-auth.json
|
|
26114
|
+
var docker_config_auth_default = {
|
|
26115
|
+
specVersion: 1,
|
|
26116
|
+
id: "secrets-infra/docker-config-auth",
|
|
26117
|
+
name: "Docker config.json auth (base64-encoded)",
|
|
26118
|
+
category: "secret",
|
|
26119
|
+
severity: "critical",
|
|
26120
|
+
matcher: {
|
|
26121
|
+
type: "regex",
|
|
26122
|
+
pattern: '"auth"\\s*:\\s*"[A-Za-z0-9+/]{12,}={0,2}"',
|
|
26123
|
+
flags: "g"
|
|
26124
|
+
},
|
|
26125
|
+
examples: ['"auths": {"https://index.docker.io/v1/": {"auth": "dXNlcjpwYXNzd29yZA=="}}']
|
|
26126
|
+
};
|
|
26127
|
+
|
|
26128
|
+
// ../../rules/secrets-infra/env-key-value.json
|
|
26129
|
+
var env_key_value_default = {
|
|
26130
|
+
specVersion: 1,
|
|
26131
|
+
id: "secrets-infra/env-key-value",
|
|
26132
|
+
name: "Environment variable assignment with sensitive key",
|
|
26133
|
+
category: "secret",
|
|
26134
|
+
severity: "high",
|
|
26135
|
+
matcher: {
|
|
26136
|
+
type: "regex",
|
|
26137
|
+
pattern: `\\b[A-Za-z0-9_]*(?:API_KEY|API_SECRET|SECRET_KEY|SECRET|TOKEN|PASSWORD|PASS|CREDENTIALS|PRIVATE_KEY|ENCRYPTION_KEY|SIGNING_KEY)\\s*=\\s*["']?([^\\s"']{4,})`,
|
|
26138
|
+
flags: "gi",
|
|
26139
|
+
captureGroup: 1
|
|
26140
|
+
},
|
|
26141
|
+
examples: ["API_KEY=aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890"]
|
|
26142
|
+
};
|
|
26143
|
+
|
|
26144
|
+
// ../../rules/secrets-infra/generic-high-entropy-secret.json
|
|
26145
|
+
var generic_high_entropy_secret_default = {
|
|
26146
|
+
specVersion: 1,
|
|
26147
|
+
id: "secrets-infra/generic-high-entropy-secret",
|
|
26148
|
+
name: "Generic high-entropy secret assigned to a credential-shaped field",
|
|
26149
|
+
category: "secret",
|
|
26150
|
+
severity: "high",
|
|
26151
|
+
matcher: {
|
|
26152
|
+
type: "regex",
|
|
26153
|
+
pattern: `(?<![A-Za-z0-9_-])[A-Za-z0-9_]{0,34}(?:(?:secret|token|passw(?:or)?d|credentials?|(?<!api[_-]?)key)(?:["']?\\s*:|\\s+=)|(?:passwd|credential|(?<!api[_-]?)(?<!secret_)(?<!private_)(?<!encryption_)(?<!signing_)key)=)\\s*["']?((?!eyJ)[A-Za-z0-9+/_\\-]{20,200})`,
|
|
26154
|
+
flags: "gi",
|
|
26155
|
+
captureGroup: 1
|
|
26156
|
+
},
|
|
26157
|
+
postValidators: ["entropy"],
|
|
26158
|
+
examples: [
|
|
26159
|
+
'authToken: "aBc123XyZ789kLmNoPqRsTuVwXyZ1234567890"',
|
|
26160
|
+
"MASTER_KEY=aBc123XyZ789kLmNoPqRsTuVwXyZ1234567890"
|
|
26161
|
+
]
|
|
26162
|
+
};
|
|
26163
|
+
|
|
26164
|
+
// ../../rules/secrets-infra/jwt-token.json
|
|
26165
|
+
var jwt_token_default = {
|
|
26166
|
+
specVersion: 1,
|
|
26167
|
+
id: "secrets-infra/jwt-token",
|
|
26168
|
+
name: "JSON Web Token (JWT)",
|
|
26169
|
+
category: "secret",
|
|
26170
|
+
severity: "high",
|
|
26171
|
+
matcher: {
|
|
26172
|
+
type: "regex",
|
|
26173
|
+
pattern: "\\beyJ[A-Za-z0-9_\\-+/]+\\.[A-Za-z0-9_\\-+/]+\\.[A-Za-z0-9_\\-+/]+\\b",
|
|
26174
|
+
flags: "g"
|
|
26175
|
+
},
|
|
26176
|
+
postValidators: ["entropy"],
|
|
26177
|
+
examples: [
|
|
26178
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
|
26179
|
+
]
|
|
26180
|
+
};
|
|
26181
|
+
|
|
26182
|
+
// ../../rules/secrets-infra/kubeconfig-token.json
|
|
26183
|
+
var kubeconfig_token_default = {
|
|
26184
|
+
specVersion: 1,
|
|
26185
|
+
id: "secrets-infra/kubeconfig-token",
|
|
26186
|
+
name: "Kubernetes kubeconfig embedded token",
|
|
26187
|
+
category: "secret",
|
|
26188
|
+
severity: "critical",
|
|
26189
|
+
matcher: {
|
|
26190
|
+
type: "regex",
|
|
26191
|
+
pattern: "\\b(?:client-(?:certificate|key)-data|token):\\s+([A-Za-z0-9+/=_.-]{20,})",
|
|
26192
|
+
flags: "g",
|
|
26193
|
+
captureGroup: 1
|
|
26194
|
+
},
|
|
26195
|
+
examples: ["token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9..."]
|
|
26196
|
+
};
|
|
26197
|
+
|
|
26198
|
+
// ../../rules/secrets-infra/password-field.json
|
|
26199
|
+
var password_field_default = {
|
|
26200
|
+
specVersion: 1,
|
|
26201
|
+
id: "secrets-infra/password-field",
|
|
26202
|
+
name: "JSON/YAML field with password value",
|
|
26203
|
+
category: "secret",
|
|
26204
|
+
severity: "high",
|
|
26205
|
+
matcher: {
|
|
26206
|
+
type: "regex",
|
|
26207
|
+
pattern: `["']?\\b(?:password|passwd|pwd|pass|secret|token|api[_-]?key|apikey)\\b["']?\\s*:\\s*["']([^"'\\n]{4,})["']`,
|
|
26208
|
+
flags: "gi",
|
|
26209
|
+
captureGroup: 1
|
|
26210
|
+
},
|
|
26211
|
+
examples: ['"password": "supersecret123"']
|
|
26212
|
+
};
|
|
26213
|
+
|
|
26214
|
+
// ../../rules/secrets-infra/pgp-private-key.json
|
|
26215
|
+
var pgp_private_key_default = {
|
|
26216
|
+
specVersion: 1,
|
|
26217
|
+
id: "secrets-infra/pgp-private-key",
|
|
26218
|
+
name: "PGP private key block",
|
|
26219
|
+
category: "secret",
|
|
26220
|
+
severity: "critical",
|
|
26221
|
+
matcher: {
|
|
26222
|
+
type: "regex",
|
|
26223
|
+
pattern: "-----BEGIN PGP PRIVATE KEY BLOCK-----",
|
|
26224
|
+
flags: "g"
|
|
26225
|
+
},
|
|
26226
|
+
examples: [
|
|
26227
|
+
"-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: Keybase Go 1.0.0\n...\n-----END PGP PRIVATE KEY BLOCK-----"
|
|
26228
|
+
]
|
|
26229
|
+
};
|
|
26230
|
+
|
|
26231
|
+
// ../../rules/secrets-infra/ssh-private-key.json
|
|
26232
|
+
var ssh_private_key_default = {
|
|
26233
|
+
specVersion: 1,
|
|
26234
|
+
id: "secrets-infra/ssh-private-key",
|
|
26235
|
+
name: "SSH private key",
|
|
26236
|
+
category: "secret",
|
|
26237
|
+
severity: "critical",
|
|
26238
|
+
matcher: {
|
|
26239
|
+
type: "regex",
|
|
26240
|
+
pattern: "-----BEGIN (?:RSA |DSA |EC |OPENSSH |SSH2 )?PRIVATE KEY-----",
|
|
26241
|
+
flags: "g"
|
|
26242
|
+
},
|
|
26243
|
+
examples: [
|
|
26244
|
+
"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----"
|
|
26245
|
+
]
|
|
26246
|
+
};
|
|
26247
|
+
|
|
26248
|
+
// ../../rules/secrets/anthropic-api-key.json
|
|
26249
|
+
var anthropic_api_key_default = {
|
|
26250
|
+
specVersion: 1,
|
|
26251
|
+
id: "secrets/anthropic-api-key",
|
|
26252
|
+
name: "Anthropic API key",
|
|
26253
|
+
category: "secret",
|
|
26254
|
+
severity: "critical",
|
|
26255
|
+
matcher: {
|
|
26256
|
+
type: "regex",
|
|
26257
|
+
pattern: "\\bsk-ant-[A-Za-z0-9_-]{40,}",
|
|
26258
|
+
flags: "g"
|
|
26259
|
+
},
|
|
26260
|
+
postValidators: ["entropy"],
|
|
26261
|
+
examples: ["sk-ant-AbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYz"]
|
|
26262
|
+
};
|
|
26263
|
+
|
|
26264
|
+
// ../../rules/secrets/aws-access-key.json
|
|
26265
|
+
var aws_access_key_default = {
|
|
26266
|
+
specVersion: 1,
|
|
26267
|
+
id: "secrets/aws-access-key",
|
|
26268
|
+
name: "AWS Access Key ID",
|
|
26269
|
+
category: "secret",
|
|
26270
|
+
severity: "critical",
|
|
26271
|
+
matcher: {
|
|
26272
|
+
type: "regex",
|
|
26273
|
+
pattern: "\\b(AKIA|ASIA|AROA|AIDA|ANPA|ANVA|APKA)[A-Z0-9]{16}\\b",
|
|
26274
|
+
flags: "g"
|
|
26275
|
+
},
|
|
26276
|
+
postValidators: ["entropy"],
|
|
26277
|
+
examples: ["AKIAIOSFODNN7EXAMPLE"]
|
|
26278
|
+
};
|
|
26279
|
+
|
|
26280
|
+
// ../../rules/secrets/aws-secret-key.json
|
|
26281
|
+
var aws_secret_key_default = {
|
|
26282
|
+
specVersion: 1,
|
|
26283
|
+
id: "secrets/aws-secret-key",
|
|
26284
|
+
name: "AWS Secret Access Key",
|
|
26285
|
+
category: "secret",
|
|
26286
|
+
severity: "critical",
|
|
26287
|
+
matcher: {
|
|
26288
|
+
type: "regex",
|
|
26289
|
+
pattern: `aws[_.-]?secret[_.-]?(?:access[_.-]?)?key["'\\s:=]{1,5}([A-Za-z0-9/+]{40})`,
|
|
26290
|
+
flags: "gi",
|
|
26291
|
+
captureGroup: 1
|
|
26292
|
+
},
|
|
26293
|
+
postValidators: ["entropy"],
|
|
26294
|
+
examples: ["AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"]
|
|
26295
|
+
};
|
|
26296
|
+
|
|
26297
|
+
// ../../rules/secrets/azure-connection-string.json
|
|
26298
|
+
var azure_connection_string_default = {
|
|
26299
|
+
specVersion: 1,
|
|
26300
|
+
id: "secrets/azure-connection-string",
|
|
26301
|
+
name: "Azure connection string",
|
|
26302
|
+
category: "secret",
|
|
26303
|
+
severity: "critical",
|
|
26304
|
+
matcher: {
|
|
26305
|
+
type: "regex",
|
|
26306
|
+
pattern: "(?<![A-Za-z0-9])(?:DefaultEndpointsProtocol|AccountEndpoint|AccountName|AccountKey|SharedAccessSignature|SharedAccessKeyName|SharedAccessKey|EndpointSuffix|BlobEndpoint|QueueEndpoint|TableEndpoint|FileEndpoint|HostName|Endpoint)=(?![{<($])[^\\x3B\\r\\n]{1,400}\\x3B(?:[A-Za-z][A-Za-z0-9_]{0,39}=[^\\x3B\\r\\n]{1,400}\\x3B){0,10}(?:DefaultEndpointsProtocol|AccountEndpoint|AccountName|AccountKey|SharedAccessSignature|SharedAccessKeyName|SharedAccessKey|EndpointSuffix|BlobEndpoint|QueueEndpoint|TableEndpoint|FileEndpoint|HostName|Endpoint)=(?![{<($])[^\\x3B\\r\\n]{1,400}(?:\\x3B[A-Za-z][A-Za-z0-9_]{0,39}=[^\\x3B\\r\\n]{1,400}){0,10}",
|
|
26307
|
+
flags: "g"
|
|
26308
|
+
},
|
|
26309
|
+
examples: ["AccountName=mystorage;AccountKey=abc123=="]
|
|
26310
|
+
};
|
|
26311
|
+
|
|
26312
|
+
// ../../rules/secrets/cloudflare-api-key.json
|
|
26313
|
+
var cloudflare_api_key_default = {
|
|
26314
|
+
specVersion: 1,
|
|
26315
|
+
id: "secrets/cloudflare-api-key",
|
|
26316
|
+
name: "Cloudflare API token",
|
|
26317
|
+
category: "secret",
|
|
26318
|
+
severity: "critical",
|
|
26319
|
+
matcher: {
|
|
26320
|
+
type: "regex",
|
|
26321
|
+
pattern: `cloudflare.{0,40}?["'\\s:=]([A-Za-z0-9_-]{40})`,
|
|
26322
|
+
flags: "gi",
|
|
26323
|
+
captureGroup: 1
|
|
26324
|
+
},
|
|
26325
|
+
postValidators: ["entropy"],
|
|
26326
|
+
examples: ["CLOUDFLARE_API_TOKEN=kQ9fL2mP7xR4tY6wB1nC8vD3jH5sZ0aG2eU4iO7p"]
|
|
26327
|
+
};
|
|
26328
|
+
|
|
26329
|
+
// ../../rules/secrets/datadog-key.json
|
|
26330
|
+
var datadog_key_default = {
|
|
26331
|
+
specVersion: 1,
|
|
26332
|
+
id: "secrets/datadog-key",
|
|
26333
|
+
name: "Datadog API/Application key",
|
|
26334
|
+
category: "secret",
|
|
26335
|
+
severity: "critical",
|
|
26336
|
+
matcher: {
|
|
26337
|
+
type: "regex",
|
|
26338
|
+
pattern: "\\b(?:DD_API_KEY|DD_APP_KEY|DATADOG_API_KEY|DATADOG_APP_KEY)[\\s:=]+([A-Za-z0-9]{32,40})\\b",
|
|
26339
|
+
flags: "gi",
|
|
26340
|
+
captureGroup: 1
|
|
26341
|
+
},
|
|
26342
|
+
postValidators: ["entropy"],
|
|
26343
|
+
examples: ["DD_API_KEY=a3F5g8H1j4K7m2N0p9Q6r1S4t7U0v3W8"]
|
|
26344
|
+
};
|
|
26345
|
+
|
|
26346
|
+
// ../../rules/secrets/digitalocean-token.json
|
|
26347
|
+
var digitalocean_token_default = {
|
|
26348
|
+
specVersion: 1,
|
|
26349
|
+
id: "secrets/digitalocean-token",
|
|
26350
|
+
name: "DigitalOcean Personal Access Token",
|
|
26351
|
+
category: "secret",
|
|
26352
|
+
severity: "critical",
|
|
26353
|
+
matcher: {
|
|
26354
|
+
type: "regex",
|
|
26355
|
+
pattern: "\\bdop_v1_[A-Za-z0-9]{64}\\b",
|
|
26356
|
+
flags: "g"
|
|
26357
|
+
},
|
|
26358
|
+
examples: ["dop_v1_1234567890123456789012345678901234567890123456789012345678901234"]
|
|
26359
|
+
};
|
|
26360
|
+
|
|
26361
|
+
// ../../rules/secrets/discord-token.json
|
|
26362
|
+
var discord_token_default = {
|
|
26363
|
+
specVersion: 1,
|
|
26364
|
+
id: "secrets/discord-token",
|
|
26365
|
+
name: "Discord bot token",
|
|
26366
|
+
category: "secret",
|
|
26367
|
+
severity: "critical",
|
|
26368
|
+
matcher: {
|
|
26369
|
+
type: "regex",
|
|
26370
|
+
pattern: "\\b[A-Za-z0-9]{24}\\.[A-Za-z0-9]{6}\\.[A-Za-z0-9_\\-]{27}\\b",
|
|
26371
|
+
flags: "g"
|
|
26372
|
+
},
|
|
26373
|
+
postValidators: ["entropy"],
|
|
26374
|
+
examples: ["AbCdEfGhIjKlMnOpQrStUvWx.YzAbCd.ABCdEfGhIjKlMnOpQrStUvWxYzAbCdEf"]
|
|
26375
|
+
};
|
|
26376
|
+
|
|
26377
|
+
// ../../rules/secrets/gcp-service-account.json
|
|
26378
|
+
var gcp_service_account_default = {
|
|
26379
|
+
specVersion: 1,
|
|
26380
|
+
id: "secrets/gcp-service-account",
|
|
26381
|
+
name: "GCP service account key",
|
|
26382
|
+
category: "secret",
|
|
26383
|
+
severity: "critical",
|
|
26384
|
+
matcher: {
|
|
26385
|
+
type: "regex",
|
|
26386
|
+
pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
|
|
26387
|
+
flags: "g"
|
|
26388
|
+
},
|
|
26389
|
+
examples: [
|
|
26390
|
+
"deploy-bot@sample-project.iam.gserviceaccount.com",
|
|
26391
|
+
"12345678-1234-1234-1234-123456789012@my-project.iam.gserviceaccount.com"
|
|
26392
|
+
]
|
|
26393
|
+
};
|
|
26394
|
+
|
|
26395
|
+
// ../../rules/secrets/github-pat.json
|
|
26396
|
+
var github_pat_default = {
|
|
26397
|
+
specVersion: 1,
|
|
26398
|
+
id: "secrets/github-pat",
|
|
26399
|
+
name: "GitHub Personal Access Token (classic and fine-grained)",
|
|
26400
|
+
category: "secret",
|
|
26401
|
+
severity: "critical",
|
|
26402
|
+
matcher: {
|
|
26403
|
+
type: "regex",
|
|
26404
|
+
pattern: "\\b(?:gh[psou]_[A-Za-z0-9]{36}|github_pat_[0-9A-Za-z_]{82})\\b",
|
|
26405
|
+
flags: "g"
|
|
26406
|
+
},
|
|
26407
|
+
examples: [
|
|
26408
|
+
"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",
|
|
26409
|
+
"ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890"
|
|
26410
|
+
]
|
|
26411
|
+
};
|
|
26412
|
+
|
|
26413
|
+
// ../../rules/secrets/gitlab-token.json
|
|
26414
|
+
var gitlab_token_default = {
|
|
26415
|
+
specVersion: 1,
|
|
26416
|
+
id: "secrets/gitlab-token",
|
|
26417
|
+
name: "GitLab Personal/Project Access Token",
|
|
26418
|
+
category: "secret",
|
|
26419
|
+
severity: "critical",
|
|
26420
|
+
matcher: {
|
|
26421
|
+
type: "regex",
|
|
26422
|
+
pattern: "\\bglpat-[A-Za-z0-9\\-_]{20,40}\\b",
|
|
26423
|
+
flags: "g"
|
|
26424
|
+
},
|
|
26425
|
+
postValidators: ["entropy"],
|
|
26426
|
+
examples: ["glpat-ABCdEfGhIjKlMnOpQrStUvWxYz1234567890"]
|
|
26427
|
+
};
|
|
26428
|
+
|
|
26429
|
+
// ../../rules/secrets/heroku-api-key.json
|
|
26430
|
+
var heroku_api_key_default = {
|
|
26431
|
+
specVersion: 1,
|
|
26432
|
+
id: "secrets/heroku-api-key",
|
|
26433
|
+
name: "Heroku API key",
|
|
26434
|
+
category: "secret",
|
|
26435
|
+
severity: "critical",
|
|
26436
|
+
matcher: {
|
|
26437
|
+
type: "regex",
|
|
26438
|
+
pattern: "heroku.{0,40}?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
|
|
26439
|
+
flags: "gi",
|
|
26440
|
+
captureGroup: 1
|
|
26441
|
+
},
|
|
26442
|
+
examples: ["HEROKU_API_KEY=3f8a1c2e-9b4d-4e6f-a1b2-c3d4e5f6a7b8"]
|
|
26443
|
+
};
|
|
26444
|
+
|
|
26445
|
+
// ../../rules/secrets/npm-token.json
|
|
26446
|
+
var npm_token_default = {
|
|
26447
|
+
specVersion: 1,
|
|
26448
|
+
id: "secrets/npm-token",
|
|
26449
|
+
name: "npm access token",
|
|
26450
|
+
category: "secret",
|
|
26451
|
+
severity: "critical",
|
|
26452
|
+
matcher: {
|
|
26453
|
+
type: "regex",
|
|
26454
|
+
pattern: "\\bnpm_[A-Za-z0-9]{36,60}\\b",
|
|
26455
|
+
flags: "g"
|
|
26456
|
+
},
|
|
26457
|
+
postValidators: ["entropy"],
|
|
26458
|
+
examples: ["npm_AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnO"]
|
|
26459
|
+
};
|
|
26460
|
+
|
|
26461
|
+
// ../../rules/secrets/openai-api-key.json
|
|
26462
|
+
var openai_api_key_default = {
|
|
26463
|
+
specVersion: 1,
|
|
26464
|
+
id: "secrets/openai-api-key",
|
|
26465
|
+
name: "OpenAI API key",
|
|
26466
|
+
category: "secret",
|
|
26467
|
+
severity: "critical",
|
|
26468
|
+
matcher: {
|
|
26469
|
+
type: "regex",
|
|
26470
|
+
pattern: "\\bsk-proj-[A-Za-z0-9]{20,60}\\b|\\bsk-[A-Za-z0-9]{40,55}\\b",
|
|
26471
|
+
flags: "g"
|
|
26472
|
+
},
|
|
26473
|
+
postValidators: ["entropy"],
|
|
26474
|
+
examples: ["sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjK"]
|
|
26475
|
+
};
|
|
26476
|
+
|
|
26477
|
+
// ../../rules/secrets/pulumi-access-token.json
|
|
26478
|
+
var pulumi_access_token_default = {
|
|
26479
|
+
specVersion: 1,
|
|
26480
|
+
id: "secrets/pulumi-access-token",
|
|
26481
|
+
name: "Pulumi access token",
|
|
26482
|
+
category: "secret",
|
|
26483
|
+
severity: "critical",
|
|
26484
|
+
matcher: {
|
|
26485
|
+
type: "regex",
|
|
26486
|
+
pattern: "\\bpul-[A-Za-z0-9]{20,60}\\b",
|
|
26487
|
+
flags: "g"
|
|
26488
|
+
},
|
|
26489
|
+
postValidators: ["entropy"],
|
|
26490
|
+
examples: ["pul-ABCdEfGhIjKlMnOpQrStUvWxYz1234567890"]
|
|
26491
|
+
};
|
|
26492
|
+
|
|
26493
|
+
// ../../rules/secrets/sendgrid-key.json
|
|
26494
|
+
var sendgrid_key_default = {
|
|
26495
|
+
specVersion: 1,
|
|
26496
|
+
id: "secrets/sendgrid-key",
|
|
26497
|
+
name: "SendGrid API key",
|
|
26498
|
+
category: "secret",
|
|
26499
|
+
severity: "critical",
|
|
26500
|
+
matcher: {
|
|
26501
|
+
type: "regex",
|
|
26502
|
+
pattern: "\\bSG\\.[A-Za-z0-9_\\-]{20,60}\\b",
|
|
26503
|
+
flags: "g"
|
|
26504
|
+
},
|
|
26505
|
+
postValidators: ["entropy"],
|
|
26506
|
+
examples: ["SG.AbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYz"]
|
|
26507
|
+
};
|
|
26508
|
+
|
|
26509
|
+
// ../../rules/secrets/slack-token.json
|
|
26510
|
+
var slack_token_default = {
|
|
26511
|
+
specVersion: 1,
|
|
26512
|
+
id: "secrets/slack-token",
|
|
26513
|
+
name: "Slack API token",
|
|
26514
|
+
category: "secret",
|
|
26515
|
+
severity: "critical",
|
|
26516
|
+
matcher: {
|
|
26517
|
+
type: "regex",
|
|
26518
|
+
pattern: "\\b(?:xox[baprs]-[A-Za-z0-9]{10,60}|xapp-[A-Za-z0-9-]{10,60})\\b",
|
|
26519
|
+
flags: "g"
|
|
26520
|
+
},
|
|
26521
|
+
examples: ["xoxb-123456789012-123456789012-ABCdEfGhIjKlMnOpQrStUvWx"]
|
|
26522
|
+
};
|
|
26523
|
+
|
|
26524
|
+
// ../../rules/secrets/stripe-live-key.json
|
|
26525
|
+
var stripe_live_key_default = {
|
|
26526
|
+
specVersion: 1,
|
|
26527
|
+
id: "secrets/stripe-live-key",
|
|
26528
|
+
name: "Stripe live API key",
|
|
26529
|
+
category: "secret",
|
|
26530
|
+
severity: "critical",
|
|
26531
|
+
matcher: {
|
|
26532
|
+
type: "regex",
|
|
26533
|
+
pattern: "\\b(?:sk|pk|rk)_live_[A-Za-z0-9]{24,40}\\b",
|
|
26534
|
+
flags: "g"
|
|
26535
|
+
},
|
|
26536
|
+
postValidators: ["entropy"],
|
|
26537
|
+
examples: ["sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ"]
|
|
26538
|
+
};
|
|
26539
|
+
|
|
26540
|
+
// ../../rules/secrets/terraform-cloud-token.json
|
|
26541
|
+
var terraform_cloud_token_default = {
|
|
26542
|
+
specVersion: 1,
|
|
26543
|
+
id: "secrets/terraform-cloud-token",
|
|
26544
|
+
name: "Terraform Cloud / TFE user token",
|
|
26545
|
+
category: "secret",
|
|
26546
|
+
severity: "critical",
|
|
26547
|
+
matcher: {
|
|
26548
|
+
type: "regex",
|
|
26549
|
+
pattern: "\\b[A-Za-z0-9]{14}\\.atlasv1\\.[A-Za-z0-9_\\-]{60,80}\\b",
|
|
26550
|
+
flags: "g"
|
|
26551
|
+
},
|
|
26552
|
+
postValidators: ["entropy"],
|
|
26553
|
+
examples: [
|
|
26554
|
+
"AbCdEfGhIjKlMnOp.atlasv1.XyZ1234567890123456789012345678901234567890123456789012345678901234567890"
|
|
26555
|
+
]
|
|
26556
|
+
};
|
|
26557
|
+
|
|
26558
|
+
// ../../rules/secrets/twilio-key.json
|
|
26559
|
+
var twilio_key_default = {
|
|
26560
|
+
specVersion: 1,
|
|
26561
|
+
id: "secrets/twilio-key",
|
|
26562
|
+
name: "Twilio Account SID",
|
|
26563
|
+
category: "secret",
|
|
26564
|
+
severity: "critical",
|
|
26565
|
+
matcher: {
|
|
26566
|
+
type: "regex",
|
|
26567
|
+
pattern: "\\bAC[A-Za-z0-9]{32}\\b",
|
|
26568
|
+
flags: "g"
|
|
26569
|
+
},
|
|
26570
|
+
examples: ["AC12345678901234567890123456789012"]
|
|
26571
|
+
};
|
|
26572
|
+
|
|
26573
|
+
// ../../rules/secrets/vault-token.json
|
|
26574
|
+
var vault_token_default = {
|
|
26575
|
+
specVersion: 1,
|
|
26576
|
+
id: "secrets/vault-token",
|
|
26577
|
+
name: "HashiCorp Vault token",
|
|
26578
|
+
category: "secret",
|
|
26579
|
+
severity: "critical",
|
|
26580
|
+
matcher: {
|
|
26581
|
+
type: "regex",
|
|
26582
|
+
pattern: "\\b(?:hvs\\.[A-Za-z0-9\\-_]{20,50}|s\\.[A-Za-z0-9\\-_]{20,50})\\b",
|
|
26583
|
+
flags: "g"
|
|
26584
|
+
},
|
|
26585
|
+
postValidators: ["entropy"],
|
|
26586
|
+
examples: ["hvs.AbCdEfGhIjKlMnOpQrStUvWxYz1234567890"]
|
|
26587
|
+
};
|
|
26588
|
+
|
|
26589
|
+
// ../../packages/plugin-sdk/src/bundled-packs.generated.ts
|
|
26590
|
+
var BUNDLED_PACKS = [
|
|
26591
|
+
{
|
|
26592
|
+
packId: "code-flaws",
|
|
26593
|
+
name: "Code Security Flaws",
|
|
26594
|
+
version: "0.1.0",
|
|
26595
|
+
rawRules: [
|
|
26596
|
+
sql_inject_concat_default,
|
|
26597
|
+
sql_inject_concat_dot_default,
|
|
26598
|
+
sql_inject_format_default,
|
|
26599
|
+
sql_inject_interp_default,
|
|
26600
|
+
cmd_inject_shell_default,
|
|
26601
|
+
cmd_inject_exec_default,
|
|
26602
|
+
cmd_inject_node_exec_default,
|
|
26603
|
+
xss_inner_html_default,
|
|
26604
|
+
xss_dangerously_set_default,
|
|
26605
|
+
xss_unescaped_render_default,
|
|
26606
|
+
deser_pickle_default,
|
|
26607
|
+
deser_yaml_unsafe_default,
|
|
26608
|
+
deser_java_ois_default,
|
|
26609
|
+
hardcoded_password_default,
|
|
26610
|
+
hardcoded_secret_key_default,
|
|
26611
|
+
dev_debug_enabled_default,
|
|
26612
|
+
dev_placeholder_secret_default,
|
|
26613
|
+
dev_wildcard_cors_default,
|
|
26614
|
+
auth_ssl_verify_false_default,
|
|
26615
|
+
auth_jwt_no_verify_default,
|
|
26616
|
+
path_traversal_open_default,
|
|
26617
|
+
path_traversal_join_default,
|
|
26618
|
+
crypto_weak_hash_md5_default,
|
|
26619
|
+
crypto_weak_hash_sha1_default,
|
|
26620
|
+
crypto_insecure_random_default,
|
|
26621
|
+
prototype_pollution_merge_default,
|
|
26622
|
+
eval_dynamic_exec_default,
|
|
26623
|
+
ssrf_user_url_default,
|
|
26624
|
+
regex_redos_backtrack_default
|
|
26625
|
+
]
|
|
26626
|
+
},
|
|
26627
|
+
{
|
|
26628
|
+
packId: "core-code-context",
|
|
26629
|
+
name: "Code Context",
|
|
26630
|
+
version: "0.1.0",
|
|
26631
|
+
rawRules: [
|
|
26632
|
+
internal_ip_default,
|
|
26633
|
+
localhost_ref_default,
|
|
26634
|
+
internal_domain_default,
|
|
26635
|
+
file_path_default,
|
|
26636
|
+
stack_trace_default,
|
|
26637
|
+
internal_url_default,
|
|
26638
|
+
feature_flag_default,
|
|
26639
|
+
db_table_name_default
|
|
26640
|
+
]
|
|
26641
|
+
},
|
|
26642
|
+
{
|
|
26643
|
+
packId: "core-financial",
|
|
26644
|
+
name: "Financial Information",
|
|
26645
|
+
version: "0.1.0",
|
|
26646
|
+
rawRules: [
|
|
26647
|
+
credit_card_default,
|
|
26648
|
+
iban_default,
|
|
26649
|
+
routing_number_default,
|
|
26650
|
+
swift_default,
|
|
26651
|
+
cvv_default,
|
|
26652
|
+
cusip_default,
|
|
26653
|
+
paypal_default,
|
|
26654
|
+
salary_default
|
|
26655
|
+
]
|
|
26656
|
+
},
|
|
26657
|
+
{
|
|
26658
|
+
packId: "core-phi",
|
|
26659
|
+
name: "Protected Health Information",
|
|
26660
|
+
version: "0.1.0",
|
|
26661
|
+
rawRules: [
|
|
26662
|
+
mrn_default,
|
|
26663
|
+
hipaa_identifier_default,
|
|
26664
|
+
member_id_default,
|
|
26665
|
+
group_id_default,
|
|
26666
|
+
icd10_code_default,
|
|
26667
|
+
ndc_code_default,
|
|
26668
|
+
genetic_data_default,
|
|
26669
|
+
biometric_ref_default
|
|
26670
|
+
]
|
|
26671
|
+
},
|
|
26672
|
+
{
|
|
26673
|
+
packId: "core-pii",
|
|
26674
|
+
name: "Core PII",
|
|
26675
|
+
version: "0.2.0",
|
|
26676
|
+
rawRules: [
|
|
26677
|
+
email_default,
|
|
26678
|
+
ssn_default,
|
|
26679
|
+
phone_us_default,
|
|
26680
|
+
phone_intl_default,
|
|
26681
|
+
ip_address_default,
|
|
26682
|
+
dob_default,
|
|
26683
|
+
drivers_license_us_default,
|
|
26684
|
+
passport_us_default,
|
|
26685
|
+
home_address_default,
|
|
26686
|
+
name_default,
|
|
26687
|
+
vin_default,
|
|
26688
|
+
mac_address_default,
|
|
26689
|
+
national_id_default,
|
|
26690
|
+
zip_default
|
|
26691
|
+
]
|
|
26692
|
+
},
|
|
26693
|
+
{
|
|
26694
|
+
packId: "secrets",
|
|
26695
|
+
name: "Secrets & Credentials",
|
|
26696
|
+
version: "0.1.0",
|
|
26697
|
+
rawRules: [
|
|
26698
|
+
aws_access_key_default,
|
|
26699
|
+
aws_secret_key_default,
|
|
26700
|
+
gcp_service_account_default,
|
|
26701
|
+
azure_connection_string_default,
|
|
26702
|
+
openai_api_key_default,
|
|
26703
|
+
anthropic_api_key_default,
|
|
26704
|
+
stripe_live_key_default,
|
|
26705
|
+
slack_token_default,
|
|
26706
|
+
discord_token_default,
|
|
26707
|
+
sendgrid_key_default,
|
|
26708
|
+
twilio_key_default,
|
|
26709
|
+
gitlab_token_default,
|
|
26710
|
+
digitalocean_token_default,
|
|
26711
|
+
datadog_key_default,
|
|
26712
|
+
npm_token_default,
|
|
26713
|
+
heroku_api_key_default,
|
|
26714
|
+
cloudflare_api_key_default,
|
|
26715
|
+
pulumi_access_token_default,
|
|
26716
|
+
terraform_cloud_token_default,
|
|
26717
|
+
vault_token_default,
|
|
26718
|
+
github_pat_default
|
|
26719
|
+
]
|
|
26720
|
+
},
|
|
26721
|
+
{
|
|
26722
|
+
packId: "secrets-infra",
|
|
26723
|
+
name: "Infrastructure Secrets",
|
|
26724
|
+
version: "0.1.0",
|
|
26725
|
+
rawRules: [
|
|
26726
|
+
ssh_private_key_default,
|
|
26727
|
+
db_connection_string_default,
|
|
26728
|
+
jwt_token_default,
|
|
26729
|
+
basic_auth_url_default,
|
|
26730
|
+
basic_auth_header_default,
|
|
26731
|
+
pgp_private_key_default,
|
|
26732
|
+
bearer_token_default,
|
|
26733
|
+
env_key_value_default,
|
|
26734
|
+
password_field_default,
|
|
26735
|
+
docker_config_auth_default,
|
|
26736
|
+
kubeconfig_token_default,
|
|
26737
|
+
api_key_header_default,
|
|
26738
|
+
generic_high_entropy_secret_default
|
|
26739
|
+
]
|
|
26740
|
+
}
|
|
26741
|
+
];
|
|
26742
|
+
|
|
26743
|
+
// ../../packages/plugin-sdk/src/rule-packs.ts
|
|
26744
|
+
function registerRulePack(packId, rawRules) {
|
|
26745
|
+
const rules = rawRules.map((raw) => Rule.parse(raw));
|
|
26746
|
+
registerPack({ id: packId, rules });
|
|
26747
|
+
}
|
|
26748
|
+
function registerBundledPacks() {
|
|
26749
|
+
for (const pack of BUNDLED_PACKS) registerRulePack(pack.packId, pack.rawRules);
|
|
26750
|
+
}
|
|
26751
|
+
|
|
26752
|
+
// ../../packages/plugin-sdk/src/mask.ts
|
|
26753
|
+
var packsReady = false;
|
|
26754
|
+
var packsFailed = false;
|
|
26755
|
+
function ensureBundledPacks() {
|
|
26756
|
+
if (packsReady) return true;
|
|
26757
|
+
if (packsFailed) return false;
|
|
26758
|
+
try {
|
|
26759
|
+
registerBundledPacks();
|
|
26760
|
+
packsReady = true;
|
|
26761
|
+
return true;
|
|
26762
|
+
} catch {
|
|
26763
|
+
packsFailed = true;
|
|
26764
|
+
return false;
|
|
26765
|
+
}
|
|
26766
|
+
}
|
|
26767
|
+
function scanText(text, ruleVersions) {
|
|
26768
|
+
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
26769
|
+
try {
|
|
26770
|
+
const rules = getLoadedRules();
|
|
26771
|
+
const matches = scan(text, rules);
|
|
26772
|
+
if (matches.length === 0) return { masked: text, findings: [] };
|
|
26773
|
+
const byId = new Map(rules.map((r) => [r.id, r]));
|
|
26774
|
+
const findings = matches.map((m) => {
|
|
26775
|
+
const rule = byId.get(m.ruleId);
|
|
26776
|
+
return {
|
|
26777
|
+
ruleId: m.ruleId,
|
|
26778
|
+
ruleName: rule?.name ?? m.ruleId,
|
|
26779
|
+
ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
|
|
26780
|
+
category: m.category,
|
|
26781
|
+
severity: m.severity,
|
|
26782
|
+
span: m.span,
|
|
26783
|
+
maskedMatch: maskMatch(m.rawMatch),
|
|
26784
|
+
confidence: m.confidence
|
|
26785
|
+
};
|
|
26786
|
+
});
|
|
26787
|
+
return { masked: redact(text, matches), findings };
|
|
26788
|
+
} catch {
|
|
26789
|
+
return { masked: "[REDACTED]", findings: [] };
|
|
26790
|
+
}
|
|
26791
|
+
}
|
|
26792
|
+
function maskText(text) {
|
|
26793
|
+
return scanText(text).masked;
|
|
26794
|
+
}
|
|
26795
|
+
|
|
26796
|
+
// ../../packages/plugin-sdk/src/repo.ts
|
|
26797
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
|
|
26798
|
+
import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
|
|
26799
|
+
|
|
26800
|
+
// ../../packages/plugin-sdk/src/events.ts
|
|
26801
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
26802
|
+
|
|
26803
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26804
|
+
import { arch, hostname as hostname3, platform, release } from "os";
|
|
26805
|
+
|
|
26806
|
+
// ../../packages/plugin-sdk/src/nudge.ts
|
|
26807
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
26808
|
+
import { join as join9 } from "path";
|
|
26809
|
+
|
|
26810
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
26811
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
26812
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
26813
|
+
|
|
26814
|
+
// ../../packages/plugin-sdk/src/posture.ts
|
|
26815
|
+
function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
26816
|
+
for (const category of Object.keys(posture)) {
|
|
26817
|
+
const policyId = posture[category];
|
|
26818
|
+
if (!policyId) continue;
|
|
26819
|
+
if (mode === "fill-gaps" && repo.getCategoryAction(category) !== void 0) continue;
|
|
26820
|
+
repo.upsertCategoryAction(category, builtinPolicyToAction(policyId));
|
|
26821
|
+
}
|
|
26822
|
+
}
|
|
24088
26823
|
|
|
24089
26824
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24090
26825
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24091
|
-
import { existsSync as
|
|
24092
|
-
import { basename as basename4, join as
|
|
26826
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
26827
|
+
import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
|
|
24093
26828
|
|
|
24094
26829
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
24095
26830
|
var RawEgressError = class extends Error {
|
|
@@ -24180,8 +26915,8 @@ async function applySetupTriageSuppressions(entries, writer, opts) {
|
|
|
24180
26915
|
}
|
|
24181
26916
|
|
|
24182
26917
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24183
|
-
import { mkdirSync as
|
|
24184
|
-
import { join as
|
|
26918
|
+
import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
26919
|
+
import { join as join11 } from "path";
|
|
24185
26920
|
|
|
24186
26921
|
// src/command-registry.ts
|
|
24187
26922
|
import { readdirSync as readdirSync4 } from "fs";
|
|
@@ -24296,12 +27031,12 @@ function show(body) {
|
|
|
24296
27031
|
}
|
|
24297
27032
|
|
|
24298
27033
|
// src/triage/gate-display.ts
|
|
24299
|
-
function findContext(entry,
|
|
24300
|
-
const byFingerprint =
|
|
27034
|
+
function findContext(entry, join15) {
|
|
27035
|
+
const byFingerprint = join15.find(
|
|
24301
27036
|
(j) => j.valueFingerprint !== void 0 && j.valueFingerprint === entry.valueFingerprint
|
|
24302
27037
|
);
|
|
24303
27038
|
if (byFingerprint) return byFingerprint.maskedContext;
|
|
24304
|
-
const byRuleAndMask =
|
|
27039
|
+
const byRuleAndMask = join15.find(
|
|
24305
27040
|
(j) => j.ruleId === entry.ruleId && j.maskedMatch === entry.maskedValue
|
|
24306
27041
|
);
|
|
24307
27042
|
return byRuleAndMask?.maskedContext;
|
|
@@ -24372,13 +27107,13 @@ function renderShowcase(showcase) {
|
|
|
24372
27107
|
|
|
24373
27108
|
${blocks.join("\n\n")}`;
|
|
24374
27109
|
}
|
|
24375
|
-
function renderSuppressionGate(entries,
|
|
27110
|
+
function renderSuppressionGate(entries, join15) {
|
|
24376
27111
|
if (entries.length === 0) {
|
|
24377
27112
|
return "No false-positive suppressions to confirm \u2014 nothing will be written.";
|
|
24378
27113
|
}
|
|
24379
27114
|
const header = entries.length === 1 ? "This looks like a false positive \u2014 take a look before I suppress it:" : `These ${String(entries.length)} look like false positives \u2014 take a look before I suppress them:`;
|
|
24380
27115
|
const blocks = entries.map((entry, i) => {
|
|
24381
|
-
const context = findContext(entry,
|
|
27116
|
+
const context = findContext(entry, join15);
|
|
24382
27117
|
const lines = [
|
|
24383
27118
|
`${String(i + 1)}. ${entry.ruleId} [${entry.category}]`,
|
|
24384
27119
|
` value: ${entry.maskedValue}`,
|
|
@@ -24415,10 +27150,10 @@ function renderRecommendedPosture(posture) {
|
|
|
24415
27150
|
}
|
|
24416
27151
|
var GRID_MARK = "\u25CF";
|
|
24417
27152
|
function renderPostureGrid(posture) {
|
|
24418
|
-
const
|
|
27153
|
+
const packs2 = Object.keys(posture).sort(
|
|
24419
27154
|
(a, b) => categoryRank(a) - categoryRank(b)
|
|
24420
27155
|
);
|
|
24421
|
-
const rows =
|
|
27156
|
+
const rows = packs2.map((category) => [
|
|
24422
27157
|
category,
|
|
24423
27158
|
...BUILTIN_ORDER.map((level) => posture[category] === level ? GRID_MARK : "")
|
|
24424
27159
|
]);
|
|
@@ -24469,14 +27204,17 @@ function frameCalibration(preview, maskedFindings = [], falsePositivePatterns =
|
|
|
24469
27204
|
}
|
|
24470
27205
|
var SCAN_CLEAN_HEADLINE = "I looked over Claude's recent work \u2014 nothing needs your attention right now. You're starting clean; here's what I'd recommend:";
|
|
24471
27206
|
var NO_HISTORY_HEADLINE = "Nothing to learn from yet \u2014 Claude hasn't left any work on this machine. I'll start each detection category at a careful default:";
|
|
24472
|
-
function
|
|
24473
|
-
|
|
27207
|
+
function zeroCountFrame(posture) {
|
|
27208
|
+
return {
|
|
24474
27209
|
counts: { total: 0, important: 0, routine: 0 },
|
|
24475
27210
|
routineCategories: [],
|
|
24476
27211
|
surfacedCategories: [],
|
|
24477
27212
|
findingKinds: [],
|
|
24478
27213
|
posture
|
|
24479
27214
|
};
|
|
27215
|
+
}
|
|
27216
|
+
function frameEmptyState(cause, posture) {
|
|
27217
|
+
const frame = zeroCountFrame(posture);
|
|
24480
27218
|
const copy = cause === "scan-clean" ? `${SCAN_CLEAN_HEADLINE}
|
|
24481
27219
|
${renderRecommendedPosture(posture)}` : `${NO_HISTORY_HEADLINE}
|
|
24482
27220
|
${renderPostureGrid(posture)}`;
|
|
@@ -24605,9 +27343,9 @@ function mergeRecommendations(verdicts) {
|
|
|
24605
27343
|
}
|
|
24606
27344
|
|
|
24607
27345
|
// src/triage/plan-file.ts
|
|
24608
|
-
import { mkdtempSync, readFileSync as readFileSync7, rmdirSync, rmSync as
|
|
27346
|
+
import { mkdtempSync, readFileSync as readFileSync7, rmdirSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
24609
27347
|
import { tmpdir } from "os";
|
|
24610
|
-
import { basename as basename5, dirname as dirname3, join as
|
|
27348
|
+
import { basename as basename5, dirname as dirname3, join as join12 } from "path";
|
|
24611
27349
|
var SuppressionEntrySchema = external_exports.object({
|
|
24612
27350
|
ruleId: external_exports.string(),
|
|
24613
27351
|
category: DetectionCategory,
|
|
@@ -24662,9 +27400,9 @@ function serializePlan(plan, current) {
|
|
|
24662
27400
|
function writePlanFile(plan, current, rawValues, deps = {}) {
|
|
24663
27401
|
const serialized = serializePlan(plan, current);
|
|
24664
27402
|
assertRawFree(serialized, rawValues);
|
|
24665
|
-
const dir = (deps.mkTempDir ?? (() => mkdtempSync(
|
|
24666
|
-
const path =
|
|
24667
|
-
|
|
27403
|
+
const dir = (deps.mkTempDir ?? (() => mkdtempSync(join12(tmpdir(), "aka-plan-"))))();
|
|
27404
|
+
const path = join12(dir, "setup-plan.json");
|
|
27405
|
+
writeFileSync5(path, serialized, { encoding: "utf8", mode: 384 });
|
|
24668
27406
|
return path;
|
|
24669
27407
|
}
|
|
24670
27408
|
function readPlanFile(path) {
|
|
@@ -24673,7 +27411,7 @@ function readPlanFile(path) {
|
|
|
24673
27411
|
return PersistedPlanSchema.parse(json2);
|
|
24674
27412
|
}
|
|
24675
27413
|
function deletePlanFile(path) {
|
|
24676
|
-
|
|
27414
|
+
rmSync3(path, { force: true });
|
|
24677
27415
|
const dir = dirname3(path);
|
|
24678
27416
|
if (!basename5(dir).startsWith("aka-plan-")) return;
|
|
24679
27417
|
try {
|
|
@@ -24742,8 +27480,8 @@ function buildJoinEntries(hits) {
|
|
|
24742
27480
|
}
|
|
24743
27481
|
|
|
24744
27482
|
// src/triage/resolve.ts
|
|
24745
|
-
function resolveSuppressions(rec,
|
|
24746
|
-
const byId = new Map(
|
|
27483
|
+
function resolveSuppressions(rec, join15) {
|
|
27484
|
+
const byId = new Map(join15.map((e) => [e.id, e]));
|
|
24747
27485
|
const entries = [];
|
|
24748
27486
|
const skipped = [];
|
|
24749
27487
|
for (const cat of rec.perCategory) {
|
|
@@ -24845,7 +27583,7 @@ function parseTriageStream(text) {
|
|
|
24845
27583
|
return { hits, status: "complete" };
|
|
24846
27584
|
}
|
|
24847
27585
|
function planTriageWriteback(hits, rec) {
|
|
24848
|
-
const
|
|
27586
|
+
const join15 = buildJoinEntries(hits);
|
|
24849
27587
|
const rawValues = hits.map((h) => h.rawMatch);
|
|
24850
27588
|
const skipped = [];
|
|
24851
27589
|
const posture = {};
|
|
@@ -24885,7 +27623,7 @@ function planTriageWriteback(hits, rec) {
|
|
|
24885
27623
|
}
|
|
24886
27624
|
const { entries, skipped: resolveSkips } = resolveSuppressions(
|
|
24887
27625
|
{ perCategory: safeCategories, notes: rec.notes },
|
|
24888
|
-
|
|
27626
|
+
join15
|
|
24889
27627
|
);
|
|
24890
27628
|
skipped.push(...resolveSkips);
|
|
24891
27629
|
let notes = rec.notes;
|
|
@@ -24895,7 +27633,7 @@ function planTriageWriteback(hits, rec) {
|
|
|
24895
27633
|
if (err instanceof RawEgressError) notes = SCRUBBED_NOTES;
|
|
24896
27634
|
else throw err;
|
|
24897
27635
|
}
|
|
24898
|
-
return { entries, posture, showcase, join:
|
|
27636
|
+
return { entries, posture, showcase, join: join15, notes, skipped };
|
|
24899
27637
|
}
|
|
24900
27638
|
function recommendedPosture(evidence) {
|
|
24901
27639
|
return { ...severityFloorPosture(), ...evidence };
|
|
@@ -24960,6 +27698,11 @@ function runPreview(deps, planIO) {
|
|
|
24960
27698
|
deps.stdout(show("I didn't review anything \u2014 historical access wasn't granted."));
|
|
24961
27699
|
return 0;
|
|
24962
27700
|
}
|
|
27701
|
+
if (!deps.modelJudgeConsent()) {
|
|
27702
|
+
deps.stdout(show("I didn't send anything to the model \u2014 model-judge consent wasn't granted."));
|
|
27703
|
+
deps.stdout(frameJsonBlock(zeroCountFrame(severityFloorPosture())));
|
|
27704
|
+
return 0;
|
|
27705
|
+
}
|
|
24963
27706
|
const rawValues = hits.map((h) => h.rawMatch);
|
|
24964
27707
|
try {
|
|
24965
27708
|
const reps = dedupeForJudge(hits);
|
|
@@ -25147,9 +27890,9 @@ async function runConfirm(deps, planIO) {
|
|
|
25147
27890
|
|
|
25148
27891
|
// src/triage/judge.ts
|
|
25149
27892
|
import { execFileSync } from "child_process";
|
|
25150
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, rmSync as
|
|
27893
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
|
|
25151
27894
|
import { tmpdir as tmpdir2 } from "os";
|
|
25152
|
-
import { dirname as dirname4, join as
|
|
27895
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
25153
27896
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
25154
27897
|
|
|
25155
27898
|
// src/triage/parse-verdict.ts
|
|
@@ -25163,7 +27906,7 @@ function parseRecommendation(text) {
|
|
|
25163
27906
|
|
|
25164
27907
|
// src/triage/judge.ts
|
|
25165
27908
|
var TRIAGE_DIR = dirname4(fileURLToPath2(import.meta.url));
|
|
25166
|
-
var DEFAULT_RUBRIC_PATH =
|
|
27909
|
+
var DEFAULT_RUBRIC_PATH = join13(TRIAGE_DIR, "..", "..", "eval", "prompt.md");
|
|
25167
27910
|
function parseVerdict(stdout) {
|
|
25168
27911
|
let envelope;
|
|
25169
27912
|
try {
|
|
@@ -25190,7 +27933,7 @@ function judgeEnv() {
|
|
|
25190
27933
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
25191
27934
|
};
|
|
25192
27935
|
if (process.platform === "darwin") {
|
|
25193
|
-
env.CLAUDE_CONFIG_DIR = mkdtempSync2(
|
|
27936
|
+
env.CLAUDE_CONFIG_DIR = mkdtempSync2(join13(tmpdir2(), "aka-judge-cfg-"));
|
|
25194
27937
|
}
|
|
25195
27938
|
return env;
|
|
25196
27939
|
}
|
|
@@ -25211,9 +27954,16 @@ function spawnFailureMeta(err) {
|
|
|
25211
27954
|
if (typeof e.code === "string" && e.code) parts.push(e.code);
|
|
25212
27955
|
return parts.length > 0 ? parts.join(", ") : "unknown error";
|
|
25213
27956
|
}
|
|
27957
|
+
function toJudgePayload(hit) {
|
|
27958
|
+
const payload = { ...hit, context: maskText(hit.context) };
|
|
27959
|
+
delete payload.filePath;
|
|
27960
|
+
delete payload.valueFingerprint;
|
|
27961
|
+
delete payload.keyVersion;
|
|
27962
|
+
return payload;
|
|
27963
|
+
}
|
|
25214
27964
|
function runJudge(hits, deps) {
|
|
25215
27965
|
const rubric = deps.loadRubric?.() ?? readFileSync8(DEFAULT_RUBRIC_PATH, "utf8");
|
|
25216
|
-
const hitsJsonl = hits.map((h) => JSON.stringify(h)).join("\n");
|
|
27966
|
+
const hitsJsonl = hits.map((h) => JSON.stringify(toJudgePayload(h))).join("\n");
|
|
25217
27967
|
const fullPrompt = `${rubric}
|
|
25218
27968
|
|
|
25219
27969
|
## Hits
|
|
@@ -25234,7 +27984,7 @@ ${hitsJsonl}
|
|
|
25234
27984
|
return parseVerdict(stdout);
|
|
25235
27985
|
} finally {
|
|
25236
27986
|
if (process.platform === "darwin" && env.CLAUDE_CONFIG_DIR) {
|
|
25237
|
-
|
|
27987
|
+
rmSync4(env.CLAUDE_CONFIG_DIR, { recursive: true, force: true });
|
|
25238
27988
|
}
|
|
25239
27989
|
}
|
|
25240
27990
|
}
|
|
@@ -25254,9 +28004,9 @@ function resolveCreatedBy() {
|
|
|
25254
28004
|
}
|
|
25255
28005
|
function loadRubric() {
|
|
25256
28006
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
25257
|
-
const shipped =
|
|
25258
|
-
if (
|
|
25259
|
-
return readFileSync9(
|
|
28007
|
+
const shipped = join14(here, "triage-rubric.md");
|
|
28008
|
+
if (existsSync7(shipped)) return readFileSync9(shipped, "utf8");
|
|
28009
|
+
return readFileSync9(join14(here, "..", "eval", "prompt.md"), "utf8");
|
|
25260
28010
|
}
|
|
25261
28011
|
async function main() {
|
|
25262
28012
|
const argv = process.argv.slice(2);
|
|
@@ -25266,6 +28016,10 @@ async function main() {
|
|
|
25266
28016
|
// Called only on the preview path — the confirm path never reads a stream.
|
|
25267
28017
|
readStream: (streamPath) => streamPath !== void 0 ? readFileSync9(streamPath, "utf8") : readFileSync9(0, "utf8"),
|
|
25268
28018
|
runJudge: (hits) => runJudge(hits, { spawn: spawnClaude, loadRubric }),
|
|
28019
|
+
// The distinct model-judge egress consent, read from settings.json. When it
|
|
28020
|
+
// is absent or stale the preview skips the judge instead of sending findings
|
|
28021
|
+
// to the model API.
|
|
28022
|
+
modelJudgeConsent: () => isModelJudgeConsentValid(loadConfig().settings.modelJudgeConsent),
|
|
25269
28023
|
openDb: () => {
|
|
25270
28024
|
const db = openLocalDatabase(loadConfig().dataDir);
|
|
25271
28025
|
return {
|