@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/reconcile.js
CHANGED
|
@@ -538,6 +538,10 @@ var SQLITE_MIGRATIONS = [
|
|
|
538
538
|
{
|
|
539
539
|
tag: "0009_findings_path_expression_index",
|
|
540
540
|
sql: "-- Custom migration: partial expression index for the resolver's per-path reads.\n--\n-- openAtRestKeysForPath / resolvedAtRestKeysForPath (and the scanner's tier-3\n-- open-key probe) filter at-rest events by\n-- e.kind = 'code_change' AND json_extract(e.metadata, '$.filePath') = :path\n-- once per changed/deleted file on every scan \u2014 previously a full events scan\n-- per file. json_extract is deterministic, so SQLite allows it in an index;\n-- the WHERE kind = 'code_change' keeps the index to exactly the rows those\n-- queries can match (in-flight events are never path-addressed).\nCREATE INDEX `idx_events_code_change_path` ON `events` (json_extract(`metadata`, '$.filePath')) WHERE `kind` = 'code_change';\n"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
tag: "0010_events_session_expression_index",
|
|
544
|
+
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
541
545
|
}
|
|
542
546
|
];
|
|
543
547
|
|
|
@@ -640,6 +644,12 @@ var defaultCostModel = {
|
|
|
640
644
|
}
|
|
641
645
|
};
|
|
642
646
|
|
|
647
|
+
// ../../packages/schema/src/token/format.ts
|
|
648
|
+
var COMPACT = new Intl.NumberFormat("en-US", {
|
|
649
|
+
notation: "compact",
|
|
650
|
+
maximumFractionDigits: 1
|
|
651
|
+
});
|
|
652
|
+
|
|
643
653
|
// ../../packages/schema/src/token/token-report.ts
|
|
644
654
|
var num = (value) => value ?? 0;
|
|
645
655
|
function costUsageOf(a) {
|
|
@@ -15311,6 +15321,12 @@ var FindingInstance = external_exports.object({
|
|
|
15311
15321
|
provider: FindingProvider,
|
|
15312
15322
|
repo: external_exports.string(),
|
|
15313
15323
|
file: external_exports.string(),
|
|
15324
|
+
// Host tool that produced the scanned text (event metadata's toolName).
|
|
15325
|
+
// Present whenever the capturing hook recorded one — including
|
|
15326
|
+
// file-attributed captures (views prefer `file`); its display value is
|
|
15327
|
+
// the location fallback ("via Bash") when no filePath exists. Absent for
|
|
15328
|
+
// legacy rows and non-tool captures (prompts, worktree scans).
|
|
15329
|
+
toolName: external_exports.string().optional(),
|
|
15314
15330
|
// Effective action: override.action ?? actionTaken, translated to FindingAction.
|
|
15315
15331
|
action: FindingAction,
|
|
15316
15332
|
detectedAt: external_exports.iso.datetime(),
|
|
@@ -15367,6 +15383,9 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15367
15383
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15368
15384
|
action: external_exports.array(FindingAction).optional(),
|
|
15369
15385
|
q: external_exports.string().optional(),
|
|
15386
|
+
// Scope to findings whose event carries this session id (the Activity page's
|
|
15387
|
+
// session → findings drilldown). Findings without a session never match.
|
|
15388
|
+
sessionId: external_exports.string().optional(),
|
|
15370
15389
|
groupBy: external_exports.literal("type").optional(),
|
|
15371
15390
|
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
15372
15391
|
cursor: external_exports.string().optional()
|
|
@@ -15378,7 +15397,13 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
15378
15397
|
}),
|
|
15379
15398
|
facets: FindingFacets,
|
|
15380
15399
|
items: external_exports.array(FindingGroup),
|
|
15381
|
-
nextCursor: external_exports.string().nullable()
|
|
15400
|
+
nextCursor: external_exports.string().nullable(),
|
|
15401
|
+
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
15402
|
+
// many times that rule fired in the session's persisted transcript. Findings
|
|
15403
|
+
// here are deduplicated to unique values while the transcript tally counts
|
|
15404
|
+
// every firing, so the two numbers legitimately differ — this map lets a
|
|
15405
|
+
// session-scoped view show both.
|
|
15406
|
+
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
15382
15407
|
}).meta({ id: "ListGroupedFindingsResponse" });
|
|
15383
15408
|
var ApplyFindingActionRequest = external_exports.object({
|
|
15384
15409
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
@@ -15430,6 +15455,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15430
15455
|
"prompt",
|
|
15431
15456
|
"response",
|
|
15432
15457
|
"code_change",
|
|
15458
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15459
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15460
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15461
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15462
|
+
"tool_use",
|
|
15433
15463
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15434
15464
|
// fact the posture inspection findings reference (findings require an
|
|
15435
15465
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15784,6 +15814,10 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15784
15814
|
from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15785
15815
|
/** Upper bound on startedAt; omitted defaults to now. */
|
|
15786
15816
|
to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15817
|
+
/** Exclude zero-activity sessions — roots whose only recorded children are
|
|
15818
|
+
* bookkeeping rows (hooks, config scans), typically background `claude`
|
|
15819
|
+
* launches. Omitted = list everything. `z.stringbool()` per the note above. */
|
|
15820
|
+
excludeEmpty: external_exports.stringbool().optional(),
|
|
15787
15821
|
/** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
|
|
15788
15822
|
limit: external_exports.coerce.number().int().min(1).max(100).default(50),
|
|
15789
15823
|
/** Opaque pagination cursor (most-recent first). */
|
|
@@ -15792,7 +15826,11 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15792
15826
|
var ListActivitySessionsResponse = external_exports.object({
|
|
15793
15827
|
items: external_exports.array(ActivitySessionSummary),
|
|
15794
15828
|
/** `null` once the last page is reached. */
|
|
15795
|
-
nextCursor: external_exports.string().nullable()
|
|
15829
|
+
nextCursor: external_exports.string().nullable(),
|
|
15830
|
+
/** Zero-activity sessions matching the query's filters/range (whether or
|
|
15831
|
+
* not `excludeEmpty` dropped them from `items`) — the count a UI toggle
|
|
15832
|
+
* shows when collapsing them. */
|
|
15833
|
+
emptyCount: external_exports.number().int().nonnegative()
|
|
15796
15834
|
}).meta({ id: "ListActivitySessionsResponse" });
|
|
15797
15835
|
var ListSessionEventsQuery = external_exports.object({
|
|
15798
15836
|
/** Default 100, range 1–500. */
|
|
@@ -15821,12 +15859,18 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15821
15859
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15822
15860
|
|
|
15823
15861
|
// ../../packages/schema/src/zod/event.ts
|
|
15824
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15862
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15825
15863
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15826
15864
|
var EventMetadata = external_exports.object({
|
|
15827
15865
|
sessionId: external_exports.string().optional(),
|
|
15828
15866
|
repo: external_exports.string().optional(),
|
|
15829
15867
|
filePath: external_exports.string().optional(),
|
|
15868
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
|
|
15869
|
+
// set by the tool-scanning hooks. The tool NAME only — never the tool's
|
|
15870
|
+
// arguments or output, which can carry the very value a finding masked
|
|
15871
|
+
// (metadata is stored unredacted). Gives findings on non-file captures a
|
|
15872
|
+
// display location ("via Bash") when no filePath exists.
|
|
15873
|
+
toolName: external_exports.string().optional(),
|
|
15830
15874
|
// Set (true) by the worktree scanner when the file is excluded by the
|
|
15831
15875
|
// repo's .gitignore. Gitignored files ARE still scanned — local scratch and
|
|
15832
15876
|
// generated code can leak real secrets — but the provenance is recorded so
|
|
@@ -16134,7 +16178,7 @@ var DetectionException = external_exports.object({
|
|
|
16134
16178
|
justification: external_exports.string().min(1),
|
|
16135
16179
|
conditions: ExceptionConditions.nullable(),
|
|
16136
16180
|
createdBy: external_exports.string(),
|
|
16137
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16181
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16138
16182
|
createdAt: external_exports.iso.datetime(),
|
|
16139
16183
|
updatedAt: external_exports.iso.datetime(),
|
|
16140
16184
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16158,7 +16202,10 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16158
16202
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16159
16203
|
var KeywordMatcher = external_exports.object({
|
|
16160
16204
|
type: external_exports.literal("keyword"),
|
|
16161
|
-
|
|
16205
|
+
// An empty keyword matches at every position, yielding one zero-length span
|
|
16206
|
+
// per character. Rejected here because a keyword that matches everything is
|
|
16207
|
+
// never intentional.
|
|
16208
|
+
keywords: external_exports.array(external_exports.string().min(1)).min(1),
|
|
16162
16209
|
caseSensitive: external_exports.boolean().default(false)
|
|
16163
16210
|
});
|
|
16164
16211
|
function isValidRegex(pattern, flags) {
|
|
@@ -16303,20 +16350,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16303
16350
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16304
16351
|
fetchedAt: external_exports.iso.datetime()
|
|
16305
16352
|
}).meta({ id: "PolicyBundle" });
|
|
16306
|
-
var DEFAULT_ACTIONS = {
|
|
16307
|
-
secret: "block",
|
|
16308
|
-
pii: "redact",
|
|
16309
|
-
financial: "redact",
|
|
16310
|
-
phi: "redact",
|
|
16311
|
-
code_context: "warn",
|
|
16312
|
-
code_flaw: "warn",
|
|
16313
|
-
custom: "warn",
|
|
16314
|
-
// Config-posture findings only observe today (they land in
|
|
16315
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16316
|
-
config: "warn"
|
|
16317
|
-
};
|
|
16318
16353
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16319
16354
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16355
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16356
|
+
secret: "critical",
|
|
16357
|
+
financial: "critical",
|
|
16358
|
+
// core-financial/credit-card
|
|
16359
|
+
code_flaw: "critical",
|
|
16360
|
+
pii: "high",
|
|
16361
|
+
phi: "high",
|
|
16362
|
+
custom: "high",
|
|
16363
|
+
// user-defined; conservative
|
|
16364
|
+
code_context: "low",
|
|
16365
|
+
config: "low"
|
|
16366
|
+
// observe-only; floors to monitor regardless
|
|
16367
|
+
};
|
|
16368
|
+
function severityFloorPolicy(category) {
|
|
16369
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16370
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16371
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16372
|
+
}
|
|
16320
16373
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16321
16374
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16322
16375
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16343,6 +16396,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16343
16396
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16344
16397
|
}
|
|
16345
16398
|
};
|
|
16399
|
+
function builtinPolicyToAction(id) {
|
|
16400
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16401
|
+
}
|
|
16402
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16403
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16404
|
+
);
|
|
16346
16405
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16347
16406
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16348
16407
|
);
|
|
@@ -16395,6 +16454,10 @@ var ListEventsResponse = external_exports.object({
|
|
|
16395
16454
|
items: external_exports.array(Event),
|
|
16396
16455
|
nextCursor: external_exports.string().nullable()
|
|
16397
16456
|
}).meta({ id: "ListEventsResponse" });
|
|
16457
|
+
var IngestResponse = external_exports.object({
|
|
16458
|
+
accepted: external_exports.number().int().nonnegative(),
|
|
16459
|
+
duplicates: external_exports.number().int().nonnegative()
|
|
16460
|
+
}).meta({ id: "IngestResponse" });
|
|
16398
16461
|
var ListFindingsQuery = external_exports.object({
|
|
16399
16462
|
cursor: external_exports.string().optional(),
|
|
16400
16463
|
limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
|
|
@@ -16749,10 +16812,8 @@ function toApiProvider(sourceTool) {
|
|
|
16749
16812
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16750
16813
|
}
|
|
16751
16814
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16752
|
-
function
|
|
16753
|
-
const statuses = new Set(
|
|
16754
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16755
|
-
);
|
|
16815
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16816
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16756
16817
|
if (statuses.size === 0) return void 0;
|
|
16757
16818
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16758
16819
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16770,6 +16831,7 @@ function deriveFindingStatus(row) {
|
|
|
16770
16831
|
function buildFindingGroups(rows, opts = {}) {
|
|
16771
16832
|
const overrides = opts.overrides;
|
|
16772
16833
|
const packNames = opts.packNames;
|
|
16834
|
+
const aggregates = opts.aggregates;
|
|
16773
16835
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16774
16836
|
for (const row of rows) {
|
|
16775
16837
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16785,23 +16847,27 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16785
16847
|
provider: toApiProvider(r.sourceTool),
|
|
16786
16848
|
repo: r.repo,
|
|
16787
16849
|
file: r.file,
|
|
16850
|
+
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
16788
16851
|
action: toApiAction(effectiveDbAction),
|
|
16789
16852
|
detectedAt: r.occurredAt,
|
|
16790
16853
|
confidence: r.confidence,
|
|
16791
16854
|
status: r.status
|
|
16792
16855
|
};
|
|
16793
16856
|
});
|
|
16794
|
-
const
|
|
16857
|
+
const agg = aggregates?.get(ruleId);
|
|
16858
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16795
16859
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16796
16860
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16797
16861
|
);
|
|
16798
16862
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16799
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16863
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16800
16864
|
if (seenProviders.has(p)) return false;
|
|
16801
16865
|
seenProviders.add(p);
|
|
16802
16866
|
return true;
|
|
16803
16867
|
});
|
|
16804
|
-
const actionSet = new Set(
|
|
16868
|
+
const actionSet = new Set(
|
|
16869
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16870
|
+
);
|
|
16805
16871
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16806
16872
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16807
16873
|
const detection = {
|
|
@@ -16815,8 +16881,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16815
16881
|
contextPrefix: ""
|
|
16816
16882
|
// empty (pending privacy review)
|
|
16817
16883
|
};
|
|
16818
|
-
const status =
|
|
16819
|
-
|
|
16884
|
+
const status = foldGroupStatus(
|
|
16885
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16886
|
+
);
|
|
16887
|
+
const group = {
|
|
16820
16888
|
id: ruleId,
|
|
16821
16889
|
category: apiCategory,
|
|
16822
16890
|
subtype: ruleId,
|
|
@@ -16825,21 +16893,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16825
16893
|
match,
|
|
16826
16894
|
detection,
|
|
16827
16895
|
policy,
|
|
16828
|
-
instanceCount: instances.length,
|
|
16896
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16829
16897
|
providers,
|
|
16830
16898
|
aggregateAction,
|
|
16831
16899
|
latestDetectedAt,
|
|
16832
16900
|
instances,
|
|
16833
16901
|
status
|
|
16834
|
-
}
|
|
16902
|
+
};
|
|
16903
|
+
if (agg) {
|
|
16904
|
+
actionsCache.set(group, [...actionSet]);
|
|
16905
|
+
if (agg.searchText !== void 0) {
|
|
16906
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16907
|
+
}
|
|
16908
|
+
}
|
|
16909
|
+
groups.push(group);
|
|
16835
16910
|
}
|
|
16836
16911
|
return groups;
|
|
16837
16912
|
}
|
|
16838
16913
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16839
|
-
function
|
|
16840
|
-
|
|
16841
|
-
if (cached2 !== void 0) return cached2;
|
|
16842
|
-
const haystack = [
|
|
16914
|
+
function buildHaystack(g, extra) {
|
|
16915
|
+
return [
|
|
16843
16916
|
g.subtype,
|
|
16844
16917
|
g.category,
|
|
16845
16918
|
g.match.maskedValue,
|
|
@@ -16847,11 +16920,26 @@ function groupHaystack(g) {
|
|
|
16847
16920
|
g.id,
|
|
16848
16921
|
...g.instances.map((i) => i.repo),
|
|
16849
16922
|
...g.instances.map((i) => i.file),
|
|
16850
|
-
...g.instances.map((i) => i.
|
|
16923
|
+
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
16924
|
+
...g.instances.map((i) => i.id),
|
|
16925
|
+
...extra === void 0 ? [] : [extra]
|
|
16851
16926
|
].join(" ").toLowerCase();
|
|
16927
|
+
}
|
|
16928
|
+
function groupHaystack(g) {
|
|
16929
|
+
const cached2 = haystackCache.get(g);
|
|
16930
|
+
if (cached2 !== void 0) return cached2;
|
|
16931
|
+
const haystack = buildHaystack(g);
|
|
16852
16932
|
haystackCache.set(g, haystack);
|
|
16853
16933
|
return haystack;
|
|
16854
16934
|
}
|
|
16935
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16936
|
+
function groupActions(g) {
|
|
16937
|
+
const cached2 = actionsCache.get(g);
|
|
16938
|
+
if (cached2 !== void 0) return cached2;
|
|
16939
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16940
|
+
actionsCache.set(g, actions);
|
|
16941
|
+
return actions;
|
|
16942
|
+
}
|
|
16855
16943
|
function applyFindingFilters(groups, opts) {
|
|
16856
16944
|
let filtered = groups;
|
|
16857
16945
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16864,7 +16952,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16864
16952
|
}
|
|
16865
16953
|
if (opts.actions && opts.actions.length > 0) {
|
|
16866
16954
|
const actionSet = new Set(opts.actions);
|
|
16867
|
-
filtered = filtered.filter((g) => g.
|
|
16955
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16868
16956
|
}
|
|
16869
16957
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16870
16958
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16916,8 +17004,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16916
17004
|
});
|
|
16917
17005
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16918
17006
|
for (const g of forAction) {
|
|
16919
|
-
const
|
|
16920
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17007
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16921
17008
|
}
|
|
16922
17009
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16923
17010
|
providers: opts.providers,
|
|
@@ -17100,6 +17187,71 @@ var ProjectFilesScan = external_exports.object({
|
|
|
17100
17187
|
scannedAt: external_exports.string()
|
|
17101
17188
|
});
|
|
17102
17189
|
|
|
17190
|
+
// ../../packages/schema/src/zod/ranges.ts
|
|
17191
|
+
var TIME_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17192
|
+
var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
|
|
17193
|
+
var DEFAULT_TIME_RANGE = "7d";
|
|
17194
|
+
var RANGE_DAYS = {
|
|
17195
|
+
"7d": 7,
|
|
17196
|
+
"30d": 30,
|
|
17197
|
+
"3m": 90,
|
|
17198
|
+
"6m": 180
|
|
17199
|
+
};
|
|
17200
|
+
var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
|
|
17201
|
+
|
|
17202
|
+
// ../../packages/schema/src/zod/remediation.ts
|
|
17203
|
+
var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
|
|
17204
|
+
var MaskedFindingLocation = external_exports.object({
|
|
17205
|
+
filePath: external_exports.string(),
|
|
17206
|
+
span: Span.optional()
|
|
17207
|
+
}).strict();
|
|
17208
|
+
var MaskedSecretFinding = external_exports.object({
|
|
17209
|
+
provider: external_exports.string(),
|
|
17210
|
+
maskedToken: external_exports.string(),
|
|
17211
|
+
where: MaskedFindingLocation,
|
|
17212
|
+
state: SecretFindingState,
|
|
17213
|
+
observedAt: external_exports.iso.datetime().optional()
|
|
17214
|
+
}).strict();
|
|
17215
|
+
var RotationChecklistEntry = external_exports.object({
|
|
17216
|
+
provider: external_exports.string(),
|
|
17217
|
+
maskedToken: external_exports.string(),
|
|
17218
|
+
consolePath: external_exports.string(),
|
|
17219
|
+
occurrenceSpread: external_exports.number().int().positive()
|
|
17220
|
+
}).strict();
|
|
17221
|
+
var RemediationOption = external_exports.enum([
|
|
17222
|
+
"redact-rotation-checklist",
|
|
17223
|
+
"redact-only",
|
|
17224
|
+
"set-secret-redact",
|
|
17225
|
+
"leave"
|
|
17226
|
+
]);
|
|
17227
|
+
var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
|
|
17228
|
+
var RemediationEntryContext = external_exports.object({
|
|
17229
|
+
entrySource: RemediationEntrySource
|
|
17230
|
+
}).strict();
|
|
17231
|
+
var RemediationOptionChoice = external_exports.object({
|
|
17232
|
+
id: RemediationOption,
|
|
17233
|
+
label: external_exports.string()
|
|
17234
|
+
});
|
|
17235
|
+
var BatchedRemediationDecision = external_exports.object({
|
|
17236
|
+
kind: external_exports.literal("decision"),
|
|
17237
|
+
entrySource: RemediationEntrySource,
|
|
17238
|
+
secretCount: external_exports.number().int().positive(),
|
|
17239
|
+
prompt: external_exports.string(),
|
|
17240
|
+
options: external_exports.tuple([
|
|
17241
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
|
|
17242
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
|
|
17243
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
|
|
17244
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
|
|
17245
|
+
])
|
|
17246
|
+
});
|
|
17247
|
+
var NoRemediationDecision = external_exports.object({
|
|
17248
|
+
kind: external_exports.literal("no-decision")
|
|
17249
|
+
});
|
|
17250
|
+
var BatchedRemediation = external_exports.discriminatedUnion("kind", [
|
|
17251
|
+
BatchedRemediationDecision,
|
|
17252
|
+
NoRemediationDecision
|
|
17253
|
+
]);
|
|
17254
|
+
|
|
17103
17255
|
// ../../packages/schema/src/zod/rule-test.ts
|
|
17104
17256
|
var TestRulesRequest = external_exports.object({
|
|
17105
17257
|
rules: external_exports.array(Rule).min(1).max(100),
|
|
@@ -17158,10 +17310,8 @@ var SeveritySummaryResponse = external_exports.object({
|
|
|
17158
17310
|
// All four severity levels are always present (count may be 0).
|
|
17159
17311
|
bySeverity: external_exports.array(SeveritySummaryItem)
|
|
17160
17312
|
}).meta({ id: "SeveritySummaryResponse" });
|
|
17161
|
-
var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17162
|
-
var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
|
|
17163
17313
|
var SecurityRangeQuery = external_exports.object({
|
|
17164
|
-
range: external_exports.enum(
|
|
17314
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
|
|
17165
17315
|
});
|
|
17166
17316
|
var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
|
|
17167
17317
|
var EnforcementAction = external_exports.object({
|
|
@@ -17171,7 +17321,7 @@ var EnforcementAction = external_exports.object({
|
|
|
17171
17321
|
delta: external_exports.number().int()
|
|
17172
17322
|
}).meta({ id: "EnforcementAction" });
|
|
17173
17323
|
var EnforcementActionsResponse = external_exports.object({
|
|
17174
|
-
range:
|
|
17324
|
+
range: TimeRange,
|
|
17175
17325
|
// Sum of actions[].count in the window.
|
|
17176
17326
|
total: external_exports.number().int().nonnegative(),
|
|
17177
17327
|
// One entry per kind, always all three present (count may be 0).
|
|
@@ -17186,7 +17336,7 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
17186
17336
|
medium: external_exports.number().int().nonnegative()
|
|
17187
17337
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
17188
17338
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
17189
|
-
range:
|
|
17339
|
+
range: TimeRange,
|
|
17190
17340
|
granularity: TimeseriesGranularity,
|
|
17191
17341
|
points: external_exports.array(FindingsTimeseriesPoint)
|
|
17192
17342
|
}).meta({ id: "FindingsTimeseriesResponse" });
|
|
@@ -17201,7 +17351,7 @@ var MttrTrendPoint = external_exports.object({
|
|
|
17201
17351
|
})
|
|
17202
17352
|
}).meta({ id: "MttrTrendPoint" });
|
|
17203
17353
|
var MttrTrendResponse = external_exports.object({
|
|
17204
|
-
range:
|
|
17354
|
+
range: TimeRange,
|
|
17205
17355
|
granularity: TimeseriesGranularity,
|
|
17206
17356
|
points: external_exports.array(MttrTrendPoint)
|
|
17207
17357
|
}).meta({ id: "MttrTrendResponse" });
|
|
@@ -17228,11 +17378,11 @@ var TopSource = external_exports.object({
|
|
|
17228
17378
|
findingsCount: external_exports.number().int().nonnegative()
|
|
17229
17379
|
}).meta({ id: "TopSource" });
|
|
17230
17380
|
var TopSourcesResponse = external_exports.object({
|
|
17231
|
-
range:
|
|
17381
|
+
range: TimeRange,
|
|
17232
17382
|
items: external_exports.array(TopSource)
|
|
17233
17383
|
}).meta({ id: "TopSourcesResponse" });
|
|
17234
17384
|
var TopSourcesQuery = external_exports.object({
|
|
17235
|
-
range: external_exports.enum(
|
|
17385
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
|
|
17236
17386
|
limit: external_exports.coerce.number().int().min(1).max(50).default(5),
|
|
17237
17387
|
// Omit for both kinds.
|
|
17238
17388
|
kind: external_exports.enum(SOURCE_KINDS).optional()
|
|
@@ -17245,7 +17395,7 @@ var ScanCoverageProvider = external_exports.object({
|
|
|
17245
17395
|
supported: external_exports.boolean()
|
|
17246
17396
|
}).meta({ id: "ScanCoverageProvider" });
|
|
17247
17397
|
var ScanCoverageResponse = external_exports.object({
|
|
17248
|
-
range:
|
|
17398
|
+
range: TimeRange,
|
|
17249
17399
|
providers: external_exports.array(ScanCoverageProvider)
|
|
17250
17400
|
}).meta({ id: "ScanCoverageResponse" });
|
|
17251
17401
|
var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
|
|
@@ -17294,6 +17444,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
|
|
|
17294
17444
|
var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
|
|
17295
17445
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
17296
17446
|
|
|
17447
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17448
|
+
var TriageHit = external_exports.object({
|
|
17449
|
+
ruleId: external_exports.string(),
|
|
17450
|
+
category: DetectionCategory,
|
|
17451
|
+
severity: Severity,
|
|
17452
|
+
maskedMatch: external_exports.string(),
|
|
17453
|
+
rawMatch: external_exports.string(),
|
|
17454
|
+
context: external_exports.string(),
|
|
17455
|
+
filePath: external_exports.string().optional(),
|
|
17456
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17457
|
+
id: external_exports.string().optional(),
|
|
17458
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17459
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17460
|
+
});
|
|
17461
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17462
|
+
var TriageCategoryRec = external_exports.object({
|
|
17463
|
+
category: DetectionCategory,
|
|
17464
|
+
action: TriagePolicy,
|
|
17465
|
+
reasoning: external_exports.string(),
|
|
17466
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17467
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17468
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17469
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17470
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17471
|
+
});
|
|
17472
|
+
var TriageRecommendation = external_exports.object({
|
|
17473
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17474
|
+
notes: external_exports.string()
|
|
17475
|
+
});
|
|
17476
|
+
|
|
17477
|
+
// ../../packages/schema/src/zod/setup-frame.ts
|
|
17478
|
+
var CalibrationCounts = external_exports.object({
|
|
17479
|
+
total: external_exports.number().int().nonnegative(),
|
|
17480
|
+
important: external_exports.number().int().nonnegative(),
|
|
17481
|
+
routine: external_exports.number().int().nonnegative()
|
|
17482
|
+
}).refine((c) => c.total === c.important + c.routine, {
|
|
17483
|
+
message: "total must equal important + routine",
|
|
17484
|
+
path: ["total"]
|
|
17485
|
+
});
|
|
17486
|
+
var FalsePositivePatternValue = external_exports.object({
|
|
17487
|
+
ruleId: external_exports.string(),
|
|
17488
|
+
category: DetectionCategory,
|
|
17489
|
+
valueFingerprint: external_exports.string(),
|
|
17490
|
+
keyVersion: external_exports.number().int().nonnegative()
|
|
17491
|
+
});
|
|
17492
|
+
var FalsePositivePatternGroup = external_exports.object({
|
|
17493
|
+
pattern: external_exports.string(),
|
|
17494
|
+
count: external_exports.number().int().nonnegative(),
|
|
17495
|
+
values: external_exports.array(FalsePositivePatternValue).min(1)
|
|
17496
|
+
});
|
|
17497
|
+
var CalibrationFindingKind = external_exports.object({
|
|
17498
|
+
category: DetectionCategory,
|
|
17499
|
+
count: external_exports.number().int().nonnegative(),
|
|
17500
|
+
egress: external_exports.boolean()
|
|
17501
|
+
});
|
|
17502
|
+
var CalibrationFrame = external_exports.object({
|
|
17503
|
+
counts: CalibrationCounts,
|
|
17504
|
+
routineCategories: external_exports.array(DetectionCategory),
|
|
17505
|
+
surfacedCategories: external_exports.array(DetectionCategory),
|
|
17506
|
+
findingKinds: external_exports.array(CalibrationFindingKind),
|
|
17507
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
|
|
17508
|
+
maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
|
|
17509
|
+
falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
|
|
17510
|
+
});
|
|
17511
|
+
var CalibrationPreviewCategory = TriageCategoryRec.pick({
|
|
17512
|
+
category: true,
|
|
17513
|
+
genuineCount: true,
|
|
17514
|
+
fpCount: true
|
|
17515
|
+
}).extend({
|
|
17516
|
+
egress: external_exports.boolean()
|
|
17517
|
+
});
|
|
17518
|
+
var CalibrationPreview = external_exports.object({
|
|
17519
|
+
categories: external_exports.array(CalibrationPreviewCategory),
|
|
17520
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
|
|
17521
|
+
});
|
|
17522
|
+
var CalibrationResult = external_exports.object({
|
|
17523
|
+
frame: CalibrationFrame,
|
|
17524
|
+
copy: external_exports.string()
|
|
17525
|
+
});
|
|
17526
|
+
var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
|
|
17527
|
+
var SetupHandoffOption = external_exports.object({
|
|
17528
|
+
id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
|
|
17529
|
+
label: external_exports.string()
|
|
17530
|
+
});
|
|
17531
|
+
var DashboardHandoffOptions = external_exports.tuple([
|
|
17532
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17533
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17534
|
+
]);
|
|
17535
|
+
var ComposedRemediationOptions = external_exports.tuple([
|
|
17536
|
+
SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
|
|
17537
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17538
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17539
|
+
]);
|
|
17540
|
+
var SetupHandoffOffer = external_exports.object({
|
|
17541
|
+
worthALook: external_exports.number().int().nonnegative(),
|
|
17542
|
+
liveKeys: external_exports.number().int().nonnegative().optional(),
|
|
17543
|
+
options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
|
|
17544
|
+
}).refine(
|
|
17545
|
+
(o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
|
|
17546
|
+
{
|
|
17547
|
+
message: "the chain-entry option is present exactly when liveKeys > 0",
|
|
17548
|
+
path: ["options"]
|
|
17549
|
+
}
|
|
17550
|
+
).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
|
|
17551
|
+
message: "liveKeys is a subset of worthALook and cannot exceed it",
|
|
17552
|
+
path: ["liveKeys"]
|
|
17553
|
+
});
|
|
17554
|
+
|
|
17297
17555
|
// ../../packages/schema/src/zod/shares.ts
|
|
17298
17556
|
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17299
17557
|
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
@@ -17479,6 +17737,102 @@ function reviewSeverityRank(reasons) {
|
|
|
17479
17737
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17480
17738
|
}
|
|
17481
17739
|
|
|
17740
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17741
|
+
function escapeLikePattern(s) {
|
|
17742
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17743
|
+
}
|
|
17744
|
+
function placeholders(n) {
|
|
17745
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17746
|
+
}
|
|
17747
|
+
function containsPattern(q) {
|
|
17748
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17749
|
+
}
|
|
17750
|
+
function likeAny(exprs) {
|
|
17751
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17752
|
+
}
|
|
17753
|
+
|
|
17754
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17755
|
+
var savepointSeq = 0;
|
|
17756
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17757
|
+
if (db.isTransaction) {
|
|
17758
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17759
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17760
|
+
try {
|
|
17761
|
+
fn();
|
|
17762
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17763
|
+
} catch (error51) {
|
|
17764
|
+
try {
|
|
17765
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17766
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17767
|
+
} catch {
|
|
17768
|
+
}
|
|
17769
|
+
throw error51;
|
|
17770
|
+
}
|
|
17771
|
+
return;
|
|
17772
|
+
}
|
|
17773
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17774
|
+
try {
|
|
17775
|
+
fn();
|
|
17776
|
+
db.exec("COMMIT");
|
|
17777
|
+
} catch (error51) {
|
|
17778
|
+
try {
|
|
17779
|
+
db.exec("ROLLBACK");
|
|
17780
|
+
} catch {
|
|
17781
|
+
}
|
|
17782
|
+
throw error51;
|
|
17783
|
+
}
|
|
17784
|
+
}
|
|
17785
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17786
|
+
const nested = db.isTransaction;
|
|
17787
|
+
try {
|
|
17788
|
+
withTransaction(db, fn, mode);
|
|
17789
|
+
return true;
|
|
17790
|
+
} catch (error51) {
|
|
17791
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17792
|
+
return false;
|
|
17793
|
+
}
|
|
17794
|
+
}
|
|
17795
|
+
|
|
17796
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17797
|
+
function akaWarn(message) {
|
|
17798
|
+
process.stderr.write(`[aka] ${message}
|
|
17799
|
+
`);
|
|
17800
|
+
}
|
|
17801
|
+
|
|
17802
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17803
|
+
function evidenceObjects(sql) {
|
|
17804
|
+
const objects = [];
|
|
17805
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17806
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17807
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17808
|
+
}
|
|
17809
|
+
}
|
|
17810
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17811
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17812
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17813
|
+
}
|
|
17814
|
+
}
|
|
17815
|
+
return objects;
|
|
17816
|
+
}
|
|
17817
|
+
function schemaObjectExists(db, kind, name) {
|
|
17818
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17819
|
+
return row !== void 0;
|
|
17820
|
+
}
|
|
17821
|
+
function indexExists(db, name) {
|
|
17822
|
+
return schemaObjectExists(db, "index", name);
|
|
17823
|
+
}
|
|
17824
|
+
function columnNames(db, table, opts) {
|
|
17825
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17826
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
|
|
17827
|
+
return columns.map((c) => c.name);
|
|
17828
|
+
}
|
|
17829
|
+
function evidenceExists(db, object2) {
|
|
17830
|
+
if (object2.kind === "column") {
|
|
17831
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17832
|
+
}
|
|
17833
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17834
|
+
}
|
|
17835
|
+
|
|
17482
17836
|
// ../../packages/persistence/src/ids.ts
|
|
17483
17837
|
import { createHash } from "crypto";
|
|
17484
17838
|
function sha256Hex(input) {
|
|
@@ -17515,28 +17869,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17515
17869
|
}
|
|
17516
17870
|
|
|
17517
17871
|
// ../../packages/persistence/src/migrations.ts
|
|
17518
|
-
function evidenceObjects(sql) {
|
|
17519
|
-
const objects = [];
|
|
17520
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17521
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17522
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17523
|
-
}
|
|
17524
|
-
}
|
|
17525
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17526
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17527
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17528
|
-
}
|
|
17529
|
-
}
|
|
17530
|
-
return objects;
|
|
17531
|
-
}
|
|
17532
|
-
function evidenceExists(db, object2) {
|
|
17533
|
-
if (object2.kind === "column") {
|
|
17534
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17535
|
-
return columns.some((c) => c.name === object2.name);
|
|
17536
|
-
}
|
|
17537
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17538
|
-
return row !== void 0;
|
|
17539
|
-
}
|
|
17540
17872
|
function describeObject(object2) {
|
|
17541
17873
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17542
17874
|
}
|
|
@@ -17547,10 +17879,6 @@ function createdIndexName(statement) {
|
|
|
17547
17879
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17548
17880
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17549
17881
|
}
|
|
17550
|
-
function indexExists(db, name) {
|
|
17551
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17552
|
-
return row !== void 0;
|
|
17553
|
-
}
|
|
17554
17882
|
function applyMigrations(db) {
|
|
17555
17883
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17556
17884
|
db.exec(
|
|
@@ -17569,44 +17897,39 @@ function applyMigrations(db) {
|
|
|
17569
17897
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17570
17898
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17571
17899
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17572
|
-
const message = `
|
|
17573
|
-
|
|
17574
|
-
`);
|
|
17575
|
-
throw new Error(message);
|
|
17900
|
+
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.`;
|
|
17901
|
+
akaWarn(message);
|
|
17902
|
+
throw new Error(`[aka] ${message}`);
|
|
17576
17903
|
}
|
|
17577
17904
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17578
17905
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17579
17906
|
const statements = splitStatements(migration.sql);
|
|
17580
17907
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17581
17908
|
try {
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
const
|
|
17586
|
-
|
|
17587
|
-
if (
|
|
17588
|
-
|
|
17589
|
-
|
|
17909
|
+
withTransaction(
|
|
17910
|
+
db,
|
|
17911
|
+
() => {
|
|
17912
|
+
for (const statement of statements) {
|
|
17913
|
+
const indexName = createdIndexName(statement);
|
|
17914
|
+
if (indexName === void 0) {
|
|
17915
|
+
if (alreadyApplied) continue;
|
|
17916
|
+
} else if (indexExists(db, indexName)) {
|
|
17917
|
+
continue;
|
|
17918
|
+
}
|
|
17919
|
+
db.exec(statement);
|
|
17590
17920
|
}
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
);
|
|
17921
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17922
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17923
|
+
if (violations.length > 0) {
|
|
17924
|
+
throw new Error(
|
|
17925
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17926
|
+
);
|
|
17927
|
+
}
|
|
17599
17928
|
}
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
try {
|
|
17605
|
-
db.exec("ROLLBACK");
|
|
17606
|
-
} catch {
|
|
17607
|
-
}
|
|
17608
|
-
throw error51;
|
|
17609
|
-
}
|
|
17929
|
+
record2.run(migration.tag, Date.now());
|
|
17930
|
+
},
|
|
17931
|
+
"IMMEDIATE"
|
|
17932
|
+
);
|
|
17610
17933
|
} finally {
|
|
17611
17934
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17612
17935
|
}
|
|
@@ -17649,8 +17972,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17649
17972
|
}
|
|
17650
17973
|
];
|
|
17651
17974
|
function ensureTokenUsageColumns(db) {
|
|
17652
|
-
const
|
|
17653
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17975
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17654
17976
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17655
17977
|
if (!existing.has(column.name)) {
|
|
17656
17978
|
db.exec(column.ddl);
|
|
@@ -17688,47 +18010,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17688
18010
|
repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
|
|
17689
18011
|
}));
|
|
17690
18012
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
dropCollisions
|
|
17705
|
-
|
|
18013
|
+
withTransaction(
|
|
18014
|
+
db,
|
|
18015
|
+
() => {
|
|
18016
|
+
for (const { row, canonicalId } of legacy) {
|
|
18017
|
+
foldProject.run(
|
|
18018
|
+
canonicalId,
|
|
18019
|
+
row.url,
|
|
18020
|
+
row.name,
|
|
18021
|
+
row.attributes,
|
|
18022
|
+
row.firstSeen,
|
|
18023
|
+
row.lastSeen
|
|
18024
|
+
);
|
|
18025
|
+
repointAudit.run(canonicalId, row.id);
|
|
18026
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
18027
|
+
dropCollisions.run(row.id, canonicalId);
|
|
18028
|
+
repoint.run(canonicalId, row.id);
|
|
18029
|
+
}
|
|
18030
|
+
repointCallSite.run(canonicalId, row.id);
|
|
18031
|
+
deleteLegacy.run(row.id);
|
|
17706
18032
|
}
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
db.exec("COMMIT");
|
|
17711
|
-
} catch (error51) {
|
|
17712
|
-
try {
|
|
17713
|
-
db.exec("ROLLBACK");
|
|
17714
|
-
} catch {
|
|
17715
|
-
}
|
|
17716
|
-
throw error51;
|
|
17717
|
-
}
|
|
18033
|
+
},
|
|
18034
|
+
"IMMEDIATE"
|
|
18035
|
+
);
|
|
17718
18036
|
} catch (error51) {
|
|
17719
|
-
|
|
17720
|
-
`);
|
|
18037
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17721
18038
|
}
|
|
17722
18039
|
}
|
|
17723
18040
|
function isForeignSqliteLineage(db) {
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17727
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
18041
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18042
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17728
18043
|
}
|
|
17729
18044
|
function ensureSyncedAtColumn(db, table) {
|
|
17730
|
-
|
|
17731
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
18045
|
+
if (!columnNames(db, table).includes("synced_at")) {
|
|
17732
18046
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
17733
18047
|
}
|
|
17734
18048
|
}
|
|
@@ -17779,8 +18093,11 @@ function ensureDataDirSync(dir) {
|
|
|
17779
18093
|
} catch {
|
|
17780
18094
|
}
|
|
17781
18095
|
}
|
|
18096
|
+
function walSidecars(file2) {
|
|
18097
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
18098
|
+
}
|
|
17782
18099
|
function tightenPerms(file2) {
|
|
17783
|
-
for (const path of [file2,
|
|
18100
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17784
18101
|
try {
|
|
17785
18102
|
chmodSync(path, DATA_FILE_MODE);
|
|
17786
18103
|
} catch {
|
|
@@ -17788,12 +18105,68 @@ function tightenPerms(file2) {
|
|
|
17788
18105
|
}
|
|
17789
18106
|
}
|
|
17790
18107
|
|
|
17791
|
-
// ../../packages/persistence/src/
|
|
17792
|
-
function
|
|
17793
|
-
|
|
18108
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
18109
|
+
function safeJson(s, fallback) {
|
|
18110
|
+
if (s == null) return fallback;
|
|
18111
|
+
try {
|
|
18112
|
+
return JSON.parse(s);
|
|
18113
|
+
} catch {
|
|
18114
|
+
return fallback;
|
|
18115
|
+
}
|
|
17794
18116
|
}
|
|
17795
|
-
function
|
|
17796
|
-
|
|
18117
|
+
function parseJsonObject(s) {
|
|
18118
|
+
if (s == null) return void 0;
|
|
18119
|
+
try {
|
|
18120
|
+
const parsed = JSON.parse(s);
|
|
18121
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18122
|
+
} catch {
|
|
18123
|
+
}
|
|
18124
|
+
return void 0;
|
|
18125
|
+
}
|
|
18126
|
+
|
|
18127
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18128
|
+
function allRows(stmt, params) {
|
|
18129
|
+
if (params === void 0) return stmt.all();
|
|
18130
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18131
|
+
return stmt.all(params);
|
|
18132
|
+
}
|
|
18133
|
+
function getRow(stmt, params) {
|
|
18134
|
+
if (params === void 0) return stmt.get();
|
|
18135
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18136
|
+
return stmt.get(params);
|
|
18137
|
+
}
|
|
18138
|
+
function intToBool(raw) {
|
|
18139
|
+
return raw === 1 || raw === true;
|
|
18140
|
+
}
|
|
18141
|
+
function boolToInt(b) {
|
|
18142
|
+
return b ? 1 : 0;
|
|
18143
|
+
}
|
|
18144
|
+
function bindParams(row) {
|
|
18145
|
+
const out = {};
|
|
18146
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18147
|
+
out[key] = value === void 0 ? null : value;
|
|
18148
|
+
}
|
|
18149
|
+
return out;
|
|
18150
|
+
}
|
|
18151
|
+
function countScalar(db, sql, params) {
|
|
18152
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18153
|
+
}
|
|
18154
|
+
function countBy(db, sql, params) {
|
|
18155
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18156
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18157
|
+
map2.set(row.k, row.n);
|
|
18158
|
+
}
|
|
18159
|
+
return map2;
|
|
18160
|
+
}
|
|
18161
|
+
function mapRowsTolerant(rows, map2) {
|
|
18162
|
+
const out = [];
|
|
18163
|
+
for (const row of rows) {
|
|
18164
|
+
try {
|
|
18165
|
+
out.push(map2(row));
|
|
18166
|
+
} catch {
|
|
18167
|
+
}
|
|
18168
|
+
}
|
|
18169
|
+
return out;
|
|
17797
18170
|
}
|
|
17798
18171
|
|
|
17799
18172
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17845,15 +18218,11 @@ function encodeCursor(payload) {
|
|
|
17845
18218
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17846
18219
|
}
|
|
17847
18220
|
function decodeCursor(cursor) {
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
return parsed;
|
|
17852
|
-
}
|
|
17853
|
-
return null;
|
|
17854
|
-
} catch {
|
|
17855
|
-
return null;
|
|
18221
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18222
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18223
|
+
return parsed;
|
|
17856
18224
|
}
|
|
18225
|
+
return null;
|
|
17857
18226
|
}
|
|
17858
18227
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17859
18228
|
session: "session",
|
|
@@ -17870,15 +18239,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17870
18239
|
};
|
|
17871
18240
|
function safeParseStringArray(raw) {
|
|
17872
18241
|
if (!raw) return [];
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17876
|
-
} catch {
|
|
17877
|
-
return [];
|
|
17878
|
-
}
|
|
17879
|
-
}
|
|
17880
|
-
function toBool(raw) {
|
|
17881
|
-
return raw === 1 || raw === true;
|
|
18242
|
+
const parsed = safeJson(raw, null);
|
|
18243
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17882
18244
|
}
|
|
17883
18245
|
function toHarness(raw) {
|
|
17884
18246
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17931,8 +18293,8 @@ function buildAuditEvent(row) {
|
|
|
17931
18293
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17932
18294
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17933
18295
|
targetId: row.target_id,
|
|
17934
|
-
internal:
|
|
17935
|
-
flagged:
|
|
18296
|
+
internal: intToBool(row.internal),
|
|
18297
|
+
flagged: intToBool(row.flagged)
|
|
17936
18298
|
};
|
|
17937
18299
|
}
|
|
17938
18300
|
var TIMELINE_COLUMNS = `
|
|
@@ -17948,6 +18310,10 @@ var TIMELINE_COLUMNS = `
|
|
|
17948
18310
|
json_extract(attributes, '$.internal') AS internal,
|
|
17949
18311
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
17950
18312
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
18313
|
+
var HAS_ACTIVITY = `EXISTS (
|
|
18314
|
+
SELECT 1 FROM audit_events c
|
|
18315
|
+
WHERE c.root_session_id = audit_events.id
|
|
18316
|
+
AND c.event_type NOT IN ('hook', 'config_scan'))`;
|
|
17951
18317
|
var SqliteActivityRepository = class {
|
|
17952
18318
|
constructor(db, now = () => Date.now()) {
|
|
17953
18319
|
this.db = db;
|
|
@@ -17958,12 +18324,16 @@ var SqliteActivityRepository = class {
|
|
|
17958
18324
|
stats(tz) {
|
|
17959
18325
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
17960
18326
|
const { startMs, endMs } = window;
|
|
17961
|
-
const sessionsToday =
|
|
18327
|
+
const sessionsToday = countScalar(
|
|
18328
|
+
this.db,
|
|
17962
18329
|
`SELECT count(*) AS n FROM audit_events
|
|
17963
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
17964
|
-
|
|
18330
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
|
|
18331
|
+
AND ${HAS_ACTIVITY}`,
|
|
18332
|
+
[startMs, endMs]
|
|
18333
|
+
);
|
|
17965
18334
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
17966
|
-
const liveNow =
|
|
18335
|
+
const liveNow = countScalar(
|
|
18336
|
+
this.db,
|
|
17967
18337
|
`SELECT count(*) AS n FROM audit_events s
|
|
17968
18338
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
17969
18339
|
AND max(
|
|
@@ -17972,22 +18342,29 @@ var SqliteActivityRepository = class {
|
|
|
17972
18342
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
17973
18343
|
s.started_at
|
|
17974
18344
|
)
|
|
17975
|
-
) >=
|
|
17976
|
-
|
|
17977
|
-
|
|
18345
|
+
) >= ?`,
|
|
18346
|
+
[liveThreshold]
|
|
18347
|
+
);
|
|
18348
|
+
const toolCallsToday = countScalar(
|
|
18349
|
+
this.db,
|
|
17978
18350
|
`SELECT count(*) AS n FROM audit_events
|
|
17979
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
17980
|
-
|
|
17981
|
-
|
|
18351
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18352
|
+
[startMs, endMs]
|
|
18353
|
+
);
|
|
18354
|
+
const findingsToday = countScalar(
|
|
18355
|
+
this.db,
|
|
17982
18356
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
17983
18357
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
17984
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
17985
|
-
|
|
17986
|
-
|
|
18358
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18359
|
+
[startMs, endMs]
|
|
18360
|
+
);
|
|
18361
|
+
const egressToday = countScalar(
|
|
18362
|
+
this.db,
|
|
17987
18363
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
17988
18364
|
FROM audit_events
|
|
17989
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
17990
|
-
|
|
18365
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18366
|
+
[startMs, endMs]
|
|
18367
|
+
);
|
|
17991
18368
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
17992
18369
|
}
|
|
17993
18370
|
listSessions(query) {
|
|
@@ -18009,7 +18386,7 @@ var SqliteActivityRepository = class {
|
|
|
18009
18386
|
conditions.push("started_at <= ?");
|
|
18010
18387
|
params.push(toMs);
|
|
18011
18388
|
if (query.q) {
|
|
18012
|
-
const pattern =
|
|
18389
|
+
const pattern = containsPattern(query.q);
|
|
18013
18390
|
conditions.push(
|
|
18014
18391
|
`(content LIKE ? ESCAPE '\\'
|
|
18015
18392
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18023,13 +18400,21 @@ var SqliteActivityRepository = class {
|
|
|
18023
18400
|
);
|
|
18024
18401
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
18025
18402
|
}
|
|
18403
|
+
const emptyCount = countScalar(
|
|
18404
|
+
this.db,
|
|
18405
|
+
`SELECT count(*) AS n FROM audit_events
|
|
18406
|
+
WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
|
|
18407
|
+
params
|
|
18408
|
+
);
|
|
18409
|
+
if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
|
|
18026
18410
|
if (cursor) {
|
|
18027
18411
|
conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
|
|
18028
18412
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18029
18413
|
}
|
|
18030
18414
|
const limit = query.limit;
|
|
18031
|
-
const rows =
|
|
18032
|
-
|
|
18415
|
+
const rows = allRows(
|
|
18416
|
+
this.db.prepare(
|
|
18417
|
+
`SELECT id,
|
|
18033
18418
|
json_extract(attributes, '$.harness') AS harness,
|
|
18034
18419
|
content AS title,
|
|
18035
18420
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18042,7 +18427,9 @@ var SqliteActivityRepository = class {
|
|
|
18042
18427
|
WHERE ${conditions.join(" AND ")}
|
|
18043
18428
|
ORDER BY started_at DESC, id DESC
|
|
18044
18429
|
LIMIT ?`
|
|
18045
|
-
|
|
18430
|
+
),
|
|
18431
|
+
[...params, limit + 1]
|
|
18432
|
+
);
|
|
18046
18433
|
const hasMore = rows.length > limit;
|
|
18047
18434
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18048
18435
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18056,11 +18443,12 @@ var SqliteActivityRepository = class {
|
|
|
18056
18443
|
);
|
|
18057
18444
|
const last = page[page.length - 1];
|
|
18058
18445
|
const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
|
|
18059
|
-
return Promise.resolve({ items, nextCursor });
|
|
18446
|
+
return Promise.resolve({ items, nextCursor, emptyCount });
|
|
18060
18447
|
}
|
|
18061
18448
|
getSession(sessionId) {
|
|
18062
|
-
const rootRow =
|
|
18063
|
-
|
|
18449
|
+
const rootRow = getRow(
|
|
18450
|
+
this.db.prepare(
|
|
18451
|
+
`SELECT id,
|
|
18064
18452
|
json_extract(attributes, '$.harness') AS harness,
|
|
18065
18453
|
content AS title,
|
|
18066
18454
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18077,47 +18465,66 @@ var SqliteActivityRepository = class {
|
|
|
18077
18465
|
FROM audit_events
|
|
18078
18466
|
WHERE id = ? AND event_type = 'session'
|
|
18079
18467
|
LIMIT 1`
|
|
18080
|
-
|
|
18468
|
+
),
|
|
18469
|
+
[sessionId]
|
|
18470
|
+
);
|
|
18081
18471
|
if (!rootRow) return Promise.resolve(null);
|
|
18082
|
-
const timelineRows =
|
|
18083
|
-
|
|
18472
|
+
const timelineRows = allRows(
|
|
18473
|
+
this.db.prepare(
|
|
18474
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18084
18475
|
FROM audit_events
|
|
18085
18476
|
WHERE id = ? OR root_session_id = ?
|
|
18086
18477
|
ORDER BY started_at ASC, id ASC`
|
|
18087
|
-
|
|
18478
|
+
),
|
|
18479
|
+
[sessionId, sessionId]
|
|
18480
|
+
);
|
|
18088
18481
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18089
|
-
const tokenRow =
|
|
18090
|
-
|
|
18482
|
+
const tokenRow = getRow(
|
|
18483
|
+
this.db.prepare(
|
|
18484
|
+
`SELECT
|
|
18091
18485
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18092
18486
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18093
18487
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18094
18488
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18095
18489
|
FROM audit_events
|
|
18096
18490
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18491
|
+
),
|
|
18492
|
+
[sessionId]
|
|
18493
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18494
|
+
const primaryModel = getRow(
|
|
18495
|
+
this.db.prepare(
|
|
18496
|
+
`SELECT model, provider FROM audit_events
|
|
18100
18497
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18101
18498
|
ORDER BY started_at ASC, id ASC
|
|
18102
18499
|
LIMIT 1`
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18500
|
+
),
|
|
18501
|
+
[sessionId]
|
|
18502
|
+
);
|
|
18503
|
+
const toolRows = allRows(
|
|
18504
|
+
this.db.prepare(
|
|
18505
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18506
|
+
count(*) AS n
|
|
18507
|
+
FROM audit_events
|
|
18108
18508
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18109
18509
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18510
|
+
),
|
|
18511
|
+
[sessionId]
|
|
18512
|
+
);
|
|
18513
|
+
const modelRows = allRows(
|
|
18514
|
+
this.db.prepare(
|
|
18515
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18113
18516
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18114
18517
|
ORDER BY model`
|
|
18115
|
-
|
|
18518
|
+
),
|
|
18519
|
+
[sessionId]
|
|
18520
|
+
);
|
|
18116
18521
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18117
|
-
const commits =
|
|
18522
|
+
const commits = countScalar(
|
|
18523
|
+
this.db,
|
|
18118
18524
|
`SELECT count(*) AS n FROM audit_events
|
|
18119
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18120
|
-
|
|
18525
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18526
|
+
[sessionId]
|
|
18527
|
+
);
|
|
18121
18528
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18122
18529
|
turns: 0,
|
|
18123
18530
|
findings: 0,
|
|
@@ -18184,7 +18591,10 @@ var SqliteActivityRepository = class {
|
|
|
18184
18591
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18185
18592
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18186
18593
|
);
|
|
18187
|
-
const rows =
|
|
18594
|
+
const rows = allRows(
|
|
18595
|
+
stmt,
|
|
18596
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18597
|
+
);
|
|
18188
18598
|
const seen = /* @__PURE__ */ new Set();
|
|
18189
18599
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18190
18600
|
return Promise.resolve([...seen]);
|
|
@@ -18207,23 +18617,23 @@ var SqliteActivityRepository = class {
|
|
|
18207
18617
|
conditions.push("started_at >= ?");
|
|
18208
18618
|
params.push(opts.fromMs);
|
|
18209
18619
|
}
|
|
18210
|
-
const rows =
|
|
18211
|
-
|
|
18620
|
+
const rows = allRows(
|
|
18621
|
+
this.db.prepare(
|
|
18622
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18212
18623
|
FROM audit_events
|
|
18213
18624
|
WHERE ${conditions.join(" AND ")}`
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
}
|
|
18225
|
-
|
|
18226
|
-
return leaves;
|
|
18625
|
+
),
|
|
18626
|
+
params
|
|
18627
|
+
);
|
|
18628
|
+
return mapRowsTolerant(
|
|
18629
|
+
rows.filter(
|
|
18630
|
+
(row) => row.sessionId !== null
|
|
18631
|
+
),
|
|
18632
|
+
(row) => ({
|
|
18633
|
+
sessionId: row.sessionId,
|
|
18634
|
+
attributes: JSON.parse(row.attributes)
|
|
18635
|
+
})
|
|
18636
|
+
);
|
|
18227
18637
|
}
|
|
18228
18638
|
/**
|
|
18229
18639
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18237,57 +18647,72 @@ var SqliteActivityRepository = class {
|
|
|
18237
18647
|
);
|
|
18238
18648
|
if (sessionIds.length === 0) return result;
|
|
18239
18649
|
const inClause = placeholders(sessionIds.length);
|
|
18240
|
-
const lastActivityRows =
|
|
18241
|
-
|
|
18650
|
+
const lastActivityRows = allRows(
|
|
18651
|
+
this.db.prepare(
|
|
18652
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18242
18653
|
WHERE root_session_id IN (${inClause})
|
|
18243
18654
|
GROUP BY root_session_id`
|
|
18244
|
-
|
|
18655
|
+
),
|
|
18656
|
+
sessionIds
|
|
18657
|
+
);
|
|
18245
18658
|
for (const row of lastActivityRows) {
|
|
18246
18659
|
if (row.id === null) continue;
|
|
18247
18660
|
const entry = result.get(row.id);
|
|
18248
18661
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18249
18662
|
}
|
|
18250
|
-
const turnsRows =
|
|
18251
|
-
|
|
18663
|
+
const turnsRows = allRows(
|
|
18664
|
+
this.db.prepare(
|
|
18665
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18252
18666
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18253
18667
|
GROUP BY root_session_id`
|
|
18254
|
-
|
|
18668
|
+
),
|
|
18669
|
+
sessionIds
|
|
18670
|
+
);
|
|
18255
18671
|
for (const row of turnsRows) {
|
|
18256
18672
|
if (row.id === null) continue;
|
|
18257
18673
|
const entry = result.get(row.id);
|
|
18258
18674
|
if (entry) entry.turns = row.n;
|
|
18259
18675
|
}
|
|
18260
|
-
const runKeyRows =
|
|
18261
|
-
|
|
18676
|
+
const runKeyRows = allRows(
|
|
18677
|
+
this.db.prepare(
|
|
18678
|
+
`SELECT root_session_id AS id,
|
|
18262
18679
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18263
18680
|
FROM audit_events
|
|
18264
18681
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18265
18682
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18266
18683
|
GROUP BY root_session_id`
|
|
18267
|
-
|
|
18684
|
+
),
|
|
18685
|
+
sessionIds
|
|
18686
|
+
);
|
|
18268
18687
|
for (const row of runKeyRows) {
|
|
18269
18688
|
if (row.id === null) continue;
|
|
18270
18689
|
const entry = result.get(row.id);
|
|
18271
18690
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18272
18691
|
}
|
|
18273
|
-
const findingsRows =
|
|
18274
|
-
|
|
18692
|
+
const findingsRows = allRows(
|
|
18693
|
+
this.db.prepare(
|
|
18694
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18275
18695
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18276
18696
|
WHERE e.root_session_id IN (${inClause})
|
|
18277
18697
|
GROUP BY e.root_session_id`
|
|
18278
|
-
|
|
18698
|
+
),
|
|
18699
|
+
sessionIds
|
|
18700
|
+
);
|
|
18279
18701
|
for (const row of findingsRows) {
|
|
18280
18702
|
if (row.id === null) continue;
|
|
18281
18703
|
const entry = result.get(row.id);
|
|
18282
18704
|
if (entry) entry.findings = row.n;
|
|
18283
18705
|
}
|
|
18284
|
-
const sharesRows =
|
|
18285
|
-
|
|
18706
|
+
const sharesRows = allRows(
|
|
18707
|
+
this.db.prepare(
|
|
18708
|
+
`SELECT root_session_id AS id,
|
|
18286
18709
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18287
18710
|
FROM audit_events
|
|
18288
18711
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18289
18712
|
GROUP BY root_session_id`
|
|
18290
|
-
|
|
18713
|
+
),
|
|
18714
|
+
sessionIds
|
|
18715
|
+
);
|
|
18291
18716
|
for (const row of sharesRows) {
|
|
18292
18717
|
if (row.id === null) continue;
|
|
18293
18718
|
const entry = result.get(row.id);
|
|
@@ -18337,33 +18762,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18337
18762
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18338
18763
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18339
18764
|
runInTransaction(fn) {
|
|
18340
|
-
this.db
|
|
18341
|
-
try {
|
|
18342
|
-
fn();
|
|
18343
|
-
this.db.exec("COMMIT");
|
|
18344
|
-
} catch (err) {
|
|
18345
|
-
this.db.exec("ROLLBACK");
|
|
18346
|
-
throw err;
|
|
18347
|
-
}
|
|
18765
|
+
withTransaction(this.db, fn);
|
|
18348
18766
|
}
|
|
18349
18767
|
insertAuditEvent(input) {
|
|
18350
18768
|
const row = toAuditEventRow(input);
|
|
18351
|
-
this.insertStmt.run(
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18769
|
+
this.insertStmt.run(
|
|
18770
|
+
bindParams({
|
|
18771
|
+
id: row.id,
|
|
18772
|
+
parentId: row.parentId,
|
|
18773
|
+
rootSessionId: row.rootSessionId,
|
|
18774
|
+
eventType: row.eventType,
|
|
18775
|
+
hostId: row.hostId,
|
|
18776
|
+
harnessId: row.harnessId,
|
|
18777
|
+
sourceProjectId: row.sourceProjectId,
|
|
18778
|
+
startedAt: row.startedAt,
|
|
18779
|
+
endedAt: row.endedAt,
|
|
18780
|
+
severity: row.severity,
|
|
18781
|
+
priority: row.priority,
|
|
18782
|
+
content: row.content,
|
|
18783
|
+
contentHash: row.contentHash,
|
|
18784
|
+
attributes: row.attributes
|
|
18785
|
+
})
|
|
18786
|
+
);
|
|
18367
18787
|
}
|
|
18368
18788
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18369
18789
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18377,22 +18797,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18377
18797
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18378
18798
|
if (!Number.isFinite(startedAt)) return;
|
|
18379
18799
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18380
|
-
this.upsertLlmCallStmt.run(
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
|
|
18800
|
+
this.upsertLlmCallStmt.run(
|
|
18801
|
+
bindParams({
|
|
18802
|
+
id,
|
|
18803
|
+
parentId: input.parentId,
|
|
18804
|
+
rootSessionId: input.rootSessionId,
|
|
18805
|
+
eventType: "llm_call",
|
|
18806
|
+
hostId: null,
|
|
18807
|
+
harnessId: null,
|
|
18808
|
+
sourceProjectId: null,
|
|
18809
|
+
startedAt,
|
|
18810
|
+
endedAt: null,
|
|
18811
|
+
severity: null,
|
|
18812
|
+
priority: null,
|
|
18813
|
+
content: null,
|
|
18814
|
+
contentHash: null,
|
|
18815
|
+
attributes: JSON.stringify(input.attributes)
|
|
18816
|
+
})
|
|
18817
|
+
);
|
|
18396
18818
|
}
|
|
18397
18819
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18398
18820
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18416,25 +18838,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18416
18838
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18417
18839
|
if (!Number.isFinite(startedAt)) return;
|
|
18418
18840
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18419
|
-
this.insertStmt.run(
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18841
|
+
this.insertStmt.run(
|
|
18842
|
+
bindParams({
|
|
18843
|
+
id,
|
|
18844
|
+
parentId: input.parentId,
|
|
18845
|
+
rootSessionId: input.rootSessionId,
|
|
18846
|
+
eventType: "tool_call",
|
|
18847
|
+
hostId: null,
|
|
18848
|
+
harnessId: null,
|
|
18849
|
+
sourceProjectId: null,
|
|
18850
|
+
startedAt,
|
|
18851
|
+
endedAt: null,
|
|
18852
|
+
severity: null,
|
|
18853
|
+
priority: null,
|
|
18854
|
+
content: null,
|
|
18855
|
+
contentHash: null,
|
|
18856
|
+
attributes: JSON.stringify(input.attributes)
|
|
18857
|
+
})
|
|
18858
|
+
);
|
|
18435
18859
|
}
|
|
18436
18860
|
findById(id) {
|
|
18437
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18861
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18862
|
+
id
|
|
18863
|
+
});
|
|
18438
18864
|
}
|
|
18439
18865
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18440
18866
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18444,14 +18870,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18444
18870
|
sessionProvider(sessionId) {
|
|
18445
18871
|
const row = this.findById(sessionId);
|
|
18446
18872
|
if (!row?.attributes) return void 0;
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18450
|
-
const provider = parsed.provider;
|
|
18451
|
-
if (typeof provider === "string") return provider;
|
|
18452
|
-
}
|
|
18453
|
-
} catch {
|
|
18454
|
-
}
|
|
18873
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18874
|
+
if (typeof provider === "string") return provider;
|
|
18455
18875
|
return void 0;
|
|
18456
18876
|
}
|
|
18457
18877
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18460,11 +18880,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18460
18880
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18461
18881
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18462
18882
|
llmCallLeaves() {
|
|
18463
|
-
return
|
|
18464
|
-
|
|
18883
|
+
return allRows(
|
|
18884
|
+
this.db.prepare(
|
|
18885
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18465
18886
|
FROM audit_events
|
|
18466
18887
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18467
|
-
|
|
18888
|
+
)
|
|
18889
|
+
);
|
|
18468
18890
|
}
|
|
18469
18891
|
};
|
|
18470
18892
|
|
|
@@ -18483,16 +18905,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18483
18905
|
upsert(input) {
|
|
18484
18906
|
const id = classifiedDataId(input.class);
|
|
18485
18907
|
const row = toClassifiedDataRow(input, id);
|
|
18486
|
-
this.insertStmt.run(
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18908
|
+
this.insertStmt.run(
|
|
18909
|
+
bindParams({
|
|
18910
|
+
id: row.id,
|
|
18911
|
+
class: row.class,
|
|
18912
|
+
label: row.label,
|
|
18913
|
+
attributes: row.attributes
|
|
18914
|
+
})
|
|
18915
|
+
);
|
|
18492
18916
|
return id;
|
|
18493
18917
|
}
|
|
18494
18918
|
};
|
|
18495
18919
|
|
|
18920
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18921
|
+
function latestConfigScan(db) {
|
|
18922
|
+
return getRow(
|
|
18923
|
+
db.prepare(
|
|
18924
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18925
|
+
WHERE event_type = 'config_scan'
|
|
18926
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18927
|
+
)
|
|
18928
|
+
);
|
|
18929
|
+
}
|
|
18930
|
+
|
|
18496
18931
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18497
18932
|
var SqliteConfigInventoryRepository = class {
|
|
18498
18933
|
constructor(db) {
|
|
@@ -18500,7 +18935,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18500
18935
|
}
|
|
18501
18936
|
db;
|
|
18502
18937
|
report() {
|
|
18503
|
-
const scan2 = this.
|
|
18938
|
+
const scan2 = latestConfigScan(this.db);
|
|
18504
18939
|
if (!scan2) {
|
|
18505
18940
|
return {
|
|
18506
18941
|
scannedAt: null,
|
|
@@ -18511,17 +18946,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18511
18946
|
topics: []
|
|
18512
18947
|
};
|
|
18513
18948
|
}
|
|
18514
|
-
const rows =
|
|
18515
|
-
|
|
18949
|
+
const rows = allRows(
|
|
18950
|
+
this.db.prepare(
|
|
18951
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18516
18952
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18517
18953
|
ORDER BY object_type, title`
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18954
|
+
),
|
|
18955
|
+
{ startedAt: scan2.started_at }
|
|
18956
|
+
);
|
|
18957
|
+
const findings = allRows(
|
|
18958
|
+
this.db.prepare(
|
|
18959
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18521
18960
|
FROM inspection_findings f
|
|
18522
18961
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18523
18962
|
WHERE f.audit_event_id = :scanId`
|
|
18524
|
-
|
|
18963
|
+
),
|
|
18964
|
+
{ scanId: scan2.id }
|
|
18965
|
+
);
|
|
18525
18966
|
const skills = [];
|
|
18526
18967
|
const hooks = [];
|
|
18527
18968
|
const mcpServers = [];
|
|
@@ -18550,7 +18991,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18550
18991
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18551
18992
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18552
18993
|
trustOverrides() {
|
|
18553
|
-
const rows =
|
|
18994
|
+
const rows = allRows(
|
|
18995
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18996
|
+
);
|
|
18554
18997
|
const map2 = /* @__PURE__ */ new Map();
|
|
18555
18998
|
for (const row of rows) {
|
|
18556
18999
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18559,13 +19002,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18559
19002
|
}
|
|
18560
19003
|
return map2;
|
|
18561
19004
|
}
|
|
18562
|
-
latestScan() {
|
|
18563
|
-
return this.db.prepare(
|
|
18564
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18565
|
-
WHERE event_type = 'config_scan'
|
|
18566
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18567
|
-
).get();
|
|
18568
|
-
}
|
|
18569
19005
|
};
|
|
18570
19006
|
function toSkillItem(row, bag) {
|
|
18571
19007
|
const item = {
|
|
@@ -18676,22 +19112,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18676
19112
|
return topics;
|
|
18677
19113
|
}
|
|
18678
19114
|
function countScanErrors(attributes) {
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
const parsed = JSON.parse(attributes);
|
|
18682
|
-
const errors = parsed?.errors;
|
|
18683
|
-
return typeof errors === "number" ? errors : 0;
|
|
18684
|
-
} catch {
|
|
18685
|
-
return 0;
|
|
18686
|
-
}
|
|
19115
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
19116
|
+
return typeof errors === "number" ? errors : 0;
|
|
18687
19117
|
}
|
|
18688
19118
|
function parseBag(raw) {
|
|
18689
|
-
|
|
18690
|
-
const parsed = JSON.parse(raw);
|
|
18691
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18692
|
-
} catch {
|
|
18693
|
-
}
|
|
18694
|
-
return void 0;
|
|
19119
|
+
return parseJsonObject(raw);
|
|
18695
19120
|
}
|
|
18696
19121
|
function str(value) {
|
|
18697
19122
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18700,12 +19125,7 @@ function str(value) {
|
|
|
18700
19125
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18701
19126
|
var DAY_MS2 = 864e5;
|
|
18702
19127
|
function parseRules(rulesJson) {
|
|
18703
|
-
|
|
18704
|
-
try {
|
|
18705
|
-
raw = JSON.parse(rulesJson);
|
|
18706
|
-
} catch {
|
|
18707
|
-
return [];
|
|
18708
|
-
}
|
|
19128
|
+
const raw = safeJson(rulesJson, []);
|
|
18709
19129
|
if (!Array.isArray(raw)) return [];
|
|
18710
19130
|
const rules = [];
|
|
18711
19131
|
for (const entry of raw) {
|
|
@@ -18724,11 +19144,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18724
19144
|
db;
|
|
18725
19145
|
now;
|
|
18726
19146
|
listDetections(query) {
|
|
18727
|
-
const rows =
|
|
18728
|
-
|
|
19147
|
+
const rows = allRows(
|
|
19148
|
+
this.db.prepare(
|
|
19149
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18729
19150
|
rules_json AS rulesJson
|
|
18730
19151
|
FROM installed_packs`
|
|
18731
|
-
|
|
19152
|
+
)
|
|
19153
|
+
);
|
|
18732
19154
|
const available = this.availableByPack();
|
|
18733
19155
|
const summaries = rows.map((r) => {
|
|
18734
19156
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18737,7 +19159,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18737
19159
|
packId: r.packId,
|
|
18738
19160
|
version: r.version,
|
|
18739
19161
|
name: r.name,
|
|
18740
|
-
enabled: r.enabled
|
|
19162
|
+
enabled: intToBool(r.enabled),
|
|
18741
19163
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18742
19164
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18743
19165
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18754,19 +19176,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18754
19176
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18755
19177
|
// detail call; the table is a handful of rows).
|
|
18756
19178
|
availableByPack() {
|
|
18757
|
-
const rows =
|
|
18758
|
-
|
|
19179
|
+
const rows = allRows(
|
|
19180
|
+
this.db.prepare(
|
|
19181
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18759
19182
|
FROM available_packs`
|
|
18760
|
-
|
|
19183
|
+
)
|
|
19184
|
+
);
|
|
18761
19185
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18762
19186
|
}
|
|
18763
19187
|
getDetectionStats() {
|
|
18764
|
-
const rows =
|
|
19188
|
+
const rows = allRows(
|
|
19189
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19190
|
+
);
|
|
18765
19191
|
let rules = 0;
|
|
18766
19192
|
let active = 0;
|
|
18767
19193
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18768
19194
|
for (const r of rows) {
|
|
18769
|
-
if (r.enabled
|
|
19195
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18770
19196
|
const parsed = parseRules(r.rulesJson);
|
|
18771
19197
|
rules += parsed.length;
|
|
18772
19198
|
for (const rule of parsed) {
|
|
@@ -18784,12 +19210,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18784
19210
|
const parts = splitDetectionId(id);
|
|
18785
19211
|
if (!parts) return Promise.resolve(null);
|
|
18786
19212
|
const { namespace, packId } = parts;
|
|
18787
|
-
const row =
|
|
18788
|
-
|
|
19213
|
+
const row = getRow(
|
|
19214
|
+
this.db.prepare(
|
|
19215
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18789
19216
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18790
19217
|
FROM installed_packs
|
|
18791
19218
|
WHERE namespace = ? AND pack_id = ?`
|
|
18792
|
-
|
|
19219
|
+
),
|
|
19220
|
+
[namespace, packId]
|
|
19221
|
+
);
|
|
18793
19222
|
if (!row) return Promise.resolve(null);
|
|
18794
19223
|
const rules = parseRules(row.rulesJson);
|
|
18795
19224
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18807,7 +19236,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18807
19236
|
packId: row.packId,
|
|
18808
19237
|
version: row.version,
|
|
18809
19238
|
name: row.name,
|
|
18810
|
-
enabled: row.enabled
|
|
19239
|
+
enabled: intToBool(row.enabled),
|
|
18811
19240
|
rules,
|
|
18812
19241
|
updatedAt: new Date(row.updatedAt),
|
|
18813
19242
|
policyId: row.policyId
|
|
@@ -18822,13 +19251,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18822
19251
|
countFindingsLast30d(ruleIds) {
|
|
18823
19252
|
if (ruleIds.length === 0) return 0;
|
|
18824
19253
|
const since = this.now() - 30 * DAY_MS2;
|
|
18825
|
-
const
|
|
18826
|
-
|
|
18827
|
-
|
|
19254
|
+
const inClause = placeholders(ruleIds.length);
|
|
19255
|
+
return countScalar(
|
|
19256
|
+
this.db,
|
|
19257
|
+
`SELECT count(*) AS n
|
|
18828
19258
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18829
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18830
|
-
|
|
18831
|
-
|
|
19259
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19260
|
+
[since, ...ruleIds]
|
|
19261
|
+
);
|
|
18832
19262
|
}
|
|
18833
19263
|
};
|
|
18834
19264
|
|
|
@@ -18845,16 +19275,17 @@ var SqliteEventsRepository = class {
|
|
|
18845
19275
|
insertStmt;
|
|
18846
19276
|
insertEvent(event) {
|
|
18847
19277
|
const row = toEventRow(event);
|
|
18848
|
-
this.insertStmt.run(
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
19278
|
+
this.insertStmt.run(
|
|
19279
|
+
bindParams({
|
|
19280
|
+
id: row.id,
|
|
19281
|
+
sourceTool: row.sourceTool,
|
|
19282
|
+
kind: row.kind,
|
|
19283
|
+
occurredAt: row.occurredAt,
|
|
19284
|
+
contentHash: row.contentHash,
|
|
19285
|
+
content: row.content,
|
|
19286
|
+
metadata: row.metadata
|
|
19287
|
+
})
|
|
19288
|
+
);
|
|
18858
19289
|
}
|
|
18859
19290
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18860
19291
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18862,13 +19293,23 @@ var SqliteEventsRepository = class {
|
|
|
18862
19293
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18863
19294
|
// async EventsReadPort contract.
|
|
18864
19295
|
contentHashes() {
|
|
18865
|
-
const rows =
|
|
19296
|
+
const rows = allRows(
|
|
19297
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19298
|
+
);
|
|
18866
19299
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18867
19300
|
}
|
|
18868
19301
|
};
|
|
18869
19302
|
|
|
18870
19303
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18871
19304
|
import { randomUUID } from "crypto";
|
|
19305
|
+
|
|
19306
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19307
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19308
|
+
function isUniqueConstraintError(err) {
|
|
19309
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19310
|
+
}
|
|
19311
|
+
|
|
19312
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18872
19313
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18873
19314
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18874
19315
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18889,10 +19330,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18889
19330
|
this.name = "AmbiguousExceptionIdError";
|
|
18890
19331
|
}
|
|
18891
19332
|
};
|
|
18892
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18893
|
-
function isUniqueConstraintError(err) {
|
|
18894
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18895
|
-
}
|
|
18896
19333
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18897
19334
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18898
19335
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -18948,10 +19385,11 @@ var SqliteExceptionsRepository = class {
|
|
|
18948
19385
|
this.insertExceptionRow(id, input, now);
|
|
18949
19386
|
} catch (err) {
|
|
18950
19387
|
if (!isUniqueConstraintError(err)) throw err;
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
19388
|
+
withTransaction(
|
|
19389
|
+
this.db,
|
|
19390
|
+
() => {
|
|
19391
|
+
const superseded = this.db.prepare(
|
|
19392
|
+
`UPDATE exceptions
|
|
18955
19393
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
18956
19394
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
18957
19395
|
updated_at = :now
|
|
@@ -18959,24 +19397,27 @@ var SqliteExceptionsRepository = class {
|
|
|
18959
19397
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
18960
19398
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
18961
19399
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
19400
|
+
).run({
|
|
19401
|
+
now,
|
|
19402
|
+
revokedBy: input.createdBy,
|
|
19403
|
+
ruleId: input.ruleId,
|
|
19404
|
+
valueFingerprint: input.valueFingerprint,
|
|
19405
|
+
keyVersion: input.keyVersion
|
|
19406
|
+
});
|
|
19407
|
+
if (Number(superseded.changes) !== 1) {
|
|
19408
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19409
|
+
}
|
|
19410
|
+
this.insertExceptionRow(id, input, now);
|
|
19411
|
+
},
|
|
19412
|
+
"IMMEDIATE"
|
|
19413
|
+
);
|
|
19414
|
+
}
|
|
19415
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19416
|
+
id
|
|
19417
|
+
});
|
|
19418
|
+
if (row === void 0) {
|
|
19419
|
+
throw new Error("exception row not found immediately after insert");
|
|
18978
19420
|
}
|
|
18979
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
18980
19421
|
return parseExceptionRow(row);
|
|
18981
19422
|
}
|
|
18982
19423
|
insertExceptionRow(id, input, now) {
|
|
@@ -19014,14 +19455,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19014
19455
|
*/
|
|
19015
19456
|
list(opts) {
|
|
19016
19457
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19017
|
-
const rows =
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
} catch {
|
|
19023
|
-
}
|
|
19024
|
-
}
|
|
19458
|
+
const rows = allRows(
|
|
19459
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19460
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19461
|
+
);
|
|
19462
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19025
19463
|
return Promise.resolve(exceptions);
|
|
19026
19464
|
}
|
|
19027
19465
|
/**
|
|
@@ -19031,7 +19469,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19031
19469
|
*/
|
|
19032
19470
|
getByIdPrefix(prefix) {
|
|
19033
19471
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19034
|
-
const rows =
|
|
19472
|
+
const rows = allRows(
|
|
19473
|
+
this.db.prepare(
|
|
19474
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19475
|
+
),
|
|
19476
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19477
|
+
);
|
|
19035
19478
|
if (rows.length > 1) {
|
|
19036
19479
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19037
19480
|
}
|
|
@@ -19073,30 +19516,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19073
19516
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19074
19517
|
*/
|
|
19075
19518
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19076
|
-
const rows =
|
|
19077
|
-
|
|
19519
|
+
const rows = allRows(
|
|
19520
|
+
this.db.prepare(
|
|
19521
|
+
`SELECT * FROM exceptions
|
|
19078
19522
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19079
19523
|
ORDER BY created_at DESC, rowid DESC`
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
);
|
|
19097
|
-
} catch {
|
|
19098
|
-
}
|
|
19099
|
-
}
|
|
19524
|
+
),
|
|
19525
|
+
{ keyVersion, now }
|
|
19526
|
+
);
|
|
19527
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19528
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19529
|
+
return ExceptionBundleEntry.parse({
|
|
19530
|
+
id: row.id,
|
|
19531
|
+
ruleId: row.rule_id,
|
|
19532
|
+
valueFingerprint: row.value_fingerprint,
|
|
19533
|
+
keyVersion: row.key_version,
|
|
19534
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19535
|
+
maxUses: row.max_uses,
|
|
19536
|
+
useCount: row.use_count,
|
|
19537
|
+
conditions
|
|
19538
|
+
});
|
|
19539
|
+
});
|
|
19100
19540
|
return Promise.resolve(entries);
|
|
19101
19541
|
}
|
|
19102
19542
|
/**
|
|
@@ -19123,11 +19563,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19123
19563
|
}
|
|
19124
19564
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19125
19565
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19126
|
-
const rows =
|
|
19127
|
-
|
|
19566
|
+
const rows = allRows(
|
|
19567
|
+
this.db.prepare(
|
|
19568
|
+
`SELECT * FROM blocked_detections
|
|
19128
19569
|
WHERE blocked_at > :cutoff
|
|
19129
19570
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19130
|
-
|
|
19571
|
+
),
|
|
19572
|
+
{ cutoff: Date.now() - windowMs }
|
|
19573
|
+
);
|
|
19131
19574
|
return Promise.resolve(
|
|
19132
19575
|
rows.map((row) => ({
|
|
19133
19576
|
reference: row.reference,
|
|
@@ -19207,7 +19650,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19207
19650
|
)`;
|
|
19208
19651
|
|
|
19209
19652
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19210
|
-
var
|
|
19653
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19654
|
+
var CONCAT_SEP = ",";
|
|
19655
|
+
var TUPLE_SEP = "|";
|
|
19656
|
+
function splitConcat(value) {
|
|
19657
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19658
|
+
}
|
|
19211
19659
|
function deriveInstanceStatus(row) {
|
|
19212
19660
|
return deriveFindingStatus({
|
|
19213
19661
|
kind: row.kind,
|
|
@@ -19275,13 +19723,16 @@ var SqliteFindingsRepository = class {
|
|
|
19275
19723
|
}
|
|
19276
19724
|
recentFindings(opts) {
|
|
19277
19725
|
const limit = opts?.limit ?? 50;
|
|
19278
|
-
const rows =
|
|
19279
|
-
|
|
19726
|
+
const rows = allRows(
|
|
19727
|
+
this.db.prepare(
|
|
19728
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19280
19729
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19281
19730
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19282
19731
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19283
19732
|
LIMIT :limit`
|
|
19284
|
-
|
|
19733
|
+
),
|
|
19734
|
+
{ limit }
|
|
19735
|
+
);
|
|
19285
19736
|
return Promise.resolve(
|
|
19286
19737
|
rows.map((r) => ({
|
|
19287
19738
|
id: r.id,
|
|
@@ -19298,26 +19749,94 @@ var SqliteFindingsRepository = class {
|
|
|
19298
19749
|
}))
|
|
19299
19750
|
);
|
|
19300
19751
|
}
|
|
19752
|
+
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19753
|
+
* session-stamped events (served by idx_events_session_id), so the Activity
|
|
19754
|
+
* page can label its findings link without the grouped pipeline. */
|
|
19755
|
+
sessionFindingsCount(sessionId) {
|
|
19756
|
+
if (!sessionId) return Promise.resolve(0);
|
|
19757
|
+
return Promise.resolve(
|
|
19758
|
+
countScalar(
|
|
19759
|
+
this.db,
|
|
19760
|
+
`SELECT count(*) AS n FROM findings f
|
|
19761
|
+
JOIN events e ON e.id = f.event_id
|
|
19762
|
+
WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
|
|
19763
|
+
{ sessionId }
|
|
19764
|
+
)
|
|
19765
|
+
);
|
|
19766
|
+
}
|
|
19767
|
+
/** Per-rule transcript firing tally for one session — reads the OTHER finding
|
|
19768
|
+
* store (inspection_findings, keyed to audit_events): every detection the
|
|
19769
|
+
* transcript pass recorded, counted per firing rather than per unique value.
|
|
19770
|
+
* Rides on session-scoped grouped responses so the findings view can
|
|
19771
|
+
* reconcile the Activity page's tally with the deduped groups it lists. */
|
|
19772
|
+
sessionFirings(sessionId) {
|
|
19773
|
+
return Object.fromEntries(
|
|
19774
|
+
countBy(
|
|
19775
|
+
this.db,
|
|
19776
|
+
`SELECT d.rule_id AS k, count(*) AS n
|
|
19777
|
+
FROM inspection_findings f
|
|
19778
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19779
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19780
|
+
WHERE e.root_session_id = :sessionId
|
|
19781
|
+
GROUP BY d.rule_id`,
|
|
19782
|
+
{ sessionId }
|
|
19783
|
+
)
|
|
19784
|
+
);
|
|
19785
|
+
}
|
|
19301
19786
|
/**
|
|
19302
|
-
* Grouped findings for the dashboard — joins findings⋈events (repo/file
|
|
19303
|
-
* from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19787
|
+
* Grouped findings for the dashboard — joins findings⋈events (repo/file/
|
|
19788
|
+
* toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19304
19789
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19305
19790
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19306
19791
|
* reflect the full filtered set; `items` is the requested
|
|
19307
|
-
* page (default
|
|
19792
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19793
|
+
*
|
|
19794
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19795
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19796
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19797
|
+
* latest, search text);
|
|
19798
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19799
|
+
* populate `instances` for the table's expanded rows.
|
|
19800
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19801
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19802
|
+
* rule is ever restated in SQL.
|
|
19308
19803
|
*/
|
|
19309
19804
|
listGroupedFindings(query) {
|
|
19310
|
-
const
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
19805
|
+
const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
|
|
19806
|
+
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19807
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19808
|
+
predicate: sessionPredicate,
|
|
19809
|
+
params: sessionParams
|
|
19810
|
+
});
|
|
19811
|
+
const rows = allRows(
|
|
19812
|
+
this.db.prepare(
|
|
19813
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19814
|
+
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19815
|
+
FROM (
|
|
19816
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19817
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19818
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19819
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19820
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19821
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19822
|
+
json_extract(e.metadata, '$.toolName') AS tool_name,
|
|
19823
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19824
|
+
latest.status AS latest_status,
|
|
19825
|
+
ROW_NUMBER() OVER (
|
|
19826
|
+
PARTITION BY f.rule_id
|
|
19827
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19828
|
+
) AS rn
|
|
19829
|
+
FROM findings f
|
|
19830
|
+
JOIN events e ON e.id = f.event_id
|
|
19831
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19832
|
+
ON latest.finding_key = f.finding_key
|
|
19833
|
+
${sessionPredicate}
|
|
19834
|
+
)
|
|
19835
|
+
WHERE rn <= :cap
|
|
19836
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19837
|
+
),
|
|
19838
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
19839
|
+
);
|
|
19321
19840
|
const groupable = rows.map((r) => ({
|
|
19322
19841
|
id: r.id,
|
|
19323
19842
|
ruleId: r.rule_id,
|
|
@@ -19330,9 +19849,10 @@ var SqliteFindingsRepository = class {
|
|
|
19330
19849
|
sourceTool: r.source_tool,
|
|
19331
19850
|
repo: r.repo ?? "",
|
|
19332
19851
|
file: r.file ?? "",
|
|
19852
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
19333
19853
|
status: deriveInstanceStatus(r)
|
|
19334
19854
|
}));
|
|
19335
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19855
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19336
19856
|
const filterOpts = {
|
|
19337
19857
|
severity: query.severity,
|
|
19338
19858
|
providers: query.provider,
|
|
@@ -19348,44 +19868,134 @@ var SqliteFindingsRepository = class {
|
|
|
19348
19868
|
};
|
|
19349
19869
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19350
19870
|
const items = sorted.slice(0, limit);
|
|
19351
|
-
return Promise.resolve({
|
|
19871
|
+
return Promise.resolve({
|
|
19872
|
+
totals,
|
|
19873
|
+
facets,
|
|
19874
|
+
items,
|
|
19875
|
+
nextCursor: null,
|
|
19876
|
+
...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
|
|
19877
|
+
});
|
|
19878
|
+
}
|
|
19879
|
+
/**
|
|
19880
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19881
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19882
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19883
|
+
*
|
|
19884
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19885
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19886
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19887
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19888
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19889
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19890
|
+
* a group's row stays small however many findings it holds.
|
|
19891
|
+
*
|
|
19892
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19893
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19894
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19895
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19896
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19897
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19898
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19899
|
+
* carries a `q`.
|
|
19900
|
+
*/
|
|
19901
|
+
groupAggregates(withSearchText, scope) {
|
|
19902
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19903
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
|
|
19904
|
+
group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19905
|
+
const rows = this.db.prepare(
|
|
19906
|
+
`SELECT f.rule_id AS rule_id,
|
|
19907
|
+
count(*) AS instance_count,
|
|
19908
|
+
max(e.occurred_at) AS latest_at,
|
|
19909
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19910
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19911
|
+
group_concat(DISTINCT (
|
|
19912
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19913
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19914
|
+
coalesce(latest.status, '')
|
|
19915
|
+
)) AS status_inputs
|
|
19916
|
+
${searchTextColumns}
|
|
19917
|
+
FROM findings f
|
|
19918
|
+
JOIN events e ON e.id = f.event_id
|
|
19919
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19920
|
+
ON latest.finding_key = f.finding_key
|
|
19921
|
+
${scope.predicate}
|
|
19922
|
+
GROUP BY f.rule_id`
|
|
19923
|
+
).all(scope.params);
|
|
19924
|
+
return new Map(
|
|
19925
|
+
rows.map((r) => [
|
|
19926
|
+
r.rule_id,
|
|
19927
|
+
{
|
|
19928
|
+
instanceCount: r.instance_count,
|
|
19929
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19930
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19931
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19932
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19933
|
+
return {
|
|
19934
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19935
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19936
|
+
kind,
|
|
19937
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19938
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19939
|
+
};
|
|
19940
|
+
}),
|
|
19941
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19942
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19943
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19944
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19945
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19946
|
+
// and skip priming a haystack nothing will read.
|
|
19947
|
+
...withSearchText ? {
|
|
19948
|
+
searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
|
|
19949
|
+
} : {}
|
|
19950
|
+
}
|
|
19951
|
+
])
|
|
19952
|
+
);
|
|
19352
19953
|
}
|
|
19353
19954
|
healthSummary() {
|
|
19354
|
-
const total = this.db
|
|
19955
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19355
19956
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19356
|
-
const grouped =
|
|
19957
|
+
const grouped = allRows(
|
|
19958
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19959
|
+
);
|
|
19357
19960
|
for (const row of grouped) {
|
|
19358
19961
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19359
19962
|
}
|
|
19360
19963
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19361
|
-
const sevRows =
|
|
19362
|
-
|
|
19964
|
+
const sevRows = allRows(
|
|
19965
|
+
this.db.prepare(
|
|
19966
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19363
19967
|
FROM findings f
|
|
19364
19968
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19365
19969
|
ON latest.finding_key = f.finding_key
|
|
19366
19970
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19367
19971
|
GROUP BY f.severity`
|
|
19368
|
-
|
|
19972
|
+
)
|
|
19973
|
+
);
|
|
19369
19974
|
for (const row of sevRows) {
|
|
19370
19975
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19371
19976
|
}
|
|
19372
19977
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19373
|
-
const enabledRows =
|
|
19374
|
-
|
|
19978
|
+
const enabledRows = allRows(
|
|
19979
|
+
this.db.prepare(
|
|
19980
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19375
19981
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19376
|
-
|
|
19982
|
+
)
|
|
19983
|
+
);
|
|
19377
19984
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19378
19985
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19379
19986
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19380
19987
|
}
|
|
19381
19988
|
activityByDay(days = 7) {
|
|
19382
19989
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19383
|
-
const rows =
|
|
19384
|
-
|
|
19990
|
+
const rows = allRows(
|
|
19991
|
+
this.db.prepare(
|
|
19992
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19385
19993
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19386
19994
|
WHERE e.occurred_at >= :since
|
|
19387
19995
|
GROUP BY day, f.action_taken`
|
|
19388
|
-
|
|
19996
|
+
),
|
|
19997
|
+
{ since }
|
|
19998
|
+
);
|
|
19389
19999
|
const buckets = /* @__PURE__ */ new Map();
|
|
19390
20000
|
for (let i = 0; i < days; i++) {
|
|
19391
20001
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19457,17 +20067,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19457
20067
|
insertStmt;
|
|
19458
20068
|
insertFinding(input) {
|
|
19459
20069
|
const row = toInspectionFindingRow(input);
|
|
19460
|
-
this.insertStmt.run(
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
|
|
20070
|
+
this.insertStmt.run(
|
|
20071
|
+
bindParams({
|
|
20072
|
+
id: row.id,
|
|
20073
|
+
auditEventId: row.auditEventId,
|
|
20074
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
20075
|
+
classifiedDataId: row.classifiedDataId,
|
|
20076
|
+
spanStart: row.spanStart,
|
|
20077
|
+
spanEnd: row.spanEnd,
|
|
20078
|
+
maskedMatch: row.maskedMatch,
|
|
20079
|
+
actionTaken: row.actionTaken,
|
|
20080
|
+
confidence: row.confidence
|
|
20081
|
+
})
|
|
20082
|
+
);
|
|
19471
20083
|
}
|
|
19472
20084
|
};
|
|
19473
20085
|
|
|
@@ -19543,12 +20155,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19543
20155
|
}
|
|
19544
20156
|
function ruleIdsOf(rulesJson) {
|
|
19545
20157
|
const ids = /* @__PURE__ */ new Set();
|
|
19546
|
-
|
|
19547
|
-
try {
|
|
19548
|
-
raw = JSON.parse(rulesJson);
|
|
19549
|
-
} catch {
|
|
19550
|
-
return ids;
|
|
19551
|
-
}
|
|
20158
|
+
const raw = safeJson(rulesJson, []);
|
|
19552
20159
|
if (!Array.isArray(raw)) return ids;
|
|
19553
20160
|
for (const entry of raw) {
|
|
19554
20161
|
if (entry && typeof entry === "object") {
|
|
@@ -19621,47 +20228,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19621
20228
|
}));
|
|
19622
20229
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19623
20230
|
const now = Date.now();
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
const
|
|
19630
|
-
|
|
19631
|
-
namespace: row.namespace,
|
|
19632
|
-
packId: row.packId,
|
|
19633
|
-
version: row.version,
|
|
19634
|
-
name: row.name,
|
|
19635
|
-
rulesJson: row.rulesJson,
|
|
19636
|
-
now
|
|
19637
|
-
};
|
|
19638
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19639
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19640
|
-
this.upsertAvailableStmt.run({
|
|
19641
|
-
...params,
|
|
20231
|
+
withTransaction(
|
|
20232
|
+
this.db,
|
|
20233
|
+
() => {
|
|
20234
|
+
const mirror = this.mirrorState();
|
|
20235
|
+
let behind = false;
|
|
20236
|
+
for (const row of rows) {
|
|
20237
|
+
const params = {
|
|
19642
20238
|
id: randomUUID2(),
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
20239
|
+
namespace: row.namespace,
|
|
20240
|
+
packId: row.packId,
|
|
20241
|
+
version: row.version,
|
|
20242
|
+
name: row.name,
|
|
20243
|
+
rulesJson: row.rulesJson,
|
|
20244
|
+
now
|
|
20245
|
+
};
|
|
20246
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20247
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20248
|
+
this.upsertAvailableStmt.run({
|
|
20249
|
+
...params,
|
|
20250
|
+
id: randomUUID2(),
|
|
20251
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20252
|
+
});
|
|
20253
|
+
} else {
|
|
20254
|
+
behind = true;
|
|
20255
|
+
}
|
|
20256
|
+
this.insertMissingStmt.run(params);
|
|
19647
20257
|
}
|
|
19648
|
-
this.
|
|
19649
|
-
}
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
} catch (err) {
|
|
19653
|
-
this.db.exec("ROLLBACK");
|
|
19654
|
-
throw err;
|
|
19655
|
-
}
|
|
20258
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20259
|
+
},
|
|
20260
|
+
"IMMEDIATE"
|
|
20261
|
+
);
|
|
19656
20262
|
} catch {
|
|
19657
20263
|
}
|
|
19658
20264
|
}
|
|
19659
20265
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19660
20266
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19661
20267
|
mirrorState() {
|
|
19662
|
-
const rows =
|
|
19663
|
-
|
|
19664
|
-
|
|
20268
|
+
const rows = allRows(
|
|
20269
|
+
this.db.prepare(
|
|
20270
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20271
|
+
)
|
|
20272
|
+
);
|
|
19665
20273
|
return new Map(
|
|
19666
20274
|
rows.map((r) => [
|
|
19667
20275
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19673,7 +20281,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19673
20281
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19674
20282
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19675
20283
|
pruneAvailable(keep) {
|
|
19676
|
-
const rows =
|
|
20284
|
+
const rows = allRows(
|
|
20285
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20286
|
+
);
|
|
19677
20287
|
const keepSet = new Set(keep);
|
|
19678
20288
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19679
20289
|
for (const r of rows) {
|
|
@@ -19700,11 +20310,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19700
20310
|
if (this.db.isTransaction) {
|
|
19701
20311
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19702
20312
|
}
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
this.db
|
|
19706
|
-
|
|
19707
|
-
|
|
20313
|
+
let changed = false;
|
|
20314
|
+
withTransaction(
|
|
20315
|
+
this.db,
|
|
20316
|
+
() => {
|
|
20317
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20318
|
+
const res = this.db.prepare(
|
|
20319
|
+
`UPDATE installed_packs SET
|
|
19708
20320
|
version = (SELECT a.version FROM available_packs a
|
|
19709
20321
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19710
20322
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19715,17 +20327,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19715
20327
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19716
20328
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19717
20329
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
} catch {
|
|
19726
|
-
}
|
|
19727
|
-
throw err;
|
|
19728
|
-
}
|
|
20330
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20331
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20332
|
+
changed = Number(res.changes) > 0;
|
|
20333
|
+
},
|
|
20334
|
+
"IMMEDIATE"
|
|
20335
|
+
);
|
|
20336
|
+
return changed;
|
|
19729
20337
|
}
|
|
19730
20338
|
/**
|
|
19731
20339
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19739,9 +20347,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19739
20347
|
* JSON-level failure therefore counts as invalid.
|
|
19740
20348
|
*/
|
|
19741
20349
|
installedRuleset() {
|
|
19742
|
-
const rows =
|
|
19743
|
-
|
|
19744
|
-
|
|
20350
|
+
const rows = allRows(
|
|
20351
|
+
this.db.prepare(
|
|
20352
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20353
|
+
)
|
|
20354
|
+
);
|
|
19745
20355
|
const out = {
|
|
19746
20356
|
installedPacks: rows.length,
|
|
19747
20357
|
enabledPacks: 0,
|
|
@@ -19750,7 +20360,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19750
20360
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19751
20361
|
};
|
|
19752
20362
|
for (const row of rows) {
|
|
19753
|
-
if (row.enabled
|
|
20363
|
+
if (!intToBool(row.enabled)) continue;
|
|
19754
20364
|
out.enabledPacks += 1;
|
|
19755
20365
|
const action = policyIdToAction(row.policyId);
|
|
19756
20366
|
let raw;
|
|
@@ -19785,9 +20395,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19785
20395
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19786
20396
|
*/
|
|
19787
20397
|
newestRecordedBinary() {
|
|
19788
|
-
const rows =
|
|
19789
|
-
|
|
19790
|
-
|
|
20398
|
+
const rows = allRows(
|
|
20399
|
+
this.db.prepare(
|
|
20400
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20401
|
+
)
|
|
20402
|
+
);
|
|
19791
20403
|
let newest = null;
|
|
19792
20404
|
for (const row of rows) {
|
|
19793
20405
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19802,13 +20414,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19802
20414
|
return newest;
|
|
19803
20415
|
}
|
|
19804
20416
|
counts() {
|
|
19805
|
-
const row =
|
|
19806
|
-
|
|
20417
|
+
const row = getRow(
|
|
20418
|
+
this.db.prepare(
|
|
20419
|
+
`SELECT count(*) AS packs,
|
|
19807
20420
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19808
20421
|
coalesce(sum(enabled), 0) AS enabled
|
|
19809
20422
|
FROM installed_packs`
|
|
19810
|
-
|
|
19811
|
-
|
|
20423
|
+
)
|
|
20424
|
+
);
|
|
20425
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19812
20426
|
}
|
|
19813
20427
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19814
20428
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19821,26 +20435,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19821
20435
|
* attributed to Monitor, matching the Detections views.
|
|
19822
20436
|
*/
|
|
19823
20437
|
countsByPolicyId() {
|
|
19824
|
-
|
|
19825
|
-
|
|
20438
|
+
return countBy(
|
|
20439
|
+
this.db,
|
|
20440
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19826
20441
|
FROM installed_packs
|
|
19827
|
-
GROUP BY
|
|
19828
|
-
)
|
|
19829
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20442
|
+
GROUP BY k`
|
|
20443
|
+
);
|
|
19830
20444
|
}
|
|
19831
20445
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19832
20446
|
listByPolicyId(policyId) {
|
|
19833
|
-
const rows =
|
|
19834
|
-
|
|
20447
|
+
const rows = allRows(
|
|
20448
|
+
this.db.prepare(
|
|
20449
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19835
20450
|
FROM installed_packs
|
|
19836
20451
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19837
20452
|
ORDER BY name ASC`
|
|
19838
|
-
|
|
20453
|
+
),
|
|
20454
|
+
[policyId]
|
|
20455
|
+
);
|
|
19839
20456
|
return rows.map((r) => ({
|
|
19840
20457
|
id: `${r.namespace}/${r.packId}`,
|
|
19841
20458
|
name: r.name,
|
|
19842
20459
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19843
|
-
enabled: r.enabled
|
|
20460
|
+
enabled: intToBool(r.enabled)
|
|
19844
20461
|
}));
|
|
19845
20462
|
}
|
|
19846
20463
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19869,14 +20486,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19869
20486
|
const res = this.db.prepare(
|
|
19870
20487
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19871
20488
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19872
|
-
).run({ enabled: enabled
|
|
20489
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19873
20490
|
return Number(res.changes) > 0;
|
|
19874
20491
|
}
|
|
19875
20492
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19876
20493
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19877
20494
|
// binary's inventory hasn't changed since the last record.
|
|
19878
20495
|
storedSignature() {
|
|
19879
|
-
const rows = this.signatureStmt
|
|
20496
|
+
const rows = allRows(this.signatureStmt);
|
|
19880
20497
|
return inventorySignature(rows);
|
|
19881
20498
|
}
|
|
19882
20499
|
};
|
|
@@ -19904,42 +20521,48 @@ var SqliteInventoryRepository = class {
|
|
|
19904
20521
|
upsert(input, now = Date.now()) {
|
|
19905
20522
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19906
20523
|
const row = toInventoryRow(input, id, now);
|
|
19907
|
-
this.upsertStmt.run(
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
20524
|
+
this.upsertStmt.run(
|
|
20525
|
+
bindParams({
|
|
20526
|
+
id: row.id,
|
|
20527
|
+
objectType: row.objectType,
|
|
20528
|
+
location: row.location,
|
|
20529
|
+
title: row.title,
|
|
20530
|
+
hostId: row.hostId,
|
|
20531
|
+
attributes: row.attributes,
|
|
20532
|
+
firstSeen: row.firstSeen,
|
|
20533
|
+
lastSeen: row.lastSeen
|
|
20534
|
+
})
|
|
20535
|
+
);
|
|
19917
20536
|
return id;
|
|
19918
20537
|
}
|
|
19919
20538
|
// The full row, for round-trip assertions.
|
|
19920
20539
|
findById(id) {
|
|
19921
|
-
|
|
19922
|
-
return row;
|
|
20540
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19923
20541
|
}
|
|
19924
20542
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19925
20543
|
// served from the object_type index, never from audit_events.
|
|
19926
20544
|
distinctTitles(objectType) {
|
|
19927
|
-
const rows =
|
|
19928
|
-
|
|
20545
|
+
const rows = allRows(
|
|
20546
|
+
this.db.prepare(
|
|
20547
|
+
`SELECT DISTINCT title FROM inventory
|
|
19929
20548
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19930
20549
|
ORDER BY title`
|
|
19931
|
-
|
|
20550
|
+
),
|
|
20551
|
+
{ objectType }
|
|
20552
|
+
);
|
|
19932
20553
|
return rows.map((r) => r.title);
|
|
19933
20554
|
}
|
|
19934
20555
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19935
20556
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19936
20557
|
// QUERY PLAN).
|
|
19937
20558
|
osVersions() {
|
|
19938
|
-
const rows =
|
|
19939
|
-
|
|
20559
|
+
const rows = allRows(
|
|
20560
|
+
this.db.prepare(
|
|
20561
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19940
20562
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
19941
20563
|
ORDER BY value`
|
|
19942
|
-
|
|
20564
|
+
)
|
|
20565
|
+
);
|
|
19943
20566
|
return rows.map((r) => r.value);
|
|
19944
20567
|
}
|
|
19945
20568
|
};
|
|
@@ -19959,14 +20582,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
19959
20582
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
19960
20583
|
findingsCount: 0
|
|
19961
20584
|
};
|
|
19962
|
-
function safeJson(s, fallback) {
|
|
19963
|
-
if (s == null) return fallback;
|
|
19964
|
-
try {
|
|
19965
|
-
return JSON.parse(s);
|
|
19966
|
-
} catch {
|
|
19967
|
-
return fallback;
|
|
19968
|
-
}
|
|
19969
|
-
}
|
|
19970
20585
|
function resolveHarnessId(attrs, row) {
|
|
19971
20586
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
19972
20587
|
return attrs.provider;
|
|
@@ -20162,30 +20777,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20162
20777
|
configRowsCache;
|
|
20163
20778
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20164
20779
|
getInventoryStats() {
|
|
20165
|
-
const
|
|
20166
|
-
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
byType
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
|
|
20780
|
+
const typeCounts = countBy(
|
|
20781
|
+
this.db,
|
|
20782
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20783
|
+
);
|
|
20784
|
+
const byType = {
|
|
20785
|
+
project: 0,
|
|
20786
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20787
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20788
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20789
|
+
config: typeCounts.get("config") ?? 0
|
|
20790
|
+
};
|
|
20791
|
+
byType.project = countScalar(
|
|
20792
|
+
this.db,
|
|
20793
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20794
|
+
);
|
|
20795
|
+
const mcpTrustCounts = countBy(
|
|
20796
|
+
this.db,
|
|
20797
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20173
20798
|
FROM inventory_asset a
|
|
20174
20799
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20175
20800
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20176
20801
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20177
|
-
)
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20802
|
+
);
|
|
20803
|
+
const mcpTrust = {
|
|
20804
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20805
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20806
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20807
|
+
};
|
|
20808
|
+
const harnesses = countScalar(
|
|
20809
|
+
this.db,
|
|
20181
20810
|
`SELECT count(*) AS n FROM inventory
|
|
20182
20811
|
WHERE object_type = 'harness'
|
|
20183
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20184
|
-
|
|
20185
|
-
|
|
20186
|
-
const
|
|
20812
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20813
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20814
|
+
);
|
|
20815
|
+
const flaggedAssets = countScalar(
|
|
20816
|
+
this.db,
|
|
20817
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20818
|
+
);
|
|
20819
|
+
const flaggedProjects = countScalar(
|
|
20820
|
+
this.db,
|
|
20187
20821
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20188
|
-
)
|
|
20822
|
+
);
|
|
20189
20823
|
const configRows = this.configAssetRows();
|
|
20190
20824
|
for (const r of configRows) {
|
|
20191
20825
|
byType[r.assetType] += 1;
|
|
@@ -20442,12 +21076,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20442
21076
|
}
|
|
20443
21077
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20444
21078
|
fetchHarnessRows() {
|
|
20445
|
-
return
|
|
20446
|
-
|
|
21079
|
+
return allRows(
|
|
21080
|
+
this.db.prepare(
|
|
21081
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20447
21082
|
FROM inventory
|
|
20448
21083
|
WHERE object_type = 'harness'
|
|
20449
21084
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20450
|
-
|
|
21085
|
+
),
|
|
21086
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
21087
|
+
);
|
|
20451
21088
|
}
|
|
20452
21089
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20453
21090
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20457,12 +21094,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20457
21094
|
const params = [...harnessInvIds];
|
|
20458
21095
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20459
21096
|
if (q) {
|
|
20460
|
-
const pat =
|
|
20461
|
-
where +=
|
|
21097
|
+
const pat = containsPattern(q);
|
|
21098
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20462
21099
|
params.push(pat, pat);
|
|
20463
21100
|
}
|
|
20464
|
-
const rows =
|
|
20465
|
-
|
|
21101
|
+
const rows = allRows(
|
|
21102
|
+
this.db.prepare(
|
|
21103
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20466
21104
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20467
21105
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20468
21106
|
FROM harness_asset ha
|
|
@@ -20470,7 +21108,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20470
21108
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20471
21109
|
WHERE ${where}
|
|
20472
21110
|
ORDER BY a.name ASC`
|
|
20473
|
-
|
|
21111
|
+
),
|
|
21112
|
+
params
|
|
21113
|
+
);
|
|
20474
21114
|
for (const raw of rows) {
|
|
20475
21115
|
const harnessInvId = raw.harnessInvId;
|
|
20476
21116
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20489,21 +21129,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20489
21129
|
params.push(...types);
|
|
20490
21130
|
}
|
|
20491
21131
|
if (q) {
|
|
20492
|
-
const pat =
|
|
20493
|
-
conditions.push("
|
|
21132
|
+
const pat = containsPattern(q);
|
|
21133
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20494
21134
|
params.push(pat, pat);
|
|
20495
21135
|
}
|
|
20496
21136
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20497
21137
|
const sampleRows = this.mapAssetRows(
|
|
20498
|
-
|
|
20499
|
-
|
|
21138
|
+
allRows(
|
|
21139
|
+
this.db.prepare(
|
|
21140
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20500
21141
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20501
21142
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20502
21143
|
FROM inventory_asset a
|
|
20503
21144
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20504
21145
|
${where}
|
|
20505
21146
|
ORDER BY a.name ASC`
|
|
20506
|
-
|
|
21147
|
+
),
|
|
21148
|
+
params
|
|
21149
|
+
)
|
|
20507
21150
|
);
|
|
20508
21151
|
const configRows = this.configAssetRows(q).filter(
|
|
20509
21152
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20512,14 +21155,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20512
21155
|
}
|
|
20513
21156
|
fetchAssetById(assetId) {
|
|
20514
21157
|
const rows = this.mapAssetRows(
|
|
20515
|
-
|
|
20516
|
-
|
|
21158
|
+
allRows(
|
|
21159
|
+
this.db.prepare(
|
|
21160
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20517
21161
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20518
21162
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20519
21163
|
FROM inventory_asset a
|
|
20520
21164
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20521
21165
|
WHERE a.id = ?`
|
|
20522
|
-
|
|
21166
|
+
),
|
|
21167
|
+
[assetId]
|
|
21168
|
+
)
|
|
20523
21169
|
);
|
|
20524
21170
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20525
21171
|
}
|
|
@@ -20575,37 +21221,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20575
21221
|
return rows;
|
|
20576
21222
|
}
|
|
20577
21223
|
latestConfigScanId() {
|
|
20578
|
-
|
|
20579
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20580
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20581
|
-
).get();
|
|
20582
|
-
return row?.id ?? null;
|
|
21224
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20583
21225
|
}
|
|
20584
21226
|
fetchProjects(q) {
|
|
20585
21227
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20586
21228
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20587
21229
|
const params = [];
|
|
20588
21230
|
if (q) {
|
|
20589
|
-
const pat =
|
|
20590
|
-
sql +=
|
|
21231
|
+
const pat = containsPattern(q);
|
|
21232
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20591
21233
|
params.push(pat, pat);
|
|
20592
21234
|
}
|
|
20593
21235
|
sql += " ORDER BY name ASC";
|
|
20594
|
-
return this.db.prepare(sql)
|
|
21236
|
+
return allRows(this.db.prepare(sql), params);
|
|
20595
21237
|
}
|
|
20596
21238
|
fetchProjectById(projectId) {
|
|
20597
|
-
return
|
|
20598
|
-
|
|
20599
|
-
|
|
21239
|
+
return getRow(
|
|
21240
|
+
this.db.prepare(
|
|
21241
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21242
|
+
),
|
|
21243
|
+
[projectId]
|
|
21244
|
+
) ?? null;
|
|
20600
21245
|
}
|
|
20601
21246
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20602
21247
|
fetchProjectsByIds(projectIds) {
|
|
20603
21248
|
const map2 = /* @__PURE__ */ new Map();
|
|
20604
21249
|
if (projectIds.length === 0) return map2;
|
|
20605
|
-
const rows =
|
|
20606
|
-
|
|
21250
|
+
const rows = allRows(
|
|
21251
|
+
this.db.prepare(
|
|
21252
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20607
21253
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20608
|
-
|
|
21254
|
+
),
|
|
21255
|
+
projectIds
|
|
21256
|
+
);
|
|
20609
21257
|
for (const r of rows) map2.set(r.id, r);
|
|
20610
21258
|
return map2;
|
|
20611
21259
|
}
|
|
@@ -20616,8 +21264,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20616
21264
|
projectAggregates(projectIds) {
|
|
20617
21265
|
const map2 = /* @__PURE__ */ new Map();
|
|
20618
21266
|
if (projectIds.length === 0) return map2;
|
|
20619
|
-
const rows =
|
|
20620
|
-
|
|
21267
|
+
const rows = allRows(
|
|
21268
|
+
this.db.prepare(
|
|
21269
|
+
`SELECT f.project_id AS projectId,
|
|
20621
21270
|
coalesce(o.access, f.default_access) AS eff,
|
|
20622
21271
|
count(*) AS n,
|
|
20623
21272
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20625,7 +21274,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20625
21274
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20626
21275
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20627
21276
|
GROUP BY f.project_id, eff`
|
|
20628
|
-
|
|
21277
|
+
),
|
|
21278
|
+
projectIds
|
|
21279
|
+
);
|
|
20629
21280
|
for (const r of rows) {
|
|
20630
21281
|
let agg = map2.get(r.projectId);
|
|
20631
21282
|
if (!agg) {
|
|
@@ -20663,37 +21314,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20663
21314
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20664
21315
|
if (prefix === "") {
|
|
20665
21316
|
return this.mapFileRows(
|
|
20666
|
-
|
|
21317
|
+
allRows(
|
|
21318
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21319
|
+
[projectId]
|
|
21320
|
+
)
|
|
20667
21321
|
);
|
|
20668
21322
|
}
|
|
20669
21323
|
return this.mapFileRows(
|
|
20670
|
-
|
|
20671
|
-
this.
|
|
20672
|
-
|
|
21324
|
+
allRows(
|
|
21325
|
+
this.db.prepare(
|
|
21326
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21327
|
+
),
|
|
21328
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21329
|
+
)
|
|
20673
21330
|
);
|
|
20674
21331
|
}
|
|
20675
21332
|
fetchProjectFilesSearch(projectId, q) {
|
|
20676
|
-
const pat =
|
|
21333
|
+
const pat = containsPattern(q);
|
|
20677
21334
|
return this.mapFileRows(
|
|
20678
|
-
|
|
20679
|
-
this.
|
|
20680
|
-
|
|
20681
|
-
|
|
20682
|
-
|
|
21335
|
+
allRows(
|
|
21336
|
+
this.db.prepare(
|
|
21337
|
+
this.fileSelect(
|
|
21338
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21339
|
+
)
|
|
21340
|
+
),
|
|
21341
|
+
[projectId, pat, pat]
|
|
21342
|
+
)
|
|
20683
21343
|
);
|
|
20684
21344
|
}
|
|
20685
21345
|
fetchProjectFilesBlocked(projectId) {
|
|
20686
21346
|
return this.mapFileRows(
|
|
20687
|
-
|
|
20688
|
-
this.
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
21347
|
+
allRows(
|
|
21348
|
+
this.db.prepare(
|
|
21349
|
+
this.fileSelect(
|
|
21350
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21351
|
+
)
|
|
21352
|
+
),
|
|
21353
|
+
[projectId]
|
|
21354
|
+
)
|
|
20692
21355
|
);
|
|
20693
21356
|
}
|
|
20694
21357
|
fetchProjectFile(projectId, path) {
|
|
20695
21358
|
const rows = this.mapFileRows(
|
|
20696
|
-
|
|
21359
|
+
allRows(
|
|
21360
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21361
|
+
[projectId, path]
|
|
21362
|
+
)
|
|
20697
21363
|
);
|
|
20698
21364
|
return rows[0] ?? null;
|
|
20699
21365
|
}
|
|
@@ -20707,39 +21373,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20707
21373
|
}
|
|
20708
21374
|
db;
|
|
20709
21375
|
readPolicies() {
|
|
20710
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20711
|
-
const policies =
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
customKeywords
|
|
20724
|
-
})
|
|
20725
|
-
);
|
|
20726
|
-
} catch {
|
|
20727
|
-
}
|
|
20728
|
-
}
|
|
21376
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21377
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21378
|
+
const target = JSON.parse(row.target);
|
|
21379
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21380
|
+
return Policy.parse({
|
|
21381
|
+
id: row.id,
|
|
21382
|
+
scope: row.scope,
|
|
21383
|
+
target,
|
|
21384
|
+
action: row.action,
|
|
21385
|
+
enabled: intToBool(row.enabled),
|
|
21386
|
+
customKeywords
|
|
21387
|
+
});
|
|
21388
|
+
});
|
|
20729
21389
|
return Promise.resolve(policies);
|
|
20730
21390
|
}
|
|
20731
21391
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20732
21392
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20733
21393
|
// so a user's edits are never clobbered.
|
|
20734
21394
|
seedDefaults() {
|
|
20735
|
-
const count = this.db
|
|
21395
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20736
21396
|
if (count > 0) return;
|
|
20737
21397
|
const stmt = this.db.prepare(
|
|
20738
21398
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20739
21399
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20740
21400
|
);
|
|
20741
|
-
this.db
|
|
20742
|
-
try {
|
|
21401
|
+
failOpenTransaction(this.db, () => {
|
|
20743
21402
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20744
21403
|
stmt.run({
|
|
20745
21404
|
id: randomUUID4(),
|
|
@@ -20748,10 +21407,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20748
21407
|
now: Date.now()
|
|
20749
21408
|
});
|
|
20750
21409
|
}
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
|
|
20754
|
-
|
|
21410
|
+
});
|
|
21411
|
+
}
|
|
21412
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21413
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21414
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21415
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21416
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21417
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21418
|
+
upsertCategoryAction(category, action) {
|
|
21419
|
+
const now = Date.now();
|
|
21420
|
+
this.db.prepare(
|
|
21421
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21422
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21423
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21424
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21425
|
+
}
|
|
21426
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21427
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21428
|
+
// Returns the number of rows changed.
|
|
21429
|
+
capCategoryActions() {
|
|
21430
|
+
const info = this.db.prepare(
|
|
21431
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21432
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21433
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21434
|
+
).run({ now: Date.now() });
|
|
21435
|
+
return Number(info.changes);
|
|
21436
|
+
}
|
|
21437
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21438
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21439
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21440
|
+
getCategoryAction(category) {
|
|
21441
|
+
const row = this.db.prepare(
|
|
21442
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21443
|
+
).get({ category });
|
|
21444
|
+
return row?.action;
|
|
20755
21445
|
}
|
|
20756
21446
|
};
|
|
20757
21447
|
|
|
@@ -20827,7 +21517,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20827
21517
|
maxStampStmt;
|
|
20828
21518
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20829
21519
|
replaceForProject(projectId, scan2, now) {
|
|
20830
|
-
const
|
|
21520
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20831
21521
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20832
21522
|
for (const file2 of scan2.files) {
|
|
20833
21523
|
this.upsertStmt.run({
|
|
@@ -20910,7 +21600,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20910
21600
|
}
|
|
20911
21601
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20912
21602
|
latestByKey(key) {
|
|
20913
|
-
const row = this.latestStmt
|
|
21603
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20914
21604
|
if (!row) return void 0;
|
|
20915
21605
|
return {
|
|
20916
21606
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20927,7 +21617,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20927
21617
|
* the CLI) surfaces for that file.
|
|
20928
21618
|
*/
|
|
20929
21619
|
openAtRestKeysForPath(path) {
|
|
20930
|
-
const rows = this.openAtRestStmt
|
|
21620
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20931
21621
|
return rows.map((r) => r.finding_key);
|
|
20932
21622
|
}
|
|
20933
21623
|
/**
|
|
@@ -20938,7 +21628,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20938
21628
|
* resolution row (see scan.ts).
|
|
20939
21629
|
*/
|
|
20940
21630
|
resolvedAtRestKeysForPath(path) {
|
|
20941
|
-
const rows = this.resolvedAtRestStmt
|
|
21631
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
20942
21632
|
return rows.map((r) => r.finding_key);
|
|
20943
21633
|
}
|
|
20944
21634
|
};
|
|
@@ -20967,38 +21657,31 @@ var SqliteScanLedgerRepository = class {
|
|
|
20967
21657
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
20968
21658
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
20969
21659
|
entriesForRuleset(rulesetHash) {
|
|
20970
|
-
const rows = this.readStmt
|
|
21660
|
+
const rows = allRows(this.readStmt, {
|
|
21661
|
+
rulesetHash
|
|
21662
|
+
});
|
|
20971
21663
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
20972
21664
|
}
|
|
20973
21665
|
upsertEntries(entries) {
|
|
20974
21666
|
if (entries.length === 0) return;
|
|
20975
21667
|
const scannedAt = Date.now();
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
20983
|
-
|
|
20984
|
-
|
|
20985
|
-
scannedAt
|
|
20986
|
-
});
|
|
20987
|
-
}
|
|
20988
|
-
this.db.exec("COMMIT");
|
|
20989
|
-
} catch (err) {
|
|
20990
|
-
this.db.exec("ROLLBACK");
|
|
20991
|
-
throw err;
|
|
21668
|
+
failOpenTransaction(this.db, () => {
|
|
21669
|
+
for (const entry of entries) {
|
|
21670
|
+
this.upsertStmt.run({
|
|
21671
|
+
path: entry.path,
|
|
21672
|
+
mtime: entry.mtime,
|
|
21673
|
+
contentHash: entry.contentHash,
|
|
21674
|
+
rulesetHash: entry.rulesetHash,
|
|
21675
|
+
scannedAt
|
|
21676
|
+
});
|
|
20992
21677
|
}
|
|
20993
|
-
}
|
|
20994
|
-
}
|
|
21678
|
+
});
|
|
20995
21679
|
}
|
|
20996
21680
|
};
|
|
20997
21681
|
|
|
20998
21682
|
// ../../packages/persistence/src/repositories/security.ts
|
|
20999
21683
|
var DAY_MS4 = 864e5;
|
|
21000
21684
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
21001
|
-
var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
|
|
21002
21685
|
var ACTION_TO_KIND = {
|
|
21003
21686
|
block: "blocked",
|
|
21004
21687
|
redact: "redacted",
|
|
@@ -21013,8 +21696,14 @@ var SCAN_COVERAGE = [
|
|
|
21013
21696
|
{ provider: "copilot", coverage: 0, supported: false },
|
|
21014
21697
|
{ provider: "api", coverage: 0, supported: false }
|
|
21015
21698
|
];
|
|
21699
|
+
var GRANULARITY = {
|
|
21700
|
+
"7d": "day",
|
|
21701
|
+
"30d": "day",
|
|
21702
|
+
"3m": "week",
|
|
21703
|
+
"6m": "week"
|
|
21704
|
+
};
|
|
21016
21705
|
function granularityFor(range) {
|
|
21017
|
-
return range
|
|
21706
|
+
return GRANULARITY[range];
|
|
21018
21707
|
}
|
|
21019
21708
|
function startOfUtcDay2(ms) {
|
|
21020
21709
|
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
@@ -21068,8 +21757,9 @@ var SqliteSecurityRepository = class {
|
|
|
21068
21757
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21069
21758
|
// double-counting a key that accumulated several append-only rows.
|
|
21070
21759
|
severitySummary() {
|
|
21071
|
-
const rows =
|
|
21072
|
-
|
|
21760
|
+
const rows = allRows(
|
|
21761
|
+
this.db.prepare(
|
|
21762
|
+
`SELECT f.severity AS severity,
|
|
21073
21763
|
COUNT(*) AS count,
|
|
21074
21764
|
SUM(CASE
|
|
21075
21765
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21088,7 +21778,8 @@ var SqliteSecurityRepository = class {
|
|
|
21088
21778
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21089
21779
|
ON latest.finding_key = f.finding_key
|
|
21090
21780
|
GROUP BY f.severity`
|
|
21091
|
-
|
|
21781
|
+
)
|
|
21782
|
+
);
|
|
21092
21783
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21093
21784
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21094
21785
|
severity,
|
|
@@ -21172,14 +21863,15 @@ var SqliteSecurityRepository = class {
|
|
|
21172
21863
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21173
21864
|
const now = this.now();
|
|
21174
21865
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21175
|
-
const rows =
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21180
|
-
|
|
21181
|
-
|
|
21182
|
-
|
|
21866
|
+
const rows = allRows(
|
|
21867
|
+
this.db.prepare(
|
|
21868
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21869
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21870
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21871
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21872
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21873
|
+
// backfill left null.
|
|
21874
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21183
21875
|
(
|
|
21184
21876
|
SELECT fr.status FROM finding_resolution fr
|
|
21185
21877
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21205,14 +21897,16 @@ var SqliteSecurityRepository = class {
|
|
|
21205
21897
|
WHERE fr.finding_key = f.finding_key
|
|
21206
21898
|
AND fr.resolved_at >= :windowStart
|
|
21207
21899
|
)`
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
|
|
21900
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21901
|
+
// any resolution activity at/after the window start — a row this method
|
|
21902
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21903
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21904
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21905
|
+
// dialect-agnostic. Without this, a
|
|
21906
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21907
|
+
),
|
|
21908
|
+
{ windowStart }
|
|
21909
|
+
);
|
|
21216
21910
|
const sums = /* @__PURE__ */ new Map();
|
|
21217
21911
|
const counts = /* @__PURE__ */ new Map();
|
|
21218
21912
|
for (const r of rows) {
|
|
@@ -21242,8 +21936,9 @@ var SqliteSecurityRepository = class {
|
|
|
21242
21936
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21243
21937
|
const now = this.now();
|
|
21244
21938
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21245
|
-
const rows =
|
|
21246
|
-
|
|
21939
|
+
const rows = allRows(
|
|
21940
|
+
this.db.prepare(
|
|
21941
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21247
21942
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21248
21943
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21249
21944
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21251,7 +21946,9 @@ var SqliteSecurityRepository = class {
|
|
|
21251
21946
|
GROUP BY repo
|
|
21252
21947
|
ORDER BY c DESC, repo
|
|
21253
21948
|
LIMIT :limit`
|
|
21254
|
-
|
|
21949
|
+
),
|
|
21950
|
+
{ from, to: now, limit }
|
|
21951
|
+
);
|
|
21255
21952
|
const items = rows.map((r) => ({
|
|
21256
21953
|
id: `repo_${r.repo}`,
|
|
21257
21954
|
name: r.repo,
|
|
@@ -21273,8 +21970,9 @@ var SqliteSecurityRepository = class {
|
|
|
21273
21970
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21274
21971
|
// capped at `limit`.
|
|
21275
21972
|
recentlyResolved(limit = 20) {
|
|
21276
|
-
const rows =
|
|
21277
|
-
|
|
21973
|
+
const rows = allRows(
|
|
21974
|
+
this.db.prepare(
|
|
21975
|
+
`SELECT f.finding_key AS finding_key,
|
|
21278
21976
|
f.rule_id AS rule_id,
|
|
21279
21977
|
f.severity AS severity,
|
|
21280
21978
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21308,7 +22006,9 @@ var SqliteSecurityRepository = class {
|
|
|
21308
22006
|
) IS NOT NULL
|
|
21309
22007
|
ORDER BY latest_resolved_at DESC
|
|
21310
22008
|
LIMIT :limit`
|
|
21311
|
-
|
|
22009
|
+
),
|
|
22010
|
+
{ limit }
|
|
22011
|
+
);
|
|
21312
22012
|
const items = rows.map((r) => ({
|
|
21313
22013
|
findingKey: r.finding_key,
|
|
21314
22014
|
ruleId: r.rule_id,
|
|
@@ -21325,12 +22025,15 @@ var SqliteSecurityRepository = class {
|
|
|
21325
22025
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21326
22026
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21327
22027
|
findingsInRange(fromMs, toMs) {
|
|
21328
|
-
const rows =
|
|
21329
|
-
|
|
22028
|
+
const rows = allRows(
|
|
22029
|
+
this.db.prepare(
|
|
22030
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21330
22031
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21331
22032
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21332
22033
|
ORDER BY e.occurred_at`
|
|
21333
|
-
|
|
22034
|
+
),
|
|
22035
|
+
{ from: fromMs, to: toMs }
|
|
22036
|
+
);
|
|
21334
22037
|
return rows.map((r) => ({
|
|
21335
22038
|
occurredAt: r.occurred_at,
|
|
21336
22039
|
severity: r.severity,
|
|
@@ -21344,12 +22047,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21344
22047
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21345
22048
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21346
22049
|
function parseNetwork(networkJson) {
|
|
21347
|
-
|
|
21348
|
-
try {
|
|
21349
|
-
return JSON.parse(networkJson);
|
|
21350
|
-
} catch {
|
|
21351
|
-
return null;
|
|
21352
|
-
}
|
|
22050
|
+
return safeJson(networkJson, null);
|
|
21353
22051
|
}
|
|
21354
22052
|
function toEndpointSummary(row) {
|
|
21355
22053
|
return {
|
|
@@ -21436,27 +22134,39 @@ var SqliteSharesRepository = class {
|
|
|
21436
22134
|
}
|
|
21437
22135
|
db;
|
|
21438
22136
|
stats() {
|
|
21439
|
-
const
|
|
21440
|
-
const
|
|
21441
|
-
const
|
|
21442
|
-
const
|
|
21443
|
-
|
|
22137
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
22138
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
22139
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22140
|
+
const insecure = countScalar(
|
|
22141
|
+
this.db,
|
|
21444
22142
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21445
22143
|
);
|
|
21446
|
-
const needsReview =
|
|
22144
|
+
const needsReview = countScalar(
|
|
22145
|
+
this.db,
|
|
21447
22146
|
`SELECT count(DISTINCT d.id) AS n
|
|
21448
22147
|
FROM share_destination d
|
|
21449
22148
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21450
22149
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21451
22150
|
);
|
|
21452
|
-
const
|
|
21453
|
-
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
const
|
|
21457
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
22151
|
+
const kindCounts = countBy(
|
|
22152
|
+
this.db,
|
|
22153
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
22154
|
+
);
|
|
22155
|
+
const byKind = {
|
|
22156
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
22157
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
22158
|
+
ip: kindCounts.get("ip") ?? 0
|
|
22159
|
+
};
|
|
22160
|
+
const trustCounts = countBy(
|
|
22161
|
+
this.db,
|
|
22162
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
22163
|
+
);
|
|
22164
|
+
const byTrust = {
|
|
22165
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
22166
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
22167
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
22168
|
+
ip: trustCounts.get("ip") ?? 0
|
|
22169
|
+
};
|
|
21460
22170
|
return Promise.resolve({
|
|
21461
22171
|
destinations,
|
|
21462
22172
|
endpoints,
|
|
@@ -21572,7 +22282,7 @@ var SqliteSharesRepository = class {
|
|
|
21572
22282
|
}
|
|
21573
22283
|
let sql;
|
|
21574
22284
|
if (q) {
|
|
21575
|
-
const pattern =
|
|
22285
|
+
const pattern = containsPattern(q);
|
|
21576
22286
|
conditions.push(
|
|
21577
22287
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21578
22288
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21592,24 +22302,31 @@ var SqliteSharesRepository = class {
|
|
|
21592
22302
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21593
22303
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21594
22304
|
}
|
|
21595
|
-
const rows =
|
|
22305
|
+
const rows = allRows(
|
|
22306
|
+
this.db.prepare(sql),
|
|
22307
|
+
params
|
|
22308
|
+
);
|
|
21596
22309
|
return rows.map((r) => this.mapDestRow(r));
|
|
21597
22310
|
}
|
|
21598
22311
|
fetchDestinationById(destinationId) {
|
|
21599
|
-
const row =
|
|
21600
|
-
|
|
22312
|
+
const row = getRow(
|
|
22313
|
+
this.db.prepare(
|
|
22314
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21601
22315
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21602
22316
|
o.decision AS overrideDecision
|
|
21603
22317
|
FROM share_destination d
|
|
21604
22318
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21605
22319
|
WHERE d.id = ?`
|
|
21606
|
-
|
|
22320
|
+
),
|
|
22321
|
+
[destinationId]
|
|
22322
|
+
);
|
|
21607
22323
|
return row ? this.mapDestRow(row) : null;
|
|
21608
22324
|
}
|
|
21609
22325
|
fetchEndpoints(destinationIds) {
|
|
21610
22326
|
if (destinationIds.length === 0) return [];
|
|
21611
|
-
const rows =
|
|
21612
|
-
|
|
22327
|
+
const rows = allRows(
|
|
22328
|
+
this.db.prepare(
|
|
22329
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21613
22330
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21614
22331
|
count(c.id) AS callSiteCount
|
|
21615
22332
|
FROM share_endpoint e
|
|
@@ -21617,7 +22334,9 @@ var SqliteSharesRepository = class {
|
|
|
21617
22334
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21618
22335
|
GROUP BY e.id
|
|
21619
22336
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21620
|
-
|
|
22337
|
+
),
|
|
22338
|
+
destinationIds
|
|
22339
|
+
);
|
|
21621
22340
|
return rows.map((r) => ({
|
|
21622
22341
|
id: r.id,
|
|
21623
22342
|
destinationId: r.destinationId,
|
|
@@ -21642,13 +22361,16 @@ var SqliteSharesRepository = class {
|
|
|
21642
22361
|
}
|
|
21643
22362
|
fetchCallSites(endpointIds) {
|
|
21644
22363
|
if (endpointIds.length === 0) return [];
|
|
21645
|
-
const rows =
|
|
21646
|
-
|
|
22364
|
+
const rows = allRows(
|
|
22365
|
+
this.db.prepare(
|
|
22366
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21647
22367
|
project_id AS projectId
|
|
21648
22368
|
FROM share_call_site
|
|
21649
22369
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21650
22370
|
ORDER BY created_at ASC, id ASC`
|
|
21651
|
-
|
|
22371
|
+
),
|
|
22372
|
+
endpointIds
|
|
22373
|
+
);
|
|
21652
22374
|
return rows.map((r) => ({
|
|
21653
22375
|
id: r.id,
|
|
21654
22376
|
endpointId: r.endpointId,
|
|
@@ -21684,27 +22406,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21684
22406
|
upsert(input, now = Date.now()) {
|
|
21685
22407
|
const id = sourceProjectId(input.url);
|
|
21686
22408
|
const row = toSourceProjectRow(input, id, now);
|
|
21687
|
-
this.upsertStmt.run(
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
21692
|
-
|
|
21693
|
-
|
|
21694
|
-
|
|
22409
|
+
this.upsertStmt.run(
|
|
22410
|
+
bindParams({
|
|
22411
|
+
id: row.id,
|
|
22412
|
+
url: row.url,
|
|
22413
|
+
name: row.name,
|
|
22414
|
+
attributes: row.attributes,
|
|
22415
|
+
firstSeen: row.firstSeen,
|
|
22416
|
+
lastSeen: row.lastSeen
|
|
22417
|
+
})
|
|
22418
|
+
);
|
|
21695
22419
|
return id;
|
|
21696
22420
|
}
|
|
21697
22421
|
findById(id) {
|
|
21698
|
-
return
|
|
22422
|
+
return getRow(
|
|
22423
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22424
|
+
{
|
|
22425
|
+
id
|
|
22426
|
+
}
|
|
22427
|
+
);
|
|
21699
22428
|
}
|
|
21700
22429
|
// Distinct project names — a filter facet, served from the source_project
|
|
21701
22430
|
// table, never from the audit fact table.
|
|
21702
22431
|
distinctNames() {
|
|
21703
|
-
const rows =
|
|
21704
|
-
|
|
22432
|
+
const rows = allRows(
|
|
22433
|
+
this.db.prepare(
|
|
22434
|
+
`SELECT DISTINCT name FROM source_project
|
|
21705
22435
|
WHERE name IS NOT NULL
|
|
21706
22436
|
ORDER BY name`
|
|
21707
|
-
|
|
22437
|
+
)
|
|
22438
|
+
);
|
|
21708
22439
|
return rows.map((r) => r.name);
|
|
21709
22440
|
}
|
|
21710
22441
|
};
|
|
@@ -21723,8 +22454,7 @@ function hasLegacySampleRows(db) {
|
|
|
21723
22454
|
function purgeSampleData(db) {
|
|
21724
22455
|
try {
|
|
21725
22456
|
if (!hasLegacySampleRows(db)) return;
|
|
21726
|
-
db
|
|
21727
|
-
try {
|
|
22457
|
+
withTransaction(db, () => {
|
|
21728
22458
|
db.exec(
|
|
21729
22459
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21730
22460
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21769,11 +22499,7 @@ function purgeSampleData(db) {
|
|
|
21769
22499
|
value TEXT NOT NULL
|
|
21770
22500
|
)`);
|
|
21771
22501
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21772
|
-
|
|
21773
|
-
} catch (err) {
|
|
21774
|
-
db.exec("ROLLBACK");
|
|
21775
|
-
throw err;
|
|
21776
|
-
}
|
|
22502
|
+
});
|
|
21777
22503
|
} catch {
|
|
21778
22504
|
}
|
|
21779
22505
|
}
|
|
@@ -21792,7 +22518,7 @@ function openWithPragmas(file2) {
|
|
|
21792
22518
|
function backupLegacyStore(file2) {
|
|
21793
22519
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21794
22520
|
renameSync(file2, backup);
|
|
21795
|
-
for (const sidecar of
|
|
22521
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21796
22522
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21797
22523
|
}
|
|
21798
22524
|
return backup;
|
|
@@ -21805,9 +22531,8 @@ function openLocalDatabase(dir) {
|
|
|
21805
22531
|
db.close();
|
|
21806
22532
|
const backup = backupLegacyStore(file2);
|
|
21807
22533
|
db = openWithPragmas(file2);
|
|
21808
|
-
|
|
21809
|
-
`
|
|
21810
|
-
`
|
|
22534
|
+
akaWarn(
|
|
22535
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21811
22536
|
);
|
|
21812
22537
|
}
|
|
21813
22538
|
applyMigrations(db);
|
|
@@ -21835,96 +22560,77 @@ function openLocalDatabase(dir) {
|
|
|
21835
22560
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21836
22561
|
policies.seedDefaults();
|
|
21837
22562
|
function recordCapture(event, detected) {
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21843
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21844
|
-
db.exec("COMMIT");
|
|
21845
|
-
} catch (err) {
|
|
21846
|
-
db.exec("ROLLBACK");
|
|
21847
|
-
throw err;
|
|
21848
|
-
}
|
|
21849
|
-
} catch {
|
|
21850
|
-
}
|
|
22563
|
+
failOpenTransaction(db, () => {
|
|
22564
|
+
events.insertEvent(event);
|
|
22565
|
+
const sessionId = event.metadata?.sessionId;
|
|
22566
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22567
|
+
});
|
|
21851
22568
|
}
|
|
21852
22569
|
function ensureInventory(ctx) {
|
|
21853
22570
|
const resolved = {};
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
db.exec("COMMIT");
|
|
21875
|
-
} catch (err) {
|
|
21876
|
-
db.exec("ROLLBACK");
|
|
21877
|
-
throw err;
|
|
21878
|
-
}
|
|
21879
|
-
} catch {
|
|
21880
|
-
return {};
|
|
21881
|
-
}
|
|
21882
|
-
return resolved;
|
|
22571
|
+
const committed = failOpenTransaction(db, () => {
|
|
22572
|
+
const now = Date.now();
|
|
22573
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22574
|
+
if (ctx.harness) {
|
|
22575
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22576
|
+
}
|
|
22577
|
+
resolved.accountId = inventory.upsert(
|
|
22578
|
+
linkHost(
|
|
22579
|
+
{
|
|
22580
|
+
objectType: "user",
|
|
22581
|
+
identityKey: "local",
|
|
22582
|
+
attributes: { source: "local" }
|
|
22583
|
+
},
|
|
22584
|
+
resolved.hostId
|
|
22585
|
+
),
|
|
22586
|
+
now
|
|
22587
|
+
);
|
|
22588
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22589
|
+
});
|
|
22590
|
+
return committed ? resolved : {};
|
|
21883
22591
|
}
|
|
21884
22592
|
function recordConfigScan(record2) {
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
}
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
|
|
21903
|
-
|
|
21904
|
-
|
|
21905
|
-
confidence: finding.confidence
|
|
21906
|
-
});
|
|
21907
|
-
}
|
|
21908
|
-
db.exec("COMMIT");
|
|
21909
|
-
} catch (err) {
|
|
21910
|
-
db.exec("ROLLBACK");
|
|
21911
|
-
throw err;
|
|
22593
|
+
failOpenTransaction(db, () => {
|
|
22594
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22595
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22596
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22597
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22598
|
+
for (const def of record2.definitions ?? []) {
|
|
22599
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22600
|
+
}
|
|
22601
|
+
for (const finding of record2.findings ?? []) {
|
|
22602
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22603
|
+
if (!definitionId) continue;
|
|
22604
|
+
inspectionFindings.insertFinding({
|
|
22605
|
+
id: randomUUID8(),
|
|
22606
|
+
auditEventId: record2.scanEvent.id,
|
|
22607
|
+
inspectionDefinitionId: definitionId,
|
|
22608
|
+
span: finding.span,
|
|
22609
|
+
maskedMatch: finding.maskedMatch,
|
|
22610
|
+
actionTaken: finding.actionTaken,
|
|
22611
|
+
confidence: finding.confidence
|
|
22612
|
+
});
|
|
21912
22613
|
}
|
|
21913
|
-
}
|
|
21914
|
-
}
|
|
22614
|
+
});
|
|
21915
22615
|
}
|
|
21916
22616
|
function recordProjectFiles(projectId, scan2) {
|
|
21917
22617
|
if (scan2.files.length === 0) return;
|
|
22618
|
+
failOpenTransaction(db, () => {
|
|
22619
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22620
|
+
});
|
|
22621
|
+
}
|
|
22622
|
+
async function transaction(fn) {
|
|
22623
|
+
db.exec("BEGIN");
|
|
21918
22624
|
try {
|
|
21919
|
-
|
|
22625
|
+
const result = await fn();
|
|
22626
|
+
db.exec("COMMIT");
|
|
22627
|
+
return result;
|
|
22628
|
+
} catch (err) {
|
|
21920
22629
|
try {
|
|
21921
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21922
|
-
db.exec("COMMIT");
|
|
21923
|
-
} catch (err) {
|
|
21924
22630
|
db.exec("ROLLBACK");
|
|
21925
|
-
|
|
22631
|
+
} catch {
|
|
21926
22632
|
}
|
|
21927
|
-
|
|
22633
|
+
throw err;
|
|
21928
22634
|
}
|
|
21929
22635
|
}
|
|
21930
22636
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -21943,8 +22649,7 @@ function openLocalDatabase(dir) {
|
|
|
21943
22649
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
21944
22650
|
});
|
|
21945
22651
|
if (stale.length === 0) return;
|
|
21946
|
-
db
|
|
21947
|
-
try {
|
|
22652
|
+
withTransaction(db, () => {
|
|
21948
22653
|
for (const { id } of stale) {
|
|
21949
22654
|
db.prepare(
|
|
21950
22655
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -21956,11 +22661,7 @@ function openLocalDatabase(dir) {
|
|
|
21956
22661
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
21957
22662
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
21958
22663
|
}
|
|
21959
|
-
|
|
21960
|
-
} catch (err) {
|
|
21961
|
-
db.exec("ROLLBACK");
|
|
21962
|
-
throw err;
|
|
21963
|
-
}
|
|
22664
|
+
});
|
|
21964
22665
|
} catch {
|
|
21965
22666
|
}
|
|
21966
22667
|
}
|
|
@@ -22002,6 +22703,7 @@ function openLocalDatabase(dir) {
|
|
|
22002
22703
|
purgeSampleData: () => {
|
|
22003
22704
|
purgeSampleData(db);
|
|
22004
22705
|
},
|
|
22706
|
+
transaction,
|
|
22005
22707
|
close: () => {
|
|
22006
22708
|
db.close();
|
|
22007
22709
|
}
|
|
@@ -22094,12 +22796,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22094
22796
|
}
|
|
22095
22797
|
}
|
|
22096
22798
|
function readJson(file2) {
|
|
22799
|
+
let text;
|
|
22097
22800
|
try {
|
|
22098
|
-
|
|
22099
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22801
|
+
text = readFileSync2(file2, "utf8");
|
|
22100
22802
|
} catch {
|
|
22101
22803
|
return null;
|
|
22102
22804
|
}
|
|
22805
|
+
return parseJsonObject(text) ?? null;
|
|
22806
|
+
}
|
|
22807
|
+
|
|
22808
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22809
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22810
|
+
import { join as join5 } from "path";
|
|
22811
|
+
var MARKER = "warn-era-capped";
|
|
22812
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22813
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22814
|
+
const marker = join5(dataDir2, MARKER);
|
|
22815
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22816
|
+
const capped = db.policies.capCategoryActions();
|
|
22817
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22818
|
+
`, { mode: DATA_FILE_MODE });
|
|
22819
|
+
return { capped };
|
|
22103
22820
|
}
|
|
22104
22821
|
|
|
22105
22822
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22184,23 +22901,30 @@ function resolveProviderSafe() {
|
|
|
22184
22901
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22185
22902
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22186
22903
|
import { homedir as homedir2 } from "os";
|
|
22187
|
-
import { basename as basename2, join as
|
|
22904
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22905
|
+
|
|
22906
|
+
// ../../packages/detections/src/escape-regexp.ts
|
|
22907
|
+
function escapeRegExp(value) {
|
|
22908
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22909
|
+
}
|
|
22910
|
+
|
|
22911
|
+
// ../../packages/detections/src/matchers/limits.ts
|
|
22912
|
+
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22188
22913
|
|
|
22189
22914
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22190
22915
|
var KeywordMatcher2 = class {
|
|
22191
22916
|
match(text, rule) {
|
|
22192
22917
|
if (rule.matcher.type !== "keyword") return [];
|
|
22193
22918
|
const { keywords, caseSensitive } = rule.matcher;
|
|
22194
|
-
const haystack = caseSensitive ? text : text.toLowerCase();
|
|
22195
22919
|
const spans = [];
|
|
22196
22920
|
for (const kw of keywords) {
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
|
|
22201
|
-
|
|
22202
|
-
spans.push({ start:
|
|
22203
|
-
|
|
22921
|
+
if (kw.length === 0) continue;
|
|
22922
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22923
|
+
const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
|
|
22924
|
+
let m;
|
|
22925
|
+
while ((m = re.exec(text)) !== null) {
|
|
22926
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
22927
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22204
22928
|
}
|
|
22205
22929
|
}
|
|
22206
22930
|
return spans;
|
|
@@ -22208,7 +22932,6 @@ var KeywordMatcher2 = class {
|
|
|
22208
22932
|
};
|
|
22209
22933
|
|
|
22210
22934
|
// ../../packages/detections/src/matchers/regex.ts
|
|
22211
|
-
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22212
22935
|
var RegexMatcher2 = class {
|
|
22213
22936
|
match(text, rule) {
|
|
22214
22937
|
if (rule.matcher.type !== "regex") return [];
|
|
@@ -22294,9 +23017,6 @@ function registerPack(pack) {
|
|
|
22294
23017
|
function getLoadedRules() {
|
|
22295
23018
|
return [...packs.values()].flatMap((p) => p.rules);
|
|
22296
23019
|
}
|
|
22297
|
-
function escapeRegExp(value) {
|
|
22298
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22299
|
-
}
|
|
22300
23020
|
function isCorroborated(candidate, candidates, text) {
|
|
22301
23021
|
const req = candidate.rule.requiresNearby;
|
|
22302
23022
|
if (!req) return true;
|
|
@@ -23078,7 +23798,6 @@ var db_table_name_default = {
|
|
|
23078
23798
|
"SELECT * FROM ",
|
|
23079
23799
|
"SELECT COUNT(*) FROM ",
|
|
23080
23800
|
"INSERT INTO ",
|
|
23081
|
-
"UPDATE ",
|
|
23082
23801
|
"DELETE FROM ",
|
|
23083
23802
|
"CREATE TABLE ",
|
|
23084
23803
|
"ALTER TABLE ",
|
|
@@ -24563,8 +25282,8 @@ function scanText(text) {
|
|
|
24563
25282
|
}
|
|
24564
25283
|
|
|
24565
25284
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24566
|
-
import { existsSync as
|
|
24567
|
-
import { basename, dirname, isAbsolute, join as
|
|
25285
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25286
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24568
25287
|
function resolveRepoIdentity(cwd) {
|
|
24569
25288
|
try {
|
|
24570
25289
|
const root = findGitRoot(cwd);
|
|
@@ -24597,32 +25316,32 @@ function resolveRepoNwo(cwd) {
|
|
|
24597
25316
|
function findGitRoot(start) {
|
|
24598
25317
|
let dir = start;
|
|
24599
25318
|
for (; ; ) {
|
|
24600
|
-
if (
|
|
25319
|
+
if (existsSync3(join6(dir, ".git"))) return dir;
|
|
24601
25320
|
const parent = dirname(dir);
|
|
24602
25321
|
if (parent === dir) return void 0;
|
|
24603
25322
|
dir = parent;
|
|
24604
25323
|
}
|
|
24605
25324
|
}
|
|
24606
25325
|
function resolveGitContext(root) {
|
|
24607
|
-
const dotGit =
|
|
25326
|
+
const dotGit = join6(root, ".git");
|
|
24608
25327
|
try {
|
|
24609
25328
|
if (statSync(dotGit).isDirectory()) {
|
|
24610
|
-
return { configPath:
|
|
25329
|
+
return { configPath: join6(dotGit, "config"), headRoot: root };
|
|
24611
25330
|
}
|
|
24612
25331
|
} catch {
|
|
24613
25332
|
return void 0;
|
|
24614
25333
|
}
|
|
24615
25334
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
24616
25335
|
if (!target) return void 0;
|
|
24617
|
-
const gitdir = isAbsolute(target) ? target :
|
|
24618
|
-
if (
|
|
24619
|
-
return { configPath:
|
|
25336
|
+
const gitdir = isAbsolute(target) ? target : join6(root, target);
|
|
25337
|
+
if (existsSync3(join6(gitdir, "config"))) {
|
|
25338
|
+
return { configPath: join6(gitdir, "config"), headRoot: root };
|
|
24620
25339
|
}
|
|
24621
|
-
const commonRaw = safeRead(
|
|
25340
|
+
const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
|
|
24622
25341
|
if (!commonRaw) return void 0;
|
|
24623
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
25342
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
|
|
24624
25343
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
24625
|
-
return { configPath:
|
|
25344
|
+
return { configPath: join6(commonGitDir, "config"), headRoot };
|
|
24626
25345
|
}
|
|
24627
25346
|
function safeRead(path) {
|
|
24628
25347
|
try {
|
|
@@ -24712,20 +25431,23 @@ function resolveInventoryContext(input) {
|
|
|
24712
25431
|
}
|
|
24713
25432
|
|
|
24714
25433
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24715
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24716
|
-
import { join as
|
|
25434
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25435
|
+
import { join as join8 } from "path";
|
|
24717
25436
|
|
|
24718
25437
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24719
25438
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24720
|
-
import { existsSync as
|
|
24721
|
-
import { basename as basename3, join as
|
|
25439
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
25440
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
24722
25441
|
|
|
24723
25442
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24724
25443
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
24725
25444
|
|
|
25445
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
25446
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25447
|
+
|
|
24726
25448
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24727
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
24728
|
-
import { join as
|
|
25449
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
25450
|
+
import { join as join10 } from "path";
|
|
24729
25451
|
|
|
24730
25452
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
24731
25453
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -24918,6 +25640,14 @@ var StandaloneDataGateway = class {
|
|
|
24918
25640
|
sweepTerminalExceptions(retentionMs) {
|
|
24919
25641
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
24920
25642
|
}
|
|
25643
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
25644
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
25645
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
25646
|
+
// already-capped one).
|
|
25647
|
+
capWarnEraEnforcement(policyMode) {
|
|
25648
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
25649
|
+
return { capped };
|
|
25650
|
+
}
|
|
24921
25651
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
24922
25652
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
24923
25653
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -25023,11 +25753,11 @@ import {
|
|
|
25023
25753
|
openSync,
|
|
25024
25754
|
readFileSync as readFileSync7,
|
|
25025
25755
|
readSync,
|
|
25026
|
-
writeFileSync as
|
|
25756
|
+
writeFileSync as writeFileSync6
|
|
25027
25757
|
} from "fs";
|
|
25028
|
-
import { join as
|
|
25758
|
+
import { join as join11 } from "path";
|
|
25029
25759
|
function offsetsDir(dataDir2) {
|
|
25030
|
-
return
|
|
25760
|
+
return join11(dataDir2, "usage-offsets");
|
|
25031
25761
|
}
|
|
25032
25762
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
25033
25763
|
function safeSessionId(sessionId) {
|
|
@@ -25037,7 +25767,7 @@ function safeSessionId(sessionId) {
|
|
|
25037
25767
|
return createHash5("sha256").update(sessionId).digest("hex");
|
|
25038
25768
|
}
|
|
25039
25769
|
function offsetPath(dataDir2, sessionId) {
|
|
25040
|
-
return
|
|
25770
|
+
return join11(offsetsDir(dataDir2), safeSessionId(sessionId));
|
|
25041
25771
|
}
|
|
25042
25772
|
function readOffset(dataDir2, sessionId) {
|
|
25043
25773
|
try {
|
|
@@ -25057,7 +25787,7 @@ function writeOffset(dataDir2, sessionId, value) {
|
|
|
25057
25787
|
try {
|
|
25058
25788
|
mkdirSync5(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
|
|
25059
25789
|
const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
|
|
25060
|
-
|
|
25790
|
+
writeFileSync6(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
|
|
25061
25791
|
mode: DATA_FILE_MODE
|
|
25062
25792
|
});
|
|
25063
25793
|
} catch {
|
|
@@ -25101,7 +25831,7 @@ function readTail(transcriptPath, startOffset) {
|
|
|
25101
25831
|
// src/history/transcripts.ts
|
|
25102
25832
|
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
25103
25833
|
import { homedir as homedir3 } from "os";
|
|
25104
|
-
import { join as
|
|
25834
|
+
import { join as join12 } from "path";
|
|
25105
25835
|
function isRecord(value) {
|
|
25106
25836
|
return typeof value === "object" && value !== null;
|
|
25107
25837
|
}
|