@akasecurity/ai-tc-claude-code 0.8.1 → 0.9.0-rc1
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 +15 -4
- package/commands/setup.md +650 -70
- package/hooks/hooks.json +1 -1
- package/package.json +7 -6
- package/scripts/apply-suppressions.js +24237 -0
- package/scripts/backfill.js +1852 -937
- package/scripts/filescan.js +1636 -900
- package/scripts/firstrun.js +1796 -929
- package/scripts/intro.js +377 -119
- package/scripts/onboard.js +6162 -106
- package/scripts/package.json +1 -0
- package/scripts/post-tool-use.js +1678 -919
- package/scripts/pre-tool-use.js +1776 -931
- package/scripts/query.js +1621 -884
- package/scripts/reconcile.js +1632 -902
- package/scripts/remediate.js +26695 -0
- package/scripts/session-start.js +1680 -941
- package/scripts/start-light.js +17542 -0
- package/scripts/statusline.js +1621 -882
- package/scripts/stop.js +301 -40
- package/scripts/triage-rubric.md +92 -0
- package/scripts/user-prompt-submit.js +1654 -917
package/scripts/filescan.js
CHANGED
|
@@ -538,6 +538,10 @@ var SQLITE_MIGRATIONS = [
|
|
|
538
538
|
{
|
|
539
539
|
tag: "0009_findings_path_expression_index",
|
|
540
540
|
sql: "-- Custom migration: partial expression index for the resolver's per-path reads.\n--\n-- openAtRestKeysForPath / resolvedAtRestKeysForPath (and the scanner's tier-3\n-- open-key probe) filter at-rest events by\n-- e.kind = 'code_change' AND json_extract(e.metadata, '$.filePath') = :path\n-- once per changed/deleted file on every scan \u2014 previously a full events scan\n-- per file. json_extract is deterministic, so SQLite allows it in an index;\n-- the WHERE kind = 'code_change' keeps the index to exactly the rows those\n-- queries can match (in-flight events are never path-addressed).\nCREATE INDEX `idx_events_code_change_path` ON `events` (json_extract(`metadata`, '$.filePath')) WHERE `kind` = 'code_change';\n"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
tag: "0010_events_session_expression_index",
|
|
544
|
+
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
541
545
|
}
|
|
542
546
|
];
|
|
543
547
|
|
|
@@ -640,6 +644,12 @@ var defaultCostModel = {
|
|
|
640
644
|
}
|
|
641
645
|
};
|
|
642
646
|
|
|
647
|
+
// ../../packages/schema/src/token/format.ts
|
|
648
|
+
var COMPACT = new Intl.NumberFormat("en-US", {
|
|
649
|
+
notation: "compact",
|
|
650
|
+
maximumFractionDigits: 1
|
|
651
|
+
});
|
|
652
|
+
|
|
643
653
|
// ../../packages/schema/src/token/token-report.ts
|
|
644
654
|
var num = (value) => value ?? 0;
|
|
645
655
|
function costUsageOf(a) {
|
|
@@ -15311,6 +15321,12 @@ var FindingInstance = external_exports.object({
|
|
|
15311
15321
|
provider: FindingProvider,
|
|
15312
15322
|
repo: external_exports.string(),
|
|
15313
15323
|
file: external_exports.string(),
|
|
15324
|
+
// Host tool that produced the scanned text (event metadata's toolName).
|
|
15325
|
+
// Present whenever the capturing hook recorded one — including
|
|
15326
|
+
// file-attributed captures (views prefer `file`); its display value is
|
|
15327
|
+
// the location fallback ("via Bash") when no filePath exists. Absent for
|
|
15328
|
+
// legacy rows and non-tool captures (prompts, worktree scans).
|
|
15329
|
+
toolName: external_exports.string().optional(),
|
|
15314
15330
|
// Effective action: override.action ?? actionTaken, translated to FindingAction.
|
|
15315
15331
|
action: FindingAction,
|
|
15316
15332
|
detectedAt: external_exports.iso.datetime(),
|
|
@@ -15367,6 +15383,9 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15367
15383
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15368
15384
|
action: external_exports.array(FindingAction).optional(),
|
|
15369
15385
|
q: external_exports.string().optional(),
|
|
15386
|
+
// Scope to findings whose event carries this session id (the Activity page's
|
|
15387
|
+
// session → findings drilldown). Findings without a session never match.
|
|
15388
|
+
sessionId: external_exports.string().optional(),
|
|
15370
15389
|
groupBy: external_exports.literal("type").optional(),
|
|
15371
15390
|
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
15372
15391
|
cursor: external_exports.string().optional()
|
|
@@ -15378,7 +15397,13 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
15378
15397
|
}),
|
|
15379
15398
|
facets: FindingFacets,
|
|
15380
15399
|
items: external_exports.array(FindingGroup),
|
|
15381
|
-
nextCursor: external_exports.string().nullable()
|
|
15400
|
+
nextCursor: external_exports.string().nullable(),
|
|
15401
|
+
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
15402
|
+
// many times that rule fired in the session's persisted transcript. Findings
|
|
15403
|
+
// here are deduplicated to unique values while the transcript tally counts
|
|
15404
|
+
// every firing, so the two numbers legitimately differ — this map lets a
|
|
15405
|
+
// session-scoped view show both.
|
|
15406
|
+
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
15382
15407
|
}).meta({ id: "ListGroupedFindingsResponse" });
|
|
15383
15408
|
var ApplyFindingActionRequest = external_exports.object({
|
|
15384
15409
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
@@ -15427,6 +15452,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15427
15452
|
"prompt",
|
|
15428
15453
|
"response",
|
|
15429
15454
|
"code_change",
|
|
15455
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15456
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15457
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15458
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15459
|
+
"tool_use",
|
|
15430
15460
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15431
15461
|
// fact the posture inspection findings reference (findings require an
|
|
15432
15462
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15781,6 +15811,10 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15781
15811
|
from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15782
15812
|
/** Upper bound on startedAt; omitted defaults to now. */
|
|
15783
15813
|
to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15814
|
+
/** Exclude zero-activity sessions — roots whose only recorded children are
|
|
15815
|
+
* bookkeeping rows (hooks, config scans), typically background `claude`
|
|
15816
|
+
* launches. Omitted = list everything. `z.stringbool()` per the note above. */
|
|
15817
|
+
excludeEmpty: external_exports.stringbool().optional(),
|
|
15784
15818
|
/** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
|
|
15785
15819
|
limit: external_exports.coerce.number().int().min(1).max(100).default(50),
|
|
15786
15820
|
/** Opaque pagination cursor (most-recent first). */
|
|
@@ -15789,7 +15823,11 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15789
15823
|
var ListActivitySessionsResponse = external_exports.object({
|
|
15790
15824
|
items: external_exports.array(ActivitySessionSummary),
|
|
15791
15825
|
/** `null` once the last page is reached. */
|
|
15792
|
-
nextCursor: external_exports.string().nullable()
|
|
15826
|
+
nextCursor: external_exports.string().nullable(),
|
|
15827
|
+
/** Zero-activity sessions matching the query's filters/range (whether or
|
|
15828
|
+
* not `excludeEmpty` dropped them from `items`) — the count a UI toggle
|
|
15829
|
+
* shows when collapsing them. */
|
|
15830
|
+
emptyCount: external_exports.number().int().nonnegative()
|
|
15793
15831
|
}).meta({ id: "ListActivitySessionsResponse" });
|
|
15794
15832
|
var ListSessionEventsQuery = external_exports.object({
|
|
15795
15833
|
/** Default 100, range 1–500. */
|
|
@@ -15818,12 +15856,18 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15818
15856
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15819
15857
|
|
|
15820
15858
|
// ../../packages/schema/src/zod/event.ts
|
|
15821
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15859
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15822
15860
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15823
15861
|
var EventMetadata = external_exports.object({
|
|
15824
15862
|
sessionId: external_exports.string().optional(),
|
|
15825
15863
|
repo: external_exports.string().optional(),
|
|
15826
15864
|
filePath: external_exports.string().optional(),
|
|
15865
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
|
|
15866
|
+
// set by the tool-scanning hooks. The tool NAME only — never the tool's
|
|
15867
|
+
// arguments or output, which can carry the very value a finding masked
|
|
15868
|
+
// (metadata is stored unredacted). Gives findings on non-file captures a
|
|
15869
|
+
// display location ("via Bash") when no filePath exists.
|
|
15870
|
+
toolName: external_exports.string().optional(),
|
|
15827
15871
|
// Set (true) by the worktree scanner when the file is excluded by the
|
|
15828
15872
|
// repo's .gitignore. Gitignored files ARE still scanned — local scratch and
|
|
15829
15873
|
// generated code can leak real secrets — but the provenance is recorded so
|
|
@@ -16131,7 +16175,7 @@ var DetectionException = external_exports.object({
|
|
|
16131
16175
|
justification: external_exports.string().min(1),
|
|
16132
16176
|
conditions: ExceptionConditions.nullable(),
|
|
16133
16177
|
createdBy: external_exports.string(),
|
|
16134
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16178
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16135
16179
|
createdAt: external_exports.iso.datetime(),
|
|
16136
16180
|
updatedAt: external_exports.iso.datetime(),
|
|
16137
16181
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16155,7 +16199,10 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16155
16199
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16156
16200
|
var KeywordMatcher = external_exports.object({
|
|
16157
16201
|
type: external_exports.literal("keyword"),
|
|
16158
|
-
|
|
16202
|
+
// An empty keyword matches at every position, yielding one zero-length span
|
|
16203
|
+
// per character. Rejected here because a keyword that matches everything is
|
|
16204
|
+
// never intentional.
|
|
16205
|
+
keywords: external_exports.array(external_exports.string().min(1)).min(1),
|
|
16159
16206
|
caseSensitive: external_exports.boolean().default(false)
|
|
16160
16207
|
});
|
|
16161
16208
|
function isValidRegex(pattern, flags) {
|
|
@@ -16300,20 +16347,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16300
16347
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16301
16348
|
fetchedAt: external_exports.iso.datetime()
|
|
16302
16349
|
}).meta({ id: "PolicyBundle" });
|
|
16303
|
-
var DEFAULT_ACTIONS = {
|
|
16304
|
-
secret: "block",
|
|
16305
|
-
pii: "redact",
|
|
16306
|
-
financial: "redact",
|
|
16307
|
-
phi: "redact",
|
|
16308
|
-
code_context: "warn",
|
|
16309
|
-
code_flaw: "warn",
|
|
16310
|
-
custom: "warn",
|
|
16311
|
-
// Config-posture findings only observe today (they land in
|
|
16312
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16313
|
-
config: "warn"
|
|
16314
|
-
};
|
|
16315
16350
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16316
16351
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16352
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16353
|
+
secret: "critical",
|
|
16354
|
+
financial: "critical",
|
|
16355
|
+
// core-financial/credit-card
|
|
16356
|
+
code_flaw: "critical",
|
|
16357
|
+
pii: "high",
|
|
16358
|
+
phi: "high",
|
|
16359
|
+
custom: "high",
|
|
16360
|
+
// user-defined; conservative
|
|
16361
|
+
code_context: "low",
|
|
16362
|
+
config: "low"
|
|
16363
|
+
// observe-only; floors to monitor regardless
|
|
16364
|
+
};
|
|
16365
|
+
function severityFloorPolicy(category) {
|
|
16366
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16367
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16368
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16369
|
+
}
|
|
16317
16370
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16318
16371
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16319
16372
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16340,6 +16393,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16340
16393
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16341
16394
|
}
|
|
16342
16395
|
};
|
|
16396
|
+
function builtinPolicyToAction(id) {
|
|
16397
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16398
|
+
}
|
|
16399
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16400
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16401
|
+
);
|
|
16343
16402
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16344
16403
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16345
16404
|
);
|
|
@@ -16392,6 +16451,10 @@ var ListEventsResponse = external_exports.object({
|
|
|
16392
16451
|
items: external_exports.array(Event),
|
|
16393
16452
|
nextCursor: external_exports.string().nullable()
|
|
16394
16453
|
}).meta({ id: "ListEventsResponse" });
|
|
16454
|
+
var IngestResponse = external_exports.object({
|
|
16455
|
+
accepted: external_exports.number().int().nonnegative(),
|
|
16456
|
+
duplicates: external_exports.number().int().nonnegative()
|
|
16457
|
+
}).meta({ id: "IngestResponse" });
|
|
16395
16458
|
var ListFindingsQuery = external_exports.object({
|
|
16396
16459
|
cursor: external_exports.string().optional(),
|
|
16397
16460
|
limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
|
|
@@ -16746,10 +16809,8 @@ function toApiProvider(sourceTool) {
|
|
|
16746
16809
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16747
16810
|
}
|
|
16748
16811
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16749
|
-
function
|
|
16750
|
-
const statuses = new Set(
|
|
16751
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16752
|
-
);
|
|
16812
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16813
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16753
16814
|
if (statuses.size === 0) return void 0;
|
|
16754
16815
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16755
16816
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16767,6 +16828,7 @@ function deriveFindingStatus(row) {
|
|
|
16767
16828
|
function buildFindingGroups(rows, opts = {}) {
|
|
16768
16829
|
const overrides = opts.overrides;
|
|
16769
16830
|
const packNames = opts.packNames;
|
|
16831
|
+
const aggregates = opts.aggregates;
|
|
16770
16832
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16771
16833
|
for (const row of rows) {
|
|
16772
16834
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16782,23 +16844,27 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16782
16844
|
provider: toApiProvider(r.sourceTool),
|
|
16783
16845
|
repo: r.repo,
|
|
16784
16846
|
file: r.file,
|
|
16847
|
+
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
16785
16848
|
action: toApiAction(effectiveDbAction),
|
|
16786
16849
|
detectedAt: r.occurredAt,
|
|
16787
16850
|
confidence: r.confidence,
|
|
16788
16851
|
status: r.status
|
|
16789
16852
|
};
|
|
16790
16853
|
});
|
|
16791
|
-
const
|
|
16854
|
+
const agg = aggregates?.get(ruleId);
|
|
16855
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16792
16856
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16793
16857
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16794
16858
|
);
|
|
16795
16859
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16796
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16860
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16797
16861
|
if (seenProviders.has(p)) return false;
|
|
16798
16862
|
seenProviders.add(p);
|
|
16799
16863
|
return true;
|
|
16800
16864
|
});
|
|
16801
|
-
const actionSet = new Set(
|
|
16865
|
+
const actionSet = new Set(
|
|
16866
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16867
|
+
);
|
|
16802
16868
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16803
16869
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16804
16870
|
const detection = {
|
|
@@ -16812,8 +16878,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16812
16878
|
contextPrefix: ""
|
|
16813
16879
|
// empty (pending privacy review)
|
|
16814
16880
|
};
|
|
16815
|
-
const status =
|
|
16816
|
-
|
|
16881
|
+
const status = foldGroupStatus(
|
|
16882
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16883
|
+
);
|
|
16884
|
+
const group = {
|
|
16817
16885
|
id: ruleId,
|
|
16818
16886
|
category: apiCategory,
|
|
16819
16887
|
subtype: ruleId,
|
|
@@ -16822,21 +16890,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16822
16890
|
match,
|
|
16823
16891
|
detection,
|
|
16824
16892
|
policy,
|
|
16825
|
-
instanceCount: instances.length,
|
|
16893
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16826
16894
|
providers,
|
|
16827
16895
|
aggregateAction,
|
|
16828
16896
|
latestDetectedAt,
|
|
16829
16897
|
instances,
|
|
16830
16898
|
status
|
|
16831
|
-
}
|
|
16899
|
+
};
|
|
16900
|
+
if (agg) {
|
|
16901
|
+
actionsCache.set(group, [...actionSet]);
|
|
16902
|
+
if (agg.searchText !== void 0) {
|
|
16903
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16904
|
+
}
|
|
16905
|
+
}
|
|
16906
|
+
groups.push(group);
|
|
16832
16907
|
}
|
|
16833
16908
|
return groups;
|
|
16834
16909
|
}
|
|
16835
16910
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16836
|
-
function
|
|
16837
|
-
|
|
16838
|
-
if (cached2 !== void 0) return cached2;
|
|
16839
|
-
const haystack = [
|
|
16911
|
+
function buildHaystack(g, extra) {
|
|
16912
|
+
return [
|
|
16840
16913
|
g.subtype,
|
|
16841
16914
|
g.category,
|
|
16842
16915
|
g.match.maskedValue,
|
|
@@ -16844,11 +16917,26 @@ function groupHaystack(g) {
|
|
|
16844
16917
|
g.id,
|
|
16845
16918
|
...g.instances.map((i) => i.repo),
|
|
16846
16919
|
...g.instances.map((i) => i.file),
|
|
16847
|
-
...g.instances.map((i) => i.
|
|
16920
|
+
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
16921
|
+
...g.instances.map((i) => i.id),
|
|
16922
|
+
...extra === void 0 ? [] : [extra]
|
|
16848
16923
|
].join(" ").toLowerCase();
|
|
16924
|
+
}
|
|
16925
|
+
function groupHaystack(g) {
|
|
16926
|
+
const cached2 = haystackCache.get(g);
|
|
16927
|
+
if (cached2 !== void 0) return cached2;
|
|
16928
|
+
const haystack = buildHaystack(g);
|
|
16849
16929
|
haystackCache.set(g, haystack);
|
|
16850
16930
|
return haystack;
|
|
16851
16931
|
}
|
|
16932
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16933
|
+
function groupActions(g) {
|
|
16934
|
+
const cached2 = actionsCache.get(g);
|
|
16935
|
+
if (cached2 !== void 0) return cached2;
|
|
16936
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16937
|
+
actionsCache.set(g, actions);
|
|
16938
|
+
return actions;
|
|
16939
|
+
}
|
|
16852
16940
|
function applyFindingFilters(groups, opts) {
|
|
16853
16941
|
let filtered = groups;
|
|
16854
16942
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16861,7 +16949,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16861
16949
|
}
|
|
16862
16950
|
if (opts.actions && opts.actions.length > 0) {
|
|
16863
16951
|
const actionSet = new Set(opts.actions);
|
|
16864
|
-
filtered = filtered.filter((g) => g.
|
|
16952
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16865
16953
|
}
|
|
16866
16954
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16867
16955
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16913,8 +17001,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16913
17001
|
});
|
|
16914
17002
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16915
17003
|
for (const g of forAction) {
|
|
16916
|
-
const
|
|
16917
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17004
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16918
17005
|
}
|
|
16919
17006
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16920
17007
|
providers: opts.providers,
|
|
@@ -17097,6 +17184,71 @@ var ProjectFilesScan = external_exports.object({
|
|
|
17097
17184
|
scannedAt: external_exports.string()
|
|
17098
17185
|
});
|
|
17099
17186
|
|
|
17187
|
+
// ../../packages/schema/src/zod/ranges.ts
|
|
17188
|
+
var TIME_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17189
|
+
var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
|
|
17190
|
+
var DEFAULT_TIME_RANGE = "7d";
|
|
17191
|
+
var RANGE_DAYS = {
|
|
17192
|
+
"7d": 7,
|
|
17193
|
+
"30d": 30,
|
|
17194
|
+
"3m": 90,
|
|
17195
|
+
"6m": 180
|
|
17196
|
+
};
|
|
17197
|
+
var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
|
|
17198
|
+
|
|
17199
|
+
// ../../packages/schema/src/zod/remediation.ts
|
|
17200
|
+
var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
|
|
17201
|
+
var MaskedFindingLocation = external_exports.object({
|
|
17202
|
+
filePath: external_exports.string(),
|
|
17203
|
+
span: Span.optional()
|
|
17204
|
+
}).strict();
|
|
17205
|
+
var MaskedSecretFinding = external_exports.object({
|
|
17206
|
+
provider: external_exports.string(),
|
|
17207
|
+
maskedToken: external_exports.string(),
|
|
17208
|
+
where: MaskedFindingLocation,
|
|
17209
|
+
state: SecretFindingState,
|
|
17210
|
+
observedAt: external_exports.iso.datetime().optional()
|
|
17211
|
+
}).strict();
|
|
17212
|
+
var RotationChecklistEntry = external_exports.object({
|
|
17213
|
+
provider: external_exports.string(),
|
|
17214
|
+
maskedToken: external_exports.string(),
|
|
17215
|
+
consolePath: external_exports.string(),
|
|
17216
|
+
occurrenceSpread: external_exports.number().int().positive()
|
|
17217
|
+
}).strict();
|
|
17218
|
+
var RemediationOption = external_exports.enum([
|
|
17219
|
+
"redact-rotation-checklist",
|
|
17220
|
+
"redact-only",
|
|
17221
|
+
"set-secret-redact",
|
|
17222
|
+
"leave"
|
|
17223
|
+
]);
|
|
17224
|
+
var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
|
|
17225
|
+
var RemediationEntryContext = external_exports.object({
|
|
17226
|
+
entrySource: RemediationEntrySource
|
|
17227
|
+
}).strict();
|
|
17228
|
+
var RemediationOptionChoice = external_exports.object({
|
|
17229
|
+
id: RemediationOption,
|
|
17230
|
+
label: external_exports.string()
|
|
17231
|
+
});
|
|
17232
|
+
var BatchedRemediationDecision = external_exports.object({
|
|
17233
|
+
kind: external_exports.literal("decision"),
|
|
17234
|
+
entrySource: RemediationEntrySource,
|
|
17235
|
+
secretCount: external_exports.number().int().positive(),
|
|
17236
|
+
prompt: external_exports.string(),
|
|
17237
|
+
options: external_exports.tuple([
|
|
17238
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
|
|
17239
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
|
|
17240
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
|
|
17241
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
|
|
17242
|
+
])
|
|
17243
|
+
});
|
|
17244
|
+
var NoRemediationDecision = external_exports.object({
|
|
17245
|
+
kind: external_exports.literal("no-decision")
|
|
17246
|
+
});
|
|
17247
|
+
var BatchedRemediation = external_exports.discriminatedUnion("kind", [
|
|
17248
|
+
BatchedRemediationDecision,
|
|
17249
|
+
NoRemediationDecision
|
|
17250
|
+
]);
|
|
17251
|
+
|
|
17100
17252
|
// ../../packages/schema/src/zod/rule-test.ts
|
|
17101
17253
|
var TestRulesRequest = external_exports.object({
|
|
17102
17254
|
rules: external_exports.array(Rule).min(1).max(100),
|
|
@@ -17155,10 +17307,8 @@ var SeveritySummaryResponse = external_exports.object({
|
|
|
17155
17307
|
// All four severity levels are always present (count may be 0).
|
|
17156
17308
|
bySeverity: external_exports.array(SeveritySummaryItem)
|
|
17157
17309
|
}).meta({ id: "SeveritySummaryResponse" });
|
|
17158
|
-
var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17159
|
-
var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
|
|
17160
17310
|
var SecurityRangeQuery = external_exports.object({
|
|
17161
|
-
range: external_exports.enum(
|
|
17311
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
|
|
17162
17312
|
});
|
|
17163
17313
|
var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
|
|
17164
17314
|
var EnforcementAction = external_exports.object({
|
|
@@ -17168,7 +17318,7 @@ var EnforcementAction = external_exports.object({
|
|
|
17168
17318
|
delta: external_exports.number().int()
|
|
17169
17319
|
}).meta({ id: "EnforcementAction" });
|
|
17170
17320
|
var EnforcementActionsResponse = external_exports.object({
|
|
17171
|
-
range:
|
|
17321
|
+
range: TimeRange,
|
|
17172
17322
|
// Sum of actions[].count in the window.
|
|
17173
17323
|
total: external_exports.number().int().nonnegative(),
|
|
17174
17324
|
// One entry per kind, always all three present (count may be 0).
|
|
@@ -17183,7 +17333,7 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
17183
17333
|
medium: external_exports.number().int().nonnegative()
|
|
17184
17334
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
17185
17335
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
17186
|
-
range:
|
|
17336
|
+
range: TimeRange,
|
|
17187
17337
|
granularity: TimeseriesGranularity,
|
|
17188
17338
|
points: external_exports.array(FindingsTimeseriesPoint)
|
|
17189
17339
|
}).meta({ id: "FindingsTimeseriesResponse" });
|
|
@@ -17198,7 +17348,7 @@ var MttrTrendPoint = external_exports.object({
|
|
|
17198
17348
|
})
|
|
17199
17349
|
}).meta({ id: "MttrTrendPoint" });
|
|
17200
17350
|
var MttrTrendResponse = external_exports.object({
|
|
17201
|
-
range:
|
|
17351
|
+
range: TimeRange,
|
|
17202
17352
|
granularity: TimeseriesGranularity,
|
|
17203
17353
|
points: external_exports.array(MttrTrendPoint)
|
|
17204
17354
|
}).meta({ id: "MttrTrendResponse" });
|
|
@@ -17225,11 +17375,11 @@ var TopSource = external_exports.object({
|
|
|
17225
17375
|
findingsCount: external_exports.number().int().nonnegative()
|
|
17226
17376
|
}).meta({ id: "TopSource" });
|
|
17227
17377
|
var TopSourcesResponse = external_exports.object({
|
|
17228
|
-
range:
|
|
17378
|
+
range: TimeRange,
|
|
17229
17379
|
items: external_exports.array(TopSource)
|
|
17230
17380
|
}).meta({ id: "TopSourcesResponse" });
|
|
17231
17381
|
var TopSourcesQuery = external_exports.object({
|
|
17232
|
-
range: external_exports.enum(
|
|
17382
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
|
|
17233
17383
|
limit: external_exports.coerce.number().int().min(1).max(50).default(5),
|
|
17234
17384
|
// Omit for both kinds.
|
|
17235
17385
|
kind: external_exports.enum(SOURCE_KINDS).optional()
|
|
@@ -17242,7 +17392,7 @@ var ScanCoverageProvider = external_exports.object({
|
|
|
17242
17392
|
supported: external_exports.boolean()
|
|
17243
17393
|
}).meta({ id: "ScanCoverageProvider" });
|
|
17244
17394
|
var ScanCoverageResponse = external_exports.object({
|
|
17245
|
-
range:
|
|
17395
|
+
range: TimeRange,
|
|
17246
17396
|
providers: external_exports.array(ScanCoverageProvider)
|
|
17247
17397
|
}).meta({ id: "ScanCoverageResponse" });
|
|
17248
17398
|
var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
|
|
@@ -17291,6 +17441,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
|
|
|
17291
17441
|
var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
|
|
17292
17442
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
17293
17443
|
|
|
17444
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17445
|
+
var TriageHit = external_exports.object({
|
|
17446
|
+
ruleId: external_exports.string(),
|
|
17447
|
+
category: DetectionCategory,
|
|
17448
|
+
severity: Severity,
|
|
17449
|
+
maskedMatch: external_exports.string(),
|
|
17450
|
+
rawMatch: external_exports.string(),
|
|
17451
|
+
context: external_exports.string(),
|
|
17452
|
+
filePath: external_exports.string().optional(),
|
|
17453
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17454
|
+
id: external_exports.string().optional(),
|
|
17455
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17456
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17457
|
+
});
|
|
17458
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17459
|
+
var TriageCategoryRec = external_exports.object({
|
|
17460
|
+
category: DetectionCategory,
|
|
17461
|
+
action: TriagePolicy,
|
|
17462
|
+
reasoning: external_exports.string(),
|
|
17463
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17464
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17465
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17466
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17467
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17468
|
+
});
|
|
17469
|
+
var TriageRecommendation = external_exports.object({
|
|
17470
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17471
|
+
notes: external_exports.string()
|
|
17472
|
+
});
|
|
17473
|
+
|
|
17474
|
+
// ../../packages/schema/src/zod/setup-frame.ts
|
|
17475
|
+
var CalibrationCounts = external_exports.object({
|
|
17476
|
+
total: external_exports.number().int().nonnegative(),
|
|
17477
|
+
important: external_exports.number().int().nonnegative(),
|
|
17478
|
+
routine: external_exports.number().int().nonnegative()
|
|
17479
|
+
}).refine((c) => c.total === c.important + c.routine, {
|
|
17480
|
+
message: "total must equal important + routine",
|
|
17481
|
+
path: ["total"]
|
|
17482
|
+
});
|
|
17483
|
+
var FalsePositivePatternValue = external_exports.object({
|
|
17484
|
+
ruleId: external_exports.string(),
|
|
17485
|
+
category: DetectionCategory,
|
|
17486
|
+
valueFingerprint: external_exports.string(),
|
|
17487
|
+
keyVersion: external_exports.number().int().nonnegative()
|
|
17488
|
+
});
|
|
17489
|
+
var FalsePositivePatternGroup = external_exports.object({
|
|
17490
|
+
pattern: external_exports.string(),
|
|
17491
|
+
count: external_exports.number().int().nonnegative(),
|
|
17492
|
+
values: external_exports.array(FalsePositivePatternValue).min(1)
|
|
17493
|
+
});
|
|
17494
|
+
var CalibrationFindingKind = external_exports.object({
|
|
17495
|
+
category: DetectionCategory,
|
|
17496
|
+
count: external_exports.number().int().nonnegative(),
|
|
17497
|
+
egress: external_exports.boolean()
|
|
17498
|
+
});
|
|
17499
|
+
var CalibrationFrame = external_exports.object({
|
|
17500
|
+
counts: CalibrationCounts,
|
|
17501
|
+
routineCategories: external_exports.array(DetectionCategory),
|
|
17502
|
+
surfacedCategories: external_exports.array(DetectionCategory),
|
|
17503
|
+
findingKinds: external_exports.array(CalibrationFindingKind),
|
|
17504
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
|
|
17505
|
+
maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
|
|
17506
|
+
falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
|
|
17507
|
+
});
|
|
17508
|
+
var CalibrationPreviewCategory = TriageCategoryRec.pick({
|
|
17509
|
+
category: true,
|
|
17510
|
+
genuineCount: true,
|
|
17511
|
+
fpCount: true
|
|
17512
|
+
}).extend({
|
|
17513
|
+
egress: external_exports.boolean()
|
|
17514
|
+
});
|
|
17515
|
+
var CalibrationPreview = external_exports.object({
|
|
17516
|
+
categories: external_exports.array(CalibrationPreviewCategory),
|
|
17517
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
|
|
17518
|
+
});
|
|
17519
|
+
var CalibrationResult = external_exports.object({
|
|
17520
|
+
frame: CalibrationFrame,
|
|
17521
|
+
copy: external_exports.string()
|
|
17522
|
+
});
|
|
17523
|
+
var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
|
|
17524
|
+
var SetupHandoffOption = external_exports.object({
|
|
17525
|
+
id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
|
|
17526
|
+
label: external_exports.string()
|
|
17527
|
+
});
|
|
17528
|
+
var DashboardHandoffOptions = external_exports.tuple([
|
|
17529
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17530
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17531
|
+
]);
|
|
17532
|
+
var ComposedRemediationOptions = external_exports.tuple([
|
|
17533
|
+
SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
|
|
17534
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17535
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17536
|
+
]);
|
|
17537
|
+
var SetupHandoffOffer = external_exports.object({
|
|
17538
|
+
worthALook: external_exports.number().int().nonnegative(),
|
|
17539
|
+
liveKeys: external_exports.number().int().nonnegative().optional(),
|
|
17540
|
+
options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
|
|
17541
|
+
}).refine(
|
|
17542
|
+
(o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
|
|
17543
|
+
{
|
|
17544
|
+
message: "the chain-entry option is present exactly when liveKeys > 0",
|
|
17545
|
+
path: ["options"]
|
|
17546
|
+
}
|
|
17547
|
+
).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
|
|
17548
|
+
message: "liveKeys is a subset of worthALook and cannot exceed it",
|
|
17549
|
+
path: ["liveKeys"]
|
|
17550
|
+
});
|
|
17551
|
+
|
|
17294
17552
|
// ../../packages/schema/src/zod/shares.ts
|
|
17295
17553
|
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17296
17554
|
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
@@ -17476,6 +17734,102 @@ function reviewSeverityRank(reasons) {
|
|
|
17476
17734
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17477
17735
|
}
|
|
17478
17736
|
|
|
17737
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17738
|
+
function escapeLikePattern(s) {
|
|
17739
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17740
|
+
}
|
|
17741
|
+
function placeholders(n) {
|
|
17742
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17743
|
+
}
|
|
17744
|
+
function containsPattern(q) {
|
|
17745
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17746
|
+
}
|
|
17747
|
+
function likeAny(exprs) {
|
|
17748
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17749
|
+
}
|
|
17750
|
+
|
|
17751
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17752
|
+
var savepointSeq = 0;
|
|
17753
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17754
|
+
if (db.isTransaction) {
|
|
17755
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17756
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17757
|
+
try {
|
|
17758
|
+
fn();
|
|
17759
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17760
|
+
} catch (error51) {
|
|
17761
|
+
try {
|
|
17762
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17763
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17764
|
+
} catch {
|
|
17765
|
+
}
|
|
17766
|
+
throw error51;
|
|
17767
|
+
}
|
|
17768
|
+
return;
|
|
17769
|
+
}
|
|
17770
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17771
|
+
try {
|
|
17772
|
+
fn();
|
|
17773
|
+
db.exec("COMMIT");
|
|
17774
|
+
} catch (error51) {
|
|
17775
|
+
try {
|
|
17776
|
+
db.exec("ROLLBACK");
|
|
17777
|
+
} catch {
|
|
17778
|
+
}
|
|
17779
|
+
throw error51;
|
|
17780
|
+
}
|
|
17781
|
+
}
|
|
17782
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17783
|
+
const nested = db.isTransaction;
|
|
17784
|
+
try {
|
|
17785
|
+
withTransaction(db, fn, mode);
|
|
17786
|
+
return true;
|
|
17787
|
+
} catch (error51) {
|
|
17788
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17789
|
+
return false;
|
|
17790
|
+
}
|
|
17791
|
+
}
|
|
17792
|
+
|
|
17793
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17794
|
+
function akaWarn(message) {
|
|
17795
|
+
process.stderr.write(`[aka] ${message}
|
|
17796
|
+
`);
|
|
17797
|
+
}
|
|
17798
|
+
|
|
17799
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17800
|
+
function evidenceObjects(sql) {
|
|
17801
|
+
const objects = [];
|
|
17802
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17803
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17804
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17805
|
+
}
|
|
17806
|
+
}
|
|
17807
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17808
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17809
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17810
|
+
}
|
|
17811
|
+
}
|
|
17812
|
+
return objects;
|
|
17813
|
+
}
|
|
17814
|
+
function schemaObjectExists(db, kind, name) {
|
|
17815
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17816
|
+
return row !== void 0;
|
|
17817
|
+
}
|
|
17818
|
+
function indexExists(db, name) {
|
|
17819
|
+
return schemaObjectExists(db, "index", name);
|
|
17820
|
+
}
|
|
17821
|
+
function columnNames(db, table2, opts) {
|
|
17822
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17823
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
|
|
17824
|
+
return columns.map((c) => c.name);
|
|
17825
|
+
}
|
|
17826
|
+
function evidenceExists(db, object2) {
|
|
17827
|
+
if (object2.kind === "column") {
|
|
17828
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17829
|
+
}
|
|
17830
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17831
|
+
}
|
|
17832
|
+
|
|
17479
17833
|
// ../../packages/persistence/src/ids.ts
|
|
17480
17834
|
import { createHash } from "crypto";
|
|
17481
17835
|
function sha256Hex(input) {
|
|
@@ -17512,28 +17866,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17512
17866
|
}
|
|
17513
17867
|
|
|
17514
17868
|
// ../../packages/persistence/src/migrations.ts
|
|
17515
|
-
function evidenceObjects(sql) {
|
|
17516
|
-
const objects = [];
|
|
17517
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17518
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17519
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17520
|
-
}
|
|
17521
|
-
}
|
|
17522
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17523
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17524
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17525
|
-
}
|
|
17526
|
-
}
|
|
17527
|
-
return objects;
|
|
17528
|
-
}
|
|
17529
|
-
function evidenceExists(db, object2) {
|
|
17530
|
-
if (object2.kind === "column") {
|
|
17531
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17532
|
-
return columns.some((c) => c.name === object2.name);
|
|
17533
|
-
}
|
|
17534
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17535
|
-
return row !== void 0;
|
|
17536
|
-
}
|
|
17537
17869
|
function describeObject(object2) {
|
|
17538
17870
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17539
17871
|
}
|
|
@@ -17544,10 +17876,6 @@ function createdIndexName(statement) {
|
|
|
17544
17876
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17545
17877
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17546
17878
|
}
|
|
17547
|
-
function indexExists(db, name) {
|
|
17548
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17549
|
-
return row !== void 0;
|
|
17550
|
-
}
|
|
17551
17879
|
function applyMigrations(db) {
|
|
17552
17880
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17553
17881
|
db.exec(
|
|
@@ -17566,44 +17894,39 @@ function applyMigrations(db) {
|
|
|
17566
17894
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17567
17895
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17568
17896
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17569
|
-
const message = `
|
|
17570
|
-
|
|
17571
|
-
`);
|
|
17572
|
-
throw new Error(message);
|
|
17897
|
+
const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
|
|
17898
|
+
akaWarn(message);
|
|
17899
|
+
throw new Error(`[aka] ${message}`);
|
|
17573
17900
|
}
|
|
17574
17901
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17575
17902
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17576
17903
|
const statements = splitStatements(migration.sql);
|
|
17577
17904
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17578
17905
|
try {
|
|
17579
|
-
|
|
17580
|
-
|
|
17581
|
-
|
|
17582
|
-
const
|
|
17583
|
-
|
|
17584
|
-
if (
|
|
17585
|
-
|
|
17586
|
-
|
|
17906
|
+
withTransaction(
|
|
17907
|
+
db,
|
|
17908
|
+
() => {
|
|
17909
|
+
for (const statement of statements) {
|
|
17910
|
+
const indexName = createdIndexName(statement);
|
|
17911
|
+
if (indexName === void 0) {
|
|
17912
|
+
if (alreadyApplied) continue;
|
|
17913
|
+
} else if (indexExists(db, indexName)) {
|
|
17914
|
+
continue;
|
|
17915
|
+
}
|
|
17916
|
+
db.exec(statement);
|
|
17587
17917
|
}
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
);
|
|
17918
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17919
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17920
|
+
if (violations.length > 0) {
|
|
17921
|
+
throw new Error(
|
|
17922
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17923
|
+
);
|
|
17924
|
+
}
|
|
17596
17925
|
}
|
|
17597
|
-
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
try {
|
|
17602
|
-
db.exec("ROLLBACK");
|
|
17603
|
-
} catch {
|
|
17604
|
-
}
|
|
17605
|
-
throw error51;
|
|
17606
|
-
}
|
|
17926
|
+
record2.run(migration.tag, Date.now());
|
|
17927
|
+
},
|
|
17928
|
+
"IMMEDIATE"
|
|
17929
|
+
);
|
|
17607
17930
|
} finally {
|
|
17608
17931
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17609
17932
|
}
|
|
@@ -17646,8 +17969,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17646
17969
|
}
|
|
17647
17970
|
];
|
|
17648
17971
|
function ensureTokenUsageColumns(db) {
|
|
17649
|
-
const
|
|
17650
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17972
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17651
17973
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17652
17974
|
if (!existing.has(column.name)) {
|
|
17653
17975
|
db.exec(column.ddl);
|
|
@@ -17685,47 +18007,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17685
18007
|
repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
|
|
17686
18008
|
}));
|
|
17687
18009
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
dropCollisions
|
|
17702
|
-
|
|
18010
|
+
withTransaction(
|
|
18011
|
+
db,
|
|
18012
|
+
() => {
|
|
18013
|
+
for (const { row, canonicalId } of legacy) {
|
|
18014
|
+
foldProject.run(
|
|
18015
|
+
canonicalId,
|
|
18016
|
+
row.url,
|
|
18017
|
+
row.name,
|
|
18018
|
+
row.attributes,
|
|
18019
|
+
row.firstSeen,
|
|
18020
|
+
row.lastSeen
|
|
18021
|
+
);
|
|
18022
|
+
repointAudit.run(canonicalId, row.id);
|
|
18023
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
18024
|
+
dropCollisions.run(row.id, canonicalId);
|
|
18025
|
+
repoint.run(canonicalId, row.id);
|
|
18026
|
+
}
|
|
18027
|
+
repointCallSite.run(canonicalId, row.id);
|
|
18028
|
+
deleteLegacy.run(row.id);
|
|
17703
18029
|
}
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
db.exec("COMMIT");
|
|
17708
|
-
} catch (error51) {
|
|
17709
|
-
try {
|
|
17710
|
-
db.exec("ROLLBACK");
|
|
17711
|
-
} catch {
|
|
17712
|
-
}
|
|
17713
|
-
throw error51;
|
|
17714
|
-
}
|
|
18030
|
+
},
|
|
18031
|
+
"IMMEDIATE"
|
|
18032
|
+
);
|
|
17715
18033
|
} catch (error51) {
|
|
17716
|
-
|
|
17717
|
-
`);
|
|
18034
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17718
18035
|
}
|
|
17719
18036
|
}
|
|
17720
18037
|
function isForeignSqliteLineage(db) {
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17724
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
18038
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18039
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17725
18040
|
}
|
|
17726
18041
|
function ensureSyncedAtColumn(db, table2) {
|
|
17727
|
-
|
|
17728
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
18042
|
+
if (!columnNames(db, table2).includes("synced_at")) {
|
|
17729
18043
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
17730
18044
|
}
|
|
17731
18045
|
}
|
|
@@ -17776,8 +18090,11 @@ function ensureDataDirSync(dir) {
|
|
|
17776
18090
|
} catch {
|
|
17777
18091
|
}
|
|
17778
18092
|
}
|
|
18093
|
+
function walSidecars(file2) {
|
|
18094
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
18095
|
+
}
|
|
17779
18096
|
function tightenPerms(file2) {
|
|
17780
|
-
for (const path of [file2,
|
|
18097
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17781
18098
|
try {
|
|
17782
18099
|
chmodSync(path, DATA_FILE_MODE);
|
|
17783
18100
|
} catch {
|
|
@@ -17785,12 +18102,68 @@ function tightenPerms(file2) {
|
|
|
17785
18102
|
}
|
|
17786
18103
|
}
|
|
17787
18104
|
|
|
17788
|
-
// ../../packages/persistence/src/
|
|
17789
|
-
function
|
|
17790
|
-
|
|
18105
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
18106
|
+
function safeJson(s, fallback) {
|
|
18107
|
+
if (s == null) return fallback;
|
|
18108
|
+
try {
|
|
18109
|
+
return JSON.parse(s);
|
|
18110
|
+
} catch {
|
|
18111
|
+
return fallback;
|
|
18112
|
+
}
|
|
17791
18113
|
}
|
|
17792
|
-
function
|
|
17793
|
-
|
|
18114
|
+
function parseJsonObject(s) {
|
|
18115
|
+
if (s == null) return void 0;
|
|
18116
|
+
try {
|
|
18117
|
+
const parsed = JSON.parse(s);
|
|
18118
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18119
|
+
} catch {
|
|
18120
|
+
}
|
|
18121
|
+
return void 0;
|
|
18122
|
+
}
|
|
18123
|
+
|
|
18124
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18125
|
+
function allRows(stmt, params) {
|
|
18126
|
+
if (params === void 0) return stmt.all();
|
|
18127
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18128
|
+
return stmt.all(params);
|
|
18129
|
+
}
|
|
18130
|
+
function getRow(stmt, params) {
|
|
18131
|
+
if (params === void 0) return stmt.get();
|
|
18132
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18133
|
+
return stmt.get(params);
|
|
18134
|
+
}
|
|
18135
|
+
function intToBool(raw) {
|
|
18136
|
+
return raw === 1 || raw === true;
|
|
18137
|
+
}
|
|
18138
|
+
function boolToInt(b) {
|
|
18139
|
+
return b ? 1 : 0;
|
|
18140
|
+
}
|
|
18141
|
+
function bindParams(row) {
|
|
18142
|
+
const out = {};
|
|
18143
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18144
|
+
out[key] = value === void 0 ? null : value;
|
|
18145
|
+
}
|
|
18146
|
+
return out;
|
|
18147
|
+
}
|
|
18148
|
+
function countScalar(db, sql, params) {
|
|
18149
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18150
|
+
}
|
|
18151
|
+
function countBy(db, sql, params) {
|
|
18152
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18153
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18154
|
+
map2.set(row.k, row.n);
|
|
18155
|
+
}
|
|
18156
|
+
return map2;
|
|
18157
|
+
}
|
|
18158
|
+
function mapRowsTolerant(rows, map2) {
|
|
18159
|
+
const out = [];
|
|
18160
|
+
for (const row of rows) {
|
|
18161
|
+
try {
|
|
18162
|
+
out.push(map2(row));
|
|
18163
|
+
} catch {
|
|
18164
|
+
}
|
|
18165
|
+
}
|
|
18166
|
+
return out;
|
|
17794
18167
|
}
|
|
17795
18168
|
|
|
17796
18169
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17842,15 +18215,11 @@ function encodeCursor(payload) {
|
|
|
17842
18215
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17843
18216
|
}
|
|
17844
18217
|
function decodeCursor(cursor) {
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
return parsed;
|
|
17849
|
-
}
|
|
17850
|
-
return null;
|
|
17851
|
-
} catch {
|
|
17852
|
-
return null;
|
|
18218
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18219
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18220
|
+
return parsed;
|
|
17853
18221
|
}
|
|
18222
|
+
return null;
|
|
17854
18223
|
}
|
|
17855
18224
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17856
18225
|
session: "session",
|
|
@@ -17867,15 +18236,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17867
18236
|
};
|
|
17868
18237
|
function safeParseStringArray(raw) {
|
|
17869
18238
|
if (!raw) return [];
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17873
|
-
} catch {
|
|
17874
|
-
return [];
|
|
17875
|
-
}
|
|
17876
|
-
}
|
|
17877
|
-
function toBool(raw) {
|
|
17878
|
-
return raw === 1 || raw === true;
|
|
18239
|
+
const parsed = safeJson(raw, null);
|
|
18240
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17879
18241
|
}
|
|
17880
18242
|
function toHarness(raw) {
|
|
17881
18243
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17928,8 +18290,8 @@ function buildAuditEvent(row) {
|
|
|
17928
18290
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17929
18291
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17930
18292
|
targetId: row.target_id,
|
|
17931
|
-
internal:
|
|
17932
|
-
flagged:
|
|
18293
|
+
internal: intToBool(row.internal),
|
|
18294
|
+
flagged: intToBool(row.flagged)
|
|
17933
18295
|
};
|
|
17934
18296
|
}
|
|
17935
18297
|
var TIMELINE_COLUMNS = `
|
|
@@ -17945,6 +18307,10 @@ var TIMELINE_COLUMNS = `
|
|
|
17945
18307
|
json_extract(attributes, '$.internal') AS internal,
|
|
17946
18308
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
17947
18309
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
18310
|
+
var HAS_ACTIVITY = `EXISTS (
|
|
18311
|
+
SELECT 1 FROM audit_events c
|
|
18312
|
+
WHERE c.root_session_id = audit_events.id
|
|
18313
|
+
AND c.event_type NOT IN ('hook', 'config_scan'))`;
|
|
17948
18314
|
var SqliteActivityRepository = class {
|
|
17949
18315
|
constructor(db, now = () => Date.now()) {
|
|
17950
18316
|
this.db = db;
|
|
@@ -17955,12 +18321,16 @@ var SqliteActivityRepository = class {
|
|
|
17955
18321
|
stats(tz) {
|
|
17956
18322
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
17957
18323
|
const { startMs, endMs } = window;
|
|
17958
|
-
const sessionsToday =
|
|
18324
|
+
const sessionsToday = countScalar(
|
|
18325
|
+
this.db,
|
|
17959
18326
|
`SELECT count(*) AS n FROM audit_events
|
|
17960
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
17961
|
-
|
|
18327
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
|
|
18328
|
+
AND ${HAS_ACTIVITY}`,
|
|
18329
|
+
[startMs, endMs]
|
|
18330
|
+
);
|
|
17962
18331
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
17963
|
-
const liveNow =
|
|
18332
|
+
const liveNow = countScalar(
|
|
18333
|
+
this.db,
|
|
17964
18334
|
`SELECT count(*) AS n FROM audit_events s
|
|
17965
18335
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
17966
18336
|
AND max(
|
|
@@ -17969,22 +18339,29 @@ var SqliteActivityRepository = class {
|
|
|
17969
18339
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
17970
18340
|
s.started_at
|
|
17971
18341
|
)
|
|
17972
|
-
) >=
|
|
17973
|
-
|
|
17974
|
-
|
|
18342
|
+
) >= ?`,
|
|
18343
|
+
[liveThreshold]
|
|
18344
|
+
);
|
|
18345
|
+
const toolCallsToday = countScalar(
|
|
18346
|
+
this.db,
|
|
17975
18347
|
`SELECT count(*) AS n FROM audit_events
|
|
17976
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
17977
|
-
|
|
17978
|
-
|
|
18348
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18349
|
+
[startMs, endMs]
|
|
18350
|
+
);
|
|
18351
|
+
const findingsToday = countScalar(
|
|
18352
|
+
this.db,
|
|
17979
18353
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
17980
18354
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
17981
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
17982
|
-
|
|
17983
|
-
|
|
18355
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18356
|
+
[startMs, endMs]
|
|
18357
|
+
);
|
|
18358
|
+
const egressToday = countScalar(
|
|
18359
|
+
this.db,
|
|
17984
18360
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
17985
18361
|
FROM audit_events
|
|
17986
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
17987
|
-
|
|
18362
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18363
|
+
[startMs, endMs]
|
|
18364
|
+
);
|
|
17988
18365
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
17989
18366
|
}
|
|
17990
18367
|
listSessions(query) {
|
|
@@ -18006,7 +18383,7 @@ var SqliteActivityRepository = class {
|
|
|
18006
18383
|
conditions.push("started_at <= ?");
|
|
18007
18384
|
params.push(toMs);
|
|
18008
18385
|
if (query.q) {
|
|
18009
|
-
const pattern =
|
|
18386
|
+
const pattern = containsPattern(query.q);
|
|
18010
18387
|
conditions.push(
|
|
18011
18388
|
`(content LIKE ? ESCAPE '\\'
|
|
18012
18389
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18020,13 +18397,21 @@ var SqliteActivityRepository = class {
|
|
|
18020
18397
|
);
|
|
18021
18398
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
18022
18399
|
}
|
|
18400
|
+
const emptyCount = countScalar(
|
|
18401
|
+
this.db,
|
|
18402
|
+
`SELECT count(*) AS n FROM audit_events
|
|
18403
|
+
WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
|
|
18404
|
+
params
|
|
18405
|
+
);
|
|
18406
|
+
if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
|
|
18023
18407
|
if (cursor) {
|
|
18024
18408
|
conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
|
|
18025
18409
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18026
18410
|
}
|
|
18027
18411
|
const limit = query.limit;
|
|
18028
|
-
const rows =
|
|
18029
|
-
|
|
18412
|
+
const rows = allRows(
|
|
18413
|
+
this.db.prepare(
|
|
18414
|
+
`SELECT id,
|
|
18030
18415
|
json_extract(attributes, '$.harness') AS harness,
|
|
18031
18416
|
content AS title,
|
|
18032
18417
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18039,7 +18424,9 @@ var SqliteActivityRepository = class {
|
|
|
18039
18424
|
WHERE ${conditions.join(" AND ")}
|
|
18040
18425
|
ORDER BY started_at DESC, id DESC
|
|
18041
18426
|
LIMIT ?`
|
|
18042
|
-
|
|
18427
|
+
),
|
|
18428
|
+
[...params, limit + 1]
|
|
18429
|
+
);
|
|
18043
18430
|
const hasMore = rows.length > limit;
|
|
18044
18431
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18045
18432
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18053,11 +18440,12 @@ var SqliteActivityRepository = class {
|
|
|
18053
18440
|
);
|
|
18054
18441
|
const last = page[page.length - 1];
|
|
18055
18442
|
const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
|
|
18056
|
-
return Promise.resolve({ items, nextCursor });
|
|
18443
|
+
return Promise.resolve({ items, nextCursor, emptyCount });
|
|
18057
18444
|
}
|
|
18058
18445
|
getSession(sessionId) {
|
|
18059
|
-
const rootRow =
|
|
18060
|
-
|
|
18446
|
+
const rootRow = getRow(
|
|
18447
|
+
this.db.prepare(
|
|
18448
|
+
`SELECT id,
|
|
18061
18449
|
json_extract(attributes, '$.harness') AS harness,
|
|
18062
18450
|
content AS title,
|
|
18063
18451
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18074,47 +18462,66 @@ var SqliteActivityRepository = class {
|
|
|
18074
18462
|
FROM audit_events
|
|
18075
18463
|
WHERE id = ? AND event_type = 'session'
|
|
18076
18464
|
LIMIT 1`
|
|
18077
|
-
|
|
18465
|
+
),
|
|
18466
|
+
[sessionId]
|
|
18467
|
+
);
|
|
18078
18468
|
if (!rootRow) return Promise.resolve(null);
|
|
18079
|
-
const timelineRows =
|
|
18080
|
-
|
|
18469
|
+
const timelineRows = allRows(
|
|
18470
|
+
this.db.prepare(
|
|
18471
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18081
18472
|
FROM audit_events
|
|
18082
18473
|
WHERE id = ? OR root_session_id = ?
|
|
18083
18474
|
ORDER BY started_at ASC, id ASC`
|
|
18084
|
-
|
|
18475
|
+
),
|
|
18476
|
+
[sessionId, sessionId]
|
|
18477
|
+
);
|
|
18085
18478
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18086
|
-
const tokenRow =
|
|
18087
|
-
|
|
18479
|
+
const tokenRow = getRow(
|
|
18480
|
+
this.db.prepare(
|
|
18481
|
+
`SELECT
|
|
18088
18482
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18089
18483
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18090
18484
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18091
18485
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18092
18486
|
FROM audit_events
|
|
18093
18487
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18094
|
-
|
|
18095
|
-
|
|
18096
|
-
|
|
18488
|
+
),
|
|
18489
|
+
[sessionId]
|
|
18490
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18491
|
+
const primaryModel = getRow(
|
|
18492
|
+
this.db.prepare(
|
|
18493
|
+
`SELECT model, provider FROM audit_events
|
|
18097
18494
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18098
18495
|
ORDER BY started_at ASC, id ASC
|
|
18099
18496
|
LIMIT 1`
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18497
|
+
),
|
|
18498
|
+
[sessionId]
|
|
18499
|
+
);
|
|
18500
|
+
const toolRows = allRows(
|
|
18501
|
+
this.db.prepare(
|
|
18502
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18103
18503
|
count(*) AS n
|
|
18104
18504
|
FROM audit_events
|
|
18105
18505
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18106
18506
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
|
|
18507
|
+
),
|
|
18508
|
+
[sessionId]
|
|
18509
|
+
);
|
|
18510
|
+
const modelRows = allRows(
|
|
18511
|
+
this.db.prepare(
|
|
18512
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18110
18513
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18111
18514
|
ORDER BY model`
|
|
18112
|
-
|
|
18515
|
+
),
|
|
18516
|
+
[sessionId]
|
|
18517
|
+
);
|
|
18113
18518
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18114
|
-
const commits =
|
|
18519
|
+
const commits = countScalar(
|
|
18520
|
+
this.db,
|
|
18115
18521
|
`SELECT count(*) AS n FROM audit_events
|
|
18116
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18117
|
-
|
|
18522
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18523
|
+
[sessionId]
|
|
18524
|
+
);
|
|
18118
18525
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18119
18526
|
turns: 0,
|
|
18120
18527
|
findings: 0,
|
|
@@ -18181,7 +18588,10 @@ var SqliteActivityRepository = class {
|
|
|
18181
18588
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18182
18589
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18183
18590
|
);
|
|
18184
|
-
const rows =
|
|
18591
|
+
const rows = allRows(
|
|
18592
|
+
stmt,
|
|
18593
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18594
|
+
);
|
|
18185
18595
|
const seen = /* @__PURE__ */ new Set();
|
|
18186
18596
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18187
18597
|
return Promise.resolve([...seen]);
|
|
@@ -18204,23 +18614,23 @@ var SqliteActivityRepository = class {
|
|
|
18204
18614
|
conditions.push("started_at >= ?");
|
|
18205
18615
|
params.push(opts.fromMs);
|
|
18206
18616
|
}
|
|
18207
|
-
const rows =
|
|
18208
|
-
|
|
18617
|
+
const rows = allRows(
|
|
18618
|
+
this.db.prepare(
|
|
18619
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18209
18620
|
FROM audit_events
|
|
18210
18621
|
WHERE ${conditions.join(" AND ")}`
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
}
|
|
18222
|
-
|
|
18223
|
-
return leaves;
|
|
18622
|
+
),
|
|
18623
|
+
params
|
|
18624
|
+
);
|
|
18625
|
+
return mapRowsTolerant(
|
|
18626
|
+
rows.filter(
|
|
18627
|
+
(row) => row.sessionId !== null
|
|
18628
|
+
),
|
|
18629
|
+
(row) => ({
|
|
18630
|
+
sessionId: row.sessionId,
|
|
18631
|
+
attributes: JSON.parse(row.attributes)
|
|
18632
|
+
})
|
|
18633
|
+
);
|
|
18224
18634
|
}
|
|
18225
18635
|
/**
|
|
18226
18636
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18234,57 +18644,72 @@ var SqliteActivityRepository = class {
|
|
|
18234
18644
|
);
|
|
18235
18645
|
if (sessionIds.length === 0) return result;
|
|
18236
18646
|
const inClause = placeholders(sessionIds.length);
|
|
18237
|
-
const lastActivityRows =
|
|
18238
|
-
|
|
18647
|
+
const lastActivityRows = allRows(
|
|
18648
|
+
this.db.prepare(
|
|
18649
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18239
18650
|
WHERE root_session_id IN (${inClause})
|
|
18240
18651
|
GROUP BY root_session_id`
|
|
18241
|
-
|
|
18652
|
+
),
|
|
18653
|
+
sessionIds
|
|
18654
|
+
);
|
|
18242
18655
|
for (const row of lastActivityRows) {
|
|
18243
18656
|
if (row.id === null) continue;
|
|
18244
18657
|
const entry = result.get(row.id);
|
|
18245
18658
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18246
18659
|
}
|
|
18247
|
-
const turnsRows =
|
|
18248
|
-
|
|
18660
|
+
const turnsRows = allRows(
|
|
18661
|
+
this.db.prepare(
|
|
18662
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18249
18663
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18250
18664
|
GROUP BY root_session_id`
|
|
18251
|
-
|
|
18665
|
+
),
|
|
18666
|
+
sessionIds
|
|
18667
|
+
);
|
|
18252
18668
|
for (const row of turnsRows) {
|
|
18253
18669
|
if (row.id === null) continue;
|
|
18254
18670
|
const entry = result.get(row.id);
|
|
18255
18671
|
if (entry) entry.turns = row.n;
|
|
18256
18672
|
}
|
|
18257
|
-
const runKeyRows =
|
|
18258
|
-
|
|
18673
|
+
const runKeyRows = allRows(
|
|
18674
|
+
this.db.prepare(
|
|
18675
|
+
`SELECT root_session_id AS id,
|
|
18259
18676
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18260
18677
|
FROM audit_events
|
|
18261
18678
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18262
18679
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18263
18680
|
GROUP BY root_session_id`
|
|
18264
|
-
|
|
18681
|
+
),
|
|
18682
|
+
sessionIds
|
|
18683
|
+
);
|
|
18265
18684
|
for (const row of runKeyRows) {
|
|
18266
18685
|
if (row.id === null) continue;
|
|
18267
18686
|
const entry = result.get(row.id);
|
|
18268
18687
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18269
18688
|
}
|
|
18270
|
-
const findingsRows =
|
|
18271
|
-
|
|
18689
|
+
const findingsRows = allRows(
|
|
18690
|
+
this.db.prepare(
|
|
18691
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18272
18692
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18273
18693
|
WHERE e.root_session_id IN (${inClause})
|
|
18274
18694
|
GROUP BY e.root_session_id`
|
|
18275
|
-
|
|
18695
|
+
),
|
|
18696
|
+
sessionIds
|
|
18697
|
+
);
|
|
18276
18698
|
for (const row of findingsRows) {
|
|
18277
18699
|
if (row.id === null) continue;
|
|
18278
18700
|
const entry = result.get(row.id);
|
|
18279
18701
|
if (entry) entry.findings = row.n;
|
|
18280
18702
|
}
|
|
18281
|
-
const sharesRows =
|
|
18282
|
-
|
|
18703
|
+
const sharesRows = allRows(
|
|
18704
|
+
this.db.prepare(
|
|
18705
|
+
`SELECT root_session_id AS id,
|
|
18283
18706
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18284
18707
|
FROM audit_events
|
|
18285
18708
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18286
18709
|
GROUP BY root_session_id`
|
|
18287
|
-
|
|
18710
|
+
),
|
|
18711
|
+
sessionIds
|
|
18712
|
+
);
|
|
18288
18713
|
for (const row of sharesRows) {
|
|
18289
18714
|
if (row.id === null) continue;
|
|
18290
18715
|
const entry = result.get(row.id);
|
|
@@ -18334,33 +18759,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18334
18759
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18335
18760
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18336
18761
|
runInTransaction(fn) {
|
|
18337
|
-
this.db
|
|
18338
|
-
try {
|
|
18339
|
-
fn();
|
|
18340
|
-
this.db.exec("COMMIT");
|
|
18341
|
-
} catch (err) {
|
|
18342
|
-
this.db.exec("ROLLBACK");
|
|
18343
|
-
throw err;
|
|
18344
|
-
}
|
|
18762
|
+
withTransaction(this.db, fn);
|
|
18345
18763
|
}
|
|
18346
18764
|
insertAuditEvent(input) {
|
|
18347
18765
|
const row = toAuditEventRow(input);
|
|
18348
|
-
this.insertStmt.run(
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18766
|
+
this.insertStmt.run(
|
|
18767
|
+
bindParams({
|
|
18768
|
+
id: row.id,
|
|
18769
|
+
parentId: row.parentId,
|
|
18770
|
+
rootSessionId: row.rootSessionId,
|
|
18771
|
+
eventType: row.eventType,
|
|
18772
|
+
hostId: row.hostId,
|
|
18773
|
+
harnessId: row.harnessId,
|
|
18774
|
+
sourceProjectId: row.sourceProjectId,
|
|
18775
|
+
startedAt: row.startedAt,
|
|
18776
|
+
endedAt: row.endedAt,
|
|
18777
|
+
severity: row.severity,
|
|
18778
|
+
priority: row.priority,
|
|
18779
|
+
content: row.content,
|
|
18780
|
+
contentHash: row.contentHash,
|
|
18781
|
+
attributes: row.attributes
|
|
18782
|
+
})
|
|
18783
|
+
);
|
|
18364
18784
|
}
|
|
18365
18785
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18366
18786
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18374,22 +18794,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18374
18794
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18375
18795
|
if (!Number.isFinite(startedAt)) return;
|
|
18376
18796
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18377
|
-
this.upsertLlmCallStmt.run(
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18797
|
+
this.upsertLlmCallStmt.run(
|
|
18798
|
+
bindParams({
|
|
18799
|
+
id,
|
|
18800
|
+
parentId: input.parentId,
|
|
18801
|
+
rootSessionId: input.rootSessionId,
|
|
18802
|
+
eventType: "llm_call",
|
|
18803
|
+
hostId: null,
|
|
18804
|
+
harnessId: null,
|
|
18805
|
+
sourceProjectId: null,
|
|
18806
|
+
startedAt,
|
|
18807
|
+
endedAt: null,
|
|
18808
|
+
severity: null,
|
|
18809
|
+
priority: null,
|
|
18810
|
+
content: null,
|
|
18811
|
+
contentHash: null,
|
|
18812
|
+
attributes: JSON.stringify(input.attributes)
|
|
18813
|
+
})
|
|
18814
|
+
);
|
|
18393
18815
|
}
|
|
18394
18816
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18395
18817
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18413,25 +18835,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18413
18835
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18414
18836
|
if (!Number.isFinite(startedAt)) return;
|
|
18415
18837
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18416
|
-
this.insertStmt.run(
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18838
|
+
this.insertStmt.run(
|
|
18839
|
+
bindParams({
|
|
18840
|
+
id,
|
|
18841
|
+
parentId: input.parentId,
|
|
18842
|
+
rootSessionId: input.rootSessionId,
|
|
18843
|
+
eventType: "tool_call",
|
|
18844
|
+
hostId: null,
|
|
18845
|
+
harnessId: null,
|
|
18846
|
+
sourceProjectId: null,
|
|
18847
|
+
startedAt,
|
|
18848
|
+
endedAt: null,
|
|
18849
|
+
severity: null,
|
|
18850
|
+
priority: null,
|
|
18851
|
+
content: null,
|
|
18852
|
+
contentHash: null,
|
|
18853
|
+
attributes: JSON.stringify(input.attributes)
|
|
18854
|
+
})
|
|
18855
|
+
);
|
|
18432
18856
|
}
|
|
18433
18857
|
findById(id) {
|
|
18434
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18858
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18859
|
+
id
|
|
18860
|
+
});
|
|
18435
18861
|
}
|
|
18436
18862
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18437
18863
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18441,14 +18867,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18441
18867
|
sessionProvider(sessionId) {
|
|
18442
18868
|
const row = this.findById(sessionId);
|
|
18443
18869
|
if (!row?.attributes) return void 0;
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18447
|
-
const provider = parsed.provider;
|
|
18448
|
-
if (typeof provider === "string") return provider;
|
|
18449
|
-
}
|
|
18450
|
-
} catch {
|
|
18451
|
-
}
|
|
18870
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18871
|
+
if (typeof provider === "string") return provider;
|
|
18452
18872
|
return void 0;
|
|
18453
18873
|
}
|
|
18454
18874
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18457,11 +18877,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18457
18877
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18458
18878
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18459
18879
|
llmCallLeaves() {
|
|
18460
|
-
return
|
|
18461
|
-
|
|
18880
|
+
return allRows(
|
|
18881
|
+
this.db.prepare(
|
|
18882
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18462
18883
|
FROM audit_events
|
|
18463
18884
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18464
|
-
|
|
18885
|
+
)
|
|
18886
|
+
);
|
|
18465
18887
|
}
|
|
18466
18888
|
};
|
|
18467
18889
|
|
|
@@ -18480,16 +18902,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18480
18902
|
upsert(input) {
|
|
18481
18903
|
const id = classifiedDataId(input.class);
|
|
18482
18904
|
const row = toClassifiedDataRow(input, id);
|
|
18483
|
-
this.insertStmt.run(
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18905
|
+
this.insertStmt.run(
|
|
18906
|
+
bindParams({
|
|
18907
|
+
id: row.id,
|
|
18908
|
+
class: row.class,
|
|
18909
|
+
label: row.label,
|
|
18910
|
+
attributes: row.attributes
|
|
18911
|
+
})
|
|
18912
|
+
);
|
|
18489
18913
|
return id;
|
|
18490
18914
|
}
|
|
18491
18915
|
};
|
|
18492
18916
|
|
|
18917
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18918
|
+
function latestConfigScan(db) {
|
|
18919
|
+
return getRow(
|
|
18920
|
+
db.prepare(
|
|
18921
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18922
|
+
WHERE event_type = 'config_scan'
|
|
18923
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18924
|
+
)
|
|
18925
|
+
);
|
|
18926
|
+
}
|
|
18927
|
+
|
|
18493
18928
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18494
18929
|
var SqliteConfigInventoryRepository = class {
|
|
18495
18930
|
constructor(db) {
|
|
@@ -18497,7 +18932,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18497
18932
|
}
|
|
18498
18933
|
db;
|
|
18499
18934
|
report() {
|
|
18500
|
-
const scan2 = this.
|
|
18935
|
+
const scan2 = latestConfigScan(this.db);
|
|
18501
18936
|
if (!scan2) {
|
|
18502
18937
|
return {
|
|
18503
18938
|
scannedAt: null,
|
|
@@ -18508,17 +18943,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18508
18943
|
topics: []
|
|
18509
18944
|
};
|
|
18510
18945
|
}
|
|
18511
|
-
const rows =
|
|
18512
|
-
|
|
18946
|
+
const rows = allRows(
|
|
18947
|
+
this.db.prepare(
|
|
18948
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18513
18949
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18514
18950
|
ORDER BY object_type, title`
|
|
18515
|
-
|
|
18516
|
-
|
|
18517
|
-
|
|
18951
|
+
),
|
|
18952
|
+
{ startedAt: scan2.started_at }
|
|
18953
|
+
);
|
|
18954
|
+
const findings = allRows(
|
|
18955
|
+
this.db.prepare(
|
|
18956
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18518
18957
|
FROM inspection_findings f
|
|
18519
18958
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18520
18959
|
WHERE f.audit_event_id = :scanId`
|
|
18521
|
-
|
|
18960
|
+
),
|
|
18961
|
+
{ scanId: scan2.id }
|
|
18962
|
+
);
|
|
18522
18963
|
const skills = [];
|
|
18523
18964
|
const hooks = [];
|
|
18524
18965
|
const mcpServers = [];
|
|
@@ -18547,7 +18988,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18547
18988
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18548
18989
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18549
18990
|
trustOverrides() {
|
|
18550
|
-
const rows =
|
|
18991
|
+
const rows = allRows(
|
|
18992
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18993
|
+
);
|
|
18551
18994
|
const map2 = /* @__PURE__ */ new Map();
|
|
18552
18995
|
for (const row of rows) {
|
|
18553
18996
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18556,13 +18999,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18556
18999
|
}
|
|
18557
19000
|
return map2;
|
|
18558
19001
|
}
|
|
18559
|
-
latestScan() {
|
|
18560
|
-
return this.db.prepare(
|
|
18561
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18562
|
-
WHERE event_type = 'config_scan'
|
|
18563
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18564
|
-
).get();
|
|
18565
|
-
}
|
|
18566
19002
|
};
|
|
18567
19003
|
function toSkillItem(row, bag) {
|
|
18568
19004
|
const item = {
|
|
@@ -18673,22 +19109,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18673
19109
|
return topics;
|
|
18674
19110
|
}
|
|
18675
19111
|
function countScanErrors(attributes) {
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
const parsed = JSON.parse(attributes);
|
|
18679
|
-
const errors = parsed?.errors;
|
|
18680
|
-
return typeof errors === "number" ? errors : 0;
|
|
18681
|
-
} catch {
|
|
18682
|
-
return 0;
|
|
18683
|
-
}
|
|
19112
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
19113
|
+
return typeof errors === "number" ? errors : 0;
|
|
18684
19114
|
}
|
|
18685
19115
|
function parseBag(raw) {
|
|
18686
|
-
|
|
18687
|
-
const parsed = JSON.parse(raw);
|
|
18688
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18689
|
-
} catch {
|
|
18690
|
-
}
|
|
18691
|
-
return void 0;
|
|
19116
|
+
return parseJsonObject(raw);
|
|
18692
19117
|
}
|
|
18693
19118
|
function str(value) {
|
|
18694
19119
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18697,12 +19122,7 @@ function str(value) {
|
|
|
18697
19122
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18698
19123
|
var DAY_MS2 = 864e5;
|
|
18699
19124
|
function parseRules(rulesJson) {
|
|
18700
|
-
|
|
18701
|
-
try {
|
|
18702
|
-
raw = JSON.parse(rulesJson);
|
|
18703
|
-
} catch {
|
|
18704
|
-
return [];
|
|
18705
|
-
}
|
|
19125
|
+
const raw = safeJson(rulesJson, []);
|
|
18706
19126
|
if (!Array.isArray(raw)) return [];
|
|
18707
19127
|
const rules = [];
|
|
18708
19128
|
for (const entry of raw) {
|
|
@@ -18721,11 +19141,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18721
19141
|
db;
|
|
18722
19142
|
now;
|
|
18723
19143
|
listDetections(query) {
|
|
18724
|
-
const rows =
|
|
18725
|
-
|
|
19144
|
+
const rows = allRows(
|
|
19145
|
+
this.db.prepare(
|
|
19146
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18726
19147
|
rules_json AS rulesJson
|
|
18727
19148
|
FROM installed_packs`
|
|
18728
|
-
|
|
19149
|
+
)
|
|
19150
|
+
);
|
|
18729
19151
|
const available = this.availableByPack();
|
|
18730
19152
|
const summaries = rows.map((r) => {
|
|
18731
19153
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18734,7 +19156,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18734
19156
|
packId: r.packId,
|
|
18735
19157
|
version: r.version,
|
|
18736
19158
|
name: r.name,
|
|
18737
|
-
enabled: r.enabled
|
|
19159
|
+
enabled: intToBool(r.enabled),
|
|
18738
19160
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18739
19161
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18740
19162
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18751,19 +19173,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18751
19173
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18752
19174
|
// detail call; the table is a handful of rows).
|
|
18753
19175
|
availableByPack() {
|
|
18754
|
-
const rows =
|
|
18755
|
-
|
|
19176
|
+
const rows = allRows(
|
|
19177
|
+
this.db.prepare(
|
|
19178
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18756
19179
|
FROM available_packs`
|
|
18757
|
-
|
|
19180
|
+
)
|
|
19181
|
+
);
|
|
18758
19182
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18759
19183
|
}
|
|
18760
19184
|
getDetectionStats() {
|
|
18761
|
-
const rows =
|
|
19185
|
+
const rows = allRows(
|
|
19186
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19187
|
+
);
|
|
18762
19188
|
let rules = 0;
|
|
18763
19189
|
let active = 0;
|
|
18764
19190
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18765
19191
|
for (const r of rows) {
|
|
18766
|
-
if (r.enabled
|
|
19192
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18767
19193
|
const parsed = parseRules(r.rulesJson);
|
|
18768
19194
|
rules += parsed.length;
|
|
18769
19195
|
for (const rule of parsed) {
|
|
@@ -18781,12 +19207,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18781
19207
|
const parts = splitDetectionId(id);
|
|
18782
19208
|
if (!parts) return Promise.resolve(null);
|
|
18783
19209
|
const { namespace, packId } = parts;
|
|
18784
|
-
const row =
|
|
18785
|
-
|
|
19210
|
+
const row = getRow(
|
|
19211
|
+
this.db.prepare(
|
|
19212
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18786
19213
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18787
19214
|
FROM installed_packs
|
|
18788
19215
|
WHERE namespace = ? AND pack_id = ?`
|
|
18789
|
-
|
|
19216
|
+
),
|
|
19217
|
+
[namespace, packId]
|
|
19218
|
+
);
|
|
18790
19219
|
if (!row) return Promise.resolve(null);
|
|
18791
19220
|
const rules = parseRules(row.rulesJson);
|
|
18792
19221
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18804,7 +19233,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18804
19233
|
packId: row.packId,
|
|
18805
19234
|
version: row.version,
|
|
18806
19235
|
name: row.name,
|
|
18807
|
-
enabled: row.enabled
|
|
19236
|
+
enabled: intToBool(row.enabled),
|
|
18808
19237
|
rules,
|
|
18809
19238
|
updatedAt: new Date(row.updatedAt),
|
|
18810
19239
|
policyId: row.policyId
|
|
@@ -18819,13 +19248,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18819
19248
|
countFindingsLast30d(ruleIds) {
|
|
18820
19249
|
if (ruleIds.length === 0) return 0;
|
|
18821
19250
|
const since = this.now() - 30 * DAY_MS2;
|
|
18822
|
-
const
|
|
18823
|
-
|
|
18824
|
-
|
|
19251
|
+
const inClause = placeholders(ruleIds.length);
|
|
19252
|
+
return countScalar(
|
|
19253
|
+
this.db,
|
|
19254
|
+
`SELECT count(*) AS n
|
|
18825
19255
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18826
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18827
|
-
|
|
18828
|
-
|
|
19256
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19257
|
+
[since, ...ruleIds]
|
|
19258
|
+
);
|
|
18829
19259
|
}
|
|
18830
19260
|
};
|
|
18831
19261
|
|
|
@@ -18842,16 +19272,17 @@ var SqliteEventsRepository = class {
|
|
|
18842
19272
|
insertStmt;
|
|
18843
19273
|
insertEvent(event) {
|
|
18844
19274
|
const row = toEventRow(event);
|
|
18845
|
-
this.insertStmt.run(
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
19275
|
+
this.insertStmt.run(
|
|
19276
|
+
bindParams({
|
|
19277
|
+
id: row.id,
|
|
19278
|
+
sourceTool: row.sourceTool,
|
|
19279
|
+
kind: row.kind,
|
|
19280
|
+
occurredAt: row.occurredAt,
|
|
19281
|
+
contentHash: row.contentHash,
|
|
19282
|
+
content: row.content,
|
|
19283
|
+
metadata: row.metadata
|
|
19284
|
+
})
|
|
19285
|
+
);
|
|
18855
19286
|
}
|
|
18856
19287
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18857
19288
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18859,13 +19290,23 @@ var SqliteEventsRepository = class {
|
|
|
18859
19290
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18860
19291
|
// async EventsReadPort contract.
|
|
18861
19292
|
contentHashes() {
|
|
18862
|
-
const rows =
|
|
19293
|
+
const rows = allRows(
|
|
19294
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19295
|
+
);
|
|
18863
19296
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18864
19297
|
}
|
|
18865
19298
|
};
|
|
18866
19299
|
|
|
18867
19300
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18868
19301
|
import { randomUUID } from "crypto";
|
|
19302
|
+
|
|
19303
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19304
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19305
|
+
function isUniqueConstraintError(err) {
|
|
19306
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19307
|
+
}
|
|
19308
|
+
|
|
19309
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18869
19310
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18870
19311
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18871
19312
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18886,10 +19327,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18886
19327
|
this.name = "AmbiguousExceptionIdError";
|
|
18887
19328
|
}
|
|
18888
19329
|
};
|
|
18889
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18890
|
-
function isUniqueConstraintError(err) {
|
|
18891
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18892
|
-
}
|
|
18893
19330
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18894
19331
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18895
19332
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -18945,10 +19382,11 @@ var SqliteExceptionsRepository = class {
|
|
|
18945
19382
|
this.insertExceptionRow(id, input, now);
|
|
18946
19383
|
} catch (err) {
|
|
18947
19384
|
if (!isUniqueConstraintError(err)) throw err;
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
19385
|
+
withTransaction(
|
|
19386
|
+
this.db,
|
|
19387
|
+
() => {
|
|
19388
|
+
const superseded = this.db.prepare(
|
|
19389
|
+
`UPDATE exceptions
|
|
18952
19390
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
18953
19391
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
18954
19392
|
updated_at = :now
|
|
@@ -18956,24 +19394,27 @@ var SqliteExceptionsRepository = class {
|
|
|
18956
19394
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
18957
19395
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
18958
19396
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
19397
|
+
).run({
|
|
19398
|
+
now,
|
|
19399
|
+
revokedBy: input.createdBy,
|
|
19400
|
+
ruleId: input.ruleId,
|
|
19401
|
+
valueFingerprint: input.valueFingerprint,
|
|
19402
|
+
keyVersion: input.keyVersion
|
|
19403
|
+
});
|
|
19404
|
+
if (Number(superseded.changes) !== 1) {
|
|
19405
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19406
|
+
}
|
|
19407
|
+
this.insertExceptionRow(id, input, now);
|
|
19408
|
+
},
|
|
19409
|
+
"IMMEDIATE"
|
|
19410
|
+
);
|
|
19411
|
+
}
|
|
19412
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19413
|
+
id
|
|
19414
|
+
});
|
|
19415
|
+
if (row === void 0) {
|
|
19416
|
+
throw new Error("exception row not found immediately after insert");
|
|
18975
19417
|
}
|
|
18976
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
18977
19418
|
return parseExceptionRow(row);
|
|
18978
19419
|
}
|
|
18979
19420
|
insertExceptionRow(id, input, now) {
|
|
@@ -19011,14 +19452,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19011
19452
|
*/
|
|
19012
19453
|
list(opts) {
|
|
19013
19454
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19014
|
-
const rows =
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
} catch {
|
|
19020
|
-
}
|
|
19021
|
-
}
|
|
19455
|
+
const rows = allRows(
|
|
19456
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19457
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19458
|
+
);
|
|
19459
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19022
19460
|
return Promise.resolve(exceptions);
|
|
19023
19461
|
}
|
|
19024
19462
|
/**
|
|
@@ -19028,7 +19466,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19028
19466
|
*/
|
|
19029
19467
|
getByIdPrefix(prefix) {
|
|
19030
19468
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19031
|
-
const rows =
|
|
19469
|
+
const rows = allRows(
|
|
19470
|
+
this.db.prepare(
|
|
19471
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19472
|
+
),
|
|
19473
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19474
|
+
);
|
|
19032
19475
|
if (rows.length > 1) {
|
|
19033
19476
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19034
19477
|
}
|
|
@@ -19070,30 +19513,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19070
19513
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19071
19514
|
*/
|
|
19072
19515
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19073
|
-
const rows =
|
|
19074
|
-
|
|
19516
|
+
const rows = allRows(
|
|
19517
|
+
this.db.prepare(
|
|
19518
|
+
`SELECT * FROM exceptions
|
|
19075
19519
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19076
19520
|
ORDER BY created_at DESC, rowid DESC`
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
);
|
|
19094
|
-
} catch {
|
|
19095
|
-
}
|
|
19096
|
-
}
|
|
19521
|
+
),
|
|
19522
|
+
{ keyVersion, now }
|
|
19523
|
+
);
|
|
19524
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19525
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19526
|
+
return ExceptionBundleEntry.parse({
|
|
19527
|
+
id: row.id,
|
|
19528
|
+
ruleId: row.rule_id,
|
|
19529
|
+
valueFingerprint: row.value_fingerprint,
|
|
19530
|
+
keyVersion: row.key_version,
|
|
19531
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19532
|
+
maxUses: row.max_uses,
|
|
19533
|
+
useCount: row.use_count,
|
|
19534
|
+
conditions
|
|
19535
|
+
});
|
|
19536
|
+
});
|
|
19097
19537
|
return Promise.resolve(entries);
|
|
19098
19538
|
}
|
|
19099
19539
|
/**
|
|
@@ -19120,11 +19560,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19120
19560
|
}
|
|
19121
19561
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19122
19562
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19123
|
-
const rows =
|
|
19124
|
-
|
|
19563
|
+
const rows = allRows(
|
|
19564
|
+
this.db.prepare(
|
|
19565
|
+
`SELECT * FROM blocked_detections
|
|
19125
19566
|
WHERE blocked_at > :cutoff
|
|
19126
19567
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19127
|
-
|
|
19568
|
+
),
|
|
19569
|
+
{ cutoff: Date.now() - windowMs }
|
|
19570
|
+
);
|
|
19128
19571
|
return Promise.resolve(
|
|
19129
19572
|
rows.map((row) => ({
|
|
19130
19573
|
reference: row.reference,
|
|
@@ -19204,7 +19647,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19204
19647
|
)`;
|
|
19205
19648
|
|
|
19206
19649
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19207
|
-
var
|
|
19650
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19651
|
+
var CONCAT_SEP = ",";
|
|
19652
|
+
var TUPLE_SEP = "|";
|
|
19653
|
+
function splitConcat(value) {
|
|
19654
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19655
|
+
}
|
|
19208
19656
|
function deriveInstanceStatus(row) {
|
|
19209
19657
|
return deriveFindingStatus({
|
|
19210
19658
|
kind: row.kind,
|
|
@@ -19272,13 +19720,16 @@ var SqliteFindingsRepository = class {
|
|
|
19272
19720
|
}
|
|
19273
19721
|
recentFindings(opts) {
|
|
19274
19722
|
const limit = opts?.limit ?? 50;
|
|
19275
|
-
const rows =
|
|
19276
|
-
|
|
19723
|
+
const rows = allRows(
|
|
19724
|
+
this.db.prepare(
|
|
19725
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19277
19726
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19278
19727
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19279
19728
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19280
19729
|
LIMIT :limit`
|
|
19281
|
-
|
|
19730
|
+
),
|
|
19731
|
+
{ limit }
|
|
19732
|
+
);
|
|
19282
19733
|
return Promise.resolve(
|
|
19283
19734
|
rows.map((r) => ({
|
|
19284
19735
|
id: r.id,
|
|
@@ -19295,26 +19746,94 @@ var SqliteFindingsRepository = class {
|
|
|
19295
19746
|
}))
|
|
19296
19747
|
);
|
|
19297
19748
|
}
|
|
19749
|
+
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19750
|
+
* session-stamped events (served by idx_events_session_id), so the Activity
|
|
19751
|
+
* page can label its findings link without the grouped pipeline. */
|
|
19752
|
+
sessionFindingsCount(sessionId) {
|
|
19753
|
+
if (!sessionId) return Promise.resolve(0);
|
|
19754
|
+
return Promise.resolve(
|
|
19755
|
+
countScalar(
|
|
19756
|
+
this.db,
|
|
19757
|
+
`SELECT count(*) AS n FROM findings f
|
|
19758
|
+
JOIN events e ON e.id = f.event_id
|
|
19759
|
+
WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
|
|
19760
|
+
{ sessionId }
|
|
19761
|
+
)
|
|
19762
|
+
);
|
|
19763
|
+
}
|
|
19764
|
+
/** Per-rule transcript firing tally for one session — reads the OTHER finding
|
|
19765
|
+
* store (inspection_findings, keyed to audit_events): every detection the
|
|
19766
|
+
* transcript pass recorded, counted per firing rather than per unique value.
|
|
19767
|
+
* Rides on session-scoped grouped responses so the findings view can
|
|
19768
|
+
* reconcile the Activity page's tally with the deduped groups it lists. */
|
|
19769
|
+
sessionFirings(sessionId) {
|
|
19770
|
+
return Object.fromEntries(
|
|
19771
|
+
countBy(
|
|
19772
|
+
this.db,
|
|
19773
|
+
`SELECT d.rule_id AS k, count(*) AS n
|
|
19774
|
+
FROM inspection_findings f
|
|
19775
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19776
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19777
|
+
WHERE e.root_session_id = :sessionId
|
|
19778
|
+
GROUP BY d.rule_id`,
|
|
19779
|
+
{ sessionId }
|
|
19780
|
+
)
|
|
19781
|
+
);
|
|
19782
|
+
}
|
|
19298
19783
|
/**
|
|
19299
|
-
* Grouped findings for the dashboard — joins findings⋈events (repo/file
|
|
19300
|
-
* from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19784
|
+
* Grouped findings for the dashboard — joins findings⋈events (repo/file/
|
|
19785
|
+
* toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19301
19786
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19302
19787
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19303
19788
|
* reflect the full filtered set; `items` is the requested
|
|
19304
|
-
* page (default
|
|
19789
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19790
|
+
*
|
|
19791
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19792
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19793
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19794
|
+
* latest, search text);
|
|
19795
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19796
|
+
* populate `instances` for the table's expanded rows.
|
|
19797
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19798
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19799
|
+
* rule is ever restated in SQL.
|
|
19305
19800
|
*/
|
|
19306
19801
|
listGroupedFindings(query) {
|
|
19307
|
-
const
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19802
|
+
const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
|
|
19803
|
+
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19804
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19805
|
+
predicate: sessionPredicate,
|
|
19806
|
+
params: sessionParams
|
|
19807
|
+
});
|
|
19808
|
+
const rows = allRows(
|
|
19809
|
+
this.db.prepare(
|
|
19810
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19811
|
+
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19812
|
+
FROM (
|
|
19813
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19814
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19815
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19816
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19817
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19818
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19819
|
+
json_extract(e.metadata, '$.toolName') AS tool_name,
|
|
19820
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19821
|
+
latest.status AS latest_status,
|
|
19822
|
+
ROW_NUMBER() OVER (
|
|
19823
|
+
PARTITION BY f.rule_id
|
|
19824
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19825
|
+
) AS rn
|
|
19826
|
+
FROM findings f
|
|
19827
|
+
JOIN events e ON e.id = f.event_id
|
|
19828
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19829
|
+
ON latest.finding_key = f.finding_key
|
|
19830
|
+
${sessionPredicate}
|
|
19831
|
+
)
|
|
19832
|
+
WHERE rn <= :cap
|
|
19833
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19834
|
+
),
|
|
19835
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
19836
|
+
);
|
|
19318
19837
|
const groupable = rows.map((r) => ({
|
|
19319
19838
|
id: r.id,
|
|
19320
19839
|
ruleId: r.rule_id,
|
|
@@ -19327,9 +19846,10 @@ var SqliteFindingsRepository = class {
|
|
|
19327
19846
|
sourceTool: r.source_tool,
|
|
19328
19847
|
repo: r.repo ?? "",
|
|
19329
19848
|
file: r.file ?? "",
|
|
19849
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
19330
19850
|
status: deriveInstanceStatus(r)
|
|
19331
19851
|
}));
|
|
19332
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19852
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19333
19853
|
const filterOpts = {
|
|
19334
19854
|
severity: query.severity,
|
|
19335
19855
|
providers: query.provider,
|
|
@@ -19345,44 +19865,134 @@ var SqliteFindingsRepository = class {
|
|
|
19345
19865
|
};
|
|
19346
19866
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19347
19867
|
const items = sorted.slice(0, limit);
|
|
19348
|
-
return Promise.resolve({
|
|
19868
|
+
return Promise.resolve({
|
|
19869
|
+
totals,
|
|
19870
|
+
facets,
|
|
19871
|
+
items,
|
|
19872
|
+
nextCursor: null,
|
|
19873
|
+
...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
|
|
19874
|
+
});
|
|
19875
|
+
}
|
|
19876
|
+
/**
|
|
19877
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19878
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19879
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19880
|
+
*
|
|
19881
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19882
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19883
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19884
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19885
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19886
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19887
|
+
* a group's row stays small however many findings it holds.
|
|
19888
|
+
*
|
|
19889
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19890
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19891
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19892
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19893
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19894
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19895
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19896
|
+
* carries a `q`.
|
|
19897
|
+
*/
|
|
19898
|
+
groupAggregates(withSearchText, scope) {
|
|
19899
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19900
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
|
|
19901
|
+
group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19902
|
+
const rows = this.db.prepare(
|
|
19903
|
+
`SELECT f.rule_id AS rule_id,
|
|
19904
|
+
count(*) AS instance_count,
|
|
19905
|
+
max(e.occurred_at) AS latest_at,
|
|
19906
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19907
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19908
|
+
group_concat(DISTINCT (
|
|
19909
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19910
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19911
|
+
coalesce(latest.status, '')
|
|
19912
|
+
)) AS status_inputs
|
|
19913
|
+
${searchTextColumns}
|
|
19914
|
+
FROM findings f
|
|
19915
|
+
JOIN events e ON e.id = f.event_id
|
|
19916
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19917
|
+
ON latest.finding_key = f.finding_key
|
|
19918
|
+
${scope.predicate}
|
|
19919
|
+
GROUP BY f.rule_id`
|
|
19920
|
+
).all(scope.params);
|
|
19921
|
+
return new Map(
|
|
19922
|
+
rows.map((r) => [
|
|
19923
|
+
r.rule_id,
|
|
19924
|
+
{
|
|
19925
|
+
instanceCount: r.instance_count,
|
|
19926
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19927
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19928
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19929
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19930
|
+
return {
|
|
19931
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19932
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19933
|
+
kind,
|
|
19934
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19935
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19936
|
+
};
|
|
19937
|
+
}),
|
|
19938
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19939
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19940
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19941
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19942
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19943
|
+
// and skip priming a haystack nothing will read.
|
|
19944
|
+
...withSearchText ? {
|
|
19945
|
+
searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
|
|
19946
|
+
} : {}
|
|
19947
|
+
}
|
|
19948
|
+
])
|
|
19949
|
+
);
|
|
19349
19950
|
}
|
|
19350
19951
|
healthSummary() {
|
|
19351
|
-
const total = this.db
|
|
19952
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19352
19953
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19353
|
-
const grouped =
|
|
19954
|
+
const grouped = allRows(
|
|
19955
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19956
|
+
);
|
|
19354
19957
|
for (const row of grouped) {
|
|
19355
19958
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19356
19959
|
}
|
|
19357
19960
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19358
|
-
const sevRows =
|
|
19359
|
-
|
|
19961
|
+
const sevRows = allRows(
|
|
19962
|
+
this.db.prepare(
|
|
19963
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19360
19964
|
FROM findings f
|
|
19361
19965
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19362
19966
|
ON latest.finding_key = f.finding_key
|
|
19363
19967
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19364
19968
|
GROUP BY f.severity`
|
|
19365
|
-
|
|
19969
|
+
)
|
|
19970
|
+
);
|
|
19366
19971
|
for (const row of sevRows) {
|
|
19367
19972
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19368
19973
|
}
|
|
19369
19974
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19370
|
-
const enabledRows =
|
|
19371
|
-
|
|
19975
|
+
const enabledRows = allRows(
|
|
19976
|
+
this.db.prepare(
|
|
19977
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19372
19978
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19373
|
-
|
|
19979
|
+
)
|
|
19980
|
+
);
|
|
19374
19981
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19375
19982
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19376
19983
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19377
19984
|
}
|
|
19378
19985
|
activityByDay(days = 7) {
|
|
19379
19986
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19380
|
-
const rows =
|
|
19381
|
-
|
|
19987
|
+
const rows = allRows(
|
|
19988
|
+
this.db.prepare(
|
|
19989
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19382
19990
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19383
19991
|
WHERE e.occurred_at >= :since
|
|
19384
19992
|
GROUP BY day, f.action_taken`
|
|
19385
|
-
|
|
19993
|
+
),
|
|
19994
|
+
{ since }
|
|
19995
|
+
);
|
|
19386
19996
|
const buckets = /* @__PURE__ */ new Map();
|
|
19387
19997
|
for (let i = 0; i < days; i++) {
|
|
19388
19998
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19454,17 +20064,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19454
20064
|
insertStmt;
|
|
19455
20065
|
insertFinding(input) {
|
|
19456
20066
|
const row = toInspectionFindingRow(input);
|
|
19457
|
-
this.insertStmt.run(
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
20067
|
+
this.insertStmt.run(
|
|
20068
|
+
bindParams({
|
|
20069
|
+
id: row.id,
|
|
20070
|
+
auditEventId: row.auditEventId,
|
|
20071
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
20072
|
+
classifiedDataId: row.classifiedDataId,
|
|
20073
|
+
spanStart: row.spanStart,
|
|
20074
|
+
spanEnd: row.spanEnd,
|
|
20075
|
+
maskedMatch: row.maskedMatch,
|
|
20076
|
+
actionTaken: row.actionTaken,
|
|
20077
|
+
confidence: row.confidence
|
|
20078
|
+
})
|
|
20079
|
+
);
|
|
19468
20080
|
}
|
|
19469
20081
|
};
|
|
19470
20082
|
|
|
@@ -19540,12 +20152,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19540
20152
|
}
|
|
19541
20153
|
function ruleIdsOf(rulesJson) {
|
|
19542
20154
|
const ids = /* @__PURE__ */ new Set();
|
|
19543
|
-
|
|
19544
|
-
try {
|
|
19545
|
-
raw = JSON.parse(rulesJson);
|
|
19546
|
-
} catch {
|
|
19547
|
-
return ids;
|
|
19548
|
-
}
|
|
20155
|
+
const raw = safeJson(rulesJson, []);
|
|
19549
20156
|
if (!Array.isArray(raw)) return ids;
|
|
19550
20157
|
for (const entry of raw) {
|
|
19551
20158
|
if (entry && typeof entry === "object") {
|
|
@@ -19618,47 +20225,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19618
20225
|
}));
|
|
19619
20226
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19620
20227
|
const now = Date.now();
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
const
|
|
19627
|
-
|
|
19628
|
-
namespace: row.namespace,
|
|
19629
|
-
packId: row.packId,
|
|
19630
|
-
version: row.version,
|
|
19631
|
-
name: row.name,
|
|
19632
|
-
rulesJson: row.rulesJson,
|
|
19633
|
-
now
|
|
19634
|
-
};
|
|
19635
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19636
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19637
|
-
this.upsertAvailableStmt.run({
|
|
19638
|
-
...params,
|
|
20228
|
+
withTransaction(
|
|
20229
|
+
this.db,
|
|
20230
|
+
() => {
|
|
20231
|
+
const mirror = this.mirrorState();
|
|
20232
|
+
let behind = false;
|
|
20233
|
+
for (const row of rows) {
|
|
20234
|
+
const params = {
|
|
19639
20235
|
id: randomUUID2(),
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
20236
|
+
namespace: row.namespace,
|
|
20237
|
+
packId: row.packId,
|
|
20238
|
+
version: row.version,
|
|
20239
|
+
name: row.name,
|
|
20240
|
+
rulesJson: row.rulesJson,
|
|
20241
|
+
now
|
|
20242
|
+
};
|
|
20243
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20244
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20245
|
+
this.upsertAvailableStmt.run({
|
|
20246
|
+
...params,
|
|
20247
|
+
id: randomUUID2(),
|
|
20248
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20249
|
+
});
|
|
20250
|
+
} else {
|
|
20251
|
+
behind = true;
|
|
20252
|
+
}
|
|
20253
|
+
this.insertMissingStmt.run(params);
|
|
19644
20254
|
}
|
|
19645
|
-
this.
|
|
19646
|
-
}
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
} catch (err) {
|
|
19650
|
-
this.db.exec("ROLLBACK");
|
|
19651
|
-
throw err;
|
|
19652
|
-
}
|
|
20255
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20256
|
+
},
|
|
20257
|
+
"IMMEDIATE"
|
|
20258
|
+
);
|
|
19653
20259
|
} catch {
|
|
19654
20260
|
}
|
|
19655
20261
|
}
|
|
19656
20262
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19657
20263
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19658
20264
|
mirrorState() {
|
|
19659
|
-
const rows =
|
|
19660
|
-
|
|
19661
|
-
|
|
20265
|
+
const rows = allRows(
|
|
20266
|
+
this.db.prepare(
|
|
20267
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20268
|
+
)
|
|
20269
|
+
);
|
|
19662
20270
|
return new Map(
|
|
19663
20271
|
rows.map((r) => [
|
|
19664
20272
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19670,7 +20278,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19670
20278
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19671
20279
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19672
20280
|
pruneAvailable(keep) {
|
|
19673
|
-
const rows =
|
|
20281
|
+
const rows = allRows(
|
|
20282
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20283
|
+
);
|
|
19674
20284
|
const keepSet = new Set(keep);
|
|
19675
20285
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19676
20286
|
for (const r of rows) {
|
|
@@ -19697,11 +20307,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19697
20307
|
if (this.db.isTransaction) {
|
|
19698
20308
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19699
20309
|
}
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
this.db
|
|
19703
|
-
|
|
19704
|
-
|
|
20310
|
+
let changed = false;
|
|
20311
|
+
withTransaction(
|
|
20312
|
+
this.db,
|
|
20313
|
+
() => {
|
|
20314
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20315
|
+
const res = this.db.prepare(
|
|
20316
|
+
`UPDATE installed_packs SET
|
|
19705
20317
|
version = (SELECT a.version FROM available_packs a
|
|
19706
20318
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19707
20319
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19712,17 +20324,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19712
20324
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19713
20325
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19714
20326
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
} catch {
|
|
19723
|
-
}
|
|
19724
|
-
throw err;
|
|
19725
|
-
}
|
|
20327
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20328
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20329
|
+
changed = Number(res.changes) > 0;
|
|
20330
|
+
},
|
|
20331
|
+
"IMMEDIATE"
|
|
20332
|
+
);
|
|
20333
|
+
return changed;
|
|
19726
20334
|
}
|
|
19727
20335
|
/**
|
|
19728
20336
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19736,9 +20344,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19736
20344
|
* JSON-level failure therefore counts as invalid.
|
|
19737
20345
|
*/
|
|
19738
20346
|
installedRuleset() {
|
|
19739
|
-
const rows =
|
|
19740
|
-
|
|
19741
|
-
|
|
20347
|
+
const rows = allRows(
|
|
20348
|
+
this.db.prepare(
|
|
20349
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20350
|
+
)
|
|
20351
|
+
);
|
|
19742
20352
|
const out = {
|
|
19743
20353
|
installedPacks: rows.length,
|
|
19744
20354
|
enabledPacks: 0,
|
|
@@ -19747,7 +20357,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19747
20357
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19748
20358
|
};
|
|
19749
20359
|
for (const row of rows) {
|
|
19750
|
-
if (row.enabled
|
|
20360
|
+
if (!intToBool(row.enabled)) continue;
|
|
19751
20361
|
out.enabledPacks += 1;
|
|
19752
20362
|
const action = policyIdToAction(row.policyId);
|
|
19753
20363
|
let raw;
|
|
@@ -19782,9 +20392,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19782
20392
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19783
20393
|
*/
|
|
19784
20394
|
newestRecordedBinary() {
|
|
19785
|
-
const rows =
|
|
19786
|
-
|
|
19787
|
-
|
|
20395
|
+
const rows = allRows(
|
|
20396
|
+
this.db.prepare(
|
|
20397
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20398
|
+
)
|
|
20399
|
+
);
|
|
19788
20400
|
let newest = null;
|
|
19789
20401
|
for (const row of rows) {
|
|
19790
20402
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19799,13 +20411,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19799
20411
|
return newest;
|
|
19800
20412
|
}
|
|
19801
20413
|
counts() {
|
|
19802
|
-
const row =
|
|
19803
|
-
|
|
20414
|
+
const row = getRow(
|
|
20415
|
+
this.db.prepare(
|
|
20416
|
+
`SELECT count(*) AS packs,
|
|
19804
20417
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19805
20418
|
coalesce(sum(enabled), 0) AS enabled
|
|
19806
20419
|
FROM installed_packs`
|
|
19807
|
-
|
|
19808
|
-
|
|
20420
|
+
)
|
|
20421
|
+
);
|
|
20422
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19809
20423
|
}
|
|
19810
20424
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19811
20425
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19818,26 +20432,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19818
20432
|
* attributed to Monitor, matching the Detections views.
|
|
19819
20433
|
*/
|
|
19820
20434
|
countsByPolicyId() {
|
|
19821
|
-
|
|
19822
|
-
|
|
20435
|
+
return countBy(
|
|
20436
|
+
this.db,
|
|
20437
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19823
20438
|
FROM installed_packs
|
|
19824
|
-
GROUP BY
|
|
19825
|
-
)
|
|
19826
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20439
|
+
GROUP BY k`
|
|
20440
|
+
);
|
|
19827
20441
|
}
|
|
19828
20442
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19829
20443
|
listByPolicyId(policyId) {
|
|
19830
|
-
const rows =
|
|
19831
|
-
|
|
20444
|
+
const rows = allRows(
|
|
20445
|
+
this.db.prepare(
|
|
20446
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19832
20447
|
FROM installed_packs
|
|
19833
20448
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19834
20449
|
ORDER BY name ASC`
|
|
19835
|
-
|
|
20450
|
+
),
|
|
20451
|
+
[policyId]
|
|
20452
|
+
);
|
|
19836
20453
|
return rows.map((r) => ({
|
|
19837
20454
|
id: `${r.namespace}/${r.packId}`,
|
|
19838
20455
|
name: r.name,
|
|
19839
20456
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19840
|
-
enabled: r.enabled
|
|
20457
|
+
enabled: intToBool(r.enabled)
|
|
19841
20458
|
}));
|
|
19842
20459
|
}
|
|
19843
20460
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19866,14 +20483,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19866
20483
|
const res = this.db.prepare(
|
|
19867
20484
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19868
20485
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19869
|
-
).run({ enabled: enabled
|
|
20486
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19870
20487
|
return Number(res.changes) > 0;
|
|
19871
20488
|
}
|
|
19872
20489
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19873
20490
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19874
20491
|
// binary's inventory hasn't changed since the last record.
|
|
19875
20492
|
storedSignature() {
|
|
19876
|
-
const rows = this.signatureStmt
|
|
20493
|
+
const rows = allRows(this.signatureStmt);
|
|
19877
20494
|
return inventorySignature(rows);
|
|
19878
20495
|
}
|
|
19879
20496
|
};
|
|
@@ -19901,42 +20518,48 @@ var SqliteInventoryRepository = class {
|
|
|
19901
20518
|
upsert(input, now = Date.now()) {
|
|
19902
20519
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19903
20520
|
const row = toInventoryRow(input, id, now);
|
|
19904
|
-
this.upsertStmt.run(
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
20521
|
+
this.upsertStmt.run(
|
|
20522
|
+
bindParams({
|
|
20523
|
+
id: row.id,
|
|
20524
|
+
objectType: row.objectType,
|
|
20525
|
+
location: row.location,
|
|
20526
|
+
title: row.title,
|
|
20527
|
+
hostId: row.hostId,
|
|
20528
|
+
attributes: row.attributes,
|
|
20529
|
+
firstSeen: row.firstSeen,
|
|
20530
|
+
lastSeen: row.lastSeen
|
|
20531
|
+
})
|
|
20532
|
+
);
|
|
19914
20533
|
return id;
|
|
19915
20534
|
}
|
|
19916
20535
|
// The full row, for round-trip assertions.
|
|
19917
20536
|
findById(id) {
|
|
19918
|
-
|
|
19919
|
-
return row;
|
|
20537
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19920
20538
|
}
|
|
19921
20539
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19922
20540
|
// served from the object_type index, never from audit_events.
|
|
19923
20541
|
distinctTitles(objectType) {
|
|
19924
|
-
const rows =
|
|
19925
|
-
|
|
20542
|
+
const rows = allRows(
|
|
20543
|
+
this.db.prepare(
|
|
20544
|
+
`SELECT DISTINCT title FROM inventory
|
|
19926
20545
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19927
20546
|
ORDER BY title`
|
|
19928
|
-
|
|
20547
|
+
),
|
|
20548
|
+
{ objectType }
|
|
20549
|
+
);
|
|
19929
20550
|
return rows.map((r) => r.title);
|
|
19930
20551
|
}
|
|
19931
20552
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19932
20553
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19933
20554
|
// QUERY PLAN).
|
|
19934
20555
|
osVersions() {
|
|
19935
|
-
const rows =
|
|
19936
|
-
|
|
20556
|
+
const rows = allRows(
|
|
20557
|
+
this.db.prepare(
|
|
20558
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19937
20559
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
19938
20560
|
ORDER BY value`
|
|
19939
|
-
|
|
20561
|
+
)
|
|
20562
|
+
);
|
|
19940
20563
|
return rows.map((r) => r.value);
|
|
19941
20564
|
}
|
|
19942
20565
|
};
|
|
@@ -19956,14 +20579,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
19956
20579
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
19957
20580
|
findingsCount: 0
|
|
19958
20581
|
};
|
|
19959
|
-
function safeJson(s, fallback) {
|
|
19960
|
-
if (s == null) return fallback;
|
|
19961
|
-
try {
|
|
19962
|
-
return JSON.parse(s);
|
|
19963
|
-
} catch {
|
|
19964
|
-
return fallback;
|
|
19965
|
-
}
|
|
19966
|
-
}
|
|
19967
20582
|
function resolveHarnessId(attrs, row) {
|
|
19968
20583
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
19969
20584
|
return attrs.provider;
|
|
@@ -20159,30 +20774,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20159
20774
|
configRowsCache;
|
|
20160
20775
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20161
20776
|
getInventoryStats() {
|
|
20162
|
-
const
|
|
20163
|
-
|
|
20164
|
-
|
|
20165
|
-
|
|
20166
|
-
byType
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
|
|
20777
|
+
const typeCounts = countBy(
|
|
20778
|
+
this.db,
|
|
20779
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20780
|
+
);
|
|
20781
|
+
const byType = {
|
|
20782
|
+
project: 0,
|
|
20783
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20784
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20785
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20786
|
+
config: typeCounts.get("config") ?? 0
|
|
20787
|
+
};
|
|
20788
|
+
byType.project = countScalar(
|
|
20789
|
+
this.db,
|
|
20790
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20791
|
+
);
|
|
20792
|
+
const mcpTrustCounts = countBy(
|
|
20793
|
+
this.db,
|
|
20794
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20170
20795
|
FROM inventory_asset a
|
|
20171
20796
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20172
20797
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20173
20798
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20174
|
-
)
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20799
|
+
);
|
|
20800
|
+
const mcpTrust = {
|
|
20801
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20802
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20803
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20804
|
+
};
|
|
20805
|
+
const harnesses = countScalar(
|
|
20806
|
+
this.db,
|
|
20178
20807
|
`SELECT count(*) AS n FROM inventory
|
|
20179
20808
|
WHERE object_type = 'harness'
|
|
20180
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
const
|
|
20809
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20810
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20811
|
+
);
|
|
20812
|
+
const flaggedAssets = countScalar(
|
|
20813
|
+
this.db,
|
|
20814
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20815
|
+
);
|
|
20816
|
+
const flaggedProjects = countScalar(
|
|
20817
|
+
this.db,
|
|
20184
20818
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20185
|
-
)
|
|
20819
|
+
);
|
|
20186
20820
|
const configRows = this.configAssetRows();
|
|
20187
20821
|
for (const r of configRows) {
|
|
20188
20822
|
byType[r.assetType] += 1;
|
|
@@ -20439,12 +21073,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20439
21073
|
}
|
|
20440
21074
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20441
21075
|
fetchHarnessRows() {
|
|
20442
|
-
return
|
|
20443
|
-
|
|
21076
|
+
return allRows(
|
|
21077
|
+
this.db.prepare(
|
|
21078
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20444
21079
|
FROM inventory
|
|
20445
21080
|
WHERE object_type = 'harness'
|
|
20446
21081
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20447
|
-
|
|
21082
|
+
),
|
|
21083
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
21084
|
+
);
|
|
20448
21085
|
}
|
|
20449
21086
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20450
21087
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20454,12 +21091,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20454
21091
|
const params = [...harnessInvIds];
|
|
20455
21092
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20456
21093
|
if (q) {
|
|
20457
|
-
const pat =
|
|
20458
|
-
where +=
|
|
21094
|
+
const pat = containsPattern(q);
|
|
21095
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20459
21096
|
params.push(pat, pat);
|
|
20460
21097
|
}
|
|
20461
|
-
const rows =
|
|
20462
|
-
|
|
21098
|
+
const rows = allRows(
|
|
21099
|
+
this.db.prepare(
|
|
21100
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20463
21101
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20464
21102
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20465
21103
|
FROM harness_asset ha
|
|
@@ -20467,7 +21105,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20467
21105
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20468
21106
|
WHERE ${where}
|
|
20469
21107
|
ORDER BY a.name ASC`
|
|
20470
|
-
|
|
21108
|
+
),
|
|
21109
|
+
params
|
|
21110
|
+
);
|
|
20471
21111
|
for (const raw of rows) {
|
|
20472
21112
|
const harnessInvId = raw.harnessInvId;
|
|
20473
21113
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20486,21 +21126,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20486
21126
|
params.push(...types);
|
|
20487
21127
|
}
|
|
20488
21128
|
if (q) {
|
|
20489
|
-
const pat =
|
|
20490
|
-
conditions.push("
|
|
21129
|
+
const pat = containsPattern(q);
|
|
21130
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20491
21131
|
params.push(pat, pat);
|
|
20492
21132
|
}
|
|
20493
21133
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20494
21134
|
const sampleRows = this.mapAssetRows(
|
|
20495
|
-
|
|
20496
|
-
|
|
21135
|
+
allRows(
|
|
21136
|
+
this.db.prepare(
|
|
21137
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20497
21138
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20498
21139
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20499
21140
|
FROM inventory_asset a
|
|
20500
21141
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20501
21142
|
${where}
|
|
20502
21143
|
ORDER BY a.name ASC`
|
|
20503
|
-
|
|
21144
|
+
),
|
|
21145
|
+
params
|
|
21146
|
+
)
|
|
20504
21147
|
);
|
|
20505
21148
|
const configRows = this.configAssetRows(q).filter(
|
|
20506
21149
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20509,14 +21152,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20509
21152
|
}
|
|
20510
21153
|
fetchAssetById(assetId) {
|
|
20511
21154
|
const rows = this.mapAssetRows(
|
|
20512
|
-
|
|
20513
|
-
|
|
21155
|
+
allRows(
|
|
21156
|
+
this.db.prepare(
|
|
21157
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20514
21158
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20515
21159
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20516
21160
|
FROM inventory_asset a
|
|
20517
21161
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20518
21162
|
WHERE a.id = ?`
|
|
20519
|
-
|
|
21163
|
+
),
|
|
21164
|
+
[assetId]
|
|
21165
|
+
)
|
|
20520
21166
|
);
|
|
20521
21167
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20522
21168
|
}
|
|
@@ -20572,37 +21218,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20572
21218
|
return rows;
|
|
20573
21219
|
}
|
|
20574
21220
|
latestConfigScanId() {
|
|
20575
|
-
|
|
20576
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20577
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20578
|
-
).get();
|
|
20579
|
-
return row?.id ?? null;
|
|
21221
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20580
21222
|
}
|
|
20581
21223
|
fetchProjects(q) {
|
|
20582
21224
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20583
21225
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20584
21226
|
const params = [];
|
|
20585
21227
|
if (q) {
|
|
20586
|
-
const pat =
|
|
20587
|
-
sql +=
|
|
21228
|
+
const pat = containsPattern(q);
|
|
21229
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20588
21230
|
params.push(pat, pat);
|
|
20589
21231
|
}
|
|
20590
21232
|
sql += " ORDER BY name ASC";
|
|
20591
|
-
return this.db.prepare(sql)
|
|
21233
|
+
return allRows(this.db.prepare(sql), params);
|
|
20592
21234
|
}
|
|
20593
21235
|
fetchProjectById(projectId) {
|
|
20594
|
-
return
|
|
20595
|
-
|
|
20596
|
-
|
|
21236
|
+
return getRow(
|
|
21237
|
+
this.db.prepare(
|
|
21238
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21239
|
+
),
|
|
21240
|
+
[projectId]
|
|
21241
|
+
) ?? null;
|
|
20597
21242
|
}
|
|
20598
21243
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20599
21244
|
fetchProjectsByIds(projectIds) {
|
|
20600
21245
|
const map2 = /* @__PURE__ */ new Map();
|
|
20601
21246
|
if (projectIds.length === 0) return map2;
|
|
20602
|
-
const rows =
|
|
20603
|
-
|
|
21247
|
+
const rows = allRows(
|
|
21248
|
+
this.db.prepare(
|
|
21249
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20604
21250
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20605
|
-
|
|
21251
|
+
),
|
|
21252
|
+
projectIds
|
|
21253
|
+
);
|
|
20606
21254
|
for (const r of rows) map2.set(r.id, r);
|
|
20607
21255
|
return map2;
|
|
20608
21256
|
}
|
|
@@ -20613,8 +21261,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20613
21261
|
projectAggregates(projectIds) {
|
|
20614
21262
|
const map2 = /* @__PURE__ */ new Map();
|
|
20615
21263
|
if (projectIds.length === 0) return map2;
|
|
20616
|
-
const rows =
|
|
20617
|
-
|
|
21264
|
+
const rows = allRows(
|
|
21265
|
+
this.db.prepare(
|
|
21266
|
+
`SELECT f.project_id AS projectId,
|
|
20618
21267
|
coalesce(o.access, f.default_access) AS eff,
|
|
20619
21268
|
count(*) AS n,
|
|
20620
21269
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20622,7 +21271,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20622
21271
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20623
21272
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20624
21273
|
GROUP BY f.project_id, eff`
|
|
20625
|
-
|
|
21274
|
+
),
|
|
21275
|
+
projectIds
|
|
21276
|
+
);
|
|
20626
21277
|
for (const r of rows) {
|
|
20627
21278
|
let agg = map2.get(r.projectId);
|
|
20628
21279
|
if (!agg) {
|
|
@@ -20660,37 +21311,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20660
21311
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20661
21312
|
if (prefix === "") {
|
|
20662
21313
|
return this.mapFileRows(
|
|
20663
|
-
|
|
21314
|
+
allRows(
|
|
21315
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21316
|
+
[projectId]
|
|
21317
|
+
)
|
|
20664
21318
|
);
|
|
20665
21319
|
}
|
|
20666
21320
|
return this.mapFileRows(
|
|
20667
|
-
|
|
20668
|
-
this.
|
|
20669
|
-
|
|
21321
|
+
allRows(
|
|
21322
|
+
this.db.prepare(
|
|
21323
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21324
|
+
),
|
|
21325
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21326
|
+
)
|
|
20670
21327
|
);
|
|
20671
21328
|
}
|
|
20672
21329
|
fetchProjectFilesSearch(projectId, q) {
|
|
20673
|
-
const pat =
|
|
21330
|
+
const pat = containsPattern(q);
|
|
20674
21331
|
return this.mapFileRows(
|
|
20675
|
-
|
|
20676
|
-
this.
|
|
20677
|
-
|
|
20678
|
-
|
|
20679
|
-
|
|
21332
|
+
allRows(
|
|
21333
|
+
this.db.prepare(
|
|
21334
|
+
this.fileSelect(
|
|
21335
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21336
|
+
)
|
|
21337
|
+
),
|
|
21338
|
+
[projectId, pat, pat]
|
|
21339
|
+
)
|
|
20680
21340
|
);
|
|
20681
21341
|
}
|
|
20682
21342
|
fetchProjectFilesBlocked(projectId) {
|
|
20683
21343
|
return this.mapFileRows(
|
|
20684
|
-
|
|
20685
|
-
this.
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
21344
|
+
allRows(
|
|
21345
|
+
this.db.prepare(
|
|
21346
|
+
this.fileSelect(
|
|
21347
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21348
|
+
)
|
|
21349
|
+
),
|
|
21350
|
+
[projectId]
|
|
21351
|
+
)
|
|
20689
21352
|
);
|
|
20690
21353
|
}
|
|
20691
21354
|
fetchProjectFile(projectId, path) {
|
|
20692
21355
|
const rows = this.mapFileRows(
|
|
20693
|
-
|
|
21356
|
+
allRows(
|
|
21357
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21358
|
+
[projectId, path]
|
|
21359
|
+
)
|
|
20694
21360
|
);
|
|
20695
21361
|
return rows[0] ?? null;
|
|
20696
21362
|
}
|
|
@@ -20704,39 +21370,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20704
21370
|
}
|
|
20705
21371
|
db;
|
|
20706
21372
|
readPolicies() {
|
|
20707
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20708
|
-
const policies =
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
customKeywords
|
|
20721
|
-
})
|
|
20722
|
-
);
|
|
20723
|
-
} catch {
|
|
20724
|
-
}
|
|
20725
|
-
}
|
|
21373
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21374
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21375
|
+
const target = JSON.parse(row.target);
|
|
21376
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21377
|
+
return Policy.parse({
|
|
21378
|
+
id: row.id,
|
|
21379
|
+
scope: row.scope,
|
|
21380
|
+
target,
|
|
21381
|
+
action: row.action,
|
|
21382
|
+
enabled: intToBool(row.enabled),
|
|
21383
|
+
customKeywords
|
|
21384
|
+
});
|
|
21385
|
+
});
|
|
20726
21386
|
return Promise.resolve(policies);
|
|
20727
21387
|
}
|
|
20728
21388
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20729
21389
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20730
21390
|
// so a user's edits are never clobbered.
|
|
20731
21391
|
seedDefaults() {
|
|
20732
|
-
const count = this.db
|
|
21392
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20733
21393
|
if (count > 0) return;
|
|
20734
21394
|
const stmt = this.db.prepare(
|
|
20735
21395
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20736
21396
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20737
21397
|
);
|
|
20738
|
-
this.db
|
|
20739
|
-
try {
|
|
21398
|
+
failOpenTransaction(this.db, () => {
|
|
20740
21399
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20741
21400
|
stmt.run({
|
|
20742
21401
|
id: randomUUID4(),
|
|
@@ -20745,10 +21404,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20745
21404
|
now: Date.now()
|
|
20746
21405
|
});
|
|
20747
21406
|
}
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
20751
|
-
|
|
21407
|
+
});
|
|
21408
|
+
}
|
|
21409
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21410
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21411
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21412
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21413
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21414
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21415
|
+
upsertCategoryAction(category, action) {
|
|
21416
|
+
const now = Date.now();
|
|
21417
|
+
this.db.prepare(
|
|
21418
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21419
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21420
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21421
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21422
|
+
}
|
|
21423
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21424
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21425
|
+
// Returns the number of rows changed.
|
|
21426
|
+
capCategoryActions() {
|
|
21427
|
+
const info = this.db.prepare(
|
|
21428
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21429
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21430
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21431
|
+
).run({ now: Date.now() });
|
|
21432
|
+
return Number(info.changes);
|
|
21433
|
+
}
|
|
21434
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21435
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21436
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21437
|
+
getCategoryAction(category) {
|
|
21438
|
+
const row = this.db.prepare(
|
|
21439
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21440
|
+
).get({ category });
|
|
21441
|
+
return row?.action;
|
|
20752
21442
|
}
|
|
20753
21443
|
};
|
|
20754
21444
|
|
|
@@ -20824,7 +21514,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20824
21514
|
maxStampStmt;
|
|
20825
21515
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20826
21516
|
replaceForProject(projectId, scan2, now) {
|
|
20827
|
-
const
|
|
21517
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20828
21518
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20829
21519
|
for (const file2 of scan2.files) {
|
|
20830
21520
|
this.upsertStmt.run({
|
|
@@ -20907,7 +21597,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20907
21597
|
}
|
|
20908
21598
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20909
21599
|
latestByKey(key) {
|
|
20910
|
-
const row = this.latestStmt
|
|
21600
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20911
21601
|
if (!row) return void 0;
|
|
20912
21602
|
return {
|
|
20913
21603
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20924,7 +21614,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20924
21614
|
* the CLI) surfaces for that file.
|
|
20925
21615
|
*/
|
|
20926
21616
|
openAtRestKeysForPath(path) {
|
|
20927
|
-
const rows = this.openAtRestStmt
|
|
21617
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20928
21618
|
return rows.map((r) => r.finding_key);
|
|
20929
21619
|
}
|
|
20930
21620
|
/**
|
|
@@ -20935,7 +21625,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20935
21625
|
* resolution row (see scan.ts).
|
|
20936
21626
|
*/
|
|
20937
21627
|
resolvedAtRestKeysForPath(path) {
|
|
20938
|
-
const rows = this.resolvedAtRestStmt
|
|
21628
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
20939
21629
|
return rows.map((r) => r.finding_key);
|
|
20940
21630
|
}
|
|
20941
21631
|
};
|
|
@@ -20964,38 +21654,31 @@ var SqliteScanLedgerRepository = class {
|
|
|
20964
21654
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
20965
21655
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
20966
21656
|
entriesForRuleset(rulesetHash) {
|
|
20967
|
-
const rows = this.readStmt
|
|
21657
|
+
const rows = allRows(this.readStmt, {
|
|
21658
|
+
rulesetHash
|
|
21659
|
+
});
|
|
20968
21660
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
20969
21661
|
}
|
|
20970
21662
|
upsertEntries(entries) {
|
|
20971
21663
|
if (entries.length === 0) return;
|
|
20972
21664
|
const scannedAt = Date.now();
|
|
20973
|
-
|
|
20974
|
-
|
|
20975
|
-
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
scannedAt
|
|
20983
|
-
});
|
|
20984
|
-
}
|
|
20985
|
-
this.db.exec("COMMIT");
|
|
20986
|
-
} catch (err) {
|
|
20987
|
-
this.db.exec("ROLLBACK");
|
|
20988
|
-
throw err;
|
|
21665
|
+
failOpenTransaction(this.db, () => {
|
|
21666
|
+
for (const entry of entries) {
|
|
21667
|
+
this.upsertStmt.run({
|
|
21668
|
+
path: entry.path,
|
|
21669
|
+
mtime: entry.mtime,
|
|
21670
|
+
contentHash: entry.contentHash,
|
|
21671
|
+
rulesetHash: entry.rulesetHash,
|
|
21672
|
+
scannedAt
|
|
21673
|
+
});
|
|
20989
21674
|
}
|
|
20990
|
-
}
|
|
20991
|
-
}
|
|
21675
|
+
});
|
|
20992
21676
|
}
|
|
20993
21677
|
};
|
|
20994
21678
|
|
|
20995
21679
|
// ../../packages/persistence/src/repositories/security.ts
|
|
20996
21680
|
var DAY_MS4 = 864e5;
|
|
20997
21681
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
20998
|
-
var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
|
|
20999
21682
|
var ACTION_TO_KIND = {
|
|
21000
21683
|
block: "blocked",
|
|
21001
21684
|
redact: "redacted",
|
|
@@ -21010,8 +21693,14 @@ var SCAN_COVERAGE = [
|
|
|
21010
21693
|
{ provider: "copilot", coverage: 0, supported: false },
|
|
21011
21694
|
{ provider: "api", coverage: 0, supported: false }
|
|
21012
21695
|
];
|
|
21696
|
+
var GRANULARITY = {
|
|
21697
|
+
"7d": "day",
|
|
21698
|
+
"30d": "day",
|
|
21699
|
+
"3m": "week",
|
|
21700
|
+
"6m": "week"
|
|
21701
|
+
};
|
|
21013
21702
|
function granularityFor(range) {
|
|
21014
|
-
return range
|
|
21703
|
+
return GRANULARITY[range];
|
|
21015
21704
|
}
|
|
21016
21705
|
function startOfUtcDay2(ms) {
|
|
21017
21706
|
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
@@ -21065,8 +21754,9 @@ var SqliteSecurityRepository = class {
|
|
|
21065
21754
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21066
21755
|
// double-counting a key that accumulated several append-only rows.
|
|
21067
21756
|
severitySummary() {
|
|
21068
|
-
const rows =
|
|
21069
|
-
|
|
21757
|
+
const rows = allRows(
|
|
21758
|
+
this.db.prepare(
|
|
21759
|
+
`SELECT f.severity AS severity,
|
|
21070
21760
|
COUNT(*) AS count,
|
|
21071
21761
|
SUM(CASE
|
|
21072
21762
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21085,7 +21775,8 @@ var SqliteSecurityRepository = class {
|
|
|
21085
21775
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21086
21776
|
ON latest.finding_key = f.finding_key
|
|
21087
21777
|
GROUP BY f.severity`
|
|
21088
|
-
|
|
21778
|
+
)
|
|
21779
|
+
);
|
|
21089
21780
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21090
21781
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21091
21782
|
severity,
|
|
@@ -21169,14 +21860,15 @@ var SqliteSecurityRepository = class {
|
|
|
21169
21860
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21170
21861
|
const now = this.now();
|
|
21171
21862
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21172
|
-
const rows =
|
|
21173
|
-
|
|
21174
|
-
|
|
21175
|
-
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21863
|
+
const rows = allRows(
|
|
21864
|
+
this.db.prepare(
|
|
21865
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21866
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21867
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21868
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21869
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21870
|
+
// backfill left null.
|
|
21871
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21180
21872
|
(
|
|
21181
21873
|
SELECT fr.status FROM finding_resolution fr
|
|
21182
21874
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21202,14 +21894,16 @@ var SqliteSecurityRepository = class {
|
|
|
21202
21894
|
WHERE fr.finding_key = f.finding_key
|
|
21203
21895
|
AND fr.resolved_at >= :windowStart
|
|
21204
21896
|
)`
|
|
21205
|
-
|
|
21206
|
-
|
|
21207
|
-
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21897
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21898
|
+
// any resolution activity at/after the window start — a row this method
|
|
21899
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21900
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21901
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21902
|
+
// dialect-agnostic. Without this, a
|
|
21903
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21904
|
+
),
|
|
21905
|
+
{ windowStart }
|
|
21906
|
+
);
|
|
21213
21907
|
const sums = /* @__PURE__ */ new Map();
|
|
21214
21908
|
const counts = /* @__PURE__ */ new Map();
|
|
21215
21909
|
for (const r of rows) {
|
|
@@ -21239,8 +21933,9 @@ var SqliteSecurityRepository = class {
|
|
|
21239
21933
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21240
21934
|
const now = this.now();
|
|
21241
21935
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21242
|
-
const rows =
|
|
21243
|
-
|
|
21936
|
+
const rows = allRows(
|
|
21937
|
+
this.db.prepare(
|
|
21938
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21244
21939
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21245
21940
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21246
21941
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21248,7 +21943,9 @@ var SqliteSecurityRepository = class {
|
|
|
21248
21943
|
GROUP BY repo
|
|
21249
21944
|
ORDER BY c DESC, repo
|
|
21250
21945
|
LIMIT :limit`
|
|
21251
|
-
|
|
21946
|
+
),
|
|
21947
|
+
{ from, to: now, limit }
|
|
21948
|
+
);
|
|
21252
21949
|
const items = rows.map((r) => ({
|
|
21253
21950
|
id: `repo_${r.repo}`,
|
|
21254
21951
|
name: r.repo,
|
|
@@ -21270,8 +21967,9 @@ var SqliteSecurityRepository = class {
|
|
|
21270
21967
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21271
21968
|
// capped at `limit`.
|
|
21272
21969
|
recentlyResolved(limit = 20) {
|
|
21273
|
-
const rows =
|
|
21274
|
-
|
|
21970
|
+
const rows = allRows(
|
|
21971
|
+
this.db.prepare(
|
|
21972
|
+
`SELECT f.finding_key AS finding_key,
|
|
21275
21973
|
f.rule_id AS rule_id,
|
|
21276
21974
|
f.severity AS severity,
|
|
21277
21975
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21305,7 +22003,9 @@ var SqliteSecurityRepository = class {
|
|
|
21305
22003
|
) IS NOT NULL
|
|
21306
22004
|
ORDER BY latest_resolved_at DESC
|
|
21307
22005
|
LIMIT :limit`
|
|
21308
|
-
|
|
22006
|
+
),
|
|
22007
|
+
{ limit }
|
|
22008
|
+
);
|
|
21309
22009
|
const items = rows.map((r) => ({
|
|
21310
22010
|
findingKey: r.finding_key,
|
|
21311
22011
|
ruleId: r.rule_id,
|
|
@@ -21322,12 +22022,15 @@ var SqliteSecurityRepository = class {
|
|
|
21322
22022
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21323
22023
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21324
22024
|
findingsInRange(fromMs, toMs) {
|
|
21325
|
-
const rows =
|
|
21326
|
-
|
|
22025
|
+
const rows = allRows(
|
|
22026
|
+
this.db.prepare(
|
|
22027
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21327
22028
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21328
22029
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21329
22030
|
ORDER BY e.occurred_at`
|
|
21330
|
-
|
|
22031
|
+
),
|
|
22032
|
+
{ from: fromMs, to: toMs }
|
|
22033
|
+
);
|
|
21331
22034
|
return rows.map((r) => ({
|
|
21332
22035
|
occurredAt: r.occurred_at,
|
|
21333
22036
|
severity: r.severity,
|
|
@@ -21341,12 +22044,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21341
22044
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21342
22045
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21343
22046
|
function parseNetwork(networkJson) {
|
|
21344
|
-
|
|
21345
|
-
try {
|
|
21346
|
-
return JSON.parse(networkJson);
|
|
21347
|
-
} catch {
|
|
21348
|
-
return null;
|
|
21349
|
-
}
|
|
22047
|
+
return safeJson(networkJson, null);
|
|
21350
22048
|
}
|
|
21351
22049
|
function toEndpointSummary(row) {
|
|
21352
22050
|
return {
|
|
@@ -21433,27 +22131,39 @@ var SqliteSharesRepository = class {
|
|
|
21433
22131
|
}
|
|
21434
22132
|
db;
|
|
21435
22133
|
stats() {
|
|
21436
|
-
const
|
|
21437
|
-
const
|
|
21438
|
-
const
|
|
21439
|
-
const
|
|
21440
|
-
|
|
22134
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
22135
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
22136
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22137
|
+
const insecure = countScalar(
|
|
22138
|
+
this.db,
|
|
21441
22139
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21442
22140
|
);
|
|
21443
|
-
const needsReview =
|
|
22141
|
+
const needsReview = countScalar(
|
|
22142
|
+
this.db,
|
|
21444
22143
|
`SELECT count(DISTINCT d.id) AS n
|
|
21445
22144
|
FROM share_destination d
|
|
21446
22145
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21447
22146
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21448
22147
|
);
|
|
21449
|
-
const
|
|
21450
|
-
|
|
21451
|
-
|
|
21452
|
-
|
|
21453
|
-
const
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
|
|
22148
|
+
const kindCounts = countBy(
|
|
22149
|
+
this.db,
|
|
22150
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
22151
|
+
);
|
|
22152
|
+
const byKind = {
|
|
22153
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
22154
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
22155
|
+
ip: kindCounts.get("ip") ?? 0
|
|
22156
|
+
};
|
|
22157
|
+
const trustCounts = countBy(
|
|
22158
|
+
this.db,
|
|
22159
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
22160
|
+
);
|
|
22161
|
+
const byTrust = {
|
|
22162
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
22163
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
22164
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
22165
|
+
ip: trustCounts.get("ip") ?? 0
|
|
22166
|
+
};
|
|
21457
22167
|
return Promise.resolve({
|
|
21458
22168
|
destinations,
|
|
21459
22169
|
endpoints,
|
|
@@ -21569,7 +22279,7 @@ var SqliteSharesRepository = class {
|
|
|
21569
22279
|
}
|
|
21570
22280
|
let sql;
|
|
21571
22281
|
if (q) {
|
|
21572
|
-
const pattern =
|
|
22282
|
+
const pattern = containsPattern(q);
|
|
21573
22283
|
conditions.push(
|
|
21574
22284
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21575
22285
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21589,24 +22299,31 @@ var SqliteSharesRepository = class {
|
|
|
21589
22299
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21590
22300
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21591
22301
|
}
|
|
21592
|
-
const rows =
|
|
22302
|
+
const rows = allRows(
|
|
22303
|
+
this.db.prepare(sql),
|
|
22304
|
+
params
|
|
22305
|
+
);
|
|
21593
22306
|
return rows.map((r) => this.mapDestRow(r));
|
|
21594
22307
|
}
|
|
21595
22308
|
fetchDestinationById(destinationId) {
|
|
21596
|
-
const row =
|
|
21597
|
-
|
|
22309
|
+
const row = getRow(
|
|
22310
|
+
this.db.prepare(
|
|
22311
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21598
22312
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21599
22313
|
o.decision AS overrideDecision
|
|
21600
22314
|
FROM share_destination d
|
|
21601
22315
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21602
22316
|
WHERE d.id = ?`
|
|
21603
|
-
|
|
22317
|
+
),
|
|
22318
|
+
[destinationId]
|
|
22319
|
+
);
|
|
21604
22320
|
return row ? this.mapDestRow(row) : null;
|
|
21605
22321
|
}
|
|
21606
22322
|
fetchEndpoints(destinationIds) {
|
|
21607
22323
|
if (destinationIds.length === 0) return [];
|
|
21608
|
-
const rows =
|
|
21609
|
-
|
|
22324
|
+
const rows = allRows(
|
|
22325
|
+
this.db.prepare(
|
|
22326
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21610
22327
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21611
22328
|
count(c.id) AS callSiteCount
|
|
21612
22329
|
FROM share_endpoint e
|
|
@@ -21614,7 +22331,9 @@ var SqliteSharesRepository = class {
|
|
|
21614
22331
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21615
22332
|
GROUP BY e.id
|
|
21616
22333
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21617
|
-
|
|
22334
|
+
),
|
|
22335
|
+
destinationIds
|
|
22336
|
+
);
|
|
21618
22337
|
return rows.map((r) => ({
|
|
21619
22338
|
id: r.id,
|
|
21620
22339
|
destinationId: r.destinationId,
|
|
@@ -21639,13 +22358,16 @@ var SqliteSharesRepository = class {
|
|
|
21639
22358
|
}
|
|
21640
22359
|
fetchCallSites(endpointIds) {
|
|
21641
22360
|
if (endpointIds.length === 0) return [];
|
|
21642
|
-
const rows =
|
|
21643
|
-
|
|
22361
|
+
const rows = allRows(
|
|
22362
|
+
this.db.prepare(
|
|
22363
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21644
22364
|
project_id AS projectId
|
|
21645
22365
|
FROM share_call_site
|
|
21646
22366
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21647
22367
|
ORDER BY created_at ASC, id ASC`
|
|
21648
|
-
|
|
22368
|
+
),
|
|
22369
|
+
endpointIds
|
|
22370
|
+
);
|
|
21649
22371
|
return rows.map((r) => ({
|
|
21650
22372
|
id: r.id,
|
|
21651
22373
|
endpointId: r.endpointId,
|
|
@@ -21681,27 +22403,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21681
22403
|
upsert(input, now = Date.now()) {
|
|
21682
22404
|
const id = sourceProjectId(input.url);
|
|
21683
22405
|
const row = toSourceProjectRow(input, id, now);
|
|
21684
|
-
this.upsertStmt.run(
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
22406
|
+
this.upsertStmt.run(
|
|
22407
|
+
bindParams({
|
|
22408
|
+
id: row.id,
|
|
22409
|
+
url: row.url,
|
|
22410
|
+
name: row.name,
|
|
22411
|
+
attributes: row.attributes,
|
|
22412
|
+
firstSeen: row.firstSeen,
|
|
22413
|
+
lastSeen: row.lastSeen
|
|
22414
|
+
})
|
|
22415
|
+
);
|
|
21692
22416
|
return id;
|
|
21693
22417
|
}
|
|
21694
22418
|
findById(id) {
|
|
21695
|
-
return
|
|
22419
|
+
return getRow(
|
|
22420
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22421
|
+
{
|
|
22422
|
+
id
|
|
22423
|
+
}
|
|
22424
|
+
);
|
|
21696
22425
|
}
|
|
21697
22426
|
// Distinct project names — a filter facet, served from the source_project
|
|
21698
22427
|
// table, never from the audit fact table.
|
|
21699
22428
|
distinctNames() {
|
|
21700
|
-
const rows =
|
|
21701
|
-
|
|
22429
|
+
const rows = allRows(
|
|
22430
|
+
this.db.prepare(
|
|
22431
|
+
`SELECT DISTINCT name FROM source_project
|
|
21702
22432
|
WHERE name IS NOT NULL
|
|
21703
22433
|
ORDER BY name`
|
|
21704
|
-
|
|
22434
|
+
)
|
|
22435
|
+
);
|
|
21705
22436
|
return rows.map((r) => r.name);
|
|
21706
22437
|
}
|
|
21707
22438
|
};
|
|
@@ -21720,8 +22451,7 @@ function hasLegacySampleRows(db) {
|
|
|
21720
22451
|
function purgeSampleData(db) {
|
|
21721
22452
|
try {
|
|
21722
22453
|
if (!hasLegacySampleRows(db)) return;
|
|
21723
|
-
db
|
|
21724
|
-
try {
|
|
22454
|
+
withTransaction(db, () => {
|
|
21725
22455
|
db.exec(
|
|
21726
22456
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21727
22457
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21766,11 +22496,7 @@ function purgeSampleData(db) {
|
|
|
21766
22496
|
value TEXT NOT NULL
|
|
21767
22497
|
)`);
|
|
21768
22498
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21769
|
-
|
|
21770
|
-
} catch (err) {
|
|
21771
|
-
db.exec("ROLLBACK");
|
|
21772
|
-
throw err;
|
|
21773
|
-
}
|
|
22499
|
+
});
|
|
21774
22500
|
} catch {
|
|
21775
22501
|
}
|
|
21776
22502
|
}
|
|
@@ -21789,7 +22515,7 @@ function openWithPragmas(file2) {
|
|
|
21789
22515
|
function backupLegacyStore(file2) {
|
|
21790
22516
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21791
22517
|
renameSync(file2, backup);
|
|
21792
|
-
for (const sidecar of
|
|
22518
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21793
22519
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21794
22520
|
}
|
|
21795
22521
|
return backup;
|
|
@@ -21802,9 +22528,8 @@ function openLocalDatabase(dir) {
|
|
|
21802
22528
|
db.close();
|
|
21803
22529
|
const backup = backupLegacyStore(file2);
|
|
21804
22530
|
db = openWithPragmas(file2);
|
|
21805
|
-
|
|
21806
|
-
`
|
|
21807
|
-
`
|
|
22531
|
+
akaWarn(
|
|
22532
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21808
22533
|
);
|
|
21809
22534
|
}
|
|
21810
22535
|
applyMigrations(db);
|
|
@@ -21832,96 +22557,77 @@ function openLocalDatabase(dir) {
|
|
|
21832
22557
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21833
22558
|
policies.seedDefaults();
|
|
21834
22559
|
function recordCapture(event, detected) {
|
|
21835
|
-
|
|
21836
|
-
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21841
|
-
db.exec("COMMIT");
|
|
21842
|
-
} catch (err) {
|
|
21843
|
-
db.exec("ROLLBACK");
|
|
21844
|
-
throw err;
|
|
21845
|
-
}
|
|
21846
|
-
} catch {
|
|
21847
|
-
}
|
|
22560
|
+
failOpenTransaction(db, () => {
|
|
22561
|
+
events.insertEvent(event);
|
|
22562
|
+
const sessionId = event.metadata?.sessionId;
|
|
22563
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22564
|
+
});
|
|
21848
22565
|
}
|
|
21849
22566
|
function ensureInventory(ctx) {
|
|
21850
22567
|
const resolved = {};
|
|
21851
|
-
|
|
21852
|
-
|
|
21853
|
-
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
db.exec("COMMIT");
|
|
21872
|
-
} catch (err) {
|
|
21873
|
-
db.exec("ROLLBACK");
|
|
21874
|
-
throw err;
|
|
21875
|
-
}
|
|
21876
|
-
} catch {
|
|
21877
|
-
return {};
|
|
21878
|
-
}
|
|
21879
|
-
return resolved;
|
|
22568
|
+
const committed = failOpenTransaction(db, () => {
|
|
22569
|
+
const now = Date.now();
|
|
22570
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22571
|
+
if (ctx.harness) {
|
|
22572
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22573
|
+
}
|
|
22574
|
+
resolved.accountId = inventory.upsert(
|
|
22575
|
+
linkHost(
|
|
22576
|
+
{
|
|
22577
|
+
objectType: "user",
|
|
22578
|
+
identityKey: "local",
|
|
22579
|
+
attributes: { source: "local" }
|
|
22580
|
+
},
|
|
22581
|
+
resolved.hostId
|
|
22582
|
+
),
|
|
22583
|
+
now
|
|
22584
|
+
);
|
|
22585
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22586
|
+
});
|
|
22587
|
+
return committed ? resolved : {};
|
|
21880
22588
|
}
|
|
21881
22589
|
function recordConfigScan(record2) {
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
}
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
confidence: finding.confidence
|
|
21903
|
-
});
|
|
21904
|
-
}
|
|
21905
|
-
db.exec("COMMIT");
|
|
21906
|
-
} catch (err) {
|
|
21907
|
-
db.exec("ROLLBACK");
|
|
21908
|
-
throw err;
|
|
22590
|
+
failOpenTransaction(db, () => {
|
|
22591
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22592
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22593
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22594
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22595
|
+
for (const def of record2.definitions ?? []) {
|
|
22596
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22597
|
+
}
|
|
22598
|
+
for (const finding of record2.findings ?? []) {
|
|
22599
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22600
|
+
if (!definitionId) continue;
|
|
22601
|
+
inspectionFindings.insertFinding({
|
|
22602
|
+
id: randomUUID8(),
|
|
22603
|
+
auditEventId: record2.scanEvent.id,
|
|
22604
|
+
inspectionDefinitionId: definitionId,
|
|
22605
|
+
span: finding.span,
|
|
22606
|
+
maskedMatch: finding.maskedMatch,
|
|
22607
|
+
actionTaken: finding.actionTaken,
|
|
22608
|
+
confidence: finding.confidence
|
|
22609
|
+
});
|
|
21909
22610
|
}
|
|
21910
|
-
}
|
|
21911
|
-
}
|
|
22611
|
+
});
|
|
21912
22612
|
}
|
|
21913
22613
|
function recordProjectFiles(projectId, scan2) {
|
|
21914
22614
|
if (scan2.files.length === 0) return;
|
|
22615
|
+
failOpenTransaction(db, () => {
|
|
22616
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22617
|
+
});
|
|
22618
|
+
}
|
|
22619
|
+
async function transaction(fn) {
|
|
22620
|
+
db.exec("BEGIN");
|
|
21915
22621
|
try {
|
|
21916
|
-
|
|
22622
|
+
const result = await fn();
|
|
22623
|
+
db.exec("COMMIT");
|
|
22624
|
+
return result;
|
|
22625
|
+
} catch (err) {
|
|
21917
22626
|
try {
|
|
21918
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21919
|
-
db.exec("COMMIT");
|
|
21920
|
-
} catch (err) {
|
|
21921
22627
|
db.exec("ROLLBACK");
|
|
21922
|
-
|
|
22628
|
+
} catch {
|
|
21923
22629
|
}
|
|
21924
|
-
|
|
22630
|
+
throw err;
|
|
21925
22631
|
}
|
|
21926
22632
|
}
|
|
21927
22633
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -21940,8 +22646,7 @@ function openLocalDatabase(dir) {
|
|
|
21940
22646
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
21941
22647
|
});
|
|
21942
22648
|
if (stale.length === 0) return;
|
|
21943
|
-
db
|
|
21944
|
-
try {
|
|
22649
|
+
withTransaction(db, () => {
|
|
21945
22650
|
for (const { id } of stale) {
|
|
21946
22651
|
db.prepare(
|
|
21947
22652
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -21953,11 +22658,7 @@ function openLocalDatabase(dir) {
|
|
|
21953
22658
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
21954
22659
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
21955
22660
|
}
|
|
21956
|
-
|
|
21957
|
-
} catch (err) {
|
|
21958
|
-
db.exec("ROLLBACK");
|
|
21959
|
-
throw err;
|
|
21960
|
-
}
|
|
22661
|
+
});
|
|
21961
22662
|
} catch {
|
|
21962
22663
|
}
|
|
21963
22664
|
}
|
|
@@ -21999,6 +22700,7 @@ function openLocalDatabase(dir) {
|
|
|
21999
22700
|
purgeSampleData: () => {
|
|
22000
22701
|
purgeSampleData(db);
|
|
22001
22702
|
},
|
|
22703
|
+
transaction,
|
|
22002
22704
|
close: () => {
|
|
22003
22705
|
db.close();
|
|
22004
22706
|
}
|
|
@@ -22119,12 +22821,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22119
22821
|
}
|
|
22120
22822
|
}
|
|
22121
22823
|
function readJson(file2) {
|
|
22824
|
+
let text;
|
|
22122
22825
|
try {
|
|
22123
|
-
|
|
22124
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22826
|
+
text = readFileSync2(file2, "utf8");
|
|
22125
22827
|
} catch {
|
|
22126
22828
|
return null;
|
|
22127
22829
|
}
|
|
22830
|
+
return parseJsonObject(text) ?? null;
|
|
22831
|
+
}
|
|
22832
|
+
|
|
22833
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22834
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22835
|
+
import { join as join5 } from "path";
|
|
22836
|
+
var MARKER = "warn-era-capped";
|
|
22837
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22838
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22839
|
+
const marker = join5(dataDir2, MARKER);
|
|
22840
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22841
|
+
const capped = db.policies.capCategoryActions();
|
|
22842
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22843
|
+
`, { mode: DATA_FILE_MODE });
|
|
22844
|
+
return { capped };
|
|
22128
22845
|
}
|
|
22129
22846
|
|
|
22130
22847
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22199,23 +22916,30 @@ function resolveProviderSafe() {
|
|
|
22199
22916
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22200
22917
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22201
22918
|
import { homedir as homedir2 } from "os";
|
|
22202
|
-
import { basename as basename2, join as
|
|
22919
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22920
|
+
|
|
22921
|
+
// ../../packages/detections/src/escape-regexp.ts
|
|
22922
|
+
function escapeRegExp(value) {
|
|
22923
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22924
|
+
}
|
|
22925
|
+
|
|
22926
|
+
// ../../packages/detections/src/matchers/limits.ts
|
|
22927
|
+
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22203
22928
|
|
|
22204
22929
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22205
22930
|
var KeywordMatcher2 = class {
|
|
22206
22931
|
match(text, rule) {
|
|
22207
22932
|
if (rule.matcher.type !== "keyword") return [];
|
|
22208
22933
|
const { keywords, caseSensitive } = rule.matcher;
|
|
22209
|
-
const haystack = caseSensitive ? text : text.toLowerCase();
|
|
22210
22934
|
const spans = [];
|
|
22211
22935
|
for (const kw of keywords) {
|
|
22212
|
-
|
|
22213
|
-
|
|
22214
|
-
|
|
22215
|
-
|
|
22216
|
-
|
|
22217
|
-
spans.push({ start:
|
|
22218
|
-
|
|
22936
|
+
if (kw.length === 0) continue;
|
|
22937
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22938
|
+
const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
|
|
22939
|
+
let m;
|
|
22940
|
+
while ((m = re.exec(text)) !== null) {
|
|
22941
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
22942
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22219
22943
|
}
|
|
22220
22944
|
}
|
|
22221
22945
|
return spans;
|
|
@@ -22223,7 +22947,6 @@ var KeywordMatcher2 = class {
|
|
|
22223
22947
|
};
|
|
22224
22948
|
|
|
22225
22949
|
// ../../packages/detections/src/matchers/regex.ts
|
|
22226
|
-
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22227
22950
|
var RegexMatcher2 = class {
|
|
22228
22951
|
match(text, rule) {
|
|
22229
22952
|
if (rule.matcher.type !== "regex") return [];
|
|
@@ -22309,9 +23032,6 @@ function registerPack(pack) {
|
|
|
22309
23032
|
function getLoadedRules() {
|
|
22310
23033
|
return [...packs.values()].flatMap((p) => p.rules);
|
|
22311
23034
|
}
|
|
22312
|
-
function escapeRegExp(value) {
|
|
22313
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22314
|
-
}
|
|
22315
23035
|
function isCorroborated(candidate, candidates, text) {
|
|
22316
23036
|
const req = candidate.rule.requiresNearby;
|
|
22317
23037
|
if (!req) return true;
|
|
@@ -23093,7 +23813,6 @@ var db_table_name_default = {
|
|
|
23093
23813
|
"SELECT * FROM ",
|
|
23094
23814
|
"SELECT COUNT(*) FROM ",
|
|
23095
23815
|
"INSERT INTO ",
|
|
23096
|
-
"UPDATE ",
|
|
23097
23816
|
"DELETE FROM ",
|
|
23098
23817
|
"CREATE TABLE ",
|
|
23099
23818
|
"ALTER TABLE ",
|
|
@@ -24537,8 +25256,8 @@ function bundledDetections() {
|
|
|
24537
25256
|
}
|
|
24538
25257
|
|
|
24539
25258
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24540
|
-
import { existsSync as
|
|
24541
|
-
import { basename, dirname, isAbsolute, join as
|
|
25259
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25260
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24542
25261
|
|
|
24543
25262
|
// ../../packages/plugin-sdk/src/events.ts
|
|
24544
25263
|
import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
|
|
@@ -24577,16 +25296,17 @@ function computeFindingKey(input) {
|
|
|
24577
25296
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
24578
25297
|
|
|
24579
25298
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24580
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24581
|
-
import { join as
|
|
25299
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25300
|
+
import { join as join8 } from "path";
|
|
24582
25301
|
|
|
24583
25302
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24584
25303
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24585
|
-
import { existsSync as
|
|
24586
|
-
import { basename as basename3, join as
|
|
25304
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
25305
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
24587
25306
|
|
|
24588
25307
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24589
25308
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
25309
|
+
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
24590
25310
|
var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
|
|
24591
25311
|
function entryIsActive(entry, now) {
|
|
24592
25312
|
if (entry.expiresAt !== null && Date.parse(entry.expiresAt) <= now) return false;
|
|
@@ -24659,17 +25379,22 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24659
25379
|
const fallback = DEFAULT_ACTIONS[category];
|
|
24660
25380
|
return fallback ?? "log";
|
|
24661
25381
|
}
|
|
25382
|
+
function actionForFinding(finding, excepted) {
|
|
25383
|
+
if (excepted?.has(finding)) return "allow";
|
|
25384
|
+
const action = resolveAction(finding.ruleId, finding.category);
|
|
25385
|
+
if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
|
|
25386
|
+
return "warn";
|
|
25387
|
+
}
|
|
25388
|
+
return action;
|
|
25389
|
+
}
|
|
24662
25390
|
function decide(findings, text, excepted) {
|
|
24663
25391
|
if (findings.length === 0) return { action: "log", text, findings: [] };
|
|
24664
|
-
const actionFor = (finding) =>
|
|
25392
|
+
const actionFor = (finding) => actionForFinding(finding, excepted);
|
|
24665
25393
|
let worst = "log";
|
|
24666
25394
|
for (const finding of findings) {
|
|
24667
25395
|
const action = actionFor(finding);
|
|
24668
25396
|
if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
|
|
24669
25397
|
}
|
|
24670
|
-
if (policyMode === "warn" && (worst === "block" || worst === "redact")) {
|
|
24671
|
-
return { action: "warn", text, findings };
|
|
24672
|
-
}
|
|
24673
25398
|
if (worst === "block") return { action: "block", text: null, findings };
|
|
24674
25399
|
if (worst === "redact") {
|
|
24675
25400
|
const redactFindings = findings.filter((f) => actionFor(f) === "redact");
|
|
@@ -24689,7 +25414,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24689
25414
|
const excepted = /* @__PURE__ */ new Set();
|
|
24690
25415
|
const exceptionIds = [];
|
|
24691
25416
|
try {
|
|
24692
|
-
if (policyMode === "warn") return { excepted, exceptionIds };
|
|
25417
|
+
if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn") return { excepted, exceptionIds };
|
|
24693
25418
|
const enforced = findings.filter((f) => {
|
|
24694
25419
|
const action = resolveAction(f.ruleId, f.category);
|
|
24695
25420
|
return action === "block" || action === "redact";
|
|
@@ -24742,8 +25467,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24742
25467
|
if (!key) return references;
|
|
24743
25468
|
const seen = /* @__PURE__ */ new Set();
|
|
24744
25469
|
for (const finding of decision.findings) {
|
|
24745
|
-
const action =
|
|
24746
|
-
if (action !== "block" && action !== "redact"
|
|
25470
|
+
const action = actionForFinding(finding, excepted);
|
|
25471
|
+
if (action !== "block" && action !== "redact") continue;
|
|
24747
25472
|
const fp = fingerprintOf(key, finding, fpCache);
|
|
24748
25473
|
const pair = `${finding.ruleId}:${fp}`;
|
|
24749
25474
|
if (seen.has(pair)) continue;
|
|
@@ -24832,7 +25557,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24832
25557
|
severity: match.severity,
|
|
24833
25558
|
span: match.span,
|
|
24834
25559
|
maskedMatch,
|
|
24835
|
-
actionTaken:
|
|
25560
|
+
actionTaken: actionForFinding(match, excepted),
|
|
24836
25561
|
confidence: match.confidence,
|
|
24837
25562
|
...findingKey ? { findingKey } : {}
|
|
24838
25563
|
};
|
|
@@ -24860,13 +25585,16 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24860
25585
|
return { processText, capture, rulesetFingerprint, close };
|
|
24861
25586
|
}
|
|
24862
25587
|
|
|
25588
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
25589
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25590
|
+
|
|
24863
25591
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24864
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
24865
|
-
import { join as
|
|
25592
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
25593
|
+
import { join as join10 } from "path";
|
|
24866
25594
|
|
|
24867
25595
|
// ../../packages/scanner/src/discover.ts
|
|
24868
25596
|
import { readdirSync as readdirSync3 } from "fs";
|
|
24869
|
-
import { join as
|
|
25597
|
+
import { join as join11 } from "path";
|
|
24870
25598
|
|
|
24871
25599
|
// ../../packages/scanner/src/constants.ts
|
|
24872
25600
|
var COMMON_SKIP_DIRS = ["node_modules", "__pycache__", ".venv", "venv", ".cache"];
|
|
@@ -24911,7 +25639,7 @@ function discoverGitRepos(opts) {
|
|
|
24911
25639
|
if (!entry.isDirectory()) continue;
|
|
24912
25640
|
if (DISCOVER_SKIP.has(entry.name)) continue;
|
|
24913
25641
|
if (entry.name.startsWith(".")) continue;
|
|
24914
|
-
visit(
|
|
25642
|
+
visit(join11(dir, entry.name), depth + 1);
|
|
24915
25643
|
}
|
|
24916
25644
|
}
|
|
24917
25645
|
for (const root of searchRoots) {
|
|
@@ -25014,7 +25742,7 @@ function renderMultiRepoSummary(summary, opts = {}) {
|
|
|
25014
25742
|
}
|
|
25015
25743
|
|
|
25016
25744
|
// ../../packages/scanner/src/scan.ts
|
|
25017
|
-
import { existsSync as
|
|
25745
|
+
import { existsSync as existsSync5 } from "fs";
|
|
25018
25746
|
import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
|
|
25019
25747
|
|
|
25020
25748
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
@@ -25208,6 +25936,14 @@ var StandaloneDataGateway = class {
|
|
|
25208
25936
|
sweepTerminalExceptions(retentionMs) {
|
|
25209
25937
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
25210
25938
|
}
|
|
25939
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
25940
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
25941
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
25942
|
+
// already-capped one).
|
|
25943
|
+
capWarnEraEnforcement(policyMode) {
|
|
25944
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
25945
|
+
return { capped };
|
|
25946
|
+
}
|
|
25211
25947
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
25212
25948
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
25213
25949
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -25313,7 +26049,7 @@ function computeResolutions(prior, current) {
|
|
|
25313
26049
|
// ../../packages/scanner/src/walk.ts
|
|
25314
26050
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
25315
26051
|
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
25316
|
-
import { extname, join as
|
|
26052
|
+
import { extname, join as join12, relative as relative3, sep as sep4 } from "path";
|
|
25317
26053
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
25318
26054
|
".ts",
|
|
25319
26055
|
".tsx",
|
|
@@ -25345,7 +26081,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
25345
26081
|
var DEFAULT_MAX_BYTES = 512 * 1024;
|
|
25346
26082
|
function readIgnoreLayer(dir, filename) {
|
|
25347
26083
|
try {
|
|
25348
|
-
const content = readFileSync7(
|
|
26084
|
+
const content = readFileSync7(join12(dir, filename), "utf8");
|
|
25349
26085
|
return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
|
|
25350
26086
|
} catch {
|
|
25351
26087
|
return void 0;
|
|
@@ -25379,7 +26115,7 @@ function* walkSourceFiles(opts = {}) {
|
|
|
25379
26115
|
const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
|
|
25380
26116
|
for (const entry of dirents) {
|
|
25381
26117
|
const name = entry.name;
|
|
25382
|
-
const fullPath =
|
|
26118
|
+
const fullPath = join12(dir, name);
|
|
25383
26119
|
if (entry.isDirectory()) {
|
|
25384
26120
|
const skipState = evaluate(dirSkipLayers, fullPath, true);
|
|
25385
26121
|
if (skipState !== "unignored" && (SKIP_DIRS.has(name) || skipState === "ignored")) {
|
|
@@ -25472,7 +26208,7 @@ function isUnderRoot(path, rootDir) {
|
|
|
25472
26208
|
}
|
|
25473
26209
|
async function sweepDeletedFiles(gateway, rootDir, previous) {
|
|
25474
26210
|
for (const path of previous.keys()) {
|
|
25475
|
-
if (!isUnderRoot(path, rootDir) ||
|
|
26211
|
+
if (!isUnderRoot(path, rootDir) || existsSync5(path)) continue;
|
|
25476
26212
|
await resolveRemovedFindings(gateway, path, [], { deleted: true });
|
|
25477
26213
|
}
|
|
25478
26214
|
}
|