@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/query.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
|
|
|
@@ -641,6 +645,10 @@ var defaultCostModel = {
|
|
|
641
645
|
};
|
|
642
646
|
|
|
643
647
|
// ../../packages/schema/src/token/format.ts
|
|
648
|
+
var COMPACT = new Intl.NumberFormat("en-US", {
|
|
649
|
+
notation: "compact",
|
|
650
|
+
maximumFractionDigits: 1
|
|
651
|
+
});
|
|
644
652
|
function formatUsd(value) {
|
|
645
653
|
return value > 0 && value < 0.1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
|
|
646
654
|
}
|
|
@@ -15373,6 +15381,12 @@ var FindingInstance = external_exports.object({
|
|
|
15373
15381
|
provider: FindingProvider,
|
|
15374
15382
|
repo: external_exports.string(),
|
|
15375
15383
|
file: external_exports.string(),
|
|
15384
|
+
// Host tool that produced the scanned text (event metadata's toolName).
|
|
15385
|
+
// Present whenever the capturing hook recorded one — including
|
|
15386
|
+
// file-attributed captures (views prefer `file`); its display value is
|
|
15387
|
+
// the location fallback ("via Bash") when no filePath exists. Absent for
|
|
15388
|
+
// legacy rows and non-tool captures (prompts, worktree scans).
|
|
15389
|
+
toolName: external_exports.string().optional(),
|
|
15376
15390
|
// Effective action: override.action ?? actionTaken, translated to FindingAction.
|
|
15377
15391
|
action: FindingAction,
|
|
15378
15392
|
detectedAt: external_exports.iso.datetime(),
|
|
@@ -15429,6 +15443,9 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15429
15443
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15430
15444
|
action: external_exports.array(FindingAction).optional(),
|
|
15431
15445
|
q: external_exports.string().optional(),
|
|
15446
|
+
// Scope to findings whose event carries this session id (the Activity page's
|
|
15447
|
+
// session → findings drilldown). Findings without a session never match.
|
|
15448
|
+
sessionId: external_exports.string().optional(),
|
|
15432
15449
|
groupBy: external_exports.literal("type").optional(),
|
|
15433
15450
|
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
15434
15451
|
cursor: external_exports.string().optional()
|
|
@@ -15440,7 +15457,13 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
15440
15457
|
}),
|
|
15441
15458
|
facets: FindingFacets,
|
|
15442
15459
|
items: external_exports.array(FindingGroup),
|
|
15443
|
-
nextCursor: external_exports.string().nullable()
|
|
15460
|
+
nextCursor: external_exports.string().nullable(),
|
|
15461
|
+
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
15462
|
+
// many times that rule fired in the session's persisted transcript. Findings
|
|
15463
|
+
// here are deduplicated to unique values while the transcript tally counts
|
|
15464
|
+
// every firing, so the two numbers legitimately differ — this map lets a
|
|
15465
|
+
// session-scoped view show both.
|
|
15466
|
+
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
15444
15467
|
}).meta({ id: "ListGroupedFindingsResponse" });
|
|
15445
15468
|
var ApplyFindingActionRequest = external_exports.object({
|
|
15446
15469
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
@@ -15489,6 +15512,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15489
15512
|
"prompt",
|
|
15490
15513
|
"response",
|
|
15491
15514
|
"code_change",
|
|
15515
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15516
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15517
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15518
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15519
|
+
"tool_use",
|
|
15492
15520
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15493
15521
|
// fact the posture inspection findings reference (findings require an
|
|
15494
15522
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15843,6 +15871,10 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15843
15871
|
from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15844
15872
|
/** Upper bound on startedAt; omitted defaults to now. */
|
|
15845
15873
|
to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15874
|
+
/** Exclude zero-activity sessions — roots whose only recorded children are
|
|
15875
|
+
* bookkeeping rows (hooks, config scans), typically background `claude`
|
|
15876
|
+
* launches. Omitted = list everything. `z.stringbool()` per the note above. */
|
|
15877
|
+
excludeEmpty: external_exports.stringbool().optional(),
|
|
15846
15878
|
/** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
|
|
15847
15879
|
limit: external_exports.coerce.number().int().min(1).max(100).default(50),
|
|
15848
15880
|
/** Opaque pagination cursor (most-recent first). */
|
|
@@ -15851,7 +15883,11 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15851
15883
|
var ListActivitySessionsResponse = external_exports.object({
|
|
15852
15884
|
items: external_exports.array(ActivitySessionSummary),
|
|
15853
15885
|
/** `null` once the last page is reached. */
|
|
15854
|
-
nextCursor: external_exports.string().nullable()
|
|
15886
|
+
nextCursor: external_exports.string().nullable(),
|
|
15887
|
+
/** Zero-activity sessions matching the query's filters/range (whether or
|
|
15888
|
+
* not `excludeEmpty` dropped them from `items`) — the count a UI toggle
|
|
15889
|
+
* shows when collapsing them. */
|
|
15890
|
+
emptyCount: external_exports.number().int().nonnegative()
|
|
15855
15891
|
}).meta({ id: "ListActivitySessionsResponse" });
|
|
15856
15892
|
var ListSessionEventsQuery = external_exports.object({
|
|
15857
15893
|
/** Default 100, range 1–500. */
|
|
@@ -15880,12 +15916,18 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15880
15916
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15881
15917
|
|
|
15882
15918
|
// ../../packages/schema/src/zod/event.ts
|
|
15883
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15919
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15884
15920
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15885
15921
|
var EventMetadata = external_exports.object({
|
|
15886
15922
|
sessionId: external_exports.string().optional(),
|
|
15887
15923
|
repo: external_exports.string().optional(),
|
|
15888
15924
|
filePath: external_exports.string().optional(),
|
|
15925
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
|
|
15926
|
+
// set by the tool-scanning hooks. The tool NAME only — never the tool's
|
|
15927
|
+
// arguments or output, which can carry the very value a finding masked
|
|
15928
|
+
// (metadata is stored unredacted). Gives findings on non-file captures a
|
|
15929
|
+
// display location ("via Bash") when no filePath exists.
|
|
15930
|
+
toolName: external_exports.string().optional(),
|
|
15889
15931
|
// Set (true) by the worktree scanner when the file is excluded by the
|
|
15890
15932
|
// repo's .gitignore. Gitignored files ARE still scanned — local scratch and
|
|
15891
15933
|
// generated code can leak real secrets — but the provenance is recorded so
|
|
@@ -16193,7 +16235,7 @@ var DetectionException = external_exports.object({
|
|
|
16193
16235
|
justification: external_exports.string().min(1),
|
|
16194
16236
|
conditions: ExceptionConditions.nullable(),
|
|
16195
16237
|
createdBy: external_exports.string(),
|
|
16196
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16238
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16197
16239
|
createdAt: external_exports.iso.datetime(),
|
|
16198
16240
|
updatedAt: external_exports.iso.datetime(),
|
|
16199
16241
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16217,7 +16259,10 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16217
16259
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16218
16260
|
var KeywordMatcher = external_exports.object({
|
|
16219
16261
|
type: external_exports.literal("keyword"),
|
|
16220
|
-
|
|
16262
|
+
// An empty keyword matches at every position, yielding one zero-length span
|
|
16263
|
+
// per character. Rejected here because a keyword that matches everything is
|
|
16264
|
+
// never intentional.
|
|
16265
|
+
keywords: external_exports.array(external_exports.string().min(1)).min(1),
|
|
16221
16266
|
caseSensitive: external_exports.boolean().default(false)
|
|
16222
16267
|
});
|
|
16223
16268
|
function isValidRegex(pattern, flags) {
|
|
@@ -16362,20 +16407,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16362
16407
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16363
16408
|
fetchedAt: external_exports.iso.datetime()
|
|
16364
16409
|
}).meta({ id: "PolicyBundle" });
|
|
16365
|
-
var DEFAULT_ACTIONS = {
|
|
16366
|
-
secret: "block",
|
|
16367
|
-
pii: "redact",
|
|
16368
|
-
financial: "redact",
|
|
16369
|
-
phi: "redact",
|
|
16370
|
-
code_context: "warn",
|
|
16371
|
-
code_flaw: "warn",
|
|
16372
|
-
custom: "warn",
|
|
16373
|
-
// Config-posture findings only observe today (they land in
|
|
16374
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16375
|
-
config: "warn"
|
|
16376
|
-
};
|
|
16377
16410
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16378
16411
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16412
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16413
|
+
secret: "critical",
|
|
16414
|
+
financial: "critical",
|
|
16415
|
+
// core-financial/credit-card
|
|
16416
|
+
code_flaw: "critical",
|
|
16417
|
+
pii: "high",
|
|
16418
|
+
phi: "high",
|
|
16419
|
+
custom: "high",
|
|
16420
|
+
// user-defined; conservative
|
|
16421
|
+
code_context: "low",
|
|
16422
|
+
config: "low"
|
|
16423
|
+
// observe-only; floors to monitor regardless
|
|
16424
|
+
};
|
|
16425
|
+
function severityFloorPolicy(category) {
|
|
16426
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16427
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16428
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16429
|
+
}
|
|
16379
16430
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16380
16431
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16381
16432
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16402,6 +16453,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16402
16453
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16403
16454
|
}
|
|
16404
16455
|
};
|
|
16456
|
+
function builtinPolicyToAction(id) {
|
|
16457
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16458
|
+
}
|
|
16459
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16460
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16461
|
+
);
|
|
16405
16462
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16406
16463
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16407
16464
|
);
|
|
@@ -16454,6 +16511,10 @@ var ListEventsResponse = external_exports.object({
|
|
|
16454
16511
|
items: external_exports.array(Event),
|
|
16455
16512
|
nextCursor: external_exports.string().nullable()
|
|
16456
16513
|
}).meta({ id: "ListEventsResponse" });
|
|
16514
|
+
var IngestResponse = external_exports.object({
|
|
16515
|
+
accepted: external_exports.number().int().nonnegative(),
|
|
16516
|
+
duplicates: external_exports.number().int().nonnegative()
|
|
16517
|
+
}).meta({ id: "IngestResponse" });
|
|
16457
16518
|
var ListFindingsQuery = external_exports.object({
|
|
16458
16519
|
cursor: external_exports.string().optional(),
|
|
16459
16520
|
limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
|
|
@@ -16808,10 +16869,8 @@ function toApiProvider(sourceTool) {
|
|
|
16808
16869
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16809
16870
|
}
|
|
16810
16871
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16811
|
-
function
|
|
16812
|
-
const statuses = new Set(
|
|
16813
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16814
|
-
);
|
|
16872
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16873
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16815
16874
|
if (statuses.size === 0) return void 0;
|
|
16816
16875
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16817
16876
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16829,6 +16888,7 @@ function deriveFindingStatus(row) {
|
|
|
16829
16888
|
function buildFindingGroups(rows, opts = {}) {
|
|
16830
16889
|
const overrides = opts.overrides;
|
|
16831
16890
|
const packNames = opts.packNames;
|
|
16891
|
+
const aggregates = opts.aggregates;
|
|
16832
16892
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16833
16893
|
for (const row of rows) {
|
|
16834
16894
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16844,23 +16904,27 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16844
16904
|
provider: toApiProvider(r.sourceTool),
|
|
16845
16905
|
repo: r.repo,
|
|
16846
16906
|
file: r.file,
|
|
16907
|
+
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
16847
16908
|
action: toApiAction(effectiveDbAction),
|
|
16848
16909
|
detectedAt: r.occurredAt,
|
|
16849
16910
|
confidence: r.confidence,
|
|
16850
16911
|
status: r.status
|
|
16851
16912
|
};
|
|
16852
16913
|
});
|
|
16853
|
-
const
|
|
16914
|
+
const agg = aggregates?.get(ruleId);
|
|
16915
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16854
16916
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16855
16917
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16856
16918
|
);
|
|
16857
16919
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16858
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16920
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16859
16921
|
if (seenProviders.has(p)) return false;
|
|
16860
16922
|
seenProviders.add(p);
|
|
16861
16923
|
return true;
|
|
16862
16924
|
});
|
|
16863
|
-
const actionSet = new Set(
|
|
16925
|
+
const actionSet = new Set(
|
|
16926
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16927
|
+
);
|
|
16864
16928
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16865
16929
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16866
16930
|
const detection = {
|
|
@@ -16874,8 +16938,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16874
16938
|
contextPrefix: ""
|
|
16875
16939
|
// empty (pending privacy review)
|
|
16876
16940
|
};
|
|
16877
|
-
const status =
|
|
16878
|
-
|
|
16941
|
+
const status = foldGroupStatus(
|
|
16942
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16943
|
+
);
|
|
16944
|
+
const group = {
|
|
16879
16945
|
id: ruleId,
|
|
16880
16946
|
category: apiCategory,
|
|
16881
16947
|
subtype: ruleId,
|
|
@@ -16884,21 +16950,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16884
16950
|
match,
|
|
16885
16951
|
detection,
|
|
16886
16952
|
policy,
|
|
16887
|
-
instanceCount: instances.length,
|
|
16953
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16888
16954
|
providers,
|
|
16889
16955
|
aggregateAction,
|
|
16890
16956
|
latestDetectedAt,
|
|
16891
16957
|
instances,
|
|
16892
16958
|
status
|
|
16893
|
-
}
|
|
16959
|
+
};
|
|
16960
|
+
if (agg) {
|
|
16961
|
+
actionsCache.set(group, [...actionSet]);
|
|
16962
|
+
if (agg.searchText !== void 0) {
|
|
16963
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16964
|
+
}
|
|
16965
|
+
}
|
|
16966
|
+
groups.push(group);
|
|
16894
16967
|
}
|
|
16895
16968
|
return groups;
|
|
16896
16969
|
}
|
|
16897
16970
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16898
|
-
function
|
|
16899
|
-
|
|
16900
|
-
if (cached2 !== void 0) return cached2;
|
|
16901
|
-
const haystack = [
|
|
16971
|
+
function buildHaystack(g, extra) {
|
|
16972
|
+
return [
|
|
16902
16973
|
g.subtype,
|
|
16903
16974
|
g.category,
|
|
16904
16975
|
g.match.maskedValue,
|
|
@@ -16906,11 +16977,26 @@ function groupHaystack(g) {
|
|
|
16906
16977
|
g.id,
|
|
16907
16978
|
...g.instances.map((i) => i.repo),
|
|
16908
16979
|
...g.instances.map((i) => i.file),
|
|
16909
|
-
...g.instances.map((i) => i.
|
|
16980
|
+
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
16981
|
+
...g.instances.map((i) => i.id),
|
|
16982
|
+
...extra === void 0 ? [] : [extra]
|
|
16910
16983
|
].join(" ").toLowerCase();
|
|
16984
|
+
}
|
|
16985
|
+
function groupHaystack(g) {
|
|
16986
|
+
const cached2 = haystackCache.get(g);
|
|
16987
|
+
if (cached2 !== void 0) return cached2;
|
|
16988
|
+
const haystack = buildHaystack(g);
|
|
16911
16989
|
haystackCache.set(g, haystack);
|
|
16912
16990
|
return haystack;
|
|
16913
16991
|
}
|
|
16992
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16993
|
+
function groupActions(g) {
|
|
16994
|
+
const cached2 = actionsCache.get(g);
|
|
16995
|
+
if (cached2 !== void 0) return cached2;
|
|
16996
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16997
|
+
actionsCache.set(g, actions);
|
|
16998
|
+
return actions;
|
|
16999
|
+
}
|
|
16914
17000
|
function applyFindingFilters(groups, opts) {
|
|
16915
17001
|
let filtered = groups;
|
|
16916
17002
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16923,7 +17009,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16923
17009
|
}
|
|
16924
17010
|
if (opts.actions && opts.actions.length > 0) {
|
|
16925
17011
|
const actionSet = new Set(opts.actions);
|
|
16926
|
-
filtered = filtered.filter((g) => g.
|
|
17012
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16927
17013
|
}
|
|
16928
17014
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16929
17015
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16975,8 +17061,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16975
17061
|
});
|
|
16976
17062
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16977
17063
|
for (const g of forAction) {
|
|
16978
|
-
const
|
|
16979
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17064
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16980
17065
|
}
|
|
16981
17066
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16982
17067
|
providers: opts.providers,
|
|
@@ -17159,6 +17244,71 @@ var ProjectFilesScan = external_exports.object({
|
|
|
17159
17244
|
scannedAt: external_exports.string()
|
|
17160
17245
|
});
|
|
17161
17246
|
|
|
17247
|
+
// ../../packages/schema/src/zod/ranges.ts
|
|
17248
|
+
var TIME_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17249
|
+
var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
|
|
17250
|
+
var DEFAULT_TIME_RANGE = "7d";
|
|
17251
|
+
var RANGE_DAYS = {
|
|
17252
|
+
"7d": 7,
|
|
17253
|
+
"30d": 30,
|
|
17254
|
+
"3m": 90,
|
|
17255
|
+
"6m": 180
|
|
17256
|
+
};
|
|
17257
|
+
var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
|
|
17258
|
+
|
|
17259
|
+
// ../../packages/schema/src/zod/remediation.ts
|
|
17260
|
+
var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
|
|
17261
|
+
var MaskedFindingLocation = external_exports.object({
|
|
17262
|
+
filePath: external_exports.string(),
|
|
17263
|
+
span: Span.optional()
|
|
17264
|
+
}).strict();
|
|
17265
|
+
var MaskedSecretFinding = external_exports.object({
|
|
17266
|
+
provider: external_exports.string(),
|
|
17267
|
+
maskedToken: external_exports.string(),
|
|
17268
|
+
where: MaskedFindingLocation,
|
|
17269
|
+
state: SecretFindingState,
|
|
17270
|
+
observedAt: external_exports.iso.datetime().optional()
|
|
17271
|
+
}).strict();
|
|
17272
|
+
var RotationChecklistEntry = external_exports.object({
|
|
17273
|
+
provider: external_exports.string(),
|
|
17274
|
+
maskedToken: external_exports.string(),
|
|
17275
|
+
consolePath: external_exports.string(),
|
|
17276
|
+
occurrenceSpread: external_exports.number().int().positive()
|
|
17277
|
+
}).strict();
|
|
17278
|
+
var RemediationOption = external_exports.enum([
|
|
17279
|
+
"redact-rotation-checklist",
|
|
17280
|
+
"redact-only",
|
|
17281
|
+
"set-secret-redact",
|
|
17282
|
+
"leave"
|
|
17283
|
+
]);
|
|
17284
|
+
var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
|
|
17285
|
+
var RemediationEntryContext = external_exports.object({
|
|
17286
|
+
entrySource: RemediationEntrySource
|
|
17287
|
+
}).strict();
|
|
17288
|
+
var RemediationOptionChoice = external_exports.object({
|
|
17289
|
+
id: RemediationOption,
|
|
17290
|
+
label: external_exports.string()
|
|
17291
|
+
});
|
|
17292
|
+
var BatchedRemediationDecision = external_exports.object({
|
|
17293
|
+
kind: external_exports.literal("decision"),
|
|
17294
|
+
entrySource: RemediationEntrySource,
|
|
17295
|
+
secretCount: external_exports.number().int().positive(),
|
|
17296
|
+
prompt: external_exports.string(),
|
|
17297
|
+
options: external_exports.tuple([
|
|
17298
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
|
|
17299
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
|
|
17300
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
|
|
17301
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
|
|
17302
|
+
])
|
|
17303
|
+
});
|
|
17304
|
+
var NoRemediationDecision = external_exports.object({
|
|
17305
|
+
kind: external_exports.literal("no-decision")
|
|
17306
|
+
});
|
|
17307
|
+
var BatchedRemediation = external_exports.discriminatedUnion("kind", [
|
|
17308
|
+
BatchedRemediationDecision,
|
|
17309
|
+
NoRemediationDecision
|
|
17310
|
+
]);
|
|
17311
|
+
|
|
17162
17312
|
// ../../packages/schema/src/zod/rule-test.ts
|
|
17163
17313
|
var TestRulesRequest = external_exports.object({
|
|
17164
17314
|
rules: external_exports.array(Rule).min(1).max(100),
|
|
@@ -17217,10 +17367,8 @@ var SeveritySummaryResponse = external_exports.object({
|
|
|
17217
17367
|
// All four severity levels are always present (count may be 0).
|
|
17218
17368
|
bySeverity: external_exports.array(SeveritySummaryItem)
|
|
17219
17369
|
}).meta({ id: "SeveritySummaryResponse" });
|
|
17220
|
-
var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17221
|
-
var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
|
|
17222
17370
|
var SecurityRangeQuery = external_exports.object({
|
|
17223
|
-
range: external_exports.enum(
|
|
17371
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
|
|
17224
17372
|
});
|
|
17225
17373
|
var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
|
|
17226
17374
|
var EnforcementAction = external_exports.object({
|
|
@@ -17230,7 +17378,7 @@ var EnforcementAction = external_exports.object({
|
|
|
17230
17378
|
delta: external_exports.number().int()
|
|
17231
17379
|
}).meta({ id: "EnforcementAction" });
|
|
17232
17380
|
var EnforcementActionsResponse = external_exports.object({
|
|
17233
|
-
range:
|
|
17381
|
+
range: TimeRange,
|
|
17234
17382
|
// Sum of actions[].count in the window.
|
|
17235
17383
|
total: external_exports.number().int().nonnegative(),
|
|
17236
17384
|
// One entry per kind, always all three present (count may be 0).
|
|
@@ -17245,7 +17393,7 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
17245
17393
|
medium: external_exports.number().int().nonnegative()
|
|
17246
17394
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
17247
17395
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
17248
|
-
range:
|
|
17396
|
+
range: TimeRange,
|
|
17249
17397
|
granularity: TimeseriesGranularity,
|
|
17250
17398
|
points: external_exports.array(FindingsTimeseriesPoint)
|
|
17251
17399
|
}).meta({ id: "FindingsTimeseriesResponse" });
|
|
@@ -17260,7 +17408,7 @@ var MttrTrendPoint = external_exports.object({
|
|
|
17260
17408
|
})
|
|
17261
17409
|
}).meta({ id: "MttrTrendPoint" });
|
|
17262
17410
|
var MttrTrendResponse = external_exports.object({
|
|
17263
|
-
range:
|
|
17411
|
+
range: TimeRange,
|
|
17264
17412
|
granularity: TimeseriesGranularity,
|
|
17265
17413
|
points: external_exports.array(MttrTrendPoint)
|
|
17266
17414
|
}).meta({ id: "MttrTrendResponse" });
|
|
@@ -17287,11 +17435,11 @@ var TopSource = external_exports.object({
|
|
|
17287
17435
|
findingsCount: external_exports.number().int().nonnegative()
|
|
17288
17436
|
}).meta({ id: "TopSource" });
|
|
17289
17437
|
var TopSourcesResponse = external_exports.object({
|
|
17290
|
-
range:
|
|
17438
|
+
range: TimeRange,
|
|
17291
17439
|
items: external_exports.array(TopSource)
|
|
17292
17440
|
}).meta({ id: "TopSourcesResponse" });
|
|
17293
17441
|
var TopSourcesQuery = external_exports.object({
|
|
17294
|
-
range: external_exports.enum(
|
|
17442
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
|
|
17295
17443
|
limit: external_exports.coerce.number().int().min(1).max(50).default(5),
|
|
17296
17444
|
// Omit for both kinds.
|
|
17297
17445
|
kind: external_exports.enum(SOURCE_KINDS).optional()
|
|
@@ -17304,7 +17452,7 @@ var ScanCoverageProvider = external_exports.object({
|
|
|
17304
17452
|
supported: external_exports.boolean()
|
|
17305
17453
|
}).meta({ id: "ScanCoverageProvider" });
|
|
17306
17454
|
var ScanCoverageResponse = external_exports.object({
|
|
17307
|
-
range:
|
|
17455
|
+
range: TimeRange,
|
|
17308
17456
|
providers: external_exports.array(ScanCoverageProvider)
|
|
17309
17457
|
}).meta({ id: "ScanCoverageResponse" });
|
|
17310
17458
|
var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
|
|
@@ -17353,6 +17501,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
|
|
|
17353
17501
|
var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
|
|
17354
17502
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
17355
17503
|
|
|
17504
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17505
|
+
var TriageHit = external_exports.object({
|
|
17506
|
+
ruleId: external_exports.string(),
|
|
17507
|
+
category: DetectionCategory,
|
|
17508
|
+
severity: Severity,
|
|
17509
|
+
maskedMatch: external_exports.string(),
|
|
17510
|
+
rawMatch: external_exports.string(),
|
|
17511
|
+
context: external_exports.string(),
|
|
17512
|
+
filePath: external_exports.string().optional(),
|
|
17513
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17514
|
+
id: external_exports.string().optional(),
|
|
17515
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17516
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17517
|
+
});
|
|
17518
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17519
|
+
var TriageCategoryRec = external_exports.object({
|
|
17520
|
+
category: DetectionCategory,
|
|
17521
|
+
action: TriagePolicy,
|
|
17522
|
+
reasoning: external_exports.string(),
|
|
17523
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17524
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17525
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17526
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17527
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17528
|
+
});
|
|
17529
|
+
var TriageRecommendation = external_exports.object({
|
|
17530
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17531
|
+
notes: external_exports.string()
|
|
17532
|
+
});
|
|
17533
|
+
|
|
17534
|
+
// ../../packages/schema/src/zod/setup-frame.ts
|
|
17535
|
+
var CalibrationCounts = external_exports.object({
|
|
17536
|
+
total: external_exports.number().int().nonnegative(),
|
|
17537
|
+
important: external_exports.number().int().nonnegative(),
|
|
17538
|
+
routine: external_exports.number().int().nonnegative()
|
|
17539
|
+
}).refine((c) => c.total === c.important + c.routine, {
|
|
17540
|
+
message: "total must equal important + routine",
|
|
17541
|
+
path: ["total"]
|
|
17542
|
+
});
|
|
17543
|
+
var FalsePositivePatternValue = external_exports.object({
|
|
17544
|
+
ruleId: external_exports.string(),
|
|
17545
|
+
category: DetectionCategory,
|
|
17546
|
+
valueFingerprint: external_exports.string(),
|
|
17547
|
+
keyVersion: external_exports.number().int().nonnegative()
|
|
17548
|
+
});
|
|
17549
|
+
var FalsePositivePatternGroup = external_exports.object({
|
|
17550
|
+
pattern: external_exports.string(),
|
|
17551
|
+
count: external_exports.number().int().nonnegative(),
|
|
17552
|
+
values: external_exports.array(FalsePositivePatternValue).min(1)
|
|
17553
|
+
});
|
|
17554
|
+
var CalibrationFindingKind = external_exports.object({
|
|
17555
|
+
category: DetectionCategory,
|
|
17556
|
+
count: external_exports.number().int().nonnegative(),
|
|
17557
|
+
egress: external_exports.boolean()
|
|
17558
|
+
});
|
|
17559
|
+
var CalibrationFrame = external_exports.object({
|
|
17560
|
+
counts: CalibrationCounts,
|
|
17561
|
+
routineCategories: external_exports.array(DetectionCategory),
|
|
17562
|
+
surfacedCategories: external_exports.array(DetectionCategory),
|
|
17563
|
+
findingKinds: external_exports.array(CalibrationFindingKind),
|
|
17564
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
|
|
17565
|
+
maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
|
|
17566
|
+
falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
|
|
17567
|
+
});
|
|
17568
|
+
var CalibrationPreviewCategory = TriageCategoryRec.pick({
|
|
17569
|
+
category: true,
|
|
17570
|
+
genuineCount: true,
|
|
17571
|
+
fpCount: true
|
|
17572
|
+
}).extend({
|
|
17573
|
+
egress: external_exports.boolean()
|
|
17574
|
+
});
|
|
17575
|
+
var CalibrationPreview = external_exports.object({
|
|
17576
|
+
categories: external_exports.array(CalibrationPreviewCategory),
|
|
17577
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
|
|
17578
|
+
});
|
|
17579
|
+
var CalibrationResult = external_exports.object({
|
|
17580
|
+
frame: CalibrationFrame,
|
|
17581
|
+
copy: external_exports.string()
|
|
17582
|
+
});
|
|
17583
|
+
var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
|
|
17584
|
+
var SetupHandoffOption = external_exports.object({
|
|
17585
|
+
id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
|
|
17586
|
+
label: external_exports.string()
|
|
17587
|
+
});
|
|
17588
|
+
var DashboardHandoffOptions = external_exports.tuple([
|
|
17589
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17590
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17591
|
+
]);
|
|
17592
|
+
var ComposedRemediationOptions = external_exports.tuple([
|
|
17593
|
+
SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
|
|
17594
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17595
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17596
|
+
]);
|
|
17597
|
+
var SetupHandoffOffer = external_exports.object({
|
|
17598
|
+
worthALook: external_exports.number().int().nonnegative(),
|
|
17599
|
+
liveKeys: external_exports.number().int().nonnegative().optional(),
|
|
17600
|
+
options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
|
|
17601
|
+
}).refine(
|
|
17602
|
+
(o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
|
|
17603
|
+
{
|
|
17604
|
+
message: "the chain-entry option is present exactly when liveKeys > 0",
|
|
17605
|
+
path: ["options"]
|
|
17606
|
+
}
|
|
17607
|
+
).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
|
|
17608
|
+
message: "liveKeys is a subset of worthALook and cannot exceed it",
|
|
17609
|
+
path: ["liveKeys"]
|
|
17610
|
+
});
|
|
17611
|
+
|
|
17356
17612
|
// ../../packages/schema/src/zod/shares.ts
|
|
17357
17613
|
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17358
17614
|
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
@@ -17538,6 +17794,102 @@ function reviewSeverityRank(reasons) {
|
|
|
17538
17794
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17539
17795
|
}
|
|
17540
17796
|
|
|
17797
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17798
|
+
function escapeLikePattern(s) {
|
|
17799
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17800
|
+
}
|
|
17801
|
+
function placeholders(n) {
|
|
17802
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17803
|
+
}
|
|
17804
|
+
function containsPattern(q) {
|
|
17805
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17806
|
+
}
|
|
17807
|
+
function likeAny(exprs) {
|
|
17808
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17809
|
+
}
|
|
17810
|
+
|
|
17811
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17812
|
+
var savepointSeq = 0;
|
|
17813
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17814
|
+
if (db.isTransaction) {
|
|
17815
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17816
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17817
|
+
try {
|
|
17818
|
+
fn();
|
|
17819
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17820
|
+
} catch (error51) {
|
|
17821
|
+
try {
|
|
17822
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17823
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17824
|
+
} catch {
|
|
17825
|
+
}
|
|
17826
|
+
throw error51;
|
|
17827
|
+
}
|
|
17828
|
+
return;
|
|
17829
|
+
}
|
|
17830
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17831
|
+
try {
|
|
17832
|
+
fn();
|
|
17833
|
+
db.exec("COMMIT");
|
|
17834
|
+
} catch (error51) {
|
|
17835
|
+
try {
|
|
17836
|
+
db.exec("ROLLBACK");
|
|
17837
|
+
} catch {
|
|
17838
|
+
}
|
|
17839
|
+
throw error51;
|
|
17840
|
+
}
|
|
17841
|
+
}
|
|
17842
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17843
|
+
const nested = db.isTransaction;
|
|
17844
|
+
try {
|
|
17845
|
+
withTransaction(db, fn, mode);
|
|
17846
|
+
return true;
|
|
17847
|
+
} catch (error51) {
|
|
17848
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17849
|
+
return false;
|
|
17850
|
+
}
|
|
17851
|
+
}
|
|
17852
|
+
|
|
17853
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17854
|
+
function akaWarn(message) {
|
|
17855
|
+
process.stderr.write(`[aka] ${message}
|
|
17856
|
+
`);
|
|
17857
|
+
}
|
|
17858
|
+
|
|
17859
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17860
|
+
function evidenceObjects(sql) {
|
|
17861
|
+
const objects = [];
|
|
17862
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17863
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17864
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17865
|
+
}
|
|
17866
|
+
}
|
|
17867
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17868
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17869
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17870
|
+
}
|
|
17871
|
+
}
|
|
17872
|
+
return objects;
|
|
17873
|
+
}
|
|
17874
|
+
function schemaObjectExists(db, kind, name) {
|
|
17875
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17876
|
+
return row !== void 0;
|
|
17877
|
+
}
|
|
17878
|
+
function indexExists(db, name) {
|
|
17879
|
+
return schemaObjectExists(db, "index", name);
|
|
17880
|
+
}
|
|
17881
|
+
function columnNames(db, table2, opts) {
|
|
17882
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17883
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
|
|
17884
|
+
return columns.map((c) => c.name);
|
|
17885
|
+
}
|
|
17886
|
+
function evidenceExists(db, object2) {
|
|
17887
|
+
if (object2.kind === "column") {
|
|
17888
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17889
|
+
}
|
|
17890
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17891
|
+
}
|
|
17892
|
+
|
|
17541
17893
|
// ../../packages/persistence/src/ids.ts
|
|
17542
17894
|
import { createHash } from "crypto";
|
|
17543
17895
|
function sha256Hex(input) {
|
|
@@ -17574,28 +17926,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17574
17926
|
}
|
|
17575
17927
|
|
|
17576
17928
|
// ../../packages/persistence/src/migrations.ts
|
|
17577
|
-
function evidenceObjects(sql) {
|
|
17578
|
-
const objects = [];
|
|
17579
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17580
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17581
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17582
|
-
}
|
|
17583
|
-
}
|
|
17584
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17585
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17586
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17587
|
-
}
|
|
17588
|
-
}
|
|
17589
|
-
return objects;
|
|
17590
|
-
}
|
|
17591
|
-
function evidenceExists(db, object2) {
|
|
17592
|
-
if (object2.kind === "column") {
|
|
17593
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17594
|
-
return columns.some((c) => c.name === object2.name);
|
|
17595
|
-
}
|
|
17596
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17597
|
-
return row !== void 0;
|
|
17598
|
-
}
|
|
17599
17929
|
function describeObject(object2) {
|
|
17600
17930
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17601
17931
|
}
|
|
@@ -17606,10 +17936,6 @@ function createdIndexName(statement) {
|
|
|
17606
17936
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17607
17937
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17608
17938
|
}
|
|
17609
|
-
function indexExists(db, name) {
|
|
17610
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17611
|
-
return row !== void 0;
|
|
17612
|
-
}
|
|
17613
17939
|
function applyMigrations(db) {
|
|
17614
17940
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17615
17941
|
db.exec(
|
|
@@ -17628,44 +17954,39 @@ function applyMigrations(db) {
|
|
|
17628
17954
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17629
17955
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17630
17956
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17631
|
-
const message = `
|
|
17632
|
-
|
|
17633
|
-
`);
|
|
17634
|
-
throw new Error(message);
|
|
17957
|
+
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.`;
|
|
17958
|
+
akaWarn(message);
|
|
17959
|
+
throw new Error(`[aka] ${message}`);
|
|
17635
17960
|
}
|
|
17636
17961
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17637
17962
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17638
17963
|
const statements = splitStatements(migration.sql);
|
|
17639
17964
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17640
17965
|
try {
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
const
|
|
17645
|
-
|
|
17646
|
-
if (
|
|
17647
|
-
|
|
17648
|
-
|
|
17966
|
+
withTransaction(
|
|
17967
|
+
db,
|
|
17968
|
+
() => {
|
|
17969
|
+
for (const statement of statements) {
|
|
17970
|
+
const indexName = createdIndexName(statement);
|
|
17971
|
+
if (indexName === void 0) {
|
|
17972
|
+
if (alreadyApplied) continue;
|
|
17973
|
+
} else if (indexExists(db, indexName)) {
|
|
17974
|
+
continue;
|
|
17975
|
+
}
|
|
17976
|
+
db.exec(statement);
|
|
17649
17977
|
}
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17657
|
-
);
|
|
17978
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17979
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17980
|
+
if (violations.length > 0) {
|
|
17981
|
+
throw new Error(
|
|
17982
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17983
|
+
);
|
|
17984
|
+
}
|
|
17658
17985
|
}
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
try {
|
|
17664
|
-
db.exec("ROLLBACK");
|
|
17665
|
-
} catch {
|
|
17666
|
-
}
|
|
17667
|
-
throw error51;
|
|
17668
|
-
}
|
|
17986
|
+
record2.run(migration.tag, Date.now());
|
|
17987
|
+
},
|
|
17988
|
+
"IMMEDIATE"
|
|
17989
|
+
);
|
|
17669
17990
|
} finally {
|
|
17670
17991
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17671
17992
|
}
|
|
@@ -17708,8 +18029,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17708
18029
|
}
|
|
17709
18030
|
];
|
|
17710
18031
|
function ensureTokenUsageColumns(db) {
|
|
17711
|
-
const
|
|
17712
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
18032
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17713
18033
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17714
18034
|
if (!existing.has(column.name)) {
|
|
17715
18035
|
db.exec(column.ddl);
|
|
@@ -17747,47 +18067,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17747
18067
|
repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
|
|
17748
18068
|
}));
|
|
17749
18069
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
dropCollisions
|
|
17764
|
-
|
|
18070
|
+
withTransaction(
|
|
18071
|
+
db,
|
|
18072
|
+
() => {
|
|
18073
|
+
for (const { row, canonicalId } of legacy) {
|
|
18074
|
+
foldProject.run(
|
|
18075
|
+
canonicalId,
|
|
18076
|
+
row.url,
|
|
18077
|
+
row.name,
|
|
18078
|
+
row.attributes,
|
|
18079
|
+
row.firstSeen,
|
|
18080
|
+
row.lastSeen
|
|
18081
|
+
);
|
|
18082
|
+
repointAudit.run(canonicalId, row.id);
|
|
18083
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
18084
|
+
dropCollisions.run(row.id, canonicalId);
|
|
18085
|
+
repoint.run(canonicalId, row.id);
|
|
18086
|
+
}
|
|
18087
|
+
repointCallSite.run(canonicalId, row.id);
|
|
18088
|
+
deleteLegacy.run(row.id);
|
|
17765
18089
|
}
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
db.exec("COMMIT");
|
|
17770
|
-
} catch (error51) {
|
|
17771
|
-
try {
|
|
17772
|
-
db.exec("ROLLBACK");
|
|
17773
|
-
} catch {
|
|
17774
|
-
}
|
|
17775
|
-
throw error51;
|
|
17776
|
-
}
|
|
18090
|
+
},
|
|
18091
|
+
"IMMEDIATE"
|
|
18092
|
+
);
|
|
17777
18093
|
} catch (error51) {
|
|
17778
|
-
|
|
17779
|
-
`);
|
|
18094
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17780
18095
|
}
|
|
17781
18096
|
}
|
|
17782
18097
|
function isForeignSqliteLineage(db) {
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17786
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
18098
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18099
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17787
18100
|
}
|
|
17788
18101
|
function ensureSyncedAtColumn(db, table2) {
|
|
17789
|
-
|
|
17790
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
18102
|
+
if (!columnNames(db, table2).includes("synced_at")) {
|
|
17791
18103
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
17792
18104
|
}
|
|
17793
18105
|
}
|
|
@@ -17838,8 +18150,11 @@ function ensureDataDirSync(dir) {
|
|
|
17838
18150
|
} catch {
|
|
17839
18151
|
}
|
|
17840
18152
|
}
|
|
18153
|
+
function walSidecars(file2) {
|
|
18154
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
18155
|
+
}
|
|
17841
18156
|
function tightenPerms(file2) {
|
|
17842
|
-
for (const path of [file2,
|
|
18157
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17843
18158
|
try {
|
|
17844
18159
|
chmodSync(path, DATA_FILE_MODE);
|
|
17845
18160
|
} catch {
|
|
@@ -17847,12 +18162,68 @@ function tightenPerms(file2) {
|
|
|
17847
18162
|
}
|
|
17848
18163
|
}
|
|
17849
18164
|
|
|
17850
|
-
// ../../packages/persistence/src/
|
|
17851
|
-
function
|
|
17852
|
-
|
|
18165
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
18166
|
+
function safeJson(s, fallback) {
|
|
18167
|
+
if (s == null) return fallback;
|
|
18168
|
+
try {
|
|
18169
|
+
return JSON.parse(s);
|
|
18170
|
+
} catch {
|
|
18171
|
+
return fallback;
|
|
18172
|
+
}
|
|
17853
18173
|
}
|
|
17854
|
-
function
|
|
17855
|
-
|
|
18174
|
+
function parseJsonObject(s) {
|
|
18175
|
+
if (s == null) return void 0;
|
|
18176
|
+
try {
|
|
18177
|
+
const parsed = JSON.parse(s);
|
|
18178
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18179
|
+
} catch {
|
|
18180
|
+
}
|
|
18181
|
+
return void 0;
|
|
18182
|
+
}
|
|
18183
|
+
|
|
18184
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18185
|
+
function allRows(stmt, params) {
|
|
18186
|
+
if (params === void 0) return stmt.all();
|
|
18187
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18188
|
+
return stmt.all(params);
|
|
18189
|
+
}
|
|
18190
|
+
function getRow(stmt, params) {
|
|
18191
|
+
if (params === void 0) return stmt.get();
|
|
18192
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18193
|
+
return stmt.get(params);
|
|
18194
|
+
}
|
|
18195
|
+
function intToBool(raw) {
|
|
18196
|
+
return raw === 1 || raw === true;
|
|
18197
|
+
}
|
|
18198
|
+
function boolToInt(b) {
|
|
18199
|
+
return b ? 1 : 0;
|
|
18200
|
+
}
|
|
18201
|
+
function bindParams(row) {
|
|
18202
|
+
const out = {};
|
|
18203
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18204
|
+
out[key] = value === void 0 ? null : value;
|
|
18205
|
+
}
|
|
18206
|
+
return out;
|
|
18207
|
+
}
|
|
18208
|
+
function countScalar(db, sql, params) {
|
|
18209
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18210
|
+
}
|
|
18211
|
+
function countBy(db, sql, params) {
|
|
18212
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18213
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18214
|
+
map2.set(row.k, row.n);
|
|
18215
|
+
}
|
|
18216
|
+
return map2;
|
|
18217
|
+
}
|
|
18218
|
+
function mapRowsTolerant(rows, map2) {
|
|
18219
|
+
const out = [];
|
|
18220
|
+
for (const row of rows) {
|
|
18221
|
+
try {
|
|
18222
|
+
out.push(map2(row));
|
|
18223
|
+
} catch {
|
|
18224
|
+
}
|
|
18225
|
+
}
|
|
18226
|
+
return out;
|
|
17856
18227
|
}
|
|
17857
18228
|
|
|
17858
18229
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17904,15 +18275,11 @@ function encodeCursor(payload) {
|
|
|
17904
18275
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17905
18276
|
}
|
|
17906
18277
|
function decodeCursor(cursor) {
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
return parsed;
|
|
17911
|
-
}
|
|
17912
|
-
return null;
|
|
17913
|
-
} catch {
|
|
17914
|
-
return null;
|
|
18278
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18279
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18280
|
+
return parsed;
|
|
17915
18281
|
}
|
|
18282
|
+
return null;
|
|
17916
18283
|
}
|
|
17917
18284
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17918
18285
|
session: "session",
|
|
@@ -17929,15 +18296,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17929
18296
|
};
|
|
17930
18297
|
function safeParseStringArray(raw) {
|
|
17931
18298
|
if (!raw) return [];
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17935
|
-
} catch {
|
|
17936
|
-
return [];
|
|
17937
|
-
}
|
|
17938
|
-
}
|
|
17939
|
-
function toBool(raw) {
|
|
17940
|
-
return raw === 1 || raw === true;
|
|
18299
|
+
const parsed = safeJson(raw, null);
|
|
18300
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17941
18301
|
}
|
|
17942
18302
|
function toHarness(raw) {
|
|
17943
18303
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17990,8 +18350,8 @@ function buildAuditEvent(row) {
|
|
|
17990
18350
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17991
18351
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17992
18352
|
targetId: row.target_id,
|
|
17993
|
-
internal:
|
|
17994
|
-
flagged:
|
|
18353
|
+
internal: intToBool(row.internal),
|
|
18354
|
+
flagged: intToBool(row.flagged)
|
|
17995
18355
|
};
|
|
17996
18356
|
}
|
|
17997
18357
|
var TIMELINE_COLUMNS = `
|
|
@@ -18007,6 +18367,10 @@ var TIMELINE_COLUMNS = `
|
|
|
18007
18367
|
json_extract(attributes, '$.internal') AS internal,
|
|
18008
18368
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
18009
18369
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
18370
|
+
var HAS_ACTIVITY = `EXISTS (
|
|
18371
|
+
SELECT 1 FROM audit_events c
|
|
18372
|
+
WHERE c.root_session_id = audit_events.id
|
|
18373
|
+
AND c.event_type NOT IN ('hook', 'config_scan'))`;
|
|
18010
18374
|
var SqliteActivityRepository = class {
|
|
18011
18375
|
constructor(db, now = () => Date.now()) {
|
|
18012
18376
|
this.db = db;
|
|
@@ -18017,12 +18381,16 @@ var SqliteActivityRepository = class {
|
|
|
18017
18381
|
stats(tz) {
|
|
18018
18382
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
18019
18383
|
const { startMs, endMs } = window;
|
|
18020
|
-
const sessionsToday =
|
|
18384
|
+
const sessionsToday = countScalar(
|
|
18385
|
+
this.db,
|
|
18021
18386
|
`SELECT count(*) AS n FROM audit_events
|
|
18022
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
18023
|
-
|
|
18387
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
|
|
18388
|
+
AND ${HAS_ACTIVITY}`,
|
|
18389
|
+
[startMs, endMs]
|
|
18390
|
+
);
|
|
18024
18391
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
18025
|
-
const liveNow =
|
|
18392
|
+
const liveNow = countScalar(
|
|
18393
|
+
this.db,
|
|
18026
18394
|
`SELECT count(*) AS n FROM audit_events s
|
|
18027
18395
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
18028
18396
|
AND max(
|
|
@@ -18031,22 +18399,29 @@ var SqliteActivityRepository = class {
|
|
|
18031
18399
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
18032
18400
|
s.started_at
|
|
18033
18401
|
)
|
|
18034
|
-
) >=
|
|
18035
|
-
|
|
18036
|
-
|
|
18402
|
+
) >= ?`,
|
|
18403
|
+
[liveThreshold]
|
|
18404
|
+
);
|
|
18405
|
+
const toolCallsToday = countScalar(
|
|
18406
|
+
this.db,
|
|
18037
18407
|
`SELECT count(*) AS n FROM audit_events
|
|
18038
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
18039
|
-
|
|
18040
|
-
|
|
18408
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18409
|
+
[startMs, endMs]
|
|
18410
|
+
);
|
|
18411
|
+
const findingsToday = countScalar(
|
|
18412
|
+
this.db,
|
|
18041
18413
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
18042
18414
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18043
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
18044
|
-
|
|
18045
|
-
|
|
18415
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18416
|
+
[startMs, endMs]
|
|
18417
|
+
);
|
|
18418
|
+
const egressToday = countScalar(
|
|
18419
|
+
this.db,
|
|
18046
18420
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18047
18421
|
FROM audit_events
|
|
18048
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
18049
|
-
|
|
18422
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18423
|
+
[startMs, endMs]
|
|
18424
|
+
);
|
|
18050
18425
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
18051
18426
|
}
|
|
18052
18427
|
listSessions(query) {
|
|
@@ -18068,7 +18443,7 @@ var SqliteActivityRepository = class {
|
|
|
18068
18443
|
conditions.push("started_at <= ?");
|
|
18069
18444
|
params.push(toMs);
|
|
18070
18445
|
if (query.q) {
|
|
18071
|
-
const pattern =
|
|
18446
|
+
const pattern = containsPattern(query.q);
|
|
18072
18447
|
conditions.push(
|
|
18073
18448
|
`(content LIKE ? ESCAPE '\\'
|
|
18074
18449
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18082,13 +18457,21 @@ var SqliteActivityRepository = class {
|
|
|
18082
18457
|
);
|
|
18083
18458
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
18084
18459
|
}
|
|
18460
|
+
const emptyCount = countScalar(
|
|
18461
|
+
this.db,
|
|
18462
|
+
`SELECT count(*) AS n FROM audit_events
|
|
18463
|
+
WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
|
|
18464
|
+
params
|
|
18465
|
+
);
|
|
18466
|
+
if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
|
|
18085
18467
|
if (cursor) {
|
|
18086
18468
|
conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
|
|
18087
18469
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18088
18470
|
}
|
|
18089
18471
|
const limit = query.limit;
|
|
18090
|
-
const rows =
|
|
18091
|
-
|
|
18472
|
+
const rows = allRows(
|
|
18473
|
+
this.db.prepare(
|
|
18474
|
+
`SELECT id,
|
|
18092
18475
|
json_extract(attributes, '$.harness') AS harness,
|
|
18093
18476
|
content AS title,
|
|
18094
18477
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18101,7 +18484,9 @@ var SqliteActivityRepository = class {
|
|
|
18101
18484
|
WHERE ${conditions.join(" AND ")}
|
|
18102
18485
|
ORDER BY started_at DESC, id DESC
|
|
18103
18486
|
LIMIT ?`
|
|
18104
|
-
|
|
18487
|
+
),
|
|
18488
|
+
[...params, limit + 1]
|
|
18489
|
+
);
|
|
18105
18490
|
const hasMore = rows.length > limit;
|
|
18106
18491
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18107
18492
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18115,11 +18500,12 @@ var SqliteActivityRepository = class {
|
|
|
18115
18500
|
);
|
|
18116
18501
|
const last = page[page.length - 1];
|
|
18117
18502
|
const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
|
|
18118
|
-
return Promise.resolve({ items, nextCursor });
|
|
18503
|
+
return Promise.resolve({ items, nextCursor, emptyCount });
|
|
18119
18504
|
}
|
|
18120
18505
|
getSession(sessionId) {
|
|
18121
|
-
const rootRow =
|
|
18122
|
-
|
|
18506
|
+
const rootRow = getRow(
|
|
18507
|
+
this.db.prepare(
|
|
18508
|
+
`SELECT id,
|
|
18123
18509
|
json_extract(attributes, '$.harness') AS harness,
|
|
18124
18510
|
content AS title,
|
|
18125
18511
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18136,47 +18522,66 @@ var SqliteActivityRepository = class {
|
|
|
18136
18522
|
FROM audit_events
|
|
18137
18523
|
WHERE id = ? AND event_type = 'session'
|
|
18138
18524
|
LIMIT 1`
|
|
18139
|
-
|
|
18525
|
+
),
|
|
18526
|
+
[sessionId]
|
|
18527
|
+
);
|
|
18140
18528
|
if (!rootRow) return Promise.resolve(null);
|
|
18141
|
-
const timelineRows =
|
|
18142
|
-
|
|
18529
|
+
const timelineRows = allRows(
|
|
18530
|
+
this.db.prepare(
|
|
18531
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18143
18532
|
FROM audit_events
|
|
18144
18533
|
WHERE id = ? OR root_session_id = ?
|
|
18145
18534
|
ORDER BY started_at ASC, id ASC`
|
|
18146
|
-
|
|
18535
|
+
),
|
|
18536
|
+
[sessionId, sessionId]
|
|
18537
|
+
);
|
|
18147
18538
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18148
|
-
const tokenRow =
|
|
18149
|
-
|
|
18539
|
+
const tokenRow = getRow(
|
|
18540
|
+
this.db.prepare(
|
|
18541
|
+
`SELECT
|
|
18150
18542
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18151
18543
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18152
18544
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18153
18545
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18154
18546
|
FROM audit_events
|
|
18155
18547
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18548
|
+
),
|
|
18549
|
+
[sessionId]
|
|
18550
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18551
|
+
const primaryModel = getRow(
|
|
18552
|
+
this.db.prepare(
|
|
18553
|
+
`SELECT model, provider FROM audit_events
|
|
18159
18554
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18160
18555
|
ORDER BY started_at ASC, id ASC
|
|
18161
18556
|
LIMIT 1`
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18557
|
+
),
|
|
18558
|
+
[sessionId]
|
|
18559
|
+
);
|
|
18560
|
+
const toolRows = allRows(
|
|
18561
|
+
this.db.prepare(
|
|
18562
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18165
18563
|
count(*) AS n
|
|
18166
18564
|
FROM audit_events
|
|
18167
18565
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18168
18566
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18567
|
+
),
|
|
18568
|
+
[sessionId]
|
|
18569
|
+
);
|
|
18570
|
+
const modelRows = allRows(
|
|
18571
|
+
this.db.prepare(
|
|
18572
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18172
18573
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18173
18574
|
ORDER BY model`
|
|
18174
|
-
|
|
18575
|
+
),
|
|
18576
|
+
[sessionId]
|
|
18577
|
+
);
|
|
18175
18578
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18176
|
-
const commits =
|
|
18579
|
+
const commits = countScalar(
|
|
18580
|
+
this.db,
|
|
18177
18581
|
`SELECT count(*) AS n FROM audit_events
|
|
18178
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18179
|
-
|
|
18582
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18583
|
+
[sessionId]
|
|
18584
|
+
);
|
|
18180
18585
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18181
18586
|
turns: 0,
|
|
18182
18587
|
findings: 0,
|
|
@@ -18243,7 +18648,10 @@ var SqliteActivityRepository = class {
|
|
|
18243
18648
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18244
18649
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18245
18650
|
);
|
|
18246
|
-
const rows =
|
|
18651
|
+
const rows = allRows(
|
|
18652
|
+
stmt,
|
|
18653
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18654
|
+
);
|
|
18247
18655
|
const seen = /* @__PURE__ */ new Set();
|
|
18248
18656
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18249
18657
|
return Promise.resolve([...seen]);
|
|
@@ -18266,23 +18674,23 @@ var SqliteActivityRepository = class {
|
|
|
18266
18674
|
conditions.push("started_at >= ?");
|
|
18267
18675
|
params.push(opts.fromMs);
|
|
18268
18676
|
}
|
|
18269
|
-
const rows =
|
|
18270
|
-
|
|
18677
|
+
const rows = allRows(
|
|
18678
|
+
this.db.prepare(
|
|
18679
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18271
18680
|
FROM audit_events
|
|
18272
18681
|
WHERE ${conditions.join(" AND ")}`
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
}
|
|
18284
|
-
|
|
18285
|
-
return leaves;
|
|
18682
|
+
),
|
|
18683
|
+
params
|
|
18684
|
+
);
|
|
18685
|
+
return mapRowsTolerant(
|
|
18686
|
+
rows.filter(
|
|
18687
|
+
(row) => row.sessionId !== null
|
|
18688
|
+
),
|
|
18689
|
+
(row) => ({
|
|
18690
|
+
sessionId: row.sessionId,
|
|
18691
|
+
attributes: JSON.parse(row.attributes)
|
|
18692
|
+
})
|
|
18693
|
+
);
|
|
18286
18694
|
}
|
|
18287
18695
|
/**
|
|
18288
18696
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18296,57 +18704,72 @@ var SqliteActivityRepository = class {
|
|
|
18296
18704
|
);
|
|
18297
18705
|
if (sessionIds.length === 0) return result;
|
|
18298
18706
|
const inClause = placeholders(sessionIds.length);
|
|
18299
|
-
const lastActivityRows =
|
|
18300
|
-
|
|
18707
|
+
const lastActivityRows = allRows(
|
|
18708
|
+
this.db.prepare(
|
|
18709
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18301
18710
|
WHERE root_session_id IN (${inClause})
|
|
18302
18711
|
GROUP BY root_session_id`
|
|
18303
|
-
|
|
18712
|
+
),
|
|
18713
|
+
sessionIds
|
|
18714
|
+
);
|
|
18304
18715
|
for (const row of lastActivityRows) {
|
|
18305
18716
|
if (row.id === null) continue;
|
|
18306
18717
|
const entry = result.get(row.id);
|
|
18307
18718
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18308
18719
|
}
|
|
18309
|
-
const turnsRows =
|
|
18310
|
-
|
|
18720
|
+
const turnsRows = allRows(
|
|
18721
|
+
this.db.prepare(
|
|
18722
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18311
18723
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18312
18724
|
GROUP BY root_session_id`
|
|
18313
|
-
|
|
18725
|
+
),
|
|
18726
|
+
sessionIds
|
|
18727
|
+
);
|
|
18314
18728
|
for (const row of turnsRows) {
|
|
18315
18729
|
if (row.id === null) continue;
|
|
18316
18730
|
const entry = result.get(row.id);
|
|
18317
18731
|
if (entry) entry.turns = row.n;
|
|
18318
18732
|
}
|
|
18319
|
-
const runKeyRows =
|
|
18320
|
-
|
|
18733
|
+
const runKeyRows = allRows(
|
|
18734
|
+
this.db.prepare(
|
|
18735
|
+
`SELECT root_session_id AS id,
|
|
18321
18736
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18322
18737
|
FROM audit_events
|
|
18323
18738
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18324
18739
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18325
18740
|
GROUP BY root_session_id`
|
|
18326
|
-
|
|
18741
|
+
),
|
|
18742
|
+
sessionIds
|
|
18743
|
+
);
|
|
18327
18744
|
for (const row of runKeyRows) {
|
|
18328
18745
|
if (row.id === null) continue;
|
|
18329
18746
|
const entry = result.get(row.id);
|
|
18330
18747
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18331
18748
|
}
|
|
18332
|
-
const findingsRows =
|
|
18333
|
-
|
|
18749
|
+
const findingsRows = allRows(
|
|
18750
|
+
this.db.prepare(
|
|
18751
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18334
18752
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18335
18753
|
WHERE e.root_session_id IN (${inClause})
|
|
18336
18754
|
GROUP BY e.root_session_id`
|
|
18337
|
-
|
|
18755
|
+
),
|
|
18756
|
+
sessionIds
|
|
18757
|
+
);
|
|
18338
18758
|
for (const row of findingsRows) {
|
|
18339
18759
|
if (row.id === null) continue;
|
|
18340
18760
|
const entry = result.get(row.id);
|
|
18341
18761
|
if (entry) entry.findings = row.n;
|
|
18342
18762
|
}
|
|
18343
|
-
const sharesRows =
|
|
18344
|
-
|
|
18763
|
+
const sharesRows = allRows(
|
|
18764
|
+
this.db.prepare(
|
|
18765
|
+
`SELECT root_session_id AS id,
|
|
18345
18766
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18346
18767
|
FROM audit_events
|
|
18347
18768
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18348
18769
|
GROUP BY root_session_id`
|
|
18349
|
-
|
|
18770
|
+
),
|
|
18771
|
+
sessionIds
|
|
18772
|
+
);
|
|
18350
18773
|
for (const row of sharesRows) {
|
|
18351
18774
|
if (row.id === null) continue;
|
|
18352
18775
|
const entry = result.get(row.id);
|
|
@@ -18396,33 +18819,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18396
18819
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18397
18820
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18398
18821
|
runInTransaction(fn) {
|
|
18399
|
-
this.db
|
|
18400
|
-
try {
|
|
18401
|
-
fn();
|
|
18402
|
-
this.db.exec("COMMIT");
|
|
18403
|
-
} catch (err) {
|
|
18404
|
-
this.db.exec("ROLLBACK");
|
|
18405
|
-
throw err;
|
|
18406
|
-
}
|
|
18822
|
+
withTransaction(this.db, fn);
|
|
18407
18823
|
}
|
|
18408
18824
|
insertAuditEvent(input) {
|
|
18409
18825
|
const row = toAuditEventRow(input);
|
|
18410
|
-
this.insertStmt.run(
|
|
18411
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
|
|
18415
|
-
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18826
|
+
this.insertStmt.run(
|
|
18827
|
+
bindParams({
|
|
18828
|
+
id: row.id,
|
|
18829
|
+
parentId: row.parentId,
|
|
18830
|
+
rootSessionId: row.rootSessionId,
|
|
18831
|
+
eventType: row.eventType,
|
|
18832
|
+
hostId: row.hostId,
|
|
18833
|
+
harnessId: row.harnessId,
|
|
18834
|
+
sourceProjectId: row.sourceProjectId,
|
|
18835
|
+
startedAt: row.startedAt,
|
|
18836
|
+
endedAt: row.endedAt,
|
|
18837
|
+
severity: row.severity,
|
|
18838
|
+
priority: row.priority,
|
|
18839
|
+
content: row.content,
|
|
18840
|
+
contentHash: row.contentHash,
|
|
18841
|
+
attributes: row.attributes
|
|
18842
|
+
})
|
|
18843
|
+
);
|
|
18426
18844
|
}
|
|
18427
18845
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18428
18846
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18436,22 +18854,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18436
18854
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18437
18855
|
if (!Number.isFinite(startedAt)) return;
|
|
18438
18856
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18439
|
-
this.upsertLlmCallStmt.run(
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
|
|
18450
|
-
|
|
18451
|
-
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18857
|
+
this.upsertLlmCallStmt.run(
|
|
18858
|
+
bindParams({
|
|
18859
|
+
id,
|
|
18860
|
+
parentId: input.parentId,
|
|
18861
|
+
rootSessionId: input.rootSessionId,
|
|
18862
|
+
eventType: "llm_call",
|
|
18863
|
+
hostId: null,
|
|
18864
|
+
harnessId: null,
|
|
18865
|
+
sourceProjectId: null,
|
|
18866
|
+
startedAt,
|
|
18867
|
+
endedAt: null,
|
|
18868
|
+
severity: null,
|
|
18869
|
+
priority: null,
|
|
18870
|
+
content: null,
|
|
18871
|
+
contentHash: null,
|
|
18872
|
+
attributes: JSON.stringify(input.attributes)
|
|
18873
|
+
})
|
|
18874
|
+
);
|
|
18455
18875
|
}
|
|
18456
18876
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18457
18877
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18475,25 +18895,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18475
18895
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18476
18896
|
if (!Number.isFinite(startedAt)) return;
|
|
18477
18897
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18478
|
-
this.insertStmt.run(
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18492
|
-
|
|
18493
|
-
|
|
18898
|
+
this.insertStmt.run(
|
|
18899
|
+
bindParams({
|
|
18900
|
+
id,
|
|
18901
|
+
parentId: input.parentId,
|
|
18902
|
+
rootSessionId: input.rootSessionId,
|
|
18903
|
+
eventType: "tool_call",
|
|
18904
|
+
hostId: null,
|
|
18905
|
+
harnessId: null,
|
|
18906
|
+
sourceProjectId: null,
|
|
18907
|
+
startedAt,
|
|
18908
|
+
endedAt: null,
|
|
18909
|
+
severity: null,
|
|
18910
|
+
priority: null,
|
|
18911
|
+
content: null,
|
|
18912
|
+
contentHash: null,
|
|
18913
|
+
attributes: JSON.stringify(input.attributes)
|
|
18914
|
+
})
|
|
18915
|
+
);
|
|
18494
18916
|
}
|
|
18495
18917
|
findById(id) {
|
|
18496
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18918
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18919
|
+
id
|
|
18920
|
+
});
|
|
18497
18921
|
}
|
|
18498
18922
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18499
18923
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18503,14 +18927,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18503
18927
|
sessionProvider(sessionId) {
|
|
18504
18928
|
const row = this.findById(sessionId);
|
|
18505
18929
|
if (!row?.attributes) return void 0;
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18509
|
-
const provider = parsed.provider;
|
|
18510
|
-
if (typeof provider === "string") return provider;
|
|
18511
|
-
}
|
|
18512
|
-
} catch {
|
|
18513
|
-
}
|
|
18930
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18931
|
+
if (typeof provider === "string") return provider;
|
|
18514
18932
|
return void 0;
|
|
18515
18933
|
}
|
|
18516
18934
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18519,11 +18937,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18519
18937
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18520
18938
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18521
18939
|
llmCallLeaves() {
|
|
18522
|
-
return
|
|
18523
|
-
|
|
18940
|
+
return allRows(
|
|
18941
|
+
this.db.prepare(
|
|
18942
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18524
18943
|
FROM audit_events
|
|
18525
18944
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18526
|
-
|
|
18945
|
+
)
|
|
18946
|
+
);
|
|
18527
18947
|
}
|
|
18528
18948
|
};
|
|
18529
18949
|
|
|
@@ -18542,16 +18962,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18542
18962
|
upsert(input) {
|
|
18543
18963
|
const id = classifiedDataId(input.class);
|
|
18544
18964
|
const row = toClassifiedDataRow(input, id);
|
|
18545
|
-
this.insertStmt.run(
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18965
|
+
this.insertStmt.run(
|
|
18966
|
+
bindParams({
|
|
18967
|
+
id: row.id,
|
|
18968
|
+
class: row.class,
|
|
18969
|
+
label: row.label,
|
|
18970
|
+
attributes: row.attributes
|
|
18971
|
+
})
|
|
18972
|
+
);
|
|
18551
18973
|
return id;
|
|
18552
18974
|
}
|
|
18553
18975
|
};
|
|
18554
18976
|
|
|
18977
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18978
|
+
function latestConfigScan(db) {
|
|
18979
|
+
return getRow(
|
|
18980
|
+
db.prepare(
|
|
18981
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18982
|
+
WHERE event_type = 'config_scan'
|
|
18983
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18984
|
+
)
|
|
18985
|
+
);
|
|
18986
|
+
}
|
|
18987
|
+
|
|
18555
18988
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18556
18989
|
var SqliteConfigInventoryRepository = class {
|
|
18557
18990
|
constructor(db) {
|
|
@@ -18559,7 +18992,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18559
18992
|
}
|
|
18560
18993
|
db;
|
|
18561
18994
|
report() {
|
|
18562
|
-
const scan2 = this.
|
|
18995
|
+
const scan2 = latestConfigScan(this.db);
|
|
18563
18996
|
if (!scan2) {
|
|
18564
18997
|
return {
|
|
18565
18998
|
scannedAt: null,
|
|
@@ -18570,17 +19003,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18570
19003
|
topics: []
|
|
18571
19004
|
};
|
|
18572
19005
|
}
|
|
18573
|
-
const rows =
|
|
18574
|
-
|
|
19006
|
+
const rows = allRows(
|
|
19007
|
+
this.db.prepare(
|
|
19008
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18575
19009
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18576
19010
|
ORDER BY object_type, title`
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
19011
|
+
),
|
|
19012
|
+
{ startedAt: scan2.started_at }
|
|
19013
|
+
);
|
|
19014
|
+
const findings = allRows(
|
|
19015
|
+
this.db.prepare(
|
|
19016
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18580
19017
|
FROM inspection_findings f
|
|
18581
19018
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18582
19019
|
WHERE f.audit_event_id = :scanId`
|
|
18583
|
-
|
|
19020
|
+
),
|
|
19021
|
+
{ scanId: scan2.id }
|
|
19022
|
+
);
|
|
18584
19023
|
const skills = [];
|
|
18585
19024
|
const hooks = [];
|
|
18586
19025
|
const mcpServers = [];
|
|
@@ -18609,7 +19048,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18609
19048
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18610
19049
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18611
19050
|
trustOverrides() {
|
|
18612
|
-
const rows =
|
|
19051
|
+
const rows = allRows(
|
|
19052
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
19053
|
+
);
|
|
18613
19054
|
const map2 = /* @__PURE__ */ new Map();
|
|
18614
19055
|
for (const row of rows) {
|
|
18615
19056
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18618,13 +19059,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18618
19059
|
}
|
|
18619
19060
|
return map2;
|
|
18620
19061
|
}
|
|
18621
|
-
latestScan() {
|
|
18622
|
-
return this.db.prepare(
|
|
18623
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18624
|
-
WHERE event_type = 'config_scan'
|
|
18625
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18626
|
-
).get();
|
|
18627
|
-
}
|
|
18628
19062
|
};
|
|
18629
19063
|
function toSkillItem(row, bag) {
|
|
18630
19064
|
const item = {
|
|
@@ -18735,22 +19169,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18735
19169
|
return topics;
|
|
18736
19170
|
}
|
|
18737
19171
|
function countScanErrors(attributes) {
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
const parsed = JSON.parse(attributes);
|
|
18741
|
-
const errors = parsed?.errors;
|
|
18742
|
-
return typeof errors === "number" ? errors : 0;
|
|
18743
|
-
} catch {
|
|
18744
|
-
return 0;
|
|
18745
|
-
}
|
|
19172
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
19173
|
+
return typeof errors === "number" ? errors : 0;
|
|
18746
19174
|
}
|
|
18747
19175
|
function parseBag(raw) {
|
|
18748
|
-
|
|
18749
|
-
const parsed = JSON.parse(raw);
|
|
18750
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18751
|
-
} catch {
|
|
18752
|
-
}
|
|
18753
|
-
return void 0;
|
|
19176
|
+
return parseJsonObject(raw);
|
|
18754
19177
|
}
|
|
18755
19178
|
function str(value) {
|
|
18756
19179
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18759,12 +19182,7 @@ function str(value) {
|
|
|
18759
19182
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18760
19183
|
var DAY_MS2 = 864e5;
|
|
18761
19184
|
function parseRules(rulesJson) {
|
|
18762
|
-
|
|
18763
|
-
try {
|
|
18764
|
-
raw = JSON.parse(rulesJson);
|
|
18765
|
-
} catch {
|
|
18766
|
-
return [];
|
|
18767
|
-
}
|
|
19185
|
+
const raw = safeJson(rulesJson, []);
|
|
18768
19186
|
if (!Array.isArray(raw)) return [];
|
|
18769
19187
|
const rules = [];
|
|
18770
19188
|
for (const entry of raw) {
|
|
@@ -18783,11 +19201,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18783
19201
|
db;
|
|
18784
19202
|
now;
|
|
18785
19203
|
listDetections(query) {
|
|
18786
|
-
const rows =
|
|
18787
|
-
|
|
19204
|
+
const rows = allRows(
|
|
19205
|
+
this.db.prepare(
|
|
19206
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18788
19207
|
rules_json AS rulesJson
|
|
18789
19208
|
FROM installed_packs`
|
|
18790
|
-
|
|
19209
|
+
)
|
|
19210
|
+
);
|
|
18791
19211
|
const available = this.availableByPack();
|
|
18792
19212
|
const summaries = rows.map((r) => {
|
|
18793
19213
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18796,7 +19216,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18796
19216
|
packId: r.packId,
|
|
18797
19217
|
version: r.version,
|
|
18798
19218
|
name: r.name,
|
|
18799
|
-
enabled: r.enabled
|
|
19219
|
+
enabled: intToBool(r.enabled),
|
|
18800
19220
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18801
19221
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18802
19222
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18813,19 +19233,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18813
19233
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18814
19234
|
// detail call; the table is a handful of rows).
|
|
18815
19235
|
availableByPack() {
|
|
18816
|
-
const rows =
|
|
18817
|
-
|
|
19236
|
+
const rows = allRows(
|
|
19237
|
+
this.db.prepare(
|
|
19238
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18818
19239
|
FROM available_packs`
|
|
18819
|
-
|
|
19240
|
+
)
|
|
19241
|
+
);
|
|
18820
19242
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18821
19243
|
}
|
|
18822
19244
|
getDetectionStats() {
|
|
18823
|
-
const rows =
|
|
19245
|
+
const rows = allRows(
|
|
19246
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19247
|
+
);
|
|
18824
19248
|
let rules = 0;
|
|
18825
19249
|
let active = 0;
|
|
18826
19250
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18827
19251
|
for (const r of rows) {
|
|
18828
|
-
if (r.enabled
|
|
19252
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18829
19253
|
const parsed = parseRules(r.rulesJson);
|
|
18830
19254
|
rules += parsed.length;
|
|
18831
19255
|
for (const rule of parsed) {
|
|
@@ -18843,12 +19267,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18843
19267
|
const parts = splitDetectionId(id);
|
|
18844
19268
|
if (!parts) return Promise.resolve(null);
|
|
18845
19269
|
const { namespace, packId } = parts;
|
|
18846
|
-
const row =
|
|
18847
|
-
|
|
19270
|
+
const row = getRow(
|
|
19271
|
+
this.db.prepare(
|
|
19272
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18848
19273
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18849
19274
|
FROM installed_packs
|
|
18850
19275
|
WHERE namespace = ? AND pack_id = ?`
|
|
18851
|
-
|
|
19276
|
+
),
|
|
19277
|
+
[namespace, packId]
|
|
19278
|
+
);
|
|
18852
19279
|
if (!row) return Promise.resolve(null);
|
|
18853
19280
|
const rules = parseRules(row.rulesJson);
|
|
18854
19281
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18866,7 +19293,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18866
19293
|
packId: row.packId,
|
|
18867
19294
|
version: row.version,
|
|
18868
19295
|
name: row.name,
|
|
18869
|
-
enabled: row.enabled
|
|
19296
|
+
enabled: intToBool(row.enabled),
|
|
18870
19297
|
rules,
|
|
18871
19298
|
updatedAt: new Date(row.updatedAt),
|
|
18872
19299
|
policyId: row.policyId
|
|
@@ -18881,13 +19308,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18881
19308
|
countFindingsLast30d(ruleIds) {
|
|
18882
19309
|
if (ruleIds.length === 0) return 0;
|
|
18883
19310
|
const since = this.now() - 30 * DAY_MS2;
|
|
18884
|
-
const
|
|
18885
|
-
|
|
18886
|
-
|
|
19311
|
+
const inClause = placeholders(ruleIds.length);
|
|
19312
|
+
return countScalar(
|
|
19313
|
+
this.db,
|
|
19314
|
+
`SELECT count(*) AS n
|
|
18887
19315
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18888
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18889
|
-
|
|
18890
|
-
|
|
19316
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19317
|
+
[since, ...ruleIds]
|
|
19318
|
+
);
|
|
18891
19319
|
}
|
|
18892
19320
|
};
|
|
18893
19321
|
|
|
@@ -18904,16 +19332,17 @@ var SqliteEventsRepository = class {
|
|
|
18904
19332
|
insertStmt;
|
|
18905
19333
|
insertEvent(event) {
|
|
18906
19334
|
const row = toEventRow(event);
|
|
18907
|
-
this.insertStmt.run(
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
19335
|
+
this.insertStmt.run(
|
|
19336
|
+
bindParams({
|
|
19337
|
+
id: row.id,
|
|
19338
|
+
sourceTool: row.sourceTool,
|
|
19339
|
+
kind: row.kind,
|
|
19340
|
+
occurredAt: row.occurredAt,
|
|
19341
|
+
contentHash: row.contentHash,
|
|
19342
|
+
content: row.content,
|
|
19343
|
+
metadata: row.metadata
|
|
19344
|
+
})
|
|
19345
|
+
);
|
|
18917
19346
|
}
|
|
18918
19347
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18919
19348
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18921,13 +19350,23 @@ var SqliteEventsRepository = class {
|
|
|
18921
19350
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18922
19351
|
// async EventsReadPort contract.
|
|
18923
19352
|
contentHashes() {
|
|
18924
|
-
const rows =
|
|
19353
|
+
const rows = allRows(
|
|
19354
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19355
|
+
);
|
|
18925
19356
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18926
19357
|
}
|
|
18927
19358
|
};
|
|
18928
19359
|
|
|
18929
19360
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18930
19361
|
import { randomUUID } from "crypto";
|
|
19362
|
+
|
|
19363
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19364
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19365
|
+
function isUniqueConstraintError(err) {
|
|
19366
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19367
|
+
}
|
|
19368
|
+
|
|
19369
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18931
19370
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18932
19371
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18933
19372
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18948,10 +19387,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18948
19387
|
this.name = "AmbiguousExceptionIdError";
|
|
18949
19388
|
}
|
|
18950
19389
|
};
|
|
18951
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18952
|
-
function isUniqueConstraintError(err) {
|
|
18953
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18954
|
-
}
|
|
18955
19390
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18956
19391
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18957
19392
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -19007,10 +19442,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19007
19442
|
this.insertExceptionRow(id, input, now);
|
|
19008
19443
|
} catch (err) {
|
|
19009
19444
|
if (!isUniqueConstraintError(err)) throw err;
|
|
19010
|
-
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19445
|
+
withTransaction(
|
|
19446
|
+
this.db,
|
|
19447
|
+
() => {
|
|
19448
|
+
const superseded = this.db.prepare(
|
|
19449
|
+
`UPDATE exceptions
|
|
19014
19450
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
19015
19451
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
19016
19452
|
updated_at = :now
|
|
@@ -19018,24 +19454,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19018
19454
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
19019
19455
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
19020
19456
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19457
|
+
).run({
|
|
19458
|
+
now,
|
|
19459
|
+
revokedBy: input.createdBy,
|
|
19460
|
+
ruleId: input.ruleId,
|
|
19461
|
+
valueFingerprint: input.valueFingerprint,
|
|
19462
|
+
keyVersion: input.keyVersion
|
|
19463
|
+
});
|
|
19464
|
+
if (Number(superseded.changes) !== 1) {
|
|
19465
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19466
|
+
}
|
|
19467
|
+
this.insertExceptionRow(id, input, now);
|
|
19468
|
+
},
|
|
19469
|
+
"IMMEDIATE"
|
|
19470
|
+
);
|
|
19471
|
+
}
|
|
19472
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19473
|
+
id
|
|
19474
|
+
});
|
|
19475
|
+
if (row === void 0) {
|
|
19476
|
+
throw new Error("exception row not found immediately after insert");
|
|
19037
19477
|
}
|
|
19038
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
19039
19478
|
return parseExceptionRow(row);
|
|
19040
19479
|
}
|
|
19041
19480
|
insertExceptionRow(id, input, now) {
|
|
@@ -19073,14 +19512,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19073
19512
|
*/
|
|
19074
19513
|
list(opts) {
|
|
19075
19514
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19076
|
-
const rows =
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
} catch {
|
|
19082
|
-
}
|
|
19083
|
-
}
|
|
19515
|
+
const rows = allRows(
|
|
19516
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19517
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19518
|
+
);
|
|
19519
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19084
19520
|
return Promise.resolve(exceptions);
|
|
19085
19521
|
}
|
|
19086
19522
|
/**
|
|
@@ -19090,7 +19526,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19090
19526
|
*/
|
|
19091
19527
|
getByIdPrefix(prefix) {
|
|
19092
19528
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19093
|
-
const rows =
|
|
19529
|
+
const rows = allRows(
|
|
19530
|
+
this.db.prepare(
|
|
19531
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19532
|
+
),
|
|
19533
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19534
|
+
);
|
|
19094
19535
|
if (rows.length > 1) {
|
|
19095
19536
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19096
19537
|
}
|
|
@@ -19132,30 +19573,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19132
19573
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19133
19574
|
*/
|
|
19134
19575
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19135
|
-
const rows =
|
|
19136
|
-
|
|
19576
|
+
const rows = allRows(
|
|
19577
|
+
this.db.prepare(
|
|
19578
|
+
`SELECT * FROM exceptions
|
|
19137
19579
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19138
19580
|
ORDER BY created_at DESC, rowid DESC`
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19152
|
-
|
|
19153
|
-
|
|
19154
|
-
|
|
19155
|
-
);
|
|
19156
|
-
} catch {
|
|
19157
|
-
}
|
|
19158
|
-
}
|
|
19581
|
+
),
|
|
19582
|
+
{ keyVersion, now }
|
|
19583
|
+
);
|
|
19584
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19585
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19586
|
+
return ExceptionBundleEntry.parse({
|
|
19587
|
+
id: row.id,
|
|
19588
|
+
ruleId: row.rule_id,
|
|
19589
|
+
valueFingerprint: row.value_fingerprint,
|
|
19590
|
+
keyVersion: row.key_version,
|
|
19591
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19592
|
+
maxUses: row.max_uses,
|
|
19593
|
+
useCount: row.use_count,
|
|
19594
|
+
conditions
|
|
19595
|
+
});
|
|
19596
|
+
});
|
|
19159
19597
|
return Promise.resolve(entries);
|
|
19160
19598
|
}
|
|
19161
19599
|
/**
|
|
@@ -19182,11 +19620,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19182
19620
|
}
|
|
19183
19621
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19184
19622
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19185
|
-
const rows =
|
|
19186
|
-
|
|
19623
|
+
const rows = allRows(
|
|
19624
|
+
this.db.prepare(
|
|
19625
|
+
`SELECT * FROM blocked_detections
|
|
19187
19626
|
WHERE blocked_at > :cutoff
|
|
19188
19627
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19189
|
-
|
|
19628
|
+
),
|
|
19629
|
+
{ cutoff: Date.now() - windowMs }
|
|
19630
|
+
);
|
|
19190
19631
|
return Promise.resolve(
|
|
19191
19632
|
rows.map((row) => ({
|
|
19192
19633
|
reference: row.reference,
|
|
@@ -19266,7 +19707,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19266
19707
|
)`;
|
|
19267
19708
|
|
|
19268
19709
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19269
|
-
var
|
|
19710
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19711
|
+
var CONCAT_SEP = ",";
|
|
19712
|
+
var TUPLE_SEP = "|";
|
|
19713
|
+
function splitConcat(value) {
|
|
19714
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19715
|
+
}
|
|
19270
19716
|
function deriveInstanceStatus(row) {
|
|
19271
19717
|
return deriveFindingStatus({
|
|
19272
19718
|
kind: row.kind,
|
|
@@ -19334,13 +19780,16 @@ var SqliteFindingsRepository = class {
|
|
|
19334
19780
|
}
|
|
19335
19781
|
recentFindings(opts) {
|
|
19336
19782
|
const limit = opts?.limit ?? 50;
|
|
19337
|
-
const rows =
|
|
19338
|
-
|
|
19783
|
+
const rows = allRows(
|
|
19784
|
+
this.db.prepare(
|
|
19785
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19339
19786
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19340
19787
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19341
19788
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19342
19789
|
LIMIT :limit`
|
|
19343
|
-
|
|
19790
|
+
),
|
|
19791
|
+
{ limit }
|
|
19792
|
+
);
|
|
19344
19793
|
return Promise.resolve(
|
|
19345
19794
|
rows.map((r) => ({
|
|
19346
19795
|
id: r.id,
|
|
@@ -19357,26 +19806,94 @@ var SqliteFindingsRepository = class {
|
|
|
19357
19806
|
}))
|
|
19358
19807
|
);
|
|
19359
19808
|
}
|
|
19809
|
+
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19810
|
+
* session-stamped events (served by idx_events_session_id), so the Activity
|
|
19811
|
+
* page can label its findings link without the grouped pipeline. */
|
|
19812
|
+
sessionFindingsCount(sessionId) {
|
|
19813
|
+
if (!sessionId) return Promise.resolve(0);
|
|
19814
|
+
return Promise.resolve(
|
|
19815
|
+
countScalar(
|
|
19816
|
+
this.db,
|
|
19817
|
+
`SELECT count(*) AS n FROM findings f
|
|
19818
|
+
JOIN events e ON e.id = f.event_id
|
|
19819
|
+
WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
|
|
19820
|
+
{ sessionId }
|
|
19821
|
+
)
|
|
19822
|
+
);
|
|
19823
|
+
}
|
|
19824
|
+
/** Per-rule transcript firing tally for one session — reads the OTHER finding
|
|
19825
|
+
* store (inspection_findings, keyed to audit_events): every detection the
|
|
19826
|
+
* transcript pass recorded, counted per firing rather than per unique value.
|
|
19827
|
+
* Rides on session-scoped grouped responses so the findings view can
|
|
19828
|
+
* reconcile the Activity page's tally with the deduped groups it lists. */
|
|
19829
|
+
sessionFirings(sessionId) {
|
|
19830
|
+
return Object.fromEntries(
|
|
19831
|
+
countBy(
|
|
19832
|
+
this.db,
|
|
19833
|
+
`SELECT d.rule_id AS k, count(*) AS n
|
|
19834
|
+
FROM inspection_findings f
|
|
19835
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19836
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19837
|
+
WHERE e.root_session_id = :sessionId
|
|
19838
|
+
GROUP BY d.rule_id`,
|
|
19839
|
+
{ sessionId }
|
|
19840
|
+
)
|
|
19841
|
+
);
|
|
19842
|
+
}
|
|
19360
19843
|
/**
|
|
19361
|
-
* Grouped findings for the dashboard — joins findings⋈events (repo/file
|
|
19362
|
-
* from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19844
|
+
* Grouped findings for the dashboard — joins findings⋈events (repo/file/
|
|
19845
|
+
* toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19363
19846
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19364
19847
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19365
19848
|
* reflect the full filtered set; `items` is the requested
|
|
19366
|
-
* page (default
|
|
19849
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19850
|
+
*
|
|
19851
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19852
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19853
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19854
|
+
* latest, search text);
|
|
19855
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19856
|
+
* populate `instances` for the table's expanded rows.
|
|
19857
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19858
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19859
|
+
* rule is ever restated in SQL.
|
|
19367
19860
|
*/
|
|
19368
19861
|
listGroupedFindings(query) {
|
|
19369
|
-
const
|
|
19370
|
-
|
|
19371
|
-
|
|
19372
|
-
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19862
|
+
const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
|
|
19863
|
+
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19864
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19865
|
+
predicate: sessionPredicate,
|
|
19866
|
+
params: sessionParams
|
|
19867
|
+
});
|
|
19868
|
+
const rows = allRows(
|
|
19869
|
+
this.db.prepare(
|
|
19870
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19871
|
+
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19872
|
+
FROM (
|
|
19873
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19874
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19875
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19876
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19877
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19878
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19879
|
+
json_extract(e.metadata, '$.toolName') AS tool_name,
|
|
19880
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19881
|
+
latest.status AS latest_status,
|
|
19882
|
+
ROW_NUMBER() OVER (
|
|
19883
|
+
PARTITION BY f.rule_id
|
|
19884
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19885
|
+
) AS rn
|
|
19886
|
+
FROM findings f
|
|
19887
|
+
JOIN events e ON e.id = f.event_id
|
|
19888
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19889
|
+
ON latest.finding_key = f.finding_key
|
|
19890
|
+
${sessionPredicate}
|
|
19891
|
+
)
|
|
19892
|
+
WHERE rn <= :cap
|
|
19893
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19894
|
+
),
|
|
19895
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
19896
|
+
);
|
|
19380
19897
|
const groupable = rows.map((r) => ({
|
|
19381
19898
|
id: r.id,
|
|
19382
19899
|
ruleId: r.rule_id,
|
|
@@ -19389,9 +19906,10 @@ var SqliteFindingsRepository = class {
|
|
|
19389
19906
|
sourceTool: r.source_tool,
|
|
19390
19907
|
repo: r.repo ?? "",
|
|
19391
19908
|
file: r.file ?? "",
|
|
19909
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
19392
19910
|
status: deriveInstanceStatus(r)
|
|
19393
19911
|
}));
|
|
19394
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19912
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19395
19913
|
const filterOpts = {
|
|
19396
19914
|
severity: query.severity,
|
|
19397
19915
|
providers: query.provider,
|
|
@@ -19407,44 +19925,134 @@ var SqliteFindingsRepository = class {
|
|
|
19407
19925
|
};
|
|
19408
19926
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19409
19927
|
const items = sorted.slice(0, limit);
|
|
19410
|
-
return Promise.resolve({
|
|
19928
|
+
return Promise.resolve({
|
|
19929
|
+
totals,
|
|
19930
|
+
facets,
|
|
19931
|
+
items,
|
|
19932
|
+
nextCursor: null,
|
|
19933
|
+
...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
|
|
19934
|
+
});
|
|
19935
|
+
}
|
|
19936
|
+
/**
|
|
19937
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19938
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19939
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19940
|
+
*
|
|
19941
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19942
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19943
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19944
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19945
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19946
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19947
|
+
* a group's row stays small however many findings it holds.
|
|
19948
|
+
*
|
|
19949
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19950
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19951
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19952
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19953
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19954
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19955
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19956
|
+
* carries a `q`.
|
|
19957
|
+
*/
|
|
19958
|
+
groupAggregates(withSearchText, scope) {
|
|
19959
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19960
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
|
|
19961
|
+
group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19962
|
+
const rows = this.db.prepare(
|
|
19963
|
+
`SELECT f.rule_id AS rule_id,
|
|
19964
|
+
count(*) AS instance_count,
|
|
19965
|
+
max(e.occurred_at) AS latest_at,
|
|
19966
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19967
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19968
|
+
group_concat(DISTINCT (
|
|
19969
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19970
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19971
|
+
coalesce(latest.status, '')
|
|
19972
|
+
)) AS status_inputs
|
|
19973
|
+
${searchTextColumns}
|
|
19974
|
+
FROM findings f
|
|
19975
|
+
JOIN events e ON e.id = f.event_id
|
|
19976
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19977
|
+
ON latest.finding_key = f.finding_key
|
|
19978
|
+
${scope.predicate}
|
|
19979
|
+
GROUP BY f.rule_id`
|
|
19980
|
+
).all(scope.params);
|
|
19981
|
+
return new Map(
|
|
19982
|
+
rows.map((r) => [
|
|
19983
|
+
r.rule_id,
|
|
19984
|
+
{
|
|
19985
|
+
instanceCount: r.instance_count,
|
|
19986
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19987
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19988
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19989
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19990
|
+
return {
|
|
19991
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19992
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19993
|
+
kind,
|
|
19994
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19995
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19996
|
+
};
|
|
19997
|
+
}),
|
|
19998
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19999
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
20000
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
20001
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
20002
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
20003
|
+
// and skip priming a haystack nothing will read.
|
|
20004
|
+
...withSearchText ? {
|
|
20005
|
+
searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
|
|
20006
|
+
} : {}
|
|
20007
|
+
}
|
|
20008
|
+
])
|
|
20009
|
+
);
|
|
19411
20010
|
}
|
|
19412
20011
|
healthSummary() {
|
|
19413
|
-
const total = this.db
|
|
20012
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19414
20013
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19415
|
-
const grouped =
|
|
20014
|
+
const grouped = allRows(
|
|
20015
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
20016
|
+
);
|
|
19416
20017
|
for (const row of grouped) {
|
|
19417
20018
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19418
20019
|
}
|
|
19419
20020
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19420
|
-
const sevRows =
|
|
19421
|
-
|
|
20021
|
+
const sevRows = allRows(
|
|
20022
|
+
this.db.prepare(
|
|
20023
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19422
20024
|
FROM findings f
|
|
19423
20025
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19424
20026
|
ON latest.finding_key = f.finding_key
|
|
19425
20027
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19426
20028
|
GROUP BY f.severity`
|
|
19427
|
-
|
|
20029
|
+
)
|
|
20030
|
+
);
|
|
19428
20031
|
for (const row of sevRows) {
|
|
19429
20032
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19430
20033
|
}
|
|
19431
20034
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19432
|
-
const enabledRows =
|
|
19433
|
-
|
|
20035
|
+
const enabledRows = allRows(
|
|
20036
|
+
this.db.prepare(
|
|
20037
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19434
20038
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19435
|
-
|
|
20039
|
+
)
|
|
20040
|
+
);
|
|
19436
20041
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19437
20042
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19438
20043
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19439
20044
|
}
|
|
19440
20045
|
activityByDay(days = 7) {
|
|
19441
20046
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19442
|
-
const rows =
|
|
19443
|
-
|
|
20047
|
+
const rows = allRows(
|
|
20048
|
+
this.db.prepare(
|
|
20049
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19444
20050
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19445
20051
|
WHERE e.occurred_at >= :since
|
|
19446
20052
|
GROUP BY day, f.action_taken`
|
|
19447
|
-
|
|
20053
|
+
),
|
|
20054
|
+
{ since }
|
|
20055
|
+
);
|
|
19448
20056
|
const buckets = /* @__PURE__ */ new Map();
|
|
19449
20057
|
for (let i = 0; i < days; i++) {
|
|
19450
20058
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19516,17 +20124,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19516
20124
|
insertStmt;
|
|
19517
20125
|
insertFinding(input) {
|
|
19518
20126
|
const row = toInspectionFindingRow(input);
|
|
19519
|
-
this.insertStmt.run(
|
|
19520
|
-
|
|
19521
|
-
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
20127
|
+
this.insertStmt.run(
|
|
20128
|
+
bindParams({
|
|
20129
|
+
id: row.id,
|
|
20130
|
+
auditEventId: row.auditEventId,
|
|
20131
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
20132
|
+
classifiedDataId: row.classifiedDataId,
|
|
20133
|
+
spanStart: row.spanStart,
|
|
20134
|
+
spanEnd: row.spanEnd,
|
|
20135
|
+
maskedMatch: row.maskedMatch,
|
|
20136
|
+
actionTaken: row.actionTaken,
|
|
20137
|
+
confidence: row.confidence
|
|
20138
|
+
})
|
|
20139
|
+
);
|
|
19530
20140
|
}
|
|
19531
20141
|
};
|
|
19532
20142
|
|
|
@@ -19602,12 +20212,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19602
20212
|
}
|
|
19603
20213
|
function ruleIdsOf(rulesJson) {
|
|
19604
20214
|
const ids = /* @__PURE__ */ new Set();
|
|
19605
|
-
|
|
19606
|
-
try {
|
|
19607
|
-
raw = JSON.parse(rulesJson);
|
|
19608
|
-
} catch {
|
|
19609
|
-
return ids;
|
|
19610
|
-
}
|
|
20215
|
+
const raw = safeJson(rulesJson, []);
|
|
19611
20216
|
if (!Array.isArray(raw)) return ids;
|
|
19612
20217
|
for (const entry of raw) {
|
|
19613
20218
|
if (entry && typeof entry === "object") {
|
|
@@ -19680,47 +20285,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19680
20285
|
}));
|
|
19681
20286
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19682
20287
|
const now = Date.now();
|
|
19683
|
-
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
const
|
|
19689
|
-
|
|
19690
|
-
namespace: row.namespace,
|
|
19691
|
-
packId: row.packId,
|
|
19692
|
-
version: row.version,
|
|
19693
|
-
name: row.name,
|
|
19694
|
-
rulesJson: row.rulesJson,
|
|
19695
|
-
now
|
|
19696
|
-
};
|
|
19697
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19698
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19699
|
-
this.upsertAvailableStmt.run({
|
|
19700
|
-
...params,
|
|
20288
|
+
withTransaction(
|
|
20289
|
+
this.db,
|
|
20290
|
+
() => {
|
|
20291
|
+
const mirror = this.mirrorState();
|
|
20292
|
+
let behind = false;
|
|
20293
|
+
for (const row of rows) {
|
|
20294
|
+
const params = {
|
|
19701
20295
|
id: randomUUID2(),
|
|
19702
|
-
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
|
|
20296
|
+
namespace: row.namespace,
|
|
20297
|
+
packId: row.packId,
|
|
20298
|
+
version: row.version,
|
|
20299
|
+
name: row.name,
|
|
20300
|
+
rulesJson: row.rulesJson,
|
|
20301
|
+
now
|
|
20302
|
+
};
|
|
20303
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20304
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20305
|
+
this.upsertAvailableStmt.run({
|
|
20306
|
+
...params,
|
|
20307
|
+
id: randomUUID2(),
|
|
20308
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20309
|
+
});
|
|
20310
|
+
} else {
|
|
20311
|
+
behind = true;
|
|
20312
|
+
}
|
|
20313
|
+
this.insertMissingStmt.run(params);
|
|
19706
20314
|
}
|
|
19707
|
-
this.
|
|
19708
|
-
}
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
} catch (err) {
|
|
19712
|
-
this.db.exec("ROLLBACK");
|
|
19713
|
-
throw err;
|
|
19714
|
-
}
|
|
20315
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20316
|
+
},
|
|
20317
|
+
"IMMEDIATE"
|
|
20318
|
+
);
|
|
19715
20319
|
} catch {
|
|
19716
20320
|
}
|
|
19717
20321
|
}
|
|
19718
20322
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19719
20323
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19720
20324
|
mirrorState() {
|
|
19721
|
-
const rows =
|
|
19722
|
-
|
|
19723
|
-
|
|
20325
|
+
const rows = allRows(
|
|
20326
|
+
this.db.prepare(
|
|
20327
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20328
|
+
)
|
|
20329
|
+
);
|
|
19724
20330
|
return new Map(
|
|
19725
20331
|
rows.map((r) => [
|
|
19726
20332
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19732,7 +20338,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19732
20338
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19733
20339
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19734
20340
|
pruneAvailable(keep) {
|
|
19735
|
-
const rows =
|
|
20341
|
+
const rows = allRows(
|
|
20342
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20343
|
+
);
|
|
19736
20344
|
const keepSet = new Set(keep);
|
|
19737
20345
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19738
20346
|
for (const r of rows) {
|
|
@@ -19759,11 +20367,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19759
20367
|
if (this.db.isTransaction) {
|
|
19760
20368
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19761
20369
|
}
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
this.db
|
|
19765
|
-
|
|
19766
|
-
|
|
20370
|
+
let changed = false;
|
|
20371
|
+
withTransaction(
|
|
20372
|
+
this.db,
|
|
20373
|
+
() => {
|
|
20374
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20375
|
+
const res = this.db.prepare(
|
|
20376
|
+
`UPDATE installed_packs SET
|
|
19767
20377
|
version = (SELECT a.version FROM available_packs a
|
|
19768
20378
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19769
20379
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19774,17 +20384,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19774
20384
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19775
20385
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19776
20386
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19777
|
-
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
} catch {
|
|
19785
|
-
}
|
|
19786
|
-
throw err;
|
|
19787
|
-
}
|
|
20387
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20388
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20389
|
+
changed = Number(res.changes) > 0;
|
|
20390
|
+
},
|
|
20391
|
+
"IMMEDIATE"
|
|
20392
|
+
);
|
|
20393
|
+
return changed;
|
|
19788
20394
|
}
|
|
19789
20395
|
/**
|
|
19790
20396
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19798,9 +20404,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19798
20404
|
* JSON-level failure therefore counts as invalid.
|
|
19799
20405
|
*/
|
|
19800
20406
|
installedRuleset() {
|
|
19801
|
-
const rows =
|
|
19802
|
-
|
|
19803
|
-
|
|
20407
|
+
const rows = allRows(
|
|
20408
|
+
this.db.prepare(
|
|
20409
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20410
|
+
)
|
|
20411
|
+
);
|
|
19804
20412
|
const out = {
|
|
19805
20413
|
installedPacks: rows.length,
|
|
19806
20414
|
enabledPacks: 0,
|
|
@@ -19809,7 +20417,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19809
20417
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19810
20418
|
};
|
|
19811
20419
|
for (const row of rows) {
|
|
19812
|
-
if (row.enabled
|
|
20420
|
+
if (!intToBool(row.enabled)) continue;
|
|
19813
20421
|
out.enabledPacks += 1;
|
|
19814
20422
|
const action = policyIdToAction(row.policyId);
|
|
19815
20423
|
let raw;
|
|
@@ -19844,9 +20452,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19844
20452
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19845
20453
|
*/
|
|
19846
20454
|
newestRecordedBinary() {
|
|
19847
|
-
const rows =
|
|
19848
|
-
|
|
19849
|
-
|
|
20455
|
+
const rows = allRows(
|
|
20456
|
+
this.db.prepare(
|
|
20457
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20458
|
+
)
|
|
20459
|
+
);
|
|
19850
20460
|
let newest = null;
|
|
19851
20461
|
for (const row of rows) {
|
|
19852
20462
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19861,13 +20471,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19861
20471
|
return newest;
|
|
19862
20472
|
}
|
|
19863
20473
|
counts() {
|
|
19864
|
-
const row =
|
|
19865
|
-
|
|
20474
|
+
const row = getRow(
|
|
20475
|
+
this.db.prepare(
|
|
20476
|
+
`SELECT count(*) AS packs,
|
|
19866
20477
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19867
20478
|
coalesce(sum(enabled), 0) AS enabled
|
|
19868
20479
|
FROM installed_packs`
|
|
19869
|
-
|
|
19870
|
-
|
|
20480
|
+
)
|
|
20481
|
+
);
|
|
20482
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19871
20483
|
}
|
|
19872
20484
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19873
20485
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19880,26 +20492,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19880
20492
|
* attributed to Monitor, matching the Detections views.
|
|
19881
20493
|
*/
|
|
19882
20494
|
countsByPolicyId() {
|
|
19883
|
-
|
|
19884
|
-
|
|
20495
|
+
return countBy(
|
|
20496
|
+
this.db,
|
|
20497
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19885
20498
|
FROM installed_packs
|
|
19886
|
-
GROUP BY
|
|
19887
|
-
)
|
|
19888
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20499
|
+
GROUP BY k`
|
|
20500
|
+
);
|
|
19889
20501
|
}
|
|
19890
20502
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19891
20503
|
listByPolicyId(policyId) {
|
|
19892
|
-
const rows =
|
|
19893
|
-
|
|
20504
|
+
const rows = allRows(
|
|
20505
|
+
this.db.prepare(
|
|
20506
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19894
20507
|
FROM installed_packs
|
|
19895
20508
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19896
20509
|
ORDER BY name ASC`
|
|
19897
|
-
|
|
20510
|
+
),
|
|
20511
|
+
[policyId]
|
|
20512
|
+
);
|
|
19898
20513
|
return rows.map((r) => ({
|
|
19899
20514
|
id: `${r.namespace}/${r.packId}`,
|
|
19900
20515
|
name: r.name,
|
|
19901
20516
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19902
|
-
enabled: r.enabled
|
|
20517
|
+
enabled: intToBool(r.enabled)
|
|
19903
20518
|
}));
|
|
19904
20519
|
}
|
|
19905
20520
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19928,14 +20543,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19928
20543
|
const res = this.db.prepare(
|
|
19929
20544
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19930
20545
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19931
|
-
).run({ enabled: enabled
|
|
20546
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19932
20547
|
return Number(res.changes) > 0;
|
|
19933
20548
|
}
|
|
19934
20549
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19935
20550
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19936
20551
|
// binary's inventory hasn't changed since the last record.
|
|
19937
20552
|
storedSignature() {
|
|
19938
|
-
const rows = this.signatureStmt
|
|
20553
|
+
const rows = allRows(this.signatureStmt);
|
|
19939
20554
|
return inventorySignature(rows);
|
|
19940
20555
|
}
|
|
19941
20556
|
};
|
|
@@ -19963,42 +20578,48 @@ var SqliteInventoryRepository = class {
|
|
|
19963
20578
|
upsert(input, now = Date.now()) {
|
|
19964
20579
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19965
20580
|
const row = toInventoryRow(input, id, now);
|
|
19966
|
-
this.upsertStmt.run(
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
20581
|
+
this.upsertStmt.run(
|
|
20582
|
+
bindParams({
|
|
20583
|
+
id: row.id,
|
|
20584
|
+
objectType: row.objectType,
|
|
20585
|
+
location: row.location,
|
|
20586
|
+
title: row.title,
|
|
20587
|
+
hostId: row.hostId,
|
|
20588
|
+
attributes: row.attributes,
|
|
20589
|
+
firstSeen: row.firstSeen,
|
|
20590
|
+
lastSeen: row.lastSeen
|
|
20591
|
+
})
|
|
20592
|
+
);
|
|
19976
20593
|
return id;
|
|
19977
20594
|
}
|
|
19978
20595
|
// The full row, for round-trip assertions.
|
|
19979
20596
|
findById(id) {
|
|
19980
|
-
|
|
19981
|
-
return row;
|
|
20597
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19982
20598
|
}
|
|
19983
20599
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19984
20600
|
// served from the object_type index, never from audit_events.
|
|
19985
20601
|
distinctTitles(objectType) {
|
|
19986
|
-
const rows =
|
|
19987
|
-
|
|
20602
|
+
const rows = allRows(
|
|
20603
|
+
this.db.prepare(
|
|
20604
|
+
`SELECT DISTINCT title FROM inventory
|
|
19988
20605
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19989
20606
|
ORDER BY title`
|
|
19990
|
-
|
|
20607
|
+
),
|
|
20608
|
+
{ objectType }
|
|
20609
|
+
);
|
|
19991
20610
|
return rows.map((r) => r.title);
|
|
19992
20611
|
}
|
|
19993
20612
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19994
20613
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19995
20614
|
// QUERY PLAN).
|
|
19996
20615
|
osVersions() {
|
|
19997
|
-
const rows =
|
|
19998
|
-
|
|
20616
|
+
const rows = allRows(
|
|
20617
|
+
this.db.prepare(
|
|
20618
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19999
20619
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
20000
20620
|
ORDER BY value`
|
|
20001
|
-
|
|
20621
|
+
)
|
|
20622
|
+
);
|
|
20002
20623
|
return rows.map((r) => r.value);
|
|
20003
20624
|
}
|
|
20004
20625
|
};
|
|
@@ -20018,14 +20639,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
20018
20639
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
20019
20640
|
findingsCount: 0
|
|
20020
20641
|
};
|
|
20021
|
-
function safeJson(s, fallback) {
|
|
20022
|
-
if (s == null) return fallback;
|
|
20023
|
-
try {
|
|
20024
|
-
return JSON.parse(s);
|
|
20025
|
-
} catch {
|
|
20026
|
-
return fallback;
|
|
20027
|
-
}
|
|
20028
|
-
}
|
|
20029
20642
|
function resolveHarnessId(attrs, row) {
|
|
20030
20643
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
20031
20644
|
return attrs.provider;
|
|
@@ -20221,30 +20834,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20221
20834
|
configRowsCache;
|
|
20222
20835
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20223
20836
|
getInventoryStats() {
|
|
20224
|
-
const
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
byType
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
|
|
20837
|
+
const typeCounts = countBy(
|
|
20838
|
+
this.db,
|
|
20839
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20840
|
+
);
|
|
20841
|
+
const byType = {
|
|
20842
|
+
project: 0,
|
|
20843
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20844
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20845
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20846
|
+
config: typeCounts.get("config") ?? 0
|
|
20847
|
+
};
|
|
20848
|
+
byType.project = countScalar(
|
|
20849
|
+
this.db,
|
|
20850
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20851
|
+
);
|
|
20852
|
+
const mcpTrustCounts = countBy(
|
|
20853
|
+
this.db,
|
|
20854
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20232
20855
|
FROM inventory_asset a
|
|
20233
20856
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20234
20857
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20235
20858
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20236
|
-
)
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20859
|
+
);
|
|
20860
|
+
const mcpTrust = {
|
|
20861
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20862
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20863
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20864
|
+
};
|
|
20865
|
+
const harnesses = countScalar(
|
|
20866
|
+
this.db,
|
|
20240
20867
|
`SELECT count(*) AS n FROM inventory
|
|
20241
20868
|
WHERE object_type = 'harness'
|
|
20242
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
const
|
|
20869
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20870
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20871
|
+
);
|
|
20872
|
+
const flaggedAssets = countScalar(
|
|
20873
|
+
this.db,
|
|
20874
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20875
|
+
);
|
|
20876
|
+
const flaggedProjects = countScalar(
|
|
20877
|
+
this.db,
|
|
20246
20878
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20247
|
-
)
|
|
20879
|
+
);
|
|
20248
20880
|
const configRows = this.configAssetRows();
|
|
20249
20881
|
for (const r of configRows) {
|
|
20250
20882
|
byType[r.assetType] += 1;
|
|
@@ -20501,12 +21133,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20501
21133
|
}
|
|
20502
21134
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20503
21135
|
fetchHarnessRows() {
|
|
20504
|
-
return
|
|
20505
|
-
|
|
21136
|
+
return allRows(
|
|
21137
|
+
this.db.prepare(
|
|
21138
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20506
21139
|
FROM inventory
|
|
20507
21140
|
WHERE object_type = 'harness'
|
|
20508
21141
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20509
|
-
|
|
21142
|
+
),
|
|
21143
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
21144
|
+
);
|
|
20510
21145
|
}
|
|
20511
21146
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20512
21147
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20516,12 +21151,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20516
21151
|
const params = [...harnessInvIds];
|
|
20517
21152
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20518
21153
|
if (q) {
|
|
20519
|
-
const pat =
|
|
20520
|
-
where +=
|
|
21154
|
+
const pat = containsPattern(q);
|
|
21155
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20521
21156
|
params.push(pat, pat);
|
|
20522
21157
|
}
|
|
20523
|
-
const rows =
|
|
20524
|
-
|
|
21158
|
+
const rows = allRows(
|
|
21159
|
+
this.db.prepare(
|
|
21160
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20525
21161
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20526
21162
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20527
21163
|
FROM harness_asset ha
|
|
@@ -20529,7 +21165,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20529
21165
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20530
21166
|
WHERE ${where}
|
|
20531
21167
|
ORDER BY a.name ASC`
|
|
20532
|
-
|
|
21168
|
+
),
|
|
21169
|
+
params
|
|
21170
|
+
);
|
|
20533
21171
|
for (const raw of rows) {
|
|
20534
21172
|
const harnessInvId = raw.harnessInvId;
|
|
20535
21173
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20548,21 +21186,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20548
21186
|
params.push(...types);
|
|
20549
21187
|
}
|
|
20550
21188
|
if (q) {
|
|
20551
|
-
const pat =
|
|
20552
|
-
conditions.push("
|
|
21189
|
+
const pat = containsPattern(q);
|
|
21190
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20553
21191
|
params.push(pat, pat);
|
|
20554
21192
|
}
|
|
20555
21193
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20556
21194
|
const sampleRows = this.mapAssetRows(
|
|
20557
|
-
|
|
20558
|
-
|
|
21195
|
+
allRows(
|
|
21196
|
+
this.db.prepare(
|
|
21197
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20559
21198
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20560
21199
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20561
21200
|
FROM inventory_asset a
|
|
20562
21201
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20563
21202
|
${where}
|
|
20564
21203
|
ORDER BY a.name ASC`
|
|
20565
|
-
|
|
21204
|
+
),
|
|
21205
|
+
params
|
|
21206
|
+
)
|
|
20566
21207
|
);
|
|
20567
21208
|
const configRows = this.configAssetRows(q).filter(
|
|
20568
21209
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20571,14 +21212,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20571
21212
|
}
|
|
20572
21213
|
fetchAssetById(assetId) {
|
|
20573
21214
|
const rows = this.mapAssetRows(
|
|
20574
|
-
|
|
20575
|
-
|
|
21215
|
+
allRows(
|
|
21216
|
+
this.db.prepare(
|
|
21217
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20576
21218
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20577
21219
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20578
21220
|
FROM inventory_asset a
|
|
20579
21221
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20580
21222
|
WHERE a.id = ?`
|
|
20581
|
-
|
|
21223
|
+
),
|
|
21224
|
+
[assetId]
|
|
21225
|
+
)
|
|
20582
21226
|
);
|
|
20583
21227
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20584
21228
|
}
|
|
@@ -20634,37 +21278,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20634
21278
|
return rows;
|
|
20635
21279
|
}
|
|
20636
21280
|
latestConfigScanId() {
|
|
20637
|
-
|
|
20638
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20639
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20640
|
-
).get();
|
|
20641
|
-
return row?.id ?? null;
|
|
21281
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20642
21282
|
}
|
|
20643
21283
|
fetchProjects(q) {
|
|
20644
21284
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20645
21285
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20646
21286
|
const params = [];
|
|
20647
21287
|
if (q) {
|
|
20648
|
-
const pat =
|
|
20649
|
-
sql +=
|
|
21288
|
+
const pat = containsPattern(q);
|
|
21289
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20650
21290
|
params.push(pat, pat);
|
|
20651
21291
|
}
|
|
20652
21292
|
sql += " ORDER BY name ASC";
|
|
20653
|
-
return this.db.prepare(sql)
|
|
21293
|
+
return allRows(this.db.prepare(sql), params);
|
|
20654
21294
|
}
|
|
20655
21295
|
fetchProjectById(projectId) {
|
|
20656
|
-
return
|
|
20657
|
-
|
|
20658
|
-
|
|
21296
|
+
return getRow(
|
|
21297
|
+
this.db.prepare(
|
|
21298
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21299
|
+
),
|
|
21300
|
+
[projectId]
|
|
21301
|
+
) ?? null;
|
|
20659
21302
|
}
|
|
20660
21303
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20661
21304
|
fetchProjectsByIds(projectIds) {
|
|
20662
21305
|
const map2 = /* @__PURE__ */ new Map();
|
|
20663
21306
|
if (projectIds.length === 0) return map2;
|
|
20664
|
-
const rows =
|
|
20665
|
-
|
|
21307
|
+
const rows = allRows(
|
|
21308
|
+
this.db.prepare(
|
|
21309
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20666
21310
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20667
|
-
|
|
21311
|
+
),
|
|
21312
|
+
projectIds
|
|
21313
|
+
);
|
|
20668
21314
|
for (const r of rows) map2.set(r.id, r);
|
|
20669
21315
|
return map2;
|
|
20670
21316
|
}
|
|
@@ -20675,8 +21321,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20675
21321
|
projectAggregates(projectIds) {
|
|
20676
21322
|
const map2 = /* @__PURE__ */ new Map();
|
|
20677
21323
|
if (projectIds.length === 0) return map2;
|
|
20678
|
-
const rows =
|
|
20679
|
-
|
|
21324
|
+
const rows = allRows(
|
|
21325
|
+
this.db.prepare(
|
|
21326
|
+
`SELECT f.project_id AS projectId,
|
|
20680
21327
|
coalesce(o.access, f.default_access) AS eff,
|
|
20681
21328
|
count(*) AS n,
|
|
20682
21329
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20684,7 +21331,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20684
21331
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20685
21332
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20686
21333
|
GROUP BY f.project_id, eff`
|
|
20687
|
-
|
|
21334
|
+
),
|
|
21335
|
+
projectIds
|
|
21336
|
+
);
|
|
20688
21337
|
for (const r of rows) {
|
|
20689
21338
|
let agg = map2.get(r.projectId);
|
|
20690
21339
|
if (!agg) {
|
|
@@ -20722,37 +21371,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20722
21371
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20723
21372
|
if (prefix === "") {
|
|
20724
21373
|
return this.mapFileRows(
|
|
20725
|
-
|
|
21374
|
+
allRows(
|
|
21375
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21376
|
+
[projectId]
|
|
21377
|
+
)
|
|
20726
21378
|
);
|
|
20727
21379
|
}
|
|
20728
21380
|
return this.mapFileRows(
|
|
20729
|
-
|
|
20730
|
-
this.
|
|
20731
|
-
|
|
21381
|
+
allRows(
|
|
21382
|
+
this.db.prepare(
|
|
21383
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21384
|
+
),
|
|
21385
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21386
|
+
)
|
|
20732
21387
|
);
|
|
20733
21388
|
}
|
|
20734
21389
|
fetchProjectFilesSearch(projectId, q) {
|
|
20735
|
-
const pat =
|
|
21390
|
+
const pat = containsPattern(q);
|
|
20736
21391
|
return this.mapFileRows(
|
|
20737
|
-
|
|
20738
|
-
this.
|
|
20739
|
-
|
|
20740
|
-
|
|
20741
|
-
|
|
21392
|
+
allRows(
|
|
21393
|
+
this.db.prepare(
|
|
21394
|
+
this.fileSelect(
|
|
21395
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21396
|
+
)
|
|
21397
|
+
),
|
|
21398
|
+
[projectId, pat, pat]
|
|
21399
|
+
)
|
|
20742
21400
|
);
|
|
20743
21401
|
}
|
|
20744
21402
|
fetchProjectFilesBlocked(projectId) {
|
|
20745
21403
|
return this.mapFileRows(
|
|
20746
|
-
|
|
20747
|
-
this.
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
21404
|
+
allRows(
|
|
21405
|
+
this.db.prepare(
|
|
21406
|
+
this.fileSelect(
|
|
21407
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21408
|
+
)
|
|
21409
|
+
),
|
|
21410
|
+
[projectId]
|
|
21411
|
+
)
|
|
20751
21412
|
);
|
|
20752
21413
|
}
|
|
20753
21414
|
fetchProjectFile(projectId, path) {
|
|
20754
21415
|
const rows = this.mapFileRows(
|
|
20755
|
-
|
|
21416
|
+
allRows(
|
|
21417
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21418
|
+
[projectId, path]
|
|
21419
|
+
)
|
|
20756
21420
|
);
|
|
20757
21421
|
return rows[0] ?? null;
|
|
20758
21422
|
}
|
|
@@ -20766,39 +21430,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20766
21430
|
}
|
|
20767
21431
|
db;
|
|
20768
21432
|
readPolicies() {
|
|
20769
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20770
|
-
const policies =
|
|
20771
|
-
|
|
20772
|
-
|
|
20773
|
-
|
|
20774
|
-
|
|
20775
|
-
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20780
|
-
|
|
20781
|
-
|
|
20782
|
-
customKeywords
|
|
20783
|
-
})
|
|
20784
|
-
);
|
|
20785
|
-
} catch {
|
|
20786
|
-
}
|
|
20787
|
-
}
|
|
21433
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21434
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21435
|
+
const target = JSON.parse(row.target);
|
|
21436
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21437
|
+
return Policy.parse({
|
|
21438
|
+
id: row.id,
|
|
21439
|
+
scope: row.scope,
|
|
21440
|
+
target,
|
|
21441
|
+
action: row.action,
|
|
21442
|
+
enabled: intToBool(row.enabled),
|
|
21443
|
+
customKeywords
|
|
21444
|
+
});
|
|
21445
|
+
});
|
|
20788
21446
|
return Promise.resolve(policies);
|
|
20789
21447
|
}
|
|
20790
21448
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20791
21449
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20792
21450
|
// so a user's edits are never clobbered.
|
|
20793
21451
|
seedDefaults() {
|
|
20794
|
-
const count = this.db
|
|
21452
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20795
21453
|
if (count > 0) return;
|
|
20796
21454
|
const stmt = this.db.prepare(
|
|
20797
21455
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20798
21456
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20799
21457
|
);
|
|
20800
|
-
this.db
|
|
20801
|
-
try {
|
|
21458
|
+
failOpenTransaction(this.db, () => {
|
|
20802
21459
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20803
21460
|
stmt.run({
|
|
20804
21461
|
id: randomUUID4(),
|
|
@@ -20807,10 +21464,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20807
21464
|
now: Date.now()
|
|
20808
21465
|
});
|
|
20809
21466
|
}
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
|
|
21467
|
+
});
|
|
21468
|
+
}
|
|
21469
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21470
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21471
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21472
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21473
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21474
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21475
|
+
upsertCategoryAction(category, action) {
|
|
21476
|
+
const now = Date.now();
|
|
21477
|
+
this.db.prepare(
|
|
21478
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21479
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21480
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21481
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21482
|
+
}
|
|
21483
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21484
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21485
|
+
// Returns the number of rows changed.
|
|
21486
|
+
capCategoryActions() {
|
|
21487
|
+
const info = this.db.prepare(
|
|
21488
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21489
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21490
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21491
|
+
).run({ now: Date.now() });
|
|
21492
|
+
return Number(info.changes);
|
|
21493
|
+
}
|
|
21494
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21495
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21496
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21497
|
+
getCategoryAction(category) {
|
|
21498
|
+
const row = this.db.prepare(
|
|
21499
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21500
|
+
).get({ category });
|
|
21501
|
+
return row?.action;
|
|
20814
21502
|
}
|
|
20815
21503
|
};
|
|
20816
21504
|
|
|
@@ -20886,7 +21574,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20886
21574
|
maxStampStmt;
|
|
20887
21575
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20888
21576
|
replaceForProject(projectId, scan2, now) {
|
|
20889
|
-
const
|
|
21577
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20890
21578
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20891
21579
|
for (const file2 of scan2.files) {
|
|
20892
21580
|
this.upsertStmt.run({
|
|
@@ -20969,7 +21657,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20969
21657
|
}
|
|
20970
21658
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20971
21659
|
latestByKey(key) {
|
|
20972
|
-
const row = this.latestStmt
|
|
21660
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20973
21661
|
if (!row) return void 0;
|
|
20974
21662
|
return {
|
|
20975
21663
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20986,7 +21674,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20986
21674
|
* the CLI) surfaces for that file.
|
|
20987
21675
|
*/
|
|
20988
21676
|
openAtRestKeysForPath(path) {
|
|
20989
|
-
const rows = this.openAtRestStmt
|
|
21677
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20990
21678
|
return rows.map((r) => r.finding_key);
|
|
20991
21679
|
}
|
|
20992
21680
|
/**
|
|
@@ -20997,7 +21685,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20997
21685
|
* resolution row (see scan.ts).
|
|
20998
21686
|
*/
|
|
20999
21687
|
resolvedAtRestKeysForPath(path) {
|
|
21000
|
-
const rows = this.resolvedAtRestStmt
|
|
21688
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
21001
21689
|
return rows.map((r) => r.finding_key);
|
|
21002
21690
|
}
|
|
21003
21691
|
};
|
|
@@ -21026,38 +21714,31 @@ var SqliteScanLedgerRepository = class {
|
|
|
21026
21714
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
21027
21715
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
21028
21716
|
entriesForRuleset(rulesetHash) {
|
|
21029
|
-
const rows = this.readStmt
|
|
21717
|
+
const rows = allRows(this.readStmt, {
|
|
21718
|
+
rulesetHash
|
|
21719
|
+
});
|
|
21030
21720
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
21031
21721
|
}
|
|
21032
21722
|
upsertEntries(entries) {
|
|
21033
21723
|
if (entries.length === 0) return;
|
|
21034
21724
|
const scannedAt = Date.now();
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
21038
|
-
|
|
21039
|
-
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
scannedAt
|
|
21045
|
-
});
|
|
21046
|
-
}
|
|
21047
|
-
this.db.exec("COMMIT");
|
|
21048
|
-
} catch (err) {
|
|
21049
|
-
this.db.exec("ROLLBACK");
|
|
21050
|
-
throw err;
|
|
21725
|
+
failOpenTransaction(this.db, () => {
|
|
21726
|
+
for (const entry of entries) {
|
|
21727
|
+
this.upsertStmt.run({
|
|
21728
|
+
path: entry.path,
|
|
21729
|
+
mtime: entry.mtime,
|
|
21730
|
+
contentHash: entry.contentHash,
|
|
21731
|
+
rulesetHash: entry.rulesetHash,
|
|
21732
|
+
scannedAt
|
|
21733
|
+
});
|
|
21051
21734
|
}
|
|
21052
|
-
}
|
|
21053
|
-
}
|
|
21735
|
+
});
|
|
21054
21736
|
}
|
|
21055
21737
|
};
|
|
21056
21738
|
|
|
21057
21739
|
// ../../packages/persistence/src/repositories/security.ts
|
|
21058
21740
|
var DAY_MS4 = 864e5;
|
|
21059
21741
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
21060
|
-
var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
|
|
21061
21742
|
var ACTION_TO_KIND = {
|
|
21062
21743
|
block: "blocked",
|
|
21063
21744
|
redact: "redacted",
|
|
@@ -21072,8 +21753,14 @@ var SCAN_COVERAGE = [
|
|
|
21072
21753
|
{ provider: "copilot", coverage: 0, supported: false },
|
|
21073
21754
|
{ provider: "api", coverage: 0, supported: false }
|
|
21074
21755
|
];
|
|
21756
|
+
var GRANULARITY = {
|
|
21757
|
+
"7d": "day",
|
|
21758
|
+
"30d": "day",
|
|
21759
|
+
"3m": "week",
|
|
21760
|
+
"6m": "week"
|
|
21761
|
+
};
|
|
21075
21762
|
function granularityFor(range) {
|
|
21076
|
-
return range
|
|
21763
|
+
return GRANULARITY[range];
|
|
21077
21764
|
}
|
|
21078
21765
|
function startOfUtcDay2(ms) {
|
|
21079
21766
|
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
@@ -21127,8 +21814,9 @@ var SqliteSecurityRepository = class {
|
|
|
21127
21814
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21128
21815
|
// double-counting a key that accumulated several append-only rows.
|
|
21129
21816
|
severitySummary() {
|
|
21130
|
-
const rows =
|
|
21131
|
-
|
|
21817
|
+
const rows = allRows(
|
|
21818
|
+
this.db.prepare(
|
|
21819
|
+
`SELECT f.severity AS severity,
|
|
21132
21820
|
COUNT(*) AS count,
|
|
21133
21821
|
SUM(CASE
|
|
21134
21822
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21147,7 +21835,8 @@ var SqliteSecurityRepository = class {
|
|
|
21147
21835
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21148
21836
|
ON latest.finding_key = f.finding_key
|
|
21149
21837
|
GROUP BY f.severity`
|
|
21150
|
-
|
|
21838
|
+
)
|
|
21839
|
+
);
|
|
21151
21840
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21152
21841
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21153
21842
|
severity,
|
|
@@ -21231,14 +21920,15 @@ var SqliteSecurityRepository = class {
|
|
|
21231
21920
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21232
21921
|
const now = this.now();
|
|
21233
21922
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21234
|
-
const rows =
|
|
21235
|
-
|
|
21236
|
-
|
|
21237
|
-
|
|
21238
|
-
|
|
21239
|
-
|
|
21240
|
-
|
|
21241
|
-
|
|
21923
|
+
const rows = allRows(
|
|
21924
|
+
this.db.prepare(
|
|
21925
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21926
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21927
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21928
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21929
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21930
|
+
// backfill left null.
|
|
21931
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21242
21932
|
(
|
|
21243
21933
|
SELECT fr.status FROM finding_resolution fr
|
|
21244
21934
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21264,14 +21954,16 @@ var SqliteSecurityRepository = class {
|
|
|
21264
21954
|
WHERE fr.finding_key = f.finding_key
|
|
21265
21955
|
AND fr.resolved_at >= :windowStart
|
|
21266
21956
|
)`
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
|
|
21270
|
-
|
|
21271
|
-
|
|
21272
|
-
|
|
21273
|
-
|
|
21274
|
-
|
|
21957
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21958
|
+
// any resolution activity at/after the window start — a row this method
|
|
21959
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21960
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21961
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21962
|
+
// dialect-agnostic. Without this, a
|
|
21963
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21964
|
+
),
|
|
21965
|
+
{ windowStart }
|
|
21966
|
+
);
|
|
21275
21967
|
const sums = /* @__PURE__ */ new Map();
|
|
21276
21968
|
const counts = /* @__PURE__ */ new Map();
|
|
21277
21969
|
for (const r of rows) {
|
|
@@ -21301,8 +21993,9 @@ var SqliteSecurityRepository = class {
|
|
|
21301
21993
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21302
21994
|
const now = this.now();
|
|
21303
21995
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21304
|
-
const rows =
|
|
21305
|
-
|
|
21996
|
+
const rows = allRows(
|
|
21997
|
+
this.db.prepare(
|
|
21998
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21306
21999
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21307
22000
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21308
22001
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21310,7 +22003,9 @@ var SqliteSecurityRepository = class {
|
|
|
21310
22003
|
GROUP BY repo
|
|
21311
22004
|
ORDER BY c DESC, repo
|
|
21312
22005
|
LIMIT :limit`
|
|
21313
|
-
|
|
22006
|
+
),
|
|
22007
|
+
{ from, to: now, limit }
|
|
22008
|
+
);
|
|
21314
22009
|
const items = rows.map((r) => ({
|
|
21315
22010
|
id: `repo_${r.repo}`,
|
|
21316
22011
|
name: r.repo,
|
|
@@ -21332,8 +22027,9 @@ var SqliteSecurityRepository = class {
|
|
|
21332
22027
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21333
22028
|
// capped at `limit`.
|
|
21334
22029
|
recentlyResolved(limit = 20) {
|
|
21335
|
-
const rows =
|
|
21336
|
-
|
|
22030
|
+
const rows = allRows(
|
|
22031
|
+
this.db.prepare(
|
|
22032
|
+
`SELECT f.finding_key AS finding_key,
|
|
21337
22033
|
f.rule_id AS rule_id,
|
|
21338
22034
|
f.severity AS severity,
|
|
21339
22035
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21367,7 +22063,9 @@ var SqliteSecurityRepository = class {
|
|
|
21367
22063
|
) IS NOT NULL
|
|
21368
22064
|
ORDER BY latest_resolved_at DESC
|
|
21369
22065
|
LIMIT :limit`
|
|
21370
|
-
|
|
22066
|
+
),
|
|
22067
|
+
{ limit }
|
|
22068
|
+
);
|
|
21371
22069
|
const items = rows.map((r) => ({
|
|
21372
22070
|
findingKey: r.finding_key,
|
|
21373
22071
|
ruleId: r.rule_id,
|
|
@@ -21384,12 +22082,15 @@ var SqliteSecurityRepository = class {
|
|
|
21384
22082
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21385
22083
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21386
22084
|
findingsInRange(fromMs, toMs) {
|
|
21387
|
-
const rows =
|
|
21388
|
-
|
|
22085
|
+
const rows = allRows(
|
|
22086
|
+
this.db.prepare(
|
|
22087
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21389
22088
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21390
22089
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21391
22090
|
ORDER BY e.occurred_at`
|
|
21392
|
-
|
|
22091
|
+
),
|
|
22092
|
+
{ from: fromMs, to: toMs }
|
|
22093
|
+
);
|
|
21393
22094
|
return rows.map((r) => ({
|
|
21394
22095
|
occurredAt: r.occurred_at,
|
|
21395
22096
|
severity: r.severity,
|
|
@@ -21403,12 +22104,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21403
22104
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21404
22105
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21405
22106
|
function parseNetwork(networkJson) {
|
|
21406
|
-
|
|
21407
|
-
try {
|
|
21408
|
-
return JSON.parse(networkJson);
|
|
21409
|
-
} catch {
|
|
21410
|
-
return null;
|
|
21411
|
-
}
|
|
22107
|
+
return safeJson(networkJson, null);
|
|
21412
22108
|
}
|
|
21413
22109
|
function toEndpointSummary(row) {
|
|
21414
22110
|
return {
|
|
@@ -21495,27 +22191,39 @@ var SqliteSharesRepository = class {
|
|
|
21495
22191
|
}
|
|
21496
22192
|
db;
|
|
21497
22193
|
stats() {
|
|
21498
|
-
const
|
|
21499
|
-
const
|
|
21500
|
-
const
|
|
21501
|
-
const
|
|
21502
|
-
|
|
22194
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
22195
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
22196
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22197
|
+
const insecure = countScalar(
|
|
22198
|
+
this.db,
|
|
21503
22199
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21504
22200
|
);
|
|
21505
|
-
const needsReview =
|
|
22201
|
+
const needsReview = countScalar(
|
|
22202
|
+
this.db,
|
|
21506
22203
|
`SELECT count(DISTINCT d.id) AS n
|
|
21507
22204
|
FROM share_destination d
|
|
21508
22205
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21509
22206
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21510
22207
|
);
|
|
21511
|
-
const
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
const
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
22208
|
+
const kindCounts = countBy(
|
|
22209
|
+
this.db,
|
|
22210
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
22211
|
+
);
|
|
22212
|
+
const byKind = {
|
|
22213
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
22214
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
22215
|
+
ip: kindCounts.get("ip") ?? 0
|
|
22216
|
+
};
|
|
22217
|
+
const trustCounts = countBy(
|
|
22218
|
+
this.db,
|
|
22219
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
22220
|
+
);
|
|
22221
|
+
const byTrust = {
|
|
22222
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
22223
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
22224
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
22225
|
+
ip: trustCounts.get("ip") ?? 0
|
|
22226
|
+
};
|
|
21519
22227
|
return Promise.resolve({
|
|
21520
22228
|
destinations,
|
|
21521
22229
|
endpoints,
|
|
@@ -21631,7 +22339,7 @@ var SqliteSharesRepository = class {
|
|
|
21631
22339
|
}
|
|
21632
22340
|
let sql;
|
|
21633
22341
|
if (q) {
|
|
21634
|
-
const pattern =
|
|
22342
|
+
const pattern = containsPattern(q);
|
|
21635
22343
|
conditions.push(
|
|
21636
22344
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21637
22345
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21651,24 +22359,31 @@ var SqliteSharesRepository = class {
|
|
|
21651
22359
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21652
22360
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21653
22361
|
}
|
|
21654
|
-
const rows =
|
|
22362
|
+
const rows = allRows(
|
|
22363
|
+
this.db.prepare(sql),
|
|
22364
|
+
params
|
|
22365
|
+
);
|
|
21655
22366
|
return rows.map((r) => this.mapDestRow(r));
|
|
21656
22367
|
}
|
|
21657
22368
|
fetchDestinationById(destinationId) {
|
|
21658
|
-
const row =
|
|
21659
|
-
|
|
22369
|
+
const row = getRow(
|
|
22370
|
+
this.db.prepare(
|
|
22371
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21660
22372
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21661
22373
|
o.decision AS overrideDecision
|
|
21662
22374
|
FROM share_destination d
|
|
21663
22375
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21664
22376
|
WHERE d.id = ?`
|
|
21665
|
-
|
|
22377
|
+
),
|
|
22378
|
+
[destinationId]
|
|
22379
|
+
);
|
|
21666
22380
|
return row ? this.mapDestRow(row) : null;
|
|
21667
22381
|
}
|
|
21668
22382
|
fetchEndpoints(destinationIds) {
|
|
21669
22383
|
if (destinationIds.length === 0) return [];
|
|
21670
|
-
const rows =
|
|
21671
|
-
|
|
22384
|
+
const rows = allRows(
|
|
22385
|
+
this.db.prepare(
|
|
22386
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21672
22387
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21673
22388
|
count(c.id) AS callSiteCount
|
|
21674
22389
|
FROM share_endpoint e
|
|
@@ -21676,7 +22391,9 @@ var SqliteSharesRepository = class {
|
|
|
21676
22391
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21677
22392
|
GROUP BY e.id
|
|
21678
22393
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21679
|
-
|
|
22394
|
+
),
|
|
22395
|
+
destinationIds
|
|
22396
|
+
);
|
|
21680
22397
|
return rows.map((r) => ({
|
|
21681
22398
|
id: r.id,
|
|
21682
22399
|
destinationId: r.destinationId,
|
|
@@ -21701,13 +22418,16 @@ var SqliteSharesRepository = class {
|
|
|
21701
22418
|
}
|
|
21702
22419
|
fetchCallSites(endpointIds) {
|
|
21703
22420
|
if (endpointIds.length === 0) return [];
|
|
21704
|
-
const rows =
|
|
21705
|
-
|
|
22421
|
+
const rows = allRows(
|
|
22422
|
+
this.db.prepare(
|
|
22423
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21706
22424
|
project_id AS projectId
|
|
21707
22425
|
FROM share_call_site
|
|
21708
22426
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21709
22427
|
ORDER BY created_at ASC, id ASC`
|
|
21710
|
-
|
|
22428
|
+
),
|
|
22429
|
+
endpointIds
|
|
22430
|
+
);
|
|
21711
22431
|
return rows.map((r) => ({
|
|
21712
22432
|
id: r.id,
|
|
21713
22433
|
endpointId: r.endpointId,
|
|
@@ -21743,27 +22463,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21743
22463
|
upsert(input, now = Date.now()) {
|
|
21744
22464
|
const id = sourceProjectId(input.url);
|
|
21745
22465
|
const row = toSourceProjectRow(input, id, now);
|
|
21746
|
-
this.upsertStmt.run(
|
|
21747
|
-
|
|
21748
|
-
|
|
21749
|
-
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
|
|
21753
|
-
|
|
22466
|
+
this.upsertStmt.run(
|
|
22467
|
+
bindParams({
|
|
22468
|
+
id: row.id,
|
|
22469
|
+
url: row.url,
|
|
22470
|
+
name: row.name,
|
|
22471
|
+
attributes: row.attributes,
|
|
22472
|
+
firstSeen: row.firstSeen,
|
|
22473
|
+
lastSeen: row.lastSeen
|
|
22474
|
+
})
|
|
22475
|
+
);
|
|
21754
22476
|
return id;
|
|
21755
22477
|
}
|
|
21756
22478
|
findById(id) {
|
|
21757
|
-
return
|
|
22479
|
+
return getRow(
|
|
22480
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22481
|
+
{
|
|
22482
|
+
id
|
|
22483
|
+
}
|
|
22484
|
+
);
|
|
21758
22485
|
}
|
|
21759
22486
|
// Distinct project names — a filter facet, served from the source_project
|
|
21760
22487
|
// table, never from the audit fact table.
|
|
21761
22488
|
distinctNames() {
|
|
21762
|
-
const rows =
|
|
21763
|
-
|
|
22489
|
+
const rows = allRows(
|
|
22490
|
+
this.db.prepare(
|
|
22491
|
+
`SELECT DISTINCT name FROM source_project
|
|
21764
22492
|
WHERE name IS NOT NULL
|
|
21765
22493
|
ORDER BY name`
|
|
21766
|
-
|
|
22494
|
+
)
|
|
22495
|
+
);
|
|
21767
22496
|
return rows.map((r) => r.name);
|
|
21768
22497
|
}
|
|
21769
22498
|
};
|
|
@@ -21782,8 +22511,7 @@ function hasLegacySampleRows(db) {
|
|
|
21782
22511
|
function purgeSampleData(db) {
|
|
21783
22512
|
try {
|
|
21784
22513
|
if (!hasLegacySampleRows(db)) return;
|
|
21785
|
-
db
|
|
21786
|
-
try {
|
|
22514
|
+
withTransaction(db, () => {
|
|
21787
22515
|
db.exec(
|
|
21788
22516
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21789
22517
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21828,11 +22556,7 @@ function purgeSampleData(db) {
|
|
|
21828
22556
|
value TEXT NOT NULL
|
|
21829
22557
|
)`);
|
|
21830
22558
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21831
|
-
|
|
21832
|
-
} catch (err) {
|
|
21833
|
-
db.exec("ROLLBACK");
|
|
21834
|
-
throw err;
|
|
21835
|
-
}
|
|
22559
|
+
});
|
|
21836
22560
|
} catch {
|
|
21837
22561
|
}
|
|
21838
22562
|
}
|
|
@@ -21851,7 +22575,7 @@ function openWithPragmas(file2) {
|
|
|
21851
22575
|
function backupLegacyStore(file2) {
|
|
21852
22576
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21853
22577
|
renameSync(file2, backup);
|
|
21854
|
-
for (const sidecar of
|
|
22578
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21855
22579
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21856
22580
|
}
|
|
21857
22581
|
return backup;
|
|
@@ -21864,9 +22588,8 @@ function openLocalDatabase(dir) {
|
|
|
21864
22588
|
db.close();
|
|
21865
22589
|
const backup = backupLegacyStore(file2);
|
|
21866
22590
|
db = openWithPragmas(file2);
|
|
21867
|
-
|
|
21868
|
-
`
|
|
21869
|
-
`
|
|
22591
|
+
akaWarn(
|
|
22592
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21870
22593
|
);
|
|
21871
22594
|
}
|
|
21872
22595
|
applyMigrations(db);
|
|
@@ -21894,96 +22617,77 @@ function openLocalDatabase(dir) {
|
|
|
21894
22617
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21895
22618
|
policies.seedDefaults();
|
|
21896
22619
|
function recordCapture(event, detected) {
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21903
|
-
db.exec("COMMIT");
|
|
21904
|
-
} catch (err) {
|
|
21905
|
-
db.exec("ROLLBACK");
|
|
21906
|
-
throw err;
|
|
21907
|
-
}
|
|
21908
|
-
} catch {
|
|
21909
|
-
}
|
|
22620
|
+
failOpenTransaction(db, () => {
|
|
22621
|
+
events.insertEvent(event);
|
|
22622
|
+
const sessionId = event.metadata?.sessionId;
|
|
22623
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22624
|
+
});
|
|
21910
22625
|
}
|
|
21911
22626
|
function ensureInventory(ctx) {
|
|
21912
22627
|
const resolved = {};
|
|
21913
|
-
|
|
21914
|
-
|
|
21915
|
-
|
|
21916
|
-
|
|
21917
|
-
|
|
21918
|
-
|
|
21919
|
-
|
|
21920
|
-
|
|
21921
|
-
|
|
21922
|
-
|
|
21923
|
-
|
|
21924
|
-
|
|
21925
|
-
|
|
21926
|
-
|
|
21927
|
-
|
|
21928
|
-
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
db.exec("COMMIT");
|
|
21934
|
-
} catch (err) {
|
|
21935
|
-
db.exec("ROLLBACK");
|
|
21936
|
-
throw err;
|
|
21937
|
-
}
|
|
21938
|
-
} catch {
|
|
21939
|
-
return {};
|
|
21940
|
-
}
|
|
21941
|
-
return resolved;
|
|
22628
|
+
const committed = failOpenTransaction(db, () => {
|
|
22629
|
+
const now = Date.now();
|
|
22630
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22631
|
+
if (ctx.harness) {
|
|
22632
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22633
|
+
}
|
|
22634
|
+
resolved.accountId = inventory.upsert(
|
|
22635
|
+
linkHost(
|
|
22636
|
+
{
|
|
22637
|
+
objectType: "user",
|
|
22638
|
+
identityKey: "local",
|
|
22639
|
+
attributes: { source: "local" }
|
|
22640
|
+
},
|
|
22641
|
+
resolved.hostId
|
|
22642
|
+
),
|
|
22643
|
+
now
|
|
22644
|
+
);
|
|
22645
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22646
|
+
});
|
|
22647
|
+
return committed ? resolved : {};
|
|
21942
22648
|
}
|
|
21943
22649
|
function recordConfigScan(record2) {
|
|
21944
|
-
|
|
21945
|
-
|
|
21946
|
-
|
|
21947
|
-
|
|
21948
|
-
|
|
21949
|
-
|
|
21950
|
-
|
|
21951
|
-
|
|
21952
|
-
|
|
21953
|
-
}
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
21963
|
-
|
|
21964
|
-
confidence: finding.confidence
|
|
21965
|
-
});
|
|
21966
|
-
}
|
|
21967
|
-
db.exec("COMMIT");
|
|
21968
|
-
} catch (err) {
|
|
21969
|
-
db.exec("ROLLBACK");
|
|
21970
|
-
throw err;
|
|
22650
|
+
failOpenTransaction(db, () => {
|
|
22651
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22652
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22653
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22654
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22655
|
+
for (const def of record2.definitions ?? []) {
|
|
22656
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22657
|
+
}
|
|
22658
|
+
for (const finding of record2.findings ?? []) {
|
|
22659
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22660
|
+
if (!definitionId) continue;
|
|
22661
|
+
inspectionFindings.insertFinding({
|
|
22662
|
+
id: randomUUID8(),
|
|
22663
|
+
auditEventId: record2.scanEvent.id,
|
|
22664
|
+
inspectionDefinitionId: definitionId,
|
|
22665
|
+
span: finding.span,
|
|
22666
|
+
maskedMatch: finding.maskedMatch,
|
|
22667
|
+
actionTaken: finding.actionTaken,
|
|
22668
|
+
confidence: finding.confidence
|
|
22669
|
+
});
|
|
21971
22670
|
}
|
|
21972
|
-
}
|
|
21973
|
-
}
|
|
22671
|
+
});
|
|
21974
22672
|
}
|
|
21975
22673
|
function recordProjectFiles(projectId, scan2) {
|
|
21976
22674
|
if (scan2.files.length === 0) return;
|
|
22675
|
+
failOpenTransaction(db, () => {
|
|
22676
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22677
|
+
});
|
|
22678
|
+
}
|
|
22679
|
+
async function transaction(fn) {
|
|
22680
|
+
db.exec("BEGIN");
|
|
21977
22681
|
try {
|
|
21978
|
-
|
|
22682
|
+
const result = await fn();
|
|
22683
|
+
db.exec("COMMIT");
|
|
22684
|
+
return result;
|
|
22685
|
+
} catch (err) {
|
|
21979
22686
|
try {
|
|
21980
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21981
|
-
db.exec("COMMIT");
|
|
21982
|
-
} catch (err) {
|
|
21983
22687
|
db.exec("ROLLBACK");
|
|
21984
|
-
|
|
22688
|
+
} catch {
|
|
21985
22689
|
}
|
|
21986
|
-
|
|
22690
|
+
throw err;
|
|
21987
22691
|
}
|
|
21988
22692
|
}
|
|
21989
22693
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -22002,8 +22706,7 @@ function openLocalDatabase(dir) {
|
|
|
22002
22706
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
22003
22707
|
});
|
|
22004
22708
|
if (stale.length === 0) return;
|
|
22005
|
-
db
|
|
22006
|
-
try {
|
|
22709
|
+
withTransaction(db, () => {
|
|
22007
22710
|
for (const { id } of stale) {
|
|
22008
22711
|
db.prepare(
|
|
22009
22712
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -22015,11 +22718,7 @@ function openLocalDatabase(dir) {
|
|
|
22015
22718
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
22016
22719
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
22017
22720
|
}
|
|
22018
|
-
|
|
22019
|
-
} catch (err) {
|
|
22020
|
-
db.exec("ROLLBACK");
|
|
22021
|
-
throw err;
|
|
22022
|
-
}
|
|
22721
|
+
});
|
|
22023
22722
|
} catch {
|
|
22024
22723
|
}
|
|
22025
22724
|
}
|
|
@@ -22061,6 +22760,7 @@ function openLocalDatabase(dir) {
|
|
|
22061
22760
|
purgeSampleData: () => {
|
|
22062
22761
|
purgeSampleData(db);
|
|
22063
22762
|
},
|
|
22763
|
+
transaction,
|
|
22064
22764
|
close: () => {
|
|
22065
22765
|
db.close();
|
|
22066
22766
|
}
|
|
@@ -22153,12 +22853,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22153
22853
|
}
|
|
22154
22854
|
}
|
|
22155
22855
|
function readJson(file2) {
|
|
22856
|
+
let text;
|
|
22156
22857
|
try {
|
|
22157
|
-
|
|
22158
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22858
|
+
text = readFileSync2(file2, "utf8");
|
|
22159
22859
|
} catch {
|
|
22160
22860
|
return null;
|
|
22161
22861
|
}
|
|
22862
|
+
return parseJsonObject(text) ?? null;
|
|
22863
|
+
}
|
|
22864
|
+
|
|
22865
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22866
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22867
|
+
import { join as join5 } from "path";
|
|
22868
|
+
var MARKER = "warn-era-capped";
|
|
22869
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22870
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22871
|
+
const marker = join5(dataDir2, MARKER);
|
|
22872
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22873
|
+
const capped = db.policies.capCategoryActions();
|
|
22874
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22875
|
+
`, { mode: DATA_FILE_MODE });
|
|
22876
|
+
return { capped };
|
|
22162
22877
|
}
|
|
22163
22878
|
|
|
22164
22879
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22233,23 +22948,30 @@ function resolveProviderSafe() {
|
|
|
22233
22948
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22234
22949
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22235
22950
|
import { homedir as homedir2 } from "os";
|
|
22236
|
-
import { basename as basename2, join as
|
|
22951
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22952
|
+
|
|
22953
|
+
// ../../packages/detections/src/escape-regexp.ts
|
|
22954
|
+
function escapeRegExp(value) {
|
|
22955
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22956
|
+
}
|
|
22957
|
+
|
|
22958
|
+
// ../../packages/detections/src/matchers/limits.ts
|
|
22959
|
+
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22237
22960
|
|
|
22238
22961
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22239
22962
|
var KeywordMatcher2 = class {
|
|
22240
22963
|
match(text, rule) {
|
|
22241
22964
|
if (rule.matcher.type !== "keyword") return [];
|
|
22242
22965
|
const { keywords, caseSensitive } = rule.matcher;
|
|
22243
|
-
const haystack = caseSensitive ? text : text.toLowerCase();
|
|
22244
22966
|
const spans = [];
|
|
22245
22967
|
for (const kw of keywords) {
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
spans.push({ start:
|
|
22252
|
-
|
|
22968
|
+
if (kw.length === 0) continue;
|
|
22969
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22970
|
+
const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
|
|
22971
|
+
let m;
|
|
22972
|
+
while ((m = re.exec(text)) !== null) {
|
|
22973
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
22974
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22253
22975
|
}
|
|
22254
22976
|
}
|
|
22255
22977
|
return spans;
|
|
@@ -22257,7 +22979,6 @@ var KeywordMatcher2 = class {
|
|
|
22257
22979
|
};
|
|
22258
22980
|
|
|
22259
22981
|
// ../../packages/detections/src/matchers/regex.ts
|
|
22260
|
-
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22261
22982
|
var RegexMatcher2 = class {
|
|
22262
22983
|
match(text, rule) {
|
|
22263
22984
|
if (rule.matcher.type !== "regex") return [];
|
|
@@ -22930,7 +23651,6 @@ var db_table_name_default = {
|
|
|
22930
23651
|
"SELECT * FROM ",
|
|
22931
23652
|
"SELECT COUNT(*) FROM ",
|
|
22932
23653
|
"INSERT INTO ",
|
|
22933
|
-
"UPDATE ",
|
|
22934
23654
|
"DELETE FROM ",
|
|
22935
23655
|
"CREATE TABLE ",
|
|
22936
23656
|
"ALTER TABLE ",
|
|
@@ -24367,8 +25087,8 @@ function bundledDetections() {
|
|
|
24367
25087
|
}
|
|
24368
25088
|
|
|
24369
25089
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24370
|
-
import { existsSync as
|
|
24371
|
-
import { basename, dirname, isAbsolute, join as
|
|
25090
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25091
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24372
25092
|
|
|
24373
25093
|
// ../../packages/plugin-sdk/src/events.ts
|
|
24374
25094
|
import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
|
|
@@ -24380,20 +25100,23 @@ import { createHash as createHash4 } from "crypto";
|
|
|
24380
25100
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
24381
25101
|
|
|
24382
25102
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24383
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24384
|
-
import { join as
|
|
25103
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25104
|
+
import { join as join8 } from "path";
|
|
24385
25105
|
|
|
24386
25106
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24387
25107
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24388
|
-
import { existsSync as
|
|
24389
|
-
import { basename as basename3, join as
|
|
25108
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
25109
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
24390
25110
|
|
|
24391
25111
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24392
25112
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
24393
25113
|
|
|
25114
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
25115
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25116
|
+
|
|
24394
25117
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24395
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
24396
|
-
import { join as
|
|
25118
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
25119
|
+
import { join as join10 } from "path";
|
|
24397
25120
|
|
|
24398
25121
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
24399
25122
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -24586,6 +25309,14 @@ var StandaloneDataGateway = class {
|
|
|
24586
25309
|
sweepTerminalExceptions(retentionMs) {
|
|
24587
25310
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
24588
25311
|
}
|
|
25312
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
25313
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
25314
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
25315
|
+
// already-capped one).
|
|
25316
|
+
capWarnEraEnforcement(policyMode) {
|
|
25317
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
25318
|
+
return { capped };
|
|
25319
|
+
}
|
|
24589
25320
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
24590
25321
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
24591
25322
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -24798,6 +25529,11 @@ function fenced(body) {
|
|
|
24798
25529
|
return [fence, body, fence].join("\n");
|
|
24799
25530
|
}
|
|
24800
25531
|
|
|
25532
|
+
// src/command-registry.ts
|
|
25533
|
+
import { readdirSync as readdirSync3 } from "fs";
|
|
25534
|
+
import { fileURLToPath } from "url";
|
|
25535
|
+
var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
|
|
25536
|
+
|
|
24801
25537
|
// src/render.ts
|
|
24802
25538
|
var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
24803
25539
|
var SEVERITY_GLYPH = {
|
|
@@ -24825,6 +25561,7 @@ function shortTime(iso) {
|
|
|
24825
25561
|
function empty(message) {
|
|
24826
25562
|
return message;
|
|
24827
25563
|
}
|
|
25564
|
+
var CATEGORY_ORDER2 = DetectionCategory.options;
|
|
24828
25565
|
function healthScore(summary) {
|
|
24829
25566
|
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
24830
25567
|
const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
|
|
@@ -24841,7 +25578,7 @@ function renderFindings(findings, status, severity) {
|
|
|
24841
25578
|
`${severityGlyph(f.severity)} ${f.severity}`,
|
|
24842
25579
|
f.category,
|
|
24843
25580
|
f.ruleId,
|
|
24844
|
-
f.actionTaken,
|
|
25581
|
+
toApiAction(f.actionTaken),
|
|
24845
25582
|
f.maskedMatch
|
|
24846
25583
|
]);
|
|
24847
25584
|
const heading = severity !== void 0 ? `\u25CF Recent ${severity} findings (${String(findings.length)})` : `\u25CF Recent findings (${String(findings.length)})`;
|
|
@@ -25047,7 +25784,7 @@ function renderAudit(findings) {
|
|
|
25047
25784
|
}
|
|
25048
25785
|
const rows = findings.map((f) => [
|
|
25049
25786
|
shortTime(f.occurredAt),
|
|
25050
|
-
f.actionTaken,
|
|
25787
|
+
toApiAction(f.actionTaken),
|
|
25051
25788
|
f.ruleId,
|
|
25052
25789
|
f.category,
|
|
25053
25790
|
// Join only the parts that are present so a finding missing sourceTool/kind
|