@akasecurity/ai-tc-claude-code 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -1
- package/commands/setup.md +216 -48
- package/hooks/hooks.json +1 -1
- package/package.json +6 -5
- package/scripts/apply-suppressions.js +23531 -0
- package/scripts/backfill.js +1537 -895
- package/scripts/filescan.js +1335 -864
- package/scripts/firstrun.js +1367 -869
- package/scripts/intro.js +78 -11
- package/scripts/onboard.js +5773 -40
- package/scripts/post-tool-use.js +1371 -879
- package/scripts/pre-tool-use.js +1471 -892
- package/scripts/query.js +1328 -857
- package/scripts/reconcile.js +1339 -869
- package/scripts/session-start.js +1390 -911
- package/scripts/statusline.js +1327 -856
- package/scripts/stop.js +97 -19
- package/scripts/triage-rubric.md +84 -0
- package/scripts/user-prompt-submit.js +1342 -871
package/scripts/query.js
CHANGED
|
@@ -15489,6 +15489,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15489
15489
|
"prompt",
|
|
15490
15490
|
"response",
|
|
15491
15491
|
"code_change",
|
|
15492
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15493
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15494
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15495
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15496
|
+
"tool_use",
|
|
15492
15497
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15493
15498
|
// fact the posture inspection findings reference (findings require an
|
|
15494
15499
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15880,7 +15885,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15880
15885
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15881
15886
|
|
|
15882
15887
|
// ../../packages/schema/src/zod/event.ts
|
|
15883
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15888
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15884
15889
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15885
15890
|
var EventMetadata = external_exports.object({
|
|
15886
15891
|
sessionId: external_exports.string().optional(),
|
|
@@ -16193,7 +16198,7 @@ var DetectionException = external_exports.object({
|
|
|
16193
16198
|
justification: external_exports.string().min(1),
|
|
16194
16199
|
conditions: ExceptionConditions.nullable(),
|
|
16195
16200
|
createdBy: external_exports.string(),
|
|
16196
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16201
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16197
16202
|
createdAt: external_exports.iso.datetime(),
|
|
16198
16203
|
updatedAt: external_exports.iso.datetime(),
|
|
16199
16204
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16362,20 +16367,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16362
16367
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16363
16368
|
fetchedAt: external_exports.iso.datetime()
|
|
16364
16369
|
}).meta({ id: "PolicyBundle" });
|
|
16365
|
-
var DEFAULT_ACTIONS = {
|
|
16366
|
-
secret: "block",
|
|
16367
|
-
pii: "redact",
|
|
16368
|
-
financial: "redact",
|
|
16369
|
-
phi: "redact",
|
|
16370
|
-
code_context: "warn",
|
|
16371
|
-
code_flaw: "warn",
|
|
16372
|
-
custom: "warn",
|
|
16373
|
-
// Config-posture findings only observe today (they land in
|
|
16374
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16375
|
-
config: "warn"
|
|
16376
|
-
};
|
|
16377
16370
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16378
16371
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16372
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16373
|
+
secret: "critical",
|
|
16374
|
+
financial: "critical",
|
|
16375
|
+
// core-financial/credit-card
|
|
16376
|
+
code_flaw: "critical",
|
|
16377
|
+
pii: "high",
|
|
16378
|
+
phi: "high",
|
|
16379
|
+
custom: "high",
|
|
16380
|
+
// user-defined; conservative
|
|
16381
|
+
code_context: "low",
|
|
16382
|
+
config: "low"
|
|
16383
|
+
// observe-only; floors to monitor regardless
|
|
16384
|
+
};
|
|
16385
|
+
function severityFloorPolicy(category) {
|
|
16386
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16387
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16388
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16389
|
+
}
|
|
16379
16390
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16380
16391
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16381
16392
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16402,6 +16413,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16402
16413
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16403
16414
|
}
|
|
16404
16415
|
};
|
|
16416
|
+
function builtinPolicyToAction(id) {
|
|
16417
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16418
|
+
}
|
|
16419
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16420
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16421
|
+
);
|
|
16405
16422
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16406
16423
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16407
16424
|
);
|
|
@@ -16808,10 +16825,8 @@ function toApiProvider(sourceTool) {
|
|
|
16808
16825
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16809
16826
|
}
|
|
16810
16827
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16811
|
-
function
|
|
16812
|
-
const statuses = new Set(
|
|
16813
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16814
|
-
);
|
|
16828
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16829
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16815
16830
|
if (statuses.size === 0) return void 0;
|
|
16816
16831
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16817
16832
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16829,6 +16844,7 @@ function deriveFindingStatus(row) {
|
|
|
16829
16844
|
function buildFindingGroups(rows, opts = {}) {
|
|
16830
16845
|
const overrides = opts.overrides;
|
|
16831
16846
|
const packNames = opts.packNames;
|
|
16847
|
+
const aggregates = opts.aggregates;
|
|
16832
16848
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16833
16849
|
for (const row of rows) {
|
|
16834
16850
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16850,17 +16866,20 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16850
16866
|
status: r.status
|
|
16851
16867
|
};
|
|
16852
16868
|
});
|
|
16853
|
-
const
|
|
16869
|
+
const agg = aggregates?.get(ruleId);
|
|
16870
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16854
16871
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16855
16872
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16856
16873
|
);
|
|
16857
16874
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16858
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16875
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16859
16876
|
if (seenProviders.has(p)) return false;
|
|
16860
16877
|
seenProviders.add(p);
|
|
16861
16878
|
return true;
|
|
16862
16879
|
});
|
|
16863
|
-
const actionSet = new Set(
|
|
16880
|
+
const actionSet = new Set(
|
|
16881
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16882
|
+
);
|
|
16864
16883
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16865
16884
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16866
16885
|
const detection = {
|
|
@@ -16874,8 +16893,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16874
16893
|
contextPrefix: ""
|
|
16875
16894
|
// empty (pending privacy review)
|
|
16876
16895
|
};
|
|
16877
|
-
const status =
|
|
16878
|
-
|
|
16896
|
+
const status = foldGroupStatus(
|
|
16897
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16898
|
+
);
|
|
16899
|
+
const group = {
|
|
16879
16900
|
id: ruleId,
|
|
16880
16901
|
category: apiCategory,
|
|
16881
16902
|
subtype: ruleId,
|
|
@@ -16884,21 +16905,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16884
16905
|
match,
|
|
16885
16906
|
detection,
|
|
16886
16907
|
policy,
|
|
16887
|
-
instanceCount: instances.length,
|
|
16908
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16888
16909
|
providers,
|
|
16889
16910
|
aggregateAction,
|
|
16890
16911
|
latestDetectedAt,
|
|
16891
16912
|
instances,
|
|
16892
16913
|
status
|
|
16893
|
-
}
|
|
16914
|
+
};
|
|
16915
|
+
if (agg) {
|
|
16916
|
+
actionsCache.set(group, [...actionSet]);
|
|
16917
|
+
if (agg.searchText !== void 0) {
|
|
16918
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16919
|
+
}
|
|
16920
|
+
}
|
|
16921
|
+
groups.push(group);
|
|
16894
16922
|
}
|
|
16895
16923
|
return groups;
|
|
16896
16924
|
}
|
|
16897
16925
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16898
|
-
function
|
|
16899
|
-
|
|
16900
|
-
if (cached2 !== void 0) return cached2;
|
|
16901
|
-
const haystack = [
|
|
16926
|
+
function buildHaystack(g, extra) {
|
|
16927
|
+
return [
|
|
16902
16928
|
g.subtype,
|
|
16903
16929
|
g.category,
|
|
16904
16930
|
g.match.maskedValue,
|
|
@@ -16906,11 +16932,25 @@ function groupHaystack(g) {
|
|
|
16906
16932
|
g.id,
|
|
16907
16933
|
...g.instances.map((i) => i.repo),
|
|
16908
16934
|
...g.instances.map((i) => i.file),
|
|
16909
|
-
...g.instances.map((i) => i.id)
|
|
16935
|
+
...g.instances.map((i) => i.id),
|
|
16936
|
+
...extra === void 0 ? [] : [extra]
|
|
16910
16937
|
].join(" ").toLowerCase();
|
|
16938
|
+
}
|
|
16939
|
+
function groupHaystack(g) {
|
|
16940
|
+
const cached2 = haystackCache.get(g);
|
|
16941
|
+
if (cached2 !== void 0) return cached2;
|
|
16942
|
+
const haystack = buildHaystack(g);
|
|
16911
16943
|
haystackCache.set(g, haystack);
|
|
16912
16944
|
return haystack;
|
|
16913
16945
|
}
|
|
16946
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16947
|
+
function groupActions(g) {
|
|
16948
|
+
const cached2 = actionsCache.get(g);
|
|
16949
|
+
if (cached2 !== void 0) return cached2;
|
|
16950
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16951
|
+
actionsCache.set(g, actions);
|
|
16952
|
+
return actions;
|
|
16953
|
+
}
|
|
16914
16954
|
function applyFindingFilters(groups, opts) {
|
|
16915
16955
|
let filtered = groups;
|
|
16916
16956
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16923,7 +16963,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16923
16963
|
}
|
|
16924
16964
|
if (opts.actions && opts.actions.length > 0) {
|
|
16925
16965
|
const actionSet = new Set(opts.actions);
|
|
16926
|
-
filtered = filtered.filter((g) => g.
|
|
16966
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16927
16967
|
}
|
|
16928
16968
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16929
16969
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16975,8 +17015,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16975
17015
|
});
|
|
16976
17016
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16977
17017
|
for (const g of forAction) {
|
|
16978
|
-
const
|
|
16979
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17018
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16980
17019
|
}
|
|
16981
17020
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16982
17021
|
providers: opts.providers,
|
|
@@ -17538,6 +17577,132 @@ function reviewSeverityRank(reasons) {
|
|
|
17538
17577
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17539
17578
|
}
|
|
17540
17579
|
|
|
17580
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17581
|
+
var TriageHit = external_exports.object({
|
|
17582
|
+
ruleId: external_exports.string(),
|
|
17583
|
+
category: DetectionCategory,
|
|
17584
|
+
severity: Severity,
|
|
17585
|
+
maskedMatch: external_exports.string(),
|
|
17586
|
+
rawMatch: external_exports.string(),
|
|
17587
|
+
context: external_exports.string(),
|
|
17588
|
+
filePath: external_exports.string().optional(),
|
|
17589
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17590
|
+
id: external_exports.string().optional(),
|
|
17591
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17592
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17593
|
+
});
|
|
17594
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17595
|
+
var TriageCategoryRec = external_exports.object({
|
|
17596
|
+
category: DetectionCategory,
|
|
17597
|
+
action: TriagePolicy,
|
|
17598
|
+
reasoning: external_exports.string(),
|
|
17599
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17600
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17601
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17602
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17603
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17604
|
+
});
|
|
17605
|
+
var TriageRecommendation = external_exports.object({
|
|
17606
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17607
|
+
notes: external_exports.string()
|
|
17608
|
+
});
|
|
17609
|
+
|
|
17610
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17611
|
+
function escapeLikePattern(s) {
|
|
17612
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17613
|
+
}
|
|
17614
|
+
function placeholders(n) {
|
|
17615
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17616
|
+
}
|
|
17617
|
+
function containsPattern(q) {
|
|
17618
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17619
|
+
}
|
|
17620
|
+
function likeAny(exprs) {
|
|
17621
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17622
|
+
}
|
|
17623
|
+
|
|
17624
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17625
|
+
var savepointSeq = 0;
|
|
17626
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17627
|
+
if (db.isTransaction) {
|
|
17628
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17629
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17630
|
+
try {
|
|
17631
|
+
fn();
|
|
17632
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17633
|
+
} catch (error51) {
|
|
17634
|
+
try {
|
|
17635
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17636
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17637
|
+
} catch {
|
|
17638
|
+
}
|
|
17639
|
+
throw error51;
|
|
17640
|
+
}
|
|
17641
|
+
return;
|
|
17642
|
+
}
|
|
17643
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17644
|
+
try {
|
|
17645
|
+
fn();
|
|
17646
|
+
db.exec("COMMIT");
|
|
17647
|
+
} catch (error51) {
|
|
17648
|
+
try {
|
|
17649
|
+
db.exec("ROLLBACK");
|
|
17650
|
+
} catch {
|
|
17651
|
+
}
|
|
17652
|
+
throw error51;
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17656
|
+
const nested = db.isTransaction;
|
|
17657
|
+
try {
|
|
17658
|
+
withTransaction(db, fn, mode);
|
|
17659
|
+
return true;
|
|
17660
|
+
} catch (error51) {
|
|
17661
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17662
|
+
return false;
|
|
17663
|
+
}
|
|
17664
|
+
}
|
|
17665
|
+
|
|
17666
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17667
|
+
function akaWarn(message) {
|
|
17668
|
+
process.stderr.write(`[aka] ${message}
|
|
17669
|
+
`);
|
|
17670
|
+
}
|
|
17671
|
+
|
|
17672
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17673
|
+
function evidenceObjects(sql) {
|
|
17674
|
+
const objects = [];
|
|
17675
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17676
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17677
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17678
|
+
}
|
|
17679
|
+
}
|
|
17680
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17681
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17682
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17683
|
+
}
|
|
17684
|
+
}
|
|
17685
|
+
return objects;
|
|
17686
|
+
}
|
|
17687
|
+
function schemaObjectExists(db, kind, name) {
|
|
17688
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17689
|
+
return row !== void 0;
|
|
17690
|
+
}
|
|
17691
|
+
function indexExists(db, name) {
|
|
17692
|
+
return schemaObjectExists(db, "index", name);
|
|
17693
|
+
}
|
|
17694
|
+
function columnNames(db, table2, opts) {
|
|
17695
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17696
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
|
|
17697
|
+
return columns.map((c) => c.name);
|
|
17698
|
+
}
|
|
17699
|
+
function evidenceExists(db, object2) {
|
|
17700
|
+
if (object2.kind === "column") {
|
|
17701
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17702
|
+
}
|
|
17703
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17704
|
+
}
|
|
17705
|
+
|
|
17541
17706
|
// ../../packages/persistence/src/ids.ts
|
|
17542
17707
|
import { createHash } from "crypto";
|
|
17543
17708
|
function sha256Hex(input) {
|
|
@@ -17574,28 +17739,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17574
17739
|
}
|
|
17575
17740
|
|
|
17576
17741
|
// ../../packages/persistence/src/migrations.ts
|
|
17577
|
-
function evidenceObjects(sql) {
|
|
17578
|
-
const objects = [];
|
|
17579
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17580
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17581
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17582
|
-
}
|
|
17583
|
-
}
|
|
17584
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17585
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17586
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17587
|
-
}
|
|
17588
|
-
}
|
|
17589
|
-
return objects;
|
|
17590
|
-
}
|
|
17591
|
-
function evidenceExists(db, object2) {
|
|
17592
|
-
if (object2.kind === "column") {
|
|
17593
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17594
|
-
return columns.some((c) => c.name === object2.name);
|
|
17595
|
-
}
|
|
17596
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17597
|
-
return row !== void 0;
|
|
17598
|
-
}
|
|
17599
17742
|
function describeObject(object2) {
|
|
17600
17743
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17601
17744
|
}
|
|
@@ -17606,10 +17749,6 @@ function createdIndexName(statement) {
|
|
|
17606
17749
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17607
17750
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17608
17751
|
}
|
|
17609
|
-
function indexExists(db, name) {
|
|
17610
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17611
|
-
return row !== void 0;
|
|
17612
|
-
}
|
|
17613
17752
|
function applyMigrations(db) {
|
|
17614
17753
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17615
17754
|
db.exec(
|
|
@@ -17628,44 +17767,39 @@ function applyMigrations(db) {
|
|
|
17628
17767
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17629
17768
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17630
17769
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17631
|
-
const message = `
|
|
17632
|
-
|
|
17633
|
-
`);
|
|
17634
|
-
throw new Error(message);
|
|
17770
|
+
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.`;
|
|
17771
|
+
akaWarn(message);
|
|
17772
|
+
throw new Error(`[aka] ${message}`);
|
|
17635
17773
|
}
|
|
17636
17774
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17637
17775
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17638
17776
|
const statements = splitStatements(migration.sql);
|
|
17639
17777
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17640
17778
|
try {
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
const
|
|
17645
|
-
|
|
17646
|
-
if (
|
|
17647
|
-
|
|
17648
|
-
|
|
17779
|
+
withTransaction(
|
|
17780
|
+
db,
|
|
17781
|
+
() => {
|
|
17782
|
+
for (const statement of statements) {
|
|
17783
|
+
const indexName = createdIndexName(statement);
|
|
17784
|
+
if (indexName === void 0) {
|
|
17785
|
+
if (alreadyApplied) continue;
|
|
17786
|
+
} else if (indexExists(db, indexName)) {
|
|
17787
|
+
continue;
|
|
17788
|
+
}
|
|
17789
|
+
db.exec(statement);
|
|
17649
17790
|
}
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17657
|
-
);
|
|
17791
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17792
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17793
|
+
if (violations.length > 0) {
|
|
17794
|
+
throw new Error(
|
|
17795
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17796
|
+
);
|
|
17797
|
+
}
|
|
17658
17798
|
}
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
try {
|
|
17664
|
-
db.exec("ROLLBACK");
|
|
17665
|
-
} catch {
|
|
17666
|
-
}
|
|
17667
|
-
throw error51;
|
|
17668
|
-
}
|
|
17799
|
+
record2.run(migration.tag, Date.now());
|
|
17800
|
+
},
|
|
17801
|
+
"IMMEDIATE"
|
|
17802
|
+
);
|
|
17669
17803
|
} finally {
|
|
17670
17804
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17671
17805
|
}
|
|
@@ -17708,8 +17842,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17708
17842
|
}
|
|
17709
17843
|
];
|
|
17710
17844
|
function ensureTokenUsageColumns(db) {
|
|
17711
|
-
const
|
|
17712
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17845
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17713
17846
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17714
17847
|
if (!existing.has(column.name)) {
|
|
17715
17848
|
db.exec(column.ddl);
|
|
@@ -17747,47 +17880,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17747
17880
|
repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
|
|
17748
17881
|
}));
|
|
17749
17882
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
dropCollisions
|
|
17764
|
-
|
|
17883
|
+
withTransaction(
|
|
17884
|
+
db,
|
|
17885
|
+
() => {
|
|
17886
|
+
for (const { row, canonicalId } of legacy) {
|
|
17887
|
+
foldProject.run(
|
|
17888
|
+
canonicalId,
|
|
17889
|
+
row.url,
|
|
17890
|
+
row.name,
|
|
17891
|
+
row.attributes,
|
|
17892
|
+
row.firstSeen,
|
|
17893
|
+
row.lastSeen
|
|
17894
|
+
);
|
|
17895
|
+
repointAudit.run(canonicalId, row.id);
|
|
17896
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
17897
|
+
dropCollisions.run(row.id, canonicalId);
|
|
17898
|
+
repoint.run(canonicalId, row.id);
|
|
17899
|
+
}
|
|
17900
|
+
repointCallSite.run(canonicalId, row.id);
|
|
17901
|
+
deleteLegacy.run(row.id);
|
|
17765
17902
|
}
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
db.exec("COMMIT");
|
|
17770
|
-
} catch (error51) {
|
|
17771
|
-
try {
|
|
17772
|
-
db.exec("ROLLBACK");
|
|
17773
|
-
} catch {
|
|
17774
|
-
}
|
|
17775
|
-
throw error51;
|
|
17776
|
-
}
|
|
17903
|
+
},
|
|
17904
|
+
"IMMEDIATE"
|
|
17905
|
+
);
|
|
17777
17906
|
} catch (error51) {
|
|
17778
|
-
|
|
17779
|
-
`);
|
|
17907
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17780
17908
|
}
|
|
17781
17909
|
}
|
|
17782
17910
|
function isForeignSqliteLineage(db) {
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17786
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
17911
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
17912
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17787
17913
|
}
|
|
17788
17914
|
function ensureSyncedAtColumn(db, table2) {
|
|
17789
|
-
|
|
17790
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
17915
|
+
if (!columnNames(db, table2).includes("synced_at")) {
|
|
17791
17916
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
17792
17917
|
}
|
|
17793
17918
|
}
|
|
@@ -17838,8 +17963,11 @@ function ensureDataDirSync(dir) {
|
|
|
17838
17963
|
} catch {
|
|
17839
17964
|
}
|
|
17840
17965
|
}
|
|
17966
|
+
function walSidecars(file2) {
|
|
17967
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
17968
|
+
}
|
|
17841
17969
|
function tightenPerms(file2) {
|
|
17842
|
-
for (const path of [file2,
|
|
17970
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17843
17971
|
try {
|
|
17844
17972
|
chmodSync(path, DATA_FILE_MODE);
|
|
17845
17973
|
} catch {
|
|
@@ -17847,12 +17975,68 @@ function tightenPerms(file2) {
|
|
|
17847
17975
|
}
|
|
17848
17976
|
}
|
|
17849
17977
|
|
|
17850
|
-
// ../../packages/persistence/src/
|
|
17851
|
-
function
|
|
17852
|
-
|
|
17978
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
17979
|
+
function safeJson(s, fallback) {
|
|
17980
|
+
if (s == null) return fallback;
|
|
17981
|
+
try {
|
|
17982
|
+
return JSON.parse(s);
|
|
17983
|
+
} catch {
|
|
17984
|
+
return fallback;
|
|
17985
|
+
}
|
|
17853
17986
|
}
|
|
17854
|
-
function
|
|
17855
|
-
|
|
17987
|
+
function parseJsonObject(s) {
|
|
17988
|
+
if (s == null) return void 0;
|
|
17989
|
+
try {
|
|
17990
|
+
const parsed = JSON.parse(s);
|
|
17991
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
17992
|
+
} catch {
|
|
17993
|
+
}
|
|
17994
|
+
return void 0;
|
|
17995
|
+
}
|
|
17996
|
+
|
|
17997
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
17998
|
+
function allRows(stmt, params) {
|
|
17999
|
+
if (params === void 0) return stmt.all();
|
|
18000
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18001
|
+
return stmt.all(params);
|
|
18002
|
+
}
|
|
18003
|
+
function getRow(stmt, params) {
|
|
18004
|
+
if (params === void 0) return stmt.get();
|
|
18005
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18006
|
+
return stmt.get(params);
|
|
18007
|
+
}
|
|
18008
|
+
function intToBool(raw) {
|
|
18009
|
+
return raw === 1 || raw === true;
|
|
18010
|
+
}
|
|
18011
|
+
function boolToInt(b) {
|
|
18012
|
+
return b ? 1 : 0;
|
|
18013
|
+
}
|
|
18014
|
+
function bindParams(row) {
|
|
18015
|
+
const out = {};
|
|
18016
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18017
|
+
out[key] = value === void 0 ? null : value;
|
|
18018
|
+
}
|
|
18019
|
+
return out;
|
|
18020
|
+
}
|
|
18021
|
+
function countScalar(db, sql, params) {
|
|
18022
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18023
|
+
}
|
|
18024
|
+
function countBy(db, sql, params) {
|
|
18025
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18026
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18027
|
+
map2.set(row.k, row.n);
|
|
18028
|
+
}
|
|
18029
|
+
return map2;
|
|
18030
|
+
}
|
|
18031
|
+
function mapRowsTolerant(rows, map2) {
|
|
18032
|
+
const out = [];
|
|
18033
|
+
for (const row of rows) {
|
|
18034
|
+
try {
|
|
18035
|
+
out.push(map2(row));
|
|
18036
|
+
} catch {
|
|
18037
|
+
}
|
|
18038
|
+
}
|
|
18039
|
+
return out;
|
|
17856
18040
|
}
|
|
17857
18041
|
|
|
17858
18042
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17904,15 +18088,11 @@ function encodeCursor(payload) {
|
|
|
17904
18088
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17905
18089
|
}
|
|
17906
18090
|
function decodeCursor(cursor) {
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
return parsed;
|
|
17911
|
-
}
|
|
17912
|
-
return null;
|
|
17913
|
-
} catch {
|
|
17914
|
-
return null;
|
|
18091
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18092
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18093
|
+
return parsed;
|
|
17915
18094
|
}
|
|
18095
|
+
return null;
|
|
17916
18096
|
}
|
|
17917
18097
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17918
18098
|
session: "session",
|
|
@@ -17929,15 +18109,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17929
18109
|
};
|
|
17930
18110
|
function safeParseStringArray(raw) {
|
|
17931
18111
|
if (!raw) return [];
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17935
|
-
} catch {
|
|
17936
|
-
return [];
|
|
17937
|
-
}
|
|
17938
|
-
}
|
|
17939
|
-
function toBool(raw) {
|
|
17940
|
-
return raw === 1 || raw === true;
|
|
18112
|
+
const parsed = safeJson(raw, null);
|
|
18113
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17941
18114
|
}
|
|
17942
18115
|
function toHarness(raw) {
|
|
17943
18116
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17990,8 +18163,8 @@ function buildAuditEvent(row) {
|
|
|
17990
18163
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17991
18164
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17992
18165
|
targetId: row.target_id,
|
|
17993
|
-
internal:
|
|
17994
|
-
flagged:
|
|
18166
|
+
internal: intToBool(row.internal),
|
|
18167
|
+
flagged: intToBool(row.flagged)
|
|
17995
18168
|
};
|
|
17996
18169
|
}
|
|
17997
18170
|
var TIMELINE_COLUMNS = `
|
|
@@ -18017,12 +18190,15 @@ var SqliteActivityRepository = class {
|
|
|
18017
18190
|
stats(tz) {
|
|
18018
18191
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
18019
18192
|
const { startMs, endMs } = window;
|
|
18020
|
-
const sessionsToday =
|
|
18193
|
+
const sessionsToday = countScalar(
|
|
18194
|
+
this.db,
|
|
18021
18195
|
`SELECT count(*) AS n FROM audit_events
|
|
18022
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
18023
|
-
|
|
18196
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
|
|
18197
|
+
[startMs, endMs]
|
|
18198
|
+
);
|
|
18024
18199
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
18025
|
-
const liveNow =
|
|
18200
|
+
const liveNow = countScalar(
|
|
18201
|
+
this.db,
|
|
18026
18202
|
`SELECT count(*) AS n FROM audit_events s
|
|
18027
18203
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
18028
18204
|
AND max(
|
|
@@ -18031,22 +18207,29 @@ var SqliteActivityRepository = class {
|
|
|
18031
18207
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
18032
18208
|
s.started_at
|
|
18033
18209
|
)
|
|
18034
|
-
) >=
|
|
18035
|
-
|
|
18036
|
-
|
|
18210
|
+
) >= ?`,
|
|
18211
|
+
[liveThreshold]
|
|
18212
|
+
);
|
|
18213
|
+
const toolCallsToday = countScalar(
|
|
18214
|
+
this.db,
|
|
18037
18215
|
`SELECT count(*) AS n FROM audit_events
|
|
18038
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
18039
|
-
|
|
18040
|
-
|
|
18216
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18217
|
+
[startMs, endMs]
|
|
18218
|
+
);
|
|
18219
|
+
const findingsToday = countScalar(
|
|
18220
|
+
this.db,
|
|
18041
18221
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
18042
18222
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18043
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
18044
|
-
|
|
18045
|
-
|
|
18223
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18224
|
+
[startMs, endMs]
|
|
18225
|
+
);
|
|
18226
|
+
const egressToday = countScalar(
|
|
18227
|
+
this.db,
|
|
18046
18228
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18047
18229
|
FROM audit_events
|
|
18048
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
18049
|
-
|
|
18230
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18231
|
+
[startMs, endMs]
|
|
18232
|
+
);
|
|
18050
18233
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
18051
18234
|
}
|
|
18052
18235
|
listSessions(query) {
|
|
@@ -18068,7 +18251,7 @@ var SqliteActivityRepository = class {
|
|
|
18068
18251
|
conditions.push("started_at <= ?");
|
|
18069
18252
|
params.push(toMs);
|
|
18070
18253
|
if (query.q) {
|
|
18071
|
-
const pattern =
|
|
18254
|
+
const pattern = containsPattern(query.q);
|
|
18072
18255
|
conditions.push(
|
|
18073
18256
|
`(content LIKE ? ESCAPE '\\'
|
|
18074
18257
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18087,8 +18270,9 @@ var SqliteActivityRepository = class {
|
|
|
18087
18270
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18088
18271
|
}
|
|
18089
18272
|
const limit = query.limit;
|
|
18090
|
-
const rows =
|
|
18091
|
-
|
|
18273
|
+
const rows = allRows(
|
|
18274
|
+
this.db.prepare(
|
|
18275
|
+
`SELECT id,
|
|
18092
18276
|
json_extract(attributes, '$.harness') AS harness,
|
|
18093
18277
|
content AS title,
|
|
18094
18278
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18101,7 +18285,9 @@ var SqliteActivityRepository = class {
|
|
|
18101
18285
|
WHERE ${conditions.join(" AND ")}
|
|
18102
18286
|
ORDER BY started_at DESC, id DESC
|
|
18103
18287
|
LIMIT ?`
|
|
18104
|
-
|
|
18288
|
+
),
|
|
18289
|
+
[...params, limit + 1]
|
|
18290
|
+
);
|
|
18105
18291
|
const hasMore = rows.length > limit;
|
|
18106
18292
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18107
18293
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18118,8 +18304,9 @@ var SqliteActivityRepository = class {
|
|
|
18118
18304
|
return Promise.resolve({ items, nextCursor });
|
|
18119
18305
|
}
|
|
18120
18306
|
getSession(sessionId) {
|
|
18121
|
-
const rootRow =
|
|
18122
|
-
|
|
18307
|
+
const rootRow = getRow(
|
|
18308
|
+
this.db.prepare(
|
|
18309
|
+
`SELECT id,
|
|
18123
18310
|
json_extract(attributes, '$.harness') AS harness,
|
|
18124
18311
|
content AS title,
|
|
18125
18312
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18136,47 +18323,66 @@ var SqliteActivityRepository = class {
|
|
|
18136
18323
|
FROM audit_events
|
|
18137
18324
|
WHERE id = ? AND event_type = 'session'
|
|
18138
18325
|
LIMIT 1`
|
|
18139
|
-
|
|
18326
|
+
),
|
|
18327
|
+
[sessionId]
|
|
18328
|
+
);
|
|
18140
18329
|
if (!rootRow) return Promise.resolve(null);
|
|
18141
|
-
const timelineRows =
|
|
18142
|
-
|
|
18330
|
+
const timelineRows = allRows(
|
|
18331
|
+
this.db.prepare(
|
|
18332
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18143
18333
|
FROM audit_events
|
|
18144
18334
|
WHERE id = ? OR root_session_id = ?
|
|
18145
18335
|
ORDER BY started_at ASC, id ASC`
|
|
18146
|
-
|
|
18336
|
+
),
|
|
18337
|
+
[sessionId, sessionId]
|
|
18338
|
+
);
|
|
18147
18339
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18148
|
-
const tokenRow =
|
|
18149
|
-
|
|
18340
|
+
const tokenRow = getRow(
|
|
18341
|
+
this.db.prepare(
|
|
18342
|
+
`SELECT
|
|
18150
18343
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18151
18344
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18152
18345
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18153
18346
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18154
18347
|
FROM audit_events
|
|
18155
18348
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18349
|
+
),
|
|
18350
|
+
[sessionId]
|
|
18351
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18352
|
+
const primaryModel = getRow(
|
|
18353
|
+
this.db.prepare(
|
|
18354
|
+
`SELECT model, provider FROM audit_events
|
|
18159
18355
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18160
18356
|
ORDER BY started_at ASC, id ASC
|
|
18161
18357
|
LIMIT 1`
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18358
|
+
),
|
|
18359
|
+
[sessionId]
|
|
18360
|
+
);
|
|
18361
|
+
const toolRows = allRows(
|
|
18362
|
+
this.db.prepare(
|
|
18363
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18165
18364
|
count(*) AS n
|
|
18166
18365
|
FROM audit_events
|
|
18167
18366
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18168
18367
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18368
|
+
),
|
|
18369
|
+
[sessionId]
|
|
18370
|
+
);
|
|
18371
|
+
const modelRows = allRows(
|
|
18372
|
+
this.db.prepare(
|
|
18373
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18172
18374
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18173
18375
|
ORDER BY model`
|
|
18174
|
-
|
|
18376
|
+
),
|
|
18377
|
+
[sessionId]
|
|
18378
|
+
);
|
|
18175
18379
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18176
|
-
const commits =
|
|
18380
|
+
const commits = countScalar(
|
|
18381
|
+
this.db,
|
|
18177
18382
|
`SELECT count(*) AS n FROM audit_events
|
|
18178
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18179
|
-
|
|
18383
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18384
|
+
[sessionId]
|
|
18385
|
+
);
|
|
18180
18386
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18181
18387
|
turns: 0,
|
|
18182
18388
|
findings: 0,
|
|
@@ -18243,7 +18449,10 @@ var SqliteActivityRepository = class {
|
|
|
18243
18449
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18244
18450
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18245
18451
|
);
|
|
18246
|
-
const rows =
|
|
18452
|
+
const rows = allRows(
|
|
18453
|
+
stmt,
|
|
18454
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18455
|
+
);
|
|
18247
18456
|
const seen = /* @__PURE__ */ new Set();
|
|
18248
18457
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18249
18458
|
return Promise.resolve([...seen]);
|
|
@@ -18266,23 +18475,23 @@ var SqliteActivityRepository = class {
|
|
|
18266
18475
|
conditions.push("started_at >= ?");
|
|
18267
18476
|
params.push(opts.fromMs);
|
|
18268
18477
|
}
|
|
18269
|
-
const rows =
|
|
18270
|
-
|
|
18478
|
+
const rows = allRows(
|
|
18479
|
+
this.db.prepare(
|
|
18480
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18271
18481
|
FROM audit_events
|
|
18272
18482
|
WHERE ${conditions.join(" AND ")}`
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
}
|
|
18284
|
-
|
|
18285
|
-
return leaves;
|
|
18483
|
+
),
|
|
18484
|
+
params
|
|
18485
|
+
);
|
|
18486
|
+
return mapRowsTolerant(
|
|
18487
|
+
rows.filter(
|
|
18488
|
+
(row) => row.sessionId !== null
|
|
18489
|
+
),
|
|
18490
|
+
(row) => ({
|
|
18491
|
+
sessionId: row.sessionId,
|
|
18492
|
+
attributes: JSON.parse(row.attributes)
|
|
18493
|
+
})
|
|
18494
|
+
);
|
|
18286
18495
|
}
|
|
18287
18496
|
/**
|
|
18288
18497
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18296,57 +18505,72 @@ var SqliteActivityRepository = class {
|
|
|
18296
18505
|
);
|
|
18297
18506
|
if (sessionIds.length === 0) return result;
|
|
18298
18507
|
const inClause = placeholders(sessionIds.length);
|
|
18299
|
-
const lastActivityRows =
|
|
18300
|
-
|
|
18508
|
+
const lastActivityRows = allRows(
|
|
18509
|
+
this.db.prepare(
|
|
18510
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18301
18511
|
WHERE root_session_id IN (${inClause})
|
|
18302
18512
|
GROUP BY root_session_id`
|
|
18303
|
-
|
|
18513
|
+
),
|
|
18514
|
+
sessionIds
|
|
18515
|
+
);
|
|
18304
18516
|
for (const row of lastActivityRows) {
|
|
18305
18517
|
if (row.id === null) continue;
|
|
18306
18518
|
const entry = result.get(row.id);
|
|
18307
18519
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18308
18520
|
}
|
|
18309
|
-
const turnsRows =
|
|
18310
|
-
|
|
18521
|
+
const turnsRows = allRows(
|
|
18522
|
+
this.db.prepare(
|
|
18523
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18311
18524
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18312
18525
|
GROUP BY root_session_id`
|
|
18313
|
-
|
|
18526
|
+
),
|
|
18527
|
+
sessionIds
|
|
18528
|
+
);
|
|
18314
18529
|
for (const row of turnsRows) {
|
|
18315
18530
|
if (row.id === null) continue;
|
|
18316
18531
|
const entry = result.get(row.id);
|
|
18317
18532
|
if (entry) entry.turns = row.n;
|
|
18318
18533
|
}
|
|
18319
|
-
const runKeyRows =
|
|
18320
|
-
|
|
18534
|
+
const runKeyRows = allRows(
|
|
18535
|
+
this.db.prepare(
|
|
18536
|
+
`SELECT root_session_id AS id,
|
|
18321
18537
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18322
18538
|
FROM audit_events
|
|
18323
18539
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18324
18540
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18325
18541
|
GROUP BY root_session_id`
|
|
18326
|
-
|
|
18542
|
+
),
|
|
18543
|
+
sessionIds
|
|
18544
|
+
);
|
|
18327
18545
|
for (const row of runKeyRows) {
|
|
18328
18546
|
if (row.id === null) continue;
|
|
18329
18547
|
const entry = result.get(row.id);
|
|
18330
18548
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18331
18549
|
}
|
|
18332
|
-
const findingsRows =
|
|
18333
|
-
|
|
18550
|
+
const findingsRows = allRows(
|
|
18551
|
+
this.db.prepare(
|
|
18552
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18334
18553
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18335
18554
|
WHERE e.root_session_id IN (${inClause})
|
|
18336
18555
|
GROUP BY e.root_session_id`
|
|
18337
|
-
|
|
18556
|
+
),
|
|
18557
|
+
sessionIds
|
|
18558
|
+
);
|
|
18338
18559
|
for (const row of findingsRows) {
|
|
18339
18560
|
if (row.id === null) continue;
|
|
18340
18561
|
const entry = result.get(row.id);
|
|
18341
18562
|
if (entry) entry.findings = row.n;
|
|
18342
18563
|
}
|
|
18343
|
-
const sharesRows =
|
|
18344
|
-
|
|
18564
|
+
const sharesRows = allRows(
|
|
18565
|
+
this.db.prepare(
|
|
18566
|
+
`SELECT root_session_id AS id,
|
|
18345
18567
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18346
18568
|
FROM audit_events
|
|
18347
18569
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18348
18570
|
GROUP BY root_session_id`
|
|
18349
|
-
|
|
18571
|
+
),
|
|
18572
|
+
sessionIds
|
|
18573
|
+
);
|
|
18350
18574
|
for (const row of sharesRows) {
|
|
18351
18575
|
if (row.id === null) continue;
|
|
18352
18576
|
const entry = result.get(row.id);
|
|
@@ -18396,33 +18620,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18396
18620
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18397
18621
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18398
18622
|
runInTransaction(fn) {
|
|
18399
|
-
this.db
|
|
18400
|
-
try {
|
|
18401
|
-
fn();
|
|
18402
|
-
this.db.exec("COMMIT");
|
|
18403
|
-
} catch (err) {
|
|
18404
|
-
this.db.exec("ROLLBACK");
|
|
18405
|
-
throw err;
|
|
18406
|
-
}
|
|
18623
|
+
withTransaction(this.db, fn);
|
|
18407
18624
|
}
|
|
18408
18625
|
insertAuditEvent(input) {
|
|
18409
18626
|
const row = toAuditEventRow(input);
|
|
18410
|
-
this.insertStmt.run(
|
|
18411
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
|
|
18415
|
-
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18627
|
+
this.insertStmt.run(
|
|
18628
|
+
bindParams({
|
|
18629
|
+
id: row.id,
|
|
18630
|
+
parentId: row.parentId,
|
|
18631
|
+
rootSessionId: row.rootSessionId,
|
|
18632
|
+
eventType: row.eventType,
|
|
18633
|
+
hostId: row.hostId,
|
|
18634
|
+
harnessId: row.harnessId,
|
|
18635
|
+
sourceProjectId: row.sourceProjectId,
|
|
18636
|
+
startedAt: row.startedAt,
|
|
18637
|
+
endedAt: row.endedAt,
|
|
18638
|
+
severity: row.severity,
|
|
18639
|
+
priority: row.priority,
|
|
18640
|
+
content: row.content,
|
|
18641
|
+
contentHash: row.contentHash,
|
|
18642
|
+
attributes: row.attributes
|
|
18643
|
+
})
|
|
18644
|
+
);
|
|
18426
18645
|
}
|
|
18427
18646
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18428
18647
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18436,22 +18655,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18436
18655
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18437
18656
|
if (!Number.isFinite(startedAt)) return;
|
|
18438
18657
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18439
|
-
this.upsertLlmCallStmt.run(
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
|
|
18450
|
-
|
|
18451
|
-
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18658
|
+
this.upsertLlmCallStmt.run(
|
|
18659
|
+
bindParams({
|
|
18660
|
+
id,
|
|
18661
|
+
parentId: input.parentId,
|
|
18662
|
+
rootSessionId: input.rootSessionId,
|
|
18663
|
+
eventType: "llm_call",
|
|
18664
|
+
hostId: null,
|
|
18665
|
+
harnessId: null,
|
|
18666
|
+
sourceProjectId: null,
|
|
18667
|
+
startedAt,
|
|
18668
|
+
endedAt: null,
|
|
18669
|
+
severity: null,
|
|
18670
|
+
priority: null,
|
|
18671
|
+
content: null,
|
|
18672
|
+
contentHash: null,
|
|
18673
|
+
attributes: JSON.stringify(input.attributes)
|
|
18674
|
+
})
|
|
18675
|
+
);
|
|
18455
18676
|
}
|
|
18456
18677
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18457
18678
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18475,25 +18696,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18475
18696
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18476
18697
|
if (!Number.isFinite(startedAt)) return;
|
|
18477
18698
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18478
|
-
this.insertStmt.run(
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18492
|
-
|
|
18493
|
-
|
|
18699
|
+
this.insertStmt.run(
|
|
18700
|
+
bindParams({
|
|
18701
|
+
id,
|
|
18702
|
+
parentId: input.parentId,
|
|
18703
|
+
rootSessionId: input.rootSessionId,
|
|
18704
|
+
eventType: "tool_call",
|
|
18705
|
+
hostId: null,
|
|
18706
|
+
harnessId: null,
|
|
18707
|
+
sourceProjectId: null,
|
|
18708
|
+
startedAt,
|
|
18709
|
+
endedAt: null,
|
|
18710
|
+
severity: null,
|
|
18711
|
+
priority: null,
|
|
18712
|
+
content: null,
|
|
18713
|
+
contentHash: null,
|
|
18714
|
+
attributes: JSON.stringify(input.attributes)
|
|
18715
|
+
})
|
|
18716
|
+
);
|
|
18494
18717
|
}
|
|
18495
18718
|
findById(id) {
|
|
18496
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18719
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18720
|
+
id
|
|
18721
|
+
});
|
|
18497
18722
|
}
|
|
18498
18723
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18499
18724
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18503,14 +18728,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18503
18728
|
sessionProvider(sessionId) {
|
|
18504
18729
|
const row = this.findById(sessionId);
|
|
18505
18730
|
if (!row?.attributes) return void 0;
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18509
|
-
const provider = parsed.provider;
|
|
18510
|
-
if (typeof provider === "string") return provider;
|
|
18511
|
-
}
|
|
18512
|
-
} catch {
|
|
18513
|
-
}
|
|
18731
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18732
|
+
if (typeof provider === "string") return provider;
|
|
18514
18733
|
return void 0;
|
|
18515
18734
|
}
|
|
18516
18735
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18519,11 +18738,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18519
18738
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18520
18739
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18521
18740
|
llmCallLeaves() {
|
|
18522
|
-
return
|
|
18523
|
-
|
|
18741
|
+
return allRows(
|
|
18742
|
+
this.db.prepare(
|
|
18743
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18524
18744
|
FROM audit_events
|
|
18525
18745
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18526
|
-
|
|
18746
|
+
)
|
|
18747
|
+
);
|
|
18527
18748
|
}
|
|
18528
18749
|
};
|
|
18529
18750
|
|
|
@@ -18542,16 +18763,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18542
18763
|
upsert(input) {
|
|
18543
18764
|
const id = classifiedDataId(input.class);
|
|
18544
18765
|
const row = toClassifiedDataRow(input, id);
|
|
18545
|
-
this.insertStmt.run(
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18766
|
+
this.insertStmt.run(
|
|
18767
|
+
bindParams({
|
|
18768
|
+
id: row.id,
|
|
18769
|
+
class: row.class,
|
|
18770
|
+
label: row.label,
|
|
18771
|
+
attributes: row.attributes
|
|
18772
|
+
})
|
|
18773
|
+
);
|
|
18551
18774
|
return id;
|
|
18552
18775
|
}
|
|
18553
18776
|
};
|
|
18554
18777
|
|
|
18778
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18779
|
+
function latestConfigScan(db) {
|
|
18780
|
+
return getRow(
|
|
18781
|
+
db.prepare(
|
|
18782
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18783
|
+
WHERE event_type = 'config_scan'
|
|
18784
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18785
|
+
)
|
|
18786
|
+
);
|
|
18787
|
+
}
|
|
18788
|
+
|
|
18555
18789
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18556
18790
|
var SqliteConfigInventoryRepository = class {
|
|
18557
18791
|
constructor(db) {
|
|
@@ -18559,7 +18793,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18559
18793
|
}
|
|
18560
18794
|
db;
|
|
18561
18795
|
report() {
|
|
18562
|
-
const scan2 = this.
|
|
18796
|
+
const scan2 = latestConfigScan(this.db);
|
|
18563
18797
|
if (!scan2) {
|
|
18564
18798
|
return {
|
|
18565
18799
|
scannedAt: null,
|
|
@@ -18570,17 +18804,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18570
18804
|
topics: []
|
|
18571
18805
|
};
|
|
18572
18806
|
}
|
|
18573
|
-
const rows =
|
|
18574
|
-
|
|
18807
|
+
const rows = allRows(
|
|
18808
|
+
this.db.prepare(
|
|
18809
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18575
18810
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18576
18811
|
ORDER BY object_type, title`
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18812
|
+
),
|
|
18813
|
+
{ startedAt: scan2.started_at }
|
|
18814
|
+
);
|
|
18815
|
+
const findings = allRows(
|
|
18816
|
+
this.db.prepare(
|
|
18817
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18580
18818
|
FROM inspection_findings f
|
|
18581
18819
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18582
18820
|
WHERE f.audit_event_id = :scanId`
|
|
18583
|
-
|
|
18821
|
+
),
|
|
18822
|
+
{ scanId: scan2.id }
|
|
18823
|
+
);
|
|
18584
18824
|
const skills = [];
|
|
18585
18825
|
const hooks = [];
|
|
18586
18826
|
const mcpServers = [];
|
|
@@ -18609,7 +18849,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18609
18849
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18610
18850
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18611
18851
|
trustOverrides() {
|
|
18612
|
-
const rows =
|
|
18852
|
+
const rows = allRows(
|
|
18853
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18854
|
+
);
|
|
18613
18855
|
const map2 = /* @__PURE__ */ new Map();
|
|
18614
18856
|
for (const row of rows) {
|
|
18615
18857
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18618,13 +18860,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18618
18860
|
}
|
|
18619
18861
|
return map2;
|
|
18620
18862
|
}
|
|
18621
|
-
latestScan() {
|
|
18622
|
-
return this.db.prepare(
|
|
18623
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18624
|
-
WHERE event_type = 'config_scan'
|
|
18625
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18626
|
-
).get();
|
|
18627
|
-
}
|
|
18628
18863
|
};
|
|
18629
18864
|
function toSkillItem(row, bag) {
|
|
18630
18865
|
const item = {
|
|
@@ -18735,22 +18970,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18735
18970
|
return topics;
|
|
18736
18971
|
}
|
|
18737
18972
|
function countScanErrors(attributes) {
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
const parsed = JSON.parse(attributes);
|
|
18741
|
-
const errors = parsed?.errors;
|
|
18742
|
-
return typeof errors === "number" ? errors : 0;
|
|
18743
|
-
} catch {
|
|
18744
|
-
return 0;
|
|
18745
|
-
}
|
|
18973
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
18974
|
+
return typeof errors === "number" ? errors : 0;
|
|
18746
18975
|
}
|
|
18747
18976
|
function parseBag(raw) {
|
|
18748
|
-
|
|
18749
|
-
const parsed = JSON.parse(raw);
|
|
18750
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18751
|
-
} catch {
|
|
18752
|
-
}
|
|
18753
|
-
return void 0;
|
|
18977
|
+
return parseJsonObject(raw);
|
|
18754
18978
|
}
|
|
18755
18979
|
function str(value) {
|
|
18756
18980
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18759,12 +18983,7 @@ function str(value) {
|
|
|
18759
18983
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18760
18984
|
var DAY_MS2 = 864e5;
|
|
18761
18985
|
function parseRules(rulesJson) {
|
|
18762
|
-
|
|
18763
|
-
try {
|
|
18764
|
-
raw = JSON.parse(rulesJson);
|
|
18765
|
-
} catch {
|
|
18766
|
-
return [];
|
|
18767
|
-
}
|
|
18986
|
+
const raw = safeJson(rulesJson, []);
|
|
18768
18987
|
if (!Array.isArray(raw)) return [];
|
|
18769
18988
|
const rules = [];
|
|
18770
18989
|
for (const entry of raw) {
|
|
@@ -18783,11 +19002,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18783
19002
|
db;
|
|
18784
19003
|
now;
|
|
18785
19004
|
listDetections(query) {
|
|
18786
|
-
const rows =
|
|
18787
|
-
|
|
19005
|
+
const rows = allRows(
|
|
19006
|
+
this.db.prepare(
|
|
19007
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18788
19008
|
rules_json AS rulesJson
|
|
18789
19009
|
FROM installed_packs`
|
|
18790
|
-
|
|
19010
|
+
)
|
|
19011
|
+
);
|
|
18791
19012
|
const available = this.availableByPack();
|
|
18792
19013
|
const summaries = rows.map((r) => {
|
|
18793
19014
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18796,7 +19017,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18796
19017
|
packId: r.packId,
|
|
18797
19018
|
version: r.version,
|
|
18798
19019
|
name: r.name,
|
|
18799
|
-
enabled: r.enabled
|
|
19020
|
+
enabled: intToBool(r.enabled),
|
|
18800
19021
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18801
19022
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18802
19023
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18813,19 +19034,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18813
19034
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18814
19035
|
// detail call; the table is a handful of rows).
|
|
18815
19036
|
availableByPack() {
|
|
18816
|
-
const rows =
|
|
18817
|
-
|
|
19037
|
+
const rows = allRows(
|
|
19038
|
+
this.db.prepare(
|
|
19039
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18818
19040
|
FROM available_packs`
|
|
18819
|
-
|
|
19041
|
+
)
|
|
19042
|
+
);
|
|
18820
19043
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18821
19044
|
}
|
|
18822
19045
|
getDetectionStats() {
|
|
18823
|
-
const rows =
|
|
19046
|
+
const rows = allRows(
|
|
19047
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19048
|
+
);
|
|
18824
19049
|
let rules = 0;
|
|
18825
19050
|
let active = 0;
|
|
18826
19051
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18827
19052
|
for (const r of rows) {
|
|
18828
|
-
if (r.enabled
|
|
19053
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18829
19054
|
const parsed = parseRules(r.rulesJson);
|
|
18830
19055
|
rules += parsed.length;
|
|
18831
19056
|
for (const rule of parsed) {
|
|
@@ -18843,12 +19068,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18843
19068
|
const parts = splitDetectionId(id);
|
|
18844
19069
|
if (!parts) return Promise.resolve(null);
|
|
18845
19070
|
const { namespace, packId } = parts;
|
|
18846
|
-
const row =
|
|
18847
|
-
|
|
19071
|
+
const row = getRow(
|
|
19072
|
+
this.db.prepare(
|
|
19073
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18848
19074
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18849
19075
|
FROM installed_packs
|
|
18850
19076
|
WHERE namespace = ? AND pack_id = ?`
|
|
18851
|
-
|
|
19077
|
+
),
|
|
19078
|
+
[namespace, packId]
|
|
19079
|
+
);
|
|
18852
19080
|
if (!row) return Promise.resolve(null);
|
|
18853
19081
|
const rules = parseRules(row.rulesJson);
|
|
18854
19082
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18866,7 +19094,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18866
19094
|
packId: row.packId,
|
|
18867
19095
|
version: row.version,
|
|
18868
19096
|
name: row.name,
|
|
18869
|
-
enabled: row.enabled
|
|
19097
|
+
enabled: intToBool(row.enabled),
|
|
18870
19098
|
rules,
|
|
18871
19099
|
updatedAt: new Date(row.updatedAt),
|
|
18872
19100
|
policyId: row.policyId
|
|
@@ -18881,13 +19109,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18881
19109
|
countFindingsLast30d(ruleIds) {
|
|
18882
19110
|
if (ruleIds.length === 0) return 0;
|
|
18883
19111
|
const since = this.now() - 30 * DAY_MS2;
|
|
18884
|
-
const
|
|
18885
|
-
|
|
18886
|
-
|
|
19112
|
+
const inClause = placeholders(ruleIds.length);
|
|
19113
|
+
return countScalar(
|
|
19114
|
+
this.db,
|
|
19115
|
+
`SELECT count(*) AS n
|
|
18887
19116
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18888
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18889
|
-
|
|
18890
|
-
|
|
19117
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19118
|
+
[since, ...ruleIds]
|
|
19119
|
+
);
|
|
18891
19120
|
}
|
|
18892
19121
|
};
|
|
18893
19122
|
|
|
@@ -18904,16 +19133,17 @@ var SqliteEventsRepository = class {
|
|
|
18904
19133
|
insertStmt;
|
|
18905
19134
|
insertEvent(event) {
|
|
18906
19135
|
const row = toEventRow(event);
|
|
18907
|
-
this.insertStmt.run(
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
19136
|
+
this.insertStmt.run(
|
|
19137
|
+
bindParams({
|
|
19138
|
+
id: row.id,
|
|
19139
|
+
sourceTool: row.sourceTool,
|
|
19140
|
+
kind: row.kind,
|
|
19141
|
+
occurredAt: row.occurredAt,
|
|
19142
|
+
contentHash: row.contentHash,
|
|
19143
|
+
content: row.content,
|
|
19144
|
+
metadata: row.metadata
|
|
19145
|
+
})
|
|
19146
|
+
);
|
|
18917
19147
|
}
|
|
18918
19148
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18919
19149
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18921,13 +19151,23 @@ var SqliteEventsRepository = class {
|
|
|
18921
19151
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18922
19152
|
// async EventsReadPort contract.
|
|
18923
19153
|
contentHashes() {
|
|
18924
|
-
const rows =
|
|
19154
|
+
const rows = allRows(
|
|
19155
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19156
|
+
);
|
|
18925
19157
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18926
19158
|
}
|
|
18927
19159
|
};
|
|
18928
19160
|
|
|
18929
19161
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18930
19162
|
import { randomUUID } from "crypto";
|
|
19163
|
+
|
|
19164
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19165
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19166
|
+
function isUniqueConstraintError(err) {
|
|
19167
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19168
|
+
}
|
|
19169
|
+
|
|
19170
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18931
19171
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18932
19172
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18933
19173
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18948,10 +19188,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18948
19188
|
this.name = "AmbiguousExceptionIdError";
|
|
18949
19189
|
}
|
|
18950
19190
|
};
|
|
18951
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18952
|
-
function isUniqueConstraintError(err) {
|
|
18953
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18954
|
-
}
|
|
18955
19191
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18956
19192
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18957
19193
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -19007,10 +19243,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19007
19243
|
this.insertExceptionRow(id, input, now);
|
|
19008
19244
|
} catch (err) {
|
|
19009
19245
|
if (!isUniqueConstraintError(err)) throw err;
|
|
19010
|
-
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19246
|
+
withTransaction(
|
|
19247
|
+
this.db,
|
|
19248
|
+
() => {
|
|
19249
|
+
const superseded = this.db.prepare(
|
|
19250
|
+
`UPDATE exceptions
|
|
19014
19251
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
19015
19252
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
19016
19253
|
updated_at = :now
|
|
@@ -19018,24 +19255,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19018
19255
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
19019
19256
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
19020
19257
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19258
|
+
).run({
|
|
19259
|
+
now,
|
|
19260
|
+
revokedBy: input.createdBy,
|
|
19261
|
+
ruleId: input.ruleId,
|
|
19262
|
+
valueFingerprint: input.valueFingerprint,
|
|
19263
|
+
keyVersion: input.keyVersion
|
|
19264
|
+
});
|
|
19265
|
+
if (Number(superseded.changes) !== 1) {
|
|
19266
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19267
|
+
}
|
|
19268
|
+
this.insertExceptionRow(id, input, now);
|
|
19269
|
+
},
|
|
19270
|
+
"IMMEDIATE"
|
|
19271
|
+
);
|
|
19272
|
+
}
|
|
19273
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19274
|
+
id
|
|
19275
|
+
});
|
|
19276
|
+
if (row === void 0) {
|
|
19277
|
+
throw new Error("exception row not found immediately after insert");
|
|
19037
19278
|
}
|
|
19038
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
19039
19279
|
return parseExceptionRow(row);
|
|
19040
19280
|
}
|
|
19041
19281
|
insertExceptionRow(id, input, now) {
|
|
@@ -19073,14 +19313,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19073
19313
|
*/
|
|
19074
19314
|
list(opts) {
|
|
19075
19315
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19076
|
-
const rows =
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
} catch {
|
|
19082
|
-
}
|
|
19083
|
-
}
|
|
19316
|
+
const rows = allRows(
|
|
19317
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19318
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19319
|
+
);
|
|
19320
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19084
19321
|
return Promise.resolve(exceptions);
|
|
19085
19322
|
}
|
|
19086
19323
|
/**
|
|
@@ -19090,7 +19327,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19090
19327
|
*/
|
|
19091
19328
|
getByIdPrefix(prefix) {
|
|
19092
19329
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19093
|
-
const rows =
|
|
19330
|
+
const rows = allRows(
|
|
19331
|
+
this.db.prepare(
|
|
19332
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19333
|
+
),
|
|
19334
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19335
|
+
);
|
|
19094
19336
|
if (rows.length > 1) {
|
|
19095
19337
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19096
19338
|
}
|
|
@@ -19132,30 +19374,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19132
19374
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19133
19375
|
*/
|
|
19134
19376
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19135
|
-
const rows =
|
|
19136
|
-
|
|
19377
|
+
const rows = allRows(
|
|
19378
|
+
this.db.prepare(
|
|
19379
|
+
`SELECT * FROM exceptions
|
|
19137
19380
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19138
19381
|
ORDER BY created_at DESC, rowid DESC`
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19152
|
-
|
|
19153
|
-
|
|
19154
|
-
|
|
19155
|
-
);
|
|
19156
|
-
} catch {
|
|
19157
|
-
}
|
|
19158
|
-
}
|
|
19382
|
+
),
|
|
19383
|
+
{ keyVersion, now }
|
|
19384
|
+
);
|
|
19385
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19386
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19387
|
+
return ExceptionBundleEntry.parse({
|
|
19388
|
+
id: row.id,
|
|
19389
|
+
ruleId: row.rule_id,
|
|
19390
|
+
valueFingerprint: row.value_fingerprint,
|
|
19391
|
+
keyVersion: row.key_version,
|
|
19392
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19393
|
+
maxUses: row.max_uses,
|
|
19394
|
+
useCount: row.use_count,
|
|
19395
|
+
conditions
|
|
19396
|
+
});
|
|
19397
|
+
});
|
|
19159
19398
|
return Promise.resolve(entries);
|
|
19160
19399
|
}
|
|
19161
19400
|
/**
|
|
@@ -19182,11 +19421,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19182
19421
|
}
|
|
19183
19422
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19184
19423
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19185
|
-
const rows =
|
|
19186
|
-
|
|
19424
|
+
const rows = allRows(
|
|
19425
|
+
this.db.prepare(
|
|
19426
|
+
`SELECT * FROM blocked_detections
|
|
19187
19427
|
WHERE blocked_at > :cutoff
|
|
19188
19428
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19189
|
-
|
|
19429
|
+
),
|
|
19430
|
+
{ cutoff: Date.now() - windowMs }
|
|
19431
|
+
);
|
|
19190
19432
|
return Promise.resolve(
|
|
19191
19433
|
rows.map((row) => ({
|
|
19192
19434
|
reference: row.reference,
|
|
@@ -19266,7 +19508,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19266
19508
|
)`;
|
|
19267
19509
|
|
|
19268
19510
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19269
|
-
var
|
|
19511
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19512
|
+
var CONCAT_SEP = ",";
|
|
19513
|
+
var TUPLE_SEP = "|";
|
|
19514
|
+
function splitConcat(value) {
|
|
19515
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19516
|
+
}
|
|
19270
19517
|
function deriveInstanceStatus(row) {
|
|
19271
19518
|
return deriveFindingStatus({
|
|
19272
19519
|
kind: row.kind,
|
|
@@ -19334,13 +19581,16 @@ var SqliteFindingsRepository = class {
|
|
|
19334
19581
|
}
|
|
19335
19582
|
recentFindings(opts) {
|
|
19336
19583
|
const limit = opts?.limit ?? 50;
|
|
19337
|
-
const rows =
|
|
19338
|
-
|
|
19584
|
+
const rows = allRows(
|
|
19585
|
+
this.db.prepare(
|
|
19586
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19339
19587
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19340
19588
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19341
19589
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19342
19590
|
LIMIT :limit`
|
|
19343
|
-
|
|
19591
|
+
),
|
|
19592
|
+
{ limit }
|
|
19593
|
+
);
|
|
19344
19594
|
return Promise.resolve(
|
|
19345
19595
|
rows.map((r) => ({
|
|
19346
19596
|
id: r.id,
|
|
@@ -19363,20 +19613,47 @@ var SqliteFindingsRepository = class {
|
|
|
19363
19613
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19364
19614
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19365
19615
|
* reflect the full filtered set; `items` is the requested
|
|
19366
|
-
* page (default
|
|
19616
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19617
|
+
*
|
|
19618
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19619
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19620
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19621
|
+
* latest, search text);
|
|
19622
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19623
|
+
* populate `instances` for the table's expanded rows.
|
|
19624
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19625
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19626
|
+
* rule is ever restated in SQL.
|
|
19367
19627
|
*/
|
|
19368
19628
|
listGroupedFindings(query) {
|
|
19369
|
-
const
|
|
19370
|
-
|
|
19371
|
-
|
|
19372
|
-
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19629
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
|
|
19630
|
+
const rows = allRows(
|
|
19631
|
+
this.db.prepare(
|
|
19632
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19633
|
+
occurred_at, source_tool, repo, file, kind, finding_key, latest_status
|
|
19634
|
+
FROM (
|
|
19635
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19636
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19637
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19638
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19639
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19640
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19641
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19642
|
+
latest.status AS latest_status,
|
|
19643
|
+
ROW_NUMBER() OVER (
|
|
19644
|
+
PARTITION BY f.rule_id
|
|
19645
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19646
|
+
) AS rn
|
|
19647
|
+
FROM findings f
|
|
19648
|
+
JOIN events e ON e.id = f.event_id
|
|
19649
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19650
|
+
ON latest.finding_key = f.finding_key
|
|
19651
|
+
)
|
|
19652
|
+
WHERE rn <= :cap
|
|
19653
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19654
|
+
),
|
|
19655
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP }
|
|
19656
|
+
);
|
|
19380
19657
|
const groupable = rows.map((r) => ({
|
|
19381
19658
|
id: r.id,
|
|
19382
19659
|
ruleId: r.rule_id,
|
|
@@ -19391,7 +19668,7 @@ var SqliteFindingsRepository = class {
|
|
|
19391
19668
|
file: r.file ?? "",
|
|
19392
19669
|
status: deriveInstanceStatus(r)
|
|
19393
19670
|
}));
|
|
19394
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19671
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19395
19672
|
const filterOpts = {
|
|
19396
19673
|
severity: query.severity,
|
|
19397
19674
|
providers: query.provider,
|
|
@@ -19409,42 +19686,122 @@ var SqliteFindingsRepository = class {
|
|
|
19409
19686
|
const items = sorted.slice(0, limit);
|
|
19410
19687
|
return Promise.resolve({ totals, facets, items, nextCursor: null });
|
|
19411
19688
|
}
|
|
19689
|
+
/**
|
|
19690
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19691
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19692
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19693
|
+
*
|
|
19694
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19695
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19696
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19697
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19698
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19699
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19700
|
+
* a group's row stays small however many findings it holds.
|
|
19701
|
+
*
|
|
19702
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19703
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19704
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19705
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19706
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19707
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19708
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19709
|
+
* carries a `q`.
|
|
19710
|
+
*/
|
|
19711
|
+
groupAggregates(withSearchText) {
|
|
19712
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19713
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
|
|
19714
|
+
const rows = this.db.prepare(
|
|
19715
|
+
`SELECT f.rule_id AS rule_id,
|
|
19716
|
+
count(*) AS instance_count,
|
|
19717
|
+
max(e.occurred_at) AS latest_at,
|
|
19718
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19719
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19720
|
+
group_concat(DISTINCT (
|
|
19721
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19722
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19723
|
+
coalesce(latest.status, '')
|
|
19724
|
+
)) AS status_inputs
|
|
19725
|
+
${searchTextColumns}
|
|
19726
|
+
FROM findings f
|
|
19727
|
+
JOIN events e ON e.id = f.event_id
|
|
19728
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19729
|
+
ON latest.finding_key = f.finding_key
|
|
19730
|
+
GROUP BY f.rule_id`
|
|
19731
|
+
).all();
|
|
19732
|
+
return new Map(
|
|
19733
|
+
rows.map((r) => [
|
|
19734
|
+
r.rule_id,
|
|
19735
|
+
{
|
|
19736
|
+
instanceCount: r.instance_count,
|
|
19737
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19738
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19739
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19740
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19741
|
+
return {
|
|
19742
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19743
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19744
|
+
kind,
|
|
19745
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19746
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19747
|
+
};
|
|
19748
|
+
}),
|
|
19749
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19750
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19751
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19752
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19753
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19754
|
+
// and skip priming a haystack nothing will read.
|
|
19755
|
+
...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
|
|
19756
|
+
}
|
|
19757
|
+
])
|
|
19758
|
+
);
|
|
19759
|
+
}
|
|
19412
19760
|
healthSummary() {
|
|
19413
|
-
const total = this.db
|
|
19761
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19414
19762
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19415
|
-
const grouped =
|
|
19763
|
+
const grouped = allRows(
|
|
19764
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19765
|
+
);
|
|
19416
19766
|
for (const row of grouped) {
|
|
19417
19767
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19418
19768
|
}
|
|
19419
19769
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19420
|
-
const sevRows =
|
|
19421
|
-
|
|
19770
|
+
const sevRows = allRows(
|
|
19771
|
+
this.db.prepare(
|
|
19772
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19422
19773
|
FROM findings f
|
|
19423
19774
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19424
19775
|
ON latest.finding_key = f.finding_key
|
|
19425
19776
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19426
19777
|
GROUP BY f.severity`
|
|
19427
|
-
|
|
19778
|
+
)
|
|
19779
|
+
);
|
|
19428
19780
|
for (const row of sevRows) {
|
|
19429
19781
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19430
19782
|
}
|
|
19431
19783
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19432
|
-
const enabledRows =
|
|
19433
|
-
|
|
19784
|
+
const enabledRows = allRows(
|
|
19785
|
+
this.db.prepare(
|
|
19786
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19434
19787
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19435
|
-
|
|
19788
|
+
)
|
|
19789
|
+
);
|
|
19436
19790
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19437
19791
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19438
19792
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19439
19793
|
}
|
|
19440
19794
|
activityByDay(days = 7) {
|
|
19441
19795
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19442
|
-
const rows =
|
|
19443
|
-
|
|
19796
|
+
const rows = allRows(
|
|
19797
|
+
this.db.prepare(
|
|
19798
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19444
19799
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19445
19800
|
WHERE e.occurred_at >= :since
|
|
19446
19801
|
GROUP BY day, f.action_taken`
|
|
19447
|
-
|
|
19802
|
+
),
|
|
19803
|
+
{ since }
|
|
19804
|
+
);
|
|
19448
19805
|
const buckets = /* @__PURE__ */ new Map();
|
|
19449
19806
|
for (let i = 0; i < days; i++) {
|
|
19450
19807
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19516,17 +19873,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19516
19873
|
insertStmt;
|
|
19517
19874
|
insertFinding(input) {
|
|
19518
19875
|
const row = toInspectionFindingRow(input);
|
|
19519
|
-
this.insertStmt.run(
|
|
19520
|
-
|
|
19521
|
-
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19876
|
+
this.insertStmt.run(
|
|
19877
|
+
bindParams({
|
|
19878
|
+
id: row.id,
|
|
19879
|
+
auditEventId: row.auditEventId,
|
|
19880
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
19881
|
+
classifiedDataId: row.classifiedDataId,
|
|
19882
|
+
spanStart: row.spanStart,
|
|
19883
|
+
spanEnd: row.spanEnd,
|
|
19884
|
+
maskedMatch: row.maskedMatch,
|
|
19885
|
+
actionTaken: row.actionTaken,
|
|
19886
|
+
confidence: row.confidence
|
|
19887
|
+
})
|
|
19888
|
+
);
|
|
19530
19889
|
}
|
|
19531
19890
|
};
|
|
19532
19891
|
|
|
@@ -19602,12 +19961,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19602
19961
|
}
|
|
19603
19962
|
function ruleIdsOf(rulesJson) {
|
|
19604
19963
|
const ids = /* @__PURE__ */ new Set();
|
|
19605
|
-
|
|
19606
|
-
try {
|
|
19607
|
-
raw = JSON.parse(rulesJson);
|
|
19608
|
-
} catch {
|
|
19609
|
-
return ids;
|
|
19610
|
-
}
|
|
19964
|
+
const raw = safeJson(rulesJson, []);
|
|
19611
19965
|
if (!Array.isArray(raw)) return ids;
|
|
19612
19966
|
for (const entry of raw) {
|
|
19613
19967
|
if (entry && typeof entry === "object") {
|
|
@@ -19679,48 +20033,49 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19679
20033
|
ruleIds: new Set(pack.rules.map((r) => r.id))
|
|
19680
20034
|
}));
|
|
19681
20035
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19682
|
-
const now = Date.now();
|
|
19683
|
-
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
const
|
|
19689
|
-
|
|
19690
|
-
namespace: row.namespace,
|
|
19691
|
-
packId: row.packId,
|
|
19692
|
-
version: row.version,
|
|
19693
|
-
name: row.name,
|
|
19694
|
-
rulesJson: row.rulesJson,
|
|
19695
|
-
now
|
|
19696
|
-
};
|
|
19697
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19698
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19699
|
-
this.upsertAvailableStmt.run({
|
|
19700
|
-
...params,
|
|
20036
|
+
const now = Date.now();
|
|
20037
|
+
withTransaction(
|
|
20038
|
+
this.db,
|
|
20039
|
+
() => {
|
|
20040
|
+
const mirror = this.mirrorState();
|
|
20041
|
+
let behind = false;
|
|
20042
|
+
for (const row of rows) {
|
|
20043
|
+
const params = {
|
|
19701
20044
|
id: randomUUID2(),
|
|
19702
|
-
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
|
|
20045
|
+
namespace: row.namespace,
|
|
20046
|
+
packId: row.packId,
|
|
20047
|
+
version: row.version,
|
|
20048
|
+
name: row.name,
|
|
20049
|
+
rulesJson: row.rulesJson,
|
|
20050
|
+
now
|
|
20051
|
+
};
|
|
20052
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20053
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20054
|
+
this.upsertAvailableStmt.run({
|
|
20055
|
+
...params,
|
|
20056
|
+
id: randomUUID2(),
|
|
20057
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20058
|
+
});
|
|
20059
|
+
} else {
|
|
20060
|
+
behind = true;
|
|
20061
|
+
}
|
|
20062
|
+
this.insertMissingStmt.run(params);
|
|
19706
20063
|
}
|
|
19707
|
-
this.
|
|
19708
|
-
}
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
} catch (err) {
|
|
19712
|
-
this.db.exec("ROLLBACK");
|
|
19713
|
-
throw err;
|
|
19714
|
-
}
|
|
20064
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20065
|
+
},
|
|
20066
|
+
"IMMEDIATE"
|
|
20067
|
+
);
|
|
19715
20068
|
} catch {
|
|
19716
20069
|
}
|
|
19717
20070
|
}
|
|
19718
20071
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19719
20072
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19720
20073
|
mirrorState() {
|
|
19721
|
-
const rows =
|
|
19722
|
-
|
|
19723
|
-
|
|
20074
|
+
const rows = allRows(
|
|
20075
|
+
this.db.prepare(
|
|
20076
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20077
|
+
)
|
|
20078
|
+
);
|
|
19724
20079
|
return new Map(
|
|
19725
20080
|
rows.map((r) => [
|
|
19726
20081
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19732,7 +20087,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19732
20087
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19733
20088
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19734
20089
|
pruneAvailable(keep) {
|
|
19735
|
-
const rows =
|
|
20090
|
+
const rows = allRows(
|
|
20091
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20092
|
+
);
|
|
19736
20093
|
const keepSet = new Set(keep);
|
|
19737
20094
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19738
20095
|
for (const r of rows) {
|
|
@@ -19759,11 +20116,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19759
20116
|
if (this.db.isTransaction) {
|
|
19760
20117
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19761
20118
|
}
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
this.db
|
|
19765
|
-
|
|
19766
|
-
|
|
20119
|
+
let changed = false;
|
|
20120
|
+
withTransaction(
|
|
20121
|
+
this.db,
|
|
20122
|
+
() => {
|
|
20123
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20124
|
+
const res = this.db.prepare(
|
|
20125
|
+
`UPDATE installed_packs SET
|
|
19767
20126
|
version = (SELECT a.version FROM available_packs a
|
|
19768
20127
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19769
20128
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19774,17 +20133,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19774
20133
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19775
20134
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19776
20135
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19777
|
-
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
} catch {
|
|
19785
|
-
}
|
|
19786
|
-
throw err;
|
|
19787
|
-
}
|
|
20136
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20137
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20138
|
+
changed = Number(res.changes) > 0;
|
|
20139
|
+
},
|
|
20140
|
+
"IMMEDIATE"
|
|
20141
|
+
);
|
|
20142
|
+
return changed;
|
|
19788
20143
|
}
|
|
19789
20144
|
/**
|
|
19790
20145
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19798,9 +20153,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19798
20153
|
* JSON-level failure therefore counts as invalid.
|
|
19799
20154
|
*/
|
|
19800
20155
|
installedRuleset() {
|
|
19801
|
-
const rows =
|
|
19802
|
-
|
|
19803
|
-
|
|
20156
|
+
const rows = allRows(
|
|
20157
|
+
this.db.prepare(
|
|
20158
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20159
|
+
)
|
|
20160
|
+
);
|
|
19804
20161
|
const out = {
|
|
19805
20162
|
installedPacks: rows.length,
|
|
19806
20163
|
enabledPacks: 0,
|
|
@@ -19809,7 +20166,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19809
20166
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19810
20167
|
};
|
|
19811
20168
|
for (const row of rows) {
|
|
19812
|
-
if (row.enabled
|
|
20169
|
+
if (!intToBool(row.enabled)) continue;
|
|
19813
20170
|
out.enabledPacks += 1;
|
|
19814
20171
|
const action = policyIdToAction(row.policyId);
|
|
19815
20172
|
let raw;
|
|
@@ -19844,9 +20201,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19844
20201
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19845
20202
|
*/
|
|
19846
20203
|
newestRecordedBinary() {
|
|
19847
|
-
const rows =
|
|
19848
|
-
|
|
19849
|
-
|
|
20204
|
+
const rows = allRows(
|
|
20205
|
+
this.db.prepare(
|
|
20206
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20207
|
+
)
|
|
20208
|
+
);
|
|
19850
20209
|
let newest = null;
|
|
19851
20210
|
for (const row of rows) {
|
|
19852
20211
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19861,13 +20220,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19861
20220
|
return newest;
|
|
19862
20221
|
}
|
|
19863
20222
|
counts() {
|
|
19864
|
-
const row =
|
|
19865
|
-
|
|
20223
|
+
const row = getRow(
|
|
20224
|
+
this.db.prepare(
|
|
20225
|
+
`SELECT count(*) AS packs,
|
|
19866
20226
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19867
20227
|
coalesce(sum(enabled), 0) AS enabled
|
|
19868
20228
|
FROM installed_packs`
|
|
19869
|
-
|
|
19870
|
-
|
|
20229
|
+
)
|
|
20230
|
+
);
|
|
20231
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19871
20232
|
}
|
|
19872
20233
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19873
20234
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19880,26 +20241,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19880
20241
|
* attributed to Monitor, matching the Detections views.
|
|
19881
20242
|
*/
|
|
19882
20243
|
countsByPolicyId() {
|
|
19883
|
-
|
|
19884
|
-
|
|
20244
|
+
return countBy(
|
|
20245
|
+
this.db,
|
|
20246
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19885
20247
|
FROM installed_packs
|
|
19886
|
-
GROUP BY
|
|
19887
|
-
)
|
|
19888
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20248
|
+
GROUP BY k`
|
|
20249
|
+
);
|
|
19889
20250
|
}
|
|
19890
20251
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19891
20252
|
listByPolicyId(policyId) {
|
|
19892
|
-
const rows =
|
|
19893
|
-
|
|
20253
|
+
const rows = allRows(
|
|
20254
|
+
this.db.prepare(
|
|
20255
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19894
20256
|
FROM installed_packs
|
|
19895
20257
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19896
20258
|
ORDER BY name ASC`
|
|
19897
|
-
|
|
20259
|
+
),
|
|
20260
|
+
[policyId]
|
|
20261
|
+
);
|
|
19898
20262
|
return rows.map((r) => ({
|
|
19899
20263
|
id: `${r.namespace}/${r.packId}`,
|
|
19900
20264
|
name: r.name,
|
|
19901
20265
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19902
|
-
enabled: r.enabled
|
|
20266
|
+
enabled: intToBool(r.enabled)
|
|
19903
20267
|
}));
|
|
19904
20268
|
}
|
|
19905
20269
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19928,14 +20292,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19928
20292
|
const res = this.db.prepare(
|
|
19929
20293
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19930
20294
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19931
|
-
).run({ enabled: enabled
|
|
20295
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19932
20296
|
return Number(res.changes) > 0;
|
|
19933
20297
|
}
|
|
19934
20298
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19935
20299
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19936
20300
|
// binary's inventory hasn't changed since the last record.
|
|
19937
20301
|
storedSignature() {
|
|
19938
|
-
const rows = this.signatureStmt
|
|
20302
|
+
const rows = allRows(this.signatureStmt);
|
|
19939
20303
|
return inventorySignature(rows);
|
|
19940
20304
|
}
|
|
19941
20305
|
};
|
|
@@ -19963,42 +20327,48 @@ var SqliteInventoryRepository = class {
|
|
|
19963
20327
|
upsert(input, now = Date.now()) {
|
|
19964
20328
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19965
20329
|
const row = toInventoryRow(input, id, now);
|
|
19966
|
-
this.upsertStmt.run(
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
20330
|
+
this.upsertStmt.run(
|
|
20331
|
+
bindParams({
|
|
20332
|
+
id: row.id,
|
|
20333
|
+
objectType: row.objectType,
|
|
20334
|
+
location: row.location,
|
|
20335
|
+
title: row.title,
|
|
20336
|
+
hostId: row.hostId,
|
|
20337
|
+
attributes: row.attributes,
|
|
20338
|
+
firstSeen: row.firstSeen,
|
|
20339
|
+
lastSeen: row.lastSeen
|
|
20340
|
+
})
|
|
20341
|
+
);
|
|
19976
20342
|
return id;
|
|
19977
20343
|
}
|
|
19978
20344
|
// The full row, for round-trip assertions.
|
|
19979
20345
|
findById(id) {
|
|
19980
|
-
|
|
19981
|
-
return row;
|
|
20346
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19982
20347
|
}
|
|
19983
20348
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19984
20349
|
// served from the object_type index, never from audit_events.
|
|
19985
20350
|
distinctTitles(objectType) {
|
|
19986
|
-
const rows =
|
|
19987
|
-
|
|
20351
|
+
const rows = allRows(
|
|
20352
|
+
this.db.prepare(
|
|
20353
|
+
`SELECT DISTINCT title FROM inventory
|
|
19988
20354
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19989
20355
|
ORDER BY title`
|
|
19990
|
-
|
|
20356
|
+
),
|
|
20357
|
+
{ objectType }
|
|
20358
|
+
);
|
|
19991
20359
|
return rows.map((r) => r.title);
|
|
19992
20360
|
}
|
|
19993
20361
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19994
20362
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19995
20363
|
// QUERY PLAN).
|
|
19996
20364
|
osVersions() {
|
|
19997
|
-
const rows =
|
|
19998
|
-
|
|
20365
|
+
const rows = allRows(
|
|
20366
|
+
this.db.prepare(
|
|
20367
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19999
20368
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
20000
20369
|
ORDER BY value`
|
|
20001
|
-
|
|
20370
|
+
)
|
|
20371
|
+
);
|
|
20002
20372
|
return rows.map((r) => r.value);
|
|
20003
20373
|
}
|
|
20004
20374
|
};
|
|
@@ -20018,14 +20388,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
20018
20388
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
20019
20389
|
findingsCount: 0
|
|
20020
20390
|
};
|
|
20021
|
-
function safeJson(s, fallback) {
|
|
20022
|
-
if (s == null) return fallback;
|
|
20023
|
-
try {
|
|
20024
|
-
return JSON.parse(s);
|
|
20025
|
-
} catch {
|
|
20026
|
-
return fallback;
|
|
20027
|
-
}
|
|
20028
|
-
}
|
|
20029
20391
|
function resolveHarnessId(attrs, row) {
|
|
20030
20392
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
20031
20393
|
return attrs.provider;
|
|
@@ -20221,30 +20583,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20221
20583
|
configRowsCache;
|
|
20222
20584
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20223
20585
|
getInventoryStats() {
|
|
20224
|
-
const
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
byType
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
|
|
20586
|
+
const typeCounts = countBy(
|
|
20587
|
+
this.db,
|
|
20588
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20589
|
+
);
|
|
20590
|
+
const byType = {
|
|
20591
|
+
project: 0,
|
|
20592
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20593
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20594
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20595
|
+
config: typeCounts.get("config") ?? 0
|
|
20596
|
+
};
|
|
20597
|
+
byType.project = countScalar(
|
|
20598
|
+
this.db,
|
|
20599
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20600
|
+
);
|
|
20601
|
+
const mcpTrustCounts = countBy(
|
|
20602
|
+
this.db,
|
|
20603
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20232
20604
|
FROM inventory_asset a
|
|
20233
20605
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20234
20606
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20235
20607
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20236
|
-
)
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20608
|
+
);
|
|
20609
|
+
const mcpTrust = {
|
|
20610
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20611
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20612
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20613
|
+
};
|
|
20614
|
+
const harnesses = countScalar(
|
|
20615
|
+
this.db,
|
|
20240
20616
|
`SELECT count(*) AS n FROM inventory
|
|
20241
20617
|
WHERE object_type = 'harness'
|
|
20242
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
const
|
|
20618
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20619
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20620
|
+
);
|
|
20621
|
+
const flaggedAssets = countScalar(
|
|
20622
|
+
this.db,
|
|
20623
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20624
|
+
);
|
|
20625
|
+
const flaggedProjects = countScalar(
|
|
20626
|
+
this.db,
|
|
20246
20627
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20247
|
-
)
|
|
20628
|
+
);
|
|
20248
20629
|
const configRows = this.configAssetRows();
|
|
20249
20630
|
for (const r of configRows) {
|
|
20250
20631
|
byType[r.assetType] += 1;
|
|
@@ -20501,12 +20882,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20501
20882
|
}
|
|
20502
20883
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20503
20884
|
fetchHarnessRows() {
|
|
20504
|
-
return
|
|
20505
|
-
|
|
20885
|
+
return allRows(
|
|
20886
|
+
this.db.prepare(
|
|
20887
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20506
20888
|
FROM inventory
|
|
20507
20889
|
WHERE object_type = 'harness'
|
|
20508
20890
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20509
|
-
|
|
20891
|
+
),
|
|
20892
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20893
|
+
);
|
|
20510
20894
|
}
|
|
20511
20895
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20512
20896
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20516,12 +20900,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20516
20900
|
const params = [...harnessInvIds];
|
|
20517
20901
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20518
20902
|
if (q) {
|
|
20519
|
-
const pat =
|
|
20520
|
-
where +=
|
|
20903
|
+
const pat = containsPattern(q);
|
|
20904
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20521
20905
|
params.push(pat, pat);
|
|
20522
20906
|
}
|
|
20523
|
-
const rows =
|
|
20524
|
-
|
|
20907
|
+
const rows = allRows(
|
|
20908
|
+
this.db.prepare(
|
|
20909
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20525
20910
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20526
20911
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20527
20912
|
FROM harness_asset ha
|
|
@@ -20529,7 +20914,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20529
20914
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20530
20915
|
WHERE ${where}
|
|
20531
20916
|
ORDER BY a.name ASC`
|
|
20532
|
-
|
|
20917
|
+
),
|
|
20918
|
+
params
|
|
20919
|
+
);
|
|
20533
20920
|
for (const raw of rows) {
|
|
20534
20921
|
const harnessInvId = raw.harnessInvId;
|
|
20535
20922
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20548,21 +20935,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20548
20935
|
params.push(...types);
|
|
20549
20936
|
}
|
|
20550
20937
|
if (q) {
|
|
20551
|
-
const pat =
|
|
20552
|
-
conditions.push("
|
|
20938
|
+
const pat = containsPattern(q);
|
|
20939
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20553
20940
|
params.push(pat, pat);
|
|
20554
20941
|
}
|
|
20555
20942
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20556
20943
|
const sampleRows = this.mapAssetRows(
|
|
20557
|
-
|
|
20558
|
-
|
|
20944
|
+
allRows(
|
|
20945
|
+
this.db.prepare(
|
|
20946
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20559
20947
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20560
20948
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20561
20949
|
FROM inventory_asset a
|
|
20562
20950
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20563
20951
|
${where}
|
|
20564
20952
|
ORDER BY a.name ASC`
|
|
20565
|
-
|
|
20953
|
+
),
|
|
20954
|
+
params
|
|
20955
|
+
)
|
|
20566
20956
|
);
|
|
20567
20957
|
const configRows = this.configAssetRows(q).filter(
|
|
20568
20958
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20571,14 +20961,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20571
20961
|
}
|
|
20572
20962
|
fetchAssetById(assetId) {
|
|
20573
20963
|
const rows = this.mapAssetRows(
|
|
20574
|
-
|
|
20575
|
-
|
|
20964
|
+
allRows(
|
|
20965
|
+
this.db.prepare(
|
|
20966
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20576
20967
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20577
20968
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20578
20969
|
FROM inventory_asset a
|
|
20579
20970
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20580
20971
|
WHERE a.id = ?`
|
|
20581
|
-
|
|
20972
|
+
),
|
|
20973
|
+
[assetId]
|
|
20974
|
+
)
|
|
20582
20975
|
);
|
|
20583
20976
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20584
20977
|
}
|
|
@@ -20634,37 +21027,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20634
21027
|
return rows;
|
|
20635
21028
|
}
|
|
20636
21029
|
latestConfigScanId() {
|
|
20637
|
-
|
|
20638
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20639
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20640
|
-
).get();
|
|
20641
|
-
return row?.id ?? null;
|
|
21030
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20642
21031
|
}
|
|
20643
21032
|
fetchProjects(q) {
|
|
20644
21033
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20645
21034
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20646
21035
|
const params = [];
|
|
20647
21036
|
if (q) {
|
|
20648
|
-
const pat =
|
|
20649
|
-
sql +=
|
|
21037
|
+
const pat = containsPattern(q);
|
|
21038
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20650
21039
|
params.push(pat, pat);
|
|
20651
21040
|
}
|
|
20652
21041
|
sql += " ORDER BY name ASC";
|
|
20653
|
-
return this.db.prepare(sql)
|
|
21042
|
+
return allRows(this.db.prepare(sql), params);
|
|
20654
21043
|
}
|
|
20655
21044
|
fetchProjectById(projectId) {
|
|
20656
|
-
return
|
|
20657
|
-
|
|
20658
|
-
|
|
21045
|
+
return getRow(
|
|
21046
|
+
this.db.prepare(
|
|
21047
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21048
|
+
),
|
|
21049
|
+
[projectId]
|
|
21050
|
+
) ?? null;
|
|
20659
21051
|
}
|
|
20660
21052
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20661
21053
|
fetchProjectsByIds(projectIds) {
|
|
20662
21054
|
const map2 = /* @__PURE__ */ new Map();
|
|
20663
21055
|
if (projectIds.length === 0) return map2;
|
|
20664
|
-
const rows =
|
|
20665
|
-
|
|
21056
|
+
const rows = allRows(
|
|
21057
|
+
this.db.prepare(
|
|
21058
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20666
21059
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20667
|
-
|
|
21060
|
+
),
|
|
21061
|
+
projectIds
|
|
21062
|
+
);
|
|
20668
21063
|
for (const r of rows) map2.set(r.id, r);
|
|
20669
21064
|
return map2;
|
|
20670
21065
|
}
|
|
@@ -20675,8 +21070,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20675
21070
|
projectAggregates(projectIds) {
|
|
20676
21071
|
const map2 = /* @__PURE__ */ new Map();
|
|
20677
21072
|
if (projectIds.length === 0) return map2;
|
|
20678
|
-
const rows =
|
|
20679
|
-
|
|
21073
|
+
const rows = allRows(
|
|
21074
|
+
this.db.prepare(
|
|
21075
|
+
`SELECT f.project_id AS projectId,
|
|
20680
21076
|
coalesce(o.access, f.default_access) AS eff,
|
|
20681
21077
|
count(*) AS n,
|
|
20682
21078
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20684,7 +21080,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20684
21080
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20685
21081
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20686
21082
|
GROUP BY f.project_id, eff`
|
|
20687
|
-
|
|
21083
|
+
),
|
|
21084
|
+
projectIds
|
|
21085
|
+
);
|
|
20688
21086
|
for (const r of rows) {
|
|
20689
21087
|
let agg = map2.get(r.projectId);
|
|
20690
21088
|
if (!agg) {
|
|
@@ -20722,37 +21120,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20722
21120
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20723
21121
|
if (prefix === "") {
|
|
20724
21122
|
return this.mapFileRows(
|
|
20725
|
-
|
|
21123
|
+
allRows(
|
|
21124
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21125
|
+
[projectId]
|
|
21126
|
+
)
|
|
20726
21127
|
);
|
|
20727
21128
|
}
|
|
20728
21129
|
return this.mapFileRows(
|
|
20729
|
-
|
|
20730
|
-
this.
|
|
20731
|
-
|
|
21130
|
+
allRows(
|
|
21131
|
+
this.db.prepare(
|
|
21132
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21133
|
+
),
|
|
21134
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21135
|
+
)
|
|
20732
21136
|
);
|
|
20733
21137
|
}
|
|
20734
21138
|
fetchProjectFilesSearch(projectId, q) {
|
|
20735
|
-
const pat =
|
|
21139
|
+
const pat = containsPattern(q);
|
|
20736
21140
|
return this.mapFileRows(
|
|
20737
|
-
|
|
20738
|
-
this.
|
|
20739
|
-
|
|
20740
|
-
|
|
20741
|
-
|
|
21141
|
+
allRows(
|
|
21142
|
+
this.db.prepare(
|
|
21143
|
+
this.fileSelect(
|
|
21144
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21145
|
+
)
|
|
21146
|
+
),
|
|
21147
|
+
[projectId, pat, pat]
|
|
21148
|
+
)
|
|
20742
21149
|
);
|
|
20743
21150
|
}
|
|
20744
21151
|
fetchProjectFilesBlocked(projectId) {
|
|
20745
21152
|
return this.mapFileRows(
|
|
20746
|
-
|
|
20747
|
-
this.
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
21153
|
+
allRows(
|
|
21154
|
+
this.db.prepare(
|
|
21155
|
+
this.fileSelect(
|
|
21156
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21157
|
+
)
|
|
21158
|
+
),
|
|
21159
|
+
[projectId]
|
|
21160
|
+
)
|
|
20751
21161
|
);
|
|
20752
21162
|
}
|
|
20753
21163
|
fetchProjectFile(projectId, path) {
|
|
20754
21164
|
const rows = this.mapFileRows(
|
|
20755
|
-
|
|
21165
|
+
allRows(
|
|
21166
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21167
|
+
[projectId, path]
|
|
21168
|
+
)
|
|
20756
21169
|
);
|
|
20757
21170
|
return rows[0] ?? null;
|
|
20758
21171
|
}
|
|
@@ -20766,39 +21179,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20766
21179
|
}
|
|
20767
21180
|
db;
|
|
20768
21181
|
readPolicies() {
|
|
20769
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20770
|
-
const policies =
|
|
20771
|
-
|
|
20772
|
-
|
|
20773
|
-
|
|
20774
|
-
|
|
20775
|
-
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20780
|
-
|
|
20781
|
-
|
|
20782
|
-
customKeywords
|
|
20783
|
-
})
|
|
20784
|
-
);
|
|
20785
|
-
} catch {
|
|
20786
|
-
}
|
|
20787
|
-
}
|
|
21182
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21183
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21184
|
+
const target = JSON.parse(row.target);
|
|
21185
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21186
|
+
return Policy.parse({
|
|
21187
|
+
id: row.id,
|
|
21188
|
+
scope: row.scope,
|
|
21189
|
+
target,
|
|
21190
|
+
action: row.action,
|
|
21191
|
+
enabled: intToBool(row.enabled),
|
|
21192
|
+
customKeywords
|
|
21193
|
+
});
|
|
21194
|
+
});
|
|
20788
21195
|
return Promise.resolve(policies);
|
|
20789
21196
|
}
|
|
20790
21197
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20791
21198
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20792
21199
|
// so a user's edits are never clobbered.
|
|
20793
21200
|
seedDefaults() {
|
|
20794
|
-
const count = this.db
|
|
21201
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20795
21202
|
if (count > 0) return;
|
|
20796
21203
|
const stmt = this.db.prepare(
|
|
20797
21204
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20798
21205
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20799
21206
|
);
|
|
20800
|
-
this.db
|
|
20801
|
-
try {
|
|
21207
|
+
failOpenTransaction(this.db, () => {
|
|
20802
21208
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20803
21209
|
stmt.run({
|
|
20804
21210
|
id: randomUUID4(),
|
|
@@ -20807,10 +21213,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20807
21213
|
now: Date.now()
|
|
20808
21214
|
});
|
|
20809
21215
|
}
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
|
|
21216
|
+
});
|
|
21217
|
+
}
|
|
21218
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21219
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21220
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21221
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21222
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21223
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21224
|
+
upsertCategoryAction(category, action) {
|
|
21225
|
+
const now = Date.now();
|
|
21226
|
+
this.db.prepare(
|
|
21227
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21228
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21229
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21230
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21231
|
+
}
|
|
21232
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21233
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21234
|
+
// Returns the number of rows changed.
|
|
21235
|
+
capCategoryActions() {
|
|
21236
|
+
const info = this.db.prepare(
|
|
21237
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21238
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21239
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21240
|
+
).run({ now: Date.now() });
|
|
21241
|
+
return Number(info.changes);
|
|
21242
|
+
}
|
|
21243
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21244
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21245
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21246
|
+
getCategoryAction(category) {
|
|
21247
|
+
const row = this.db.prepare(
|
|
21248
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21249
|
+
).get({ category });
|
|
21250
|
+
return row?.action;
|
|
20814
21251
|
}
|
|
20815
21252
|
};
|
|
20816
21253
|
|
|
@@ -20886,7 +21323,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20886
21323
|
maxStampStmt;
|
|
20887
21324
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20888
21325
|
replaceForProject(projectId, scan2, now) {
|
|
20889
|
-
const
|
|
21326
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20890
21327
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20891
21328
|
for (const file2 of scan2.files) {
|
|
20892
21329
|
this.upsertStmt.run({
|
|
@@ -20969,7 +21406,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20969
21406
|
}
|
|
20970
21407
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20971
21408
|
latestByKey(key) {
|
|
20972
|
-
const row = this.latestStmt
|
|
21409
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20973
21410
|
if (!row) return void 0;
|
|
20974
21411
|
return {
|
|
20975
21412
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20986,7 +21423,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20986
21423
|
* the CLI) surfaces for that file.
|
|
20987
21424
|
*/
|
|
20988
21425
|
openAtRestKeysForPath(path) {
|
|
20989
|
-
const rows = this.openAtRestStmt
|
|
21426
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20990
21427
|
return rows.map((r) => r.finding_key);
|
|
20991
21428
|
}
|
|
20992
21429
|
/**
|
|
@@ -20997,7 +21434,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20997
21434
|
* resolution row (see scan.ts).
|
|
20998
21435
|
*/
|
|
20999
21436
|
resolvedAtRestKeysForPath(path) {
|
|
21000
|
-
const rows = this.resolvedAtRestStmt
|
|
21437
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
21001
21438
|
return rows.map((r) => r.finding_key);
|
|
21002
21439
|
}
|
|
21003
21440
|
};
|
|
@@ -21026,31 +21463,25 @@ var SqliteScanLedgerRepository = class {
|
|
|
21026
21463
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
21027
21464
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
21028
21465
|
entriesForRuleset(rulesetHash) {
|
|
21029
|
-
const rows = this.readStmt
|
|
21466
|
+
const rows = allRows(this.readStmt, {
|
|
21467
|
+
rulesetHash
|
|
21468
|
+
});
|
|
21030
21469
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
21031
21470
|
}
|
|
21032
21471
|
upsertEntries(entries) {
|
|
21033
21472
|
if (entries.length === 0) return;
|
|
21034
21473
|
const scannedAt = Date.now();
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
21038
|
-
|
|
21039
|
-
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
scannedAt
|
|
21045
|
-
});
|
|
21046
|
-
}
|
|
21047
|
-
this.db.exec("COMMIT");
|
|
21048
|
-
} catch (err) {
|
|
21049
|
-
this.db.exec("ROLLBACK");
|
|
21050
|
-
throw err;
|
|
21474
|
+
failOpenTransaction(this.db, () => {
|
|
21475
|
+
for (const entry of entries) {
|
|
21476
|
+
this.upsertStmt.run({
|
|
21477
|
+
path: entry.path,
|
|
21478
|
+
mtime: entry.mtime,
|
|
21479
|
+
contentHash: entry.contentHash,
|
|
21480
|
+
rulesetHash: entry.rulesetHash,
|
|
21481
|
+
scannedAt
|
|
21482
|
+
});
|
|
21051
21483
|
}
|
|
21052
|
-
}
|
|
21053
|
-
}
|
|
21484
|
+
});
|
|
21054
21485
|
}
|
|
21055
21486
|
};
|
|
21056
21487
|
|
|
@@ -21127,8 +21558,9 @@ var SqliteSecurityRepository = class {
|
|
|
21127
21558
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21128
21559
|
// double-counting a key that accumulated several append-only rows.
|
|
21129
21560
|
severitySummary() {
|
|
21130
|
-
const rows =
|
|
21131
|
-
|
|
21561
|
+
const rows = allRows(
|
|
21562
|
+
this.db.prepare(
|
|
21563
|
+
`SELECT f.severity AS severity,
|
|
21132
21564
|
COUNT(*) AS count,
|
|
21133
21565
|
SUM(CASE
|
|
21134
21566
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21147,7 +21579,8 @@ var SqliteSecurityRepository = class {
|
|
|
21147
21579
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21148
21580
|
ON latest.finding_key = f.finding_key
|
|
21149
21581
|
GROUP BY f.severity`
|
|
21150
|
-
|
|
21582
|
+
)
|
|
21583
|
+
);
|
|
21151
21584
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21152
21585
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21153
21586
|
severity,
|
|
@@ -21231,14 +21664,15 @@ var SqliteSecurityRepository = class {
|
|
|
21231
21664
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21232
21665
|
const now = this.now();
|
|
21233
21666
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21234
|
-
const rows =
|
|
21235
|
-
|
|
21236
|
-
|
|
21237
|
-
|
|
21238
|
-
|
|
21239
|
-
|
|
21240
|
-
|
|
21241
|
-
|
|
21667
|
+
const rows = allRows(
|
|
21668
|
+
this.db.prepare(
|
|
21669
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21670
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21671
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21672
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21673
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21674
|
+
// backfill left null.
|
|
21675
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21242
21676
|
(
|
|
21243
21677
|
SELECT fr.status FROM finding_resolution fr
|
|
21244
21678
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21264,14 +21698,16 @@ var SqliteSecurityRepository = class {
|
|
|
21264
21698
|
WHERE fr.finding_key = f.finding_key
|
|
21265
21699
|
AND fr.resolved_at >= :windowStart
|
|
21266
21700
|
)`
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
|
|
21270
|
-
|
|
21271
|
-
|
|
21272
|
-
|
|
21273
|
-
|
|
21274
|
-
|
|
21701
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21702
|
+
// any resolution activity at/after the window start — a row this method
|
|
21703
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21704
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21705
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21706
|
+
// dialect-agnostic. Without this, a
|
|
21707
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21708
|
+
),
|
|
21709
|
+
{ windowStart }
|
|
21710
|
+
);
|
|
21275
21711
|
const sums = /* @__PURE__ */ new Map();
|
|
21276
21712
|
const counts = /* @__PURE__ */ new Map();
|
|
21277
21713
|
for (const r of rows) {
|
|
@@ -21301,8 +21737,9 @@ var SqliteSecurityRepository = class {
|
|
|
21301
21737
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21302
21738
|
const now = this.now();
|
|
21303
21739
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21304
|
-
const rows =
|
|
21305
|
-
|
|
21740
|
+
const rows = allRows(
|
|
21741
|
+
this.db.prepare(
|
|
21742
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21306
21743
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21307
21744
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21308
21745
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21310,7 +21747,9 @@ var SqliteSecurityRepository = class {
|
|
|
21310
21747
|
GROUP BY repo
|
|
21311
21748
|
ORDER BY c DESC, repo
|
|
21312
21749
|
LIMIT :limit`
|
|
21313
|
-
|
|
21750
|
+
),
|
|
21751
|
+
{ from, to: now, limit }
|
|
21752
|
+
);
|
|
21314
21753
|
const items = rows.map((r) => ({
|
|
21315
21754
|
id: `repo_${r.repo}`,
|
|
21316
21755
|
name: r.repo,
|
|
@@ -21332,8 +21771,9 @@ var SqliteSecurityRepository = class {
|
|
|
21332
21771
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21333
21772
|
// capped at `limit`.
|
|
21334
21773
|
recentlyResolved(limit = 20) {
|
|
21335
|
-
const rows =
|
|
21336
|
-
|
|
21774
|
+
const rows = allRows(
|
|
21775
|
+
this.db.prepare(
|
|
21776
|
+
`SELECT f.finding_key AS finding_key,
|
|
21337
21777
|
f.rule_id AS rule_id,
|
|
21338
21778
|
f.severity AS severity,
|
|
21339
21779
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21367,7 +21807,9 @@ var SqliteSecurityRepository = class {
|
|
|
21367
21807
|
) IS NOT NULL
|
|
21368
21808
|
ORDER BY latest_resolved_at DESC
|
|
21369
21809
|
LIMIT :limit`
|
|
21370
|
-
|
|
21810
|
+
),
|
|
21811
|
+
{ limit }
|
|
21812
|
+
);
|
|
21371
21813
|
const items = rows.map((r) => ({
|
|
21372
21814
|
findingKey: r.finding_key,
|
|
21373
21815
|
ruleId: r.rule_id,
|
|
@@ -21384,12 +21826,15 @@ var SqliteSecurityRepository = class {
|
|
|
21384
21826
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21385
21827
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21386
21828
|
findingsInRange(fromMs, toMs) {
|
|
21387
|
-
const rows =
|
|
21388
|
-
|
|
21829
|
+
const rows = allRows(
|
|
21830
|
+
this.db.prepare(
|
|
21831
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21389
21832
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21390
21833
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21391
21834
|
ORDER BY e.occurred_at`
|
|
21392
|
-
|
|
21835
|
+
),
|
|
21836
|
+
{ from: fromMs, to: toMs }
|
|
21837
|
+
);
|
|
21393
21838
|
return rows.map((r) => ({
|
|
21394
21839
|
occurredAt: r.occurred_at,
|
|
21395
21840
|
severity: r.severity,
|
|
@@ -21403,12 +21848,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21403
21848
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21404
21849
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21405
21850
|
function parseNetwork(networkJson) {
|
|
21406
|
-
|
|
21407
|
-
try {
|
|
21408
|
-
return JSON.parse(networkJson);
|
|
21409
|
-
} catch {
|
|
21410
|
-
return null;
|
|
21411
|
-
}
|
|
21851
|
+
return safeJson(networkJson, null);
|
|
21412
21852
|
}
|
|
21413
21853
|
function toEndpointSummary(row) {
|
|
21414
21854
|
return {
|
|
@@ -21495,27 +21935,39 @@ var SqliteSharesRepository = class {
|
|
|
21495
21935
|
}
|
|
21496
21936
|
db;
|
|
21497
21937
|
stats() {
|
|
21498
|
-
const
|
|
21499
|
-
const
|
|
21500
|
-
const
|
|
21501
|
-
const
|
|
21502
|
-
|
|
21938
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
21939
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
21940
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
21941
|
+
const insecure = countScalar(
|
|
21942
|
+
this.db,
|
|
21503
21943
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21504
21944
|
);
|
|
21505
|
-
const needsReview =
|
|
21945
|
+
const needsReview = countScalar(
|
|
21946
|
+
this.db,
|
|
21506
21947
|
`SELECT count(DISTINCT d.id) AS n
|
|
21507
21948
|
FROM share_destination d
|
|
21508
21949
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21509
21950
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21510
21951
|
);
|
|
21511
|
-
const
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
const
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21952
|
+
const kindCounts = countBy(
|
|
21953
|
+
this.db,
|
|
21954
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
21955
|
+
);
|
|
21956
|
+
const byKind = {
|
|
21957
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
21958
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
21959
|
+
ip: kindCounts.get("ip") ?? 0
|
|
21960
|
+
};
|
|
21961
|
+
const trustCounts = countBy(
|
|
21962
|
+
this.db,
|
|
21963
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
21964
|
+
);
|
|
21965
|
+
const byTrust = {
|
|
21966
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
21967
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
21968
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
21969
|
+
ip: trustCounts.get("ip") ?? 0
|
|
21970
|
+
};
|
|
21519
21971
|
return Promise.resolve({
|
|
21520
21972
|
destinations,
|
|
21521
21973
|
endpoints,
|
|
@@ -21631,7 +22083,7 @@ var SqliteSharesRepository = class {
|
|
|
21631
22083
|
}
|
|
21632
22084
|
let sql;
|
|
21633
22085
|
if (q) {
|
|
21634
|
-
const pattern =
|
|
22086
|
+
const pattern = containsPattern(q);
|
|
21635
22087
|
conditions.push(
|
|
21636
22088
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21637
22089
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21651,24 +22103,31 @@ var SqliteSharesRepository = class {
|
|
|
21651
22103
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21652
22104
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21653
22105
|
}
|
|
21654
|
-
const rows =
|
|
22106
|
+
const rows = allRows(
|
|
22107
|
+
this.db.prepare(sql),
|
|
22108
|
+
params
|
|
22109
|
+
);
|
|
21655
22110
|
return rows.map((r) => this.mapDestRow(r));
|
|
21656
22111
|
}
|
|
21657
22112
|
fetchDestinationById(destinationId) {
|
|
21658
|
-
const row =
|
|
21659
|
-
|
|
22113
|
+
const row = getRow(
|
|
22114
|
+
this.db.prepare(
|
|
22115
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21660
22116
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21661
22117
|
o.decision AS overrideDecision
|
|
21662
22118
|
FROM share_destination d
|
|
21663
22119
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21664
22120
|
WHERE d.id = ?`
|
|
21665
|
-
|
|
22121
|
+
),
|
|
22122
|
+
[destinationId]
|
|
22123
|
+
);
|
|
21666
22124
|
return row ? this.mapDestRow(row) : null;
|
|
21667
22125
|
}
|
|
21668
22126
|
fetchEndpoints(destinationIds) {
|
|
21669
22127
|
if (destinationIds.length === 0) return [];
|
|
21670
|
-
const rows =
|
|
21671
|
-
|
|
22128
|
+
const rows = allRows(
|
|
22129
|
+
this.db.prepare(
|
|
22130
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21672
22131
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21673
22132
|
count(c.id) AS callSiteCount
|
|
21674
22133
|
FROM share_endpoint e
|
|
@@ -21676,7 +22135,9 @@ var SqliteSharesRepository = class {
|
|
|
21676
22135
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21677
22136
|
GROUP BY e.id
|
|
21678
22137
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21679
|
-
|
|
22138
|
+
),
|
|
22139
|
+
destinationIds
|
|
22140
|
+
);
|
|
21680
22141
|
return rows.map((r) => ({
|
|
21681
22142
|
id: r.id,
|
|
21682
22143
|
destinationId: r.destinationId,
|
|
@@ -21701,13 +22162,16 @@ var SqliteSharesRepository = class {
|
|
|
21701
22162
|
}
|
|
21702
22163
|
fetchCallSites(endpointIds) {
|
|
21703
22164
|
if (endpointIds.length === 0) return [];
|
|
21704
|
-
const rows =
|
|
21705
|
-
|
|
22165
|
+
const rows = allRows(
|
|
22166
|
+
this.db.prepare(
|
|
22167
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21706
22168
|
project_id AS projectId
|
|
21707
22169
|
FROM share_call_site
|
|
21708
22170
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21709
22171
|
ORDER BY created_at ASC, id ASC`
|
|
21710
|
-
|
|
22172
|
+
),
|
|
22173
|
+
endpointIds
|
|
22174
|
+
);
|
|
21711
22175
|
return rows.map((r) => ({
|
|
21712
22176
|
id: r.id,
|
|
21713
22177
|
endpointId: r.endpointId,
|
|
@@ -21743,27 +22207,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21743
22207
|
upsert(input, now = Date.now()) {
|
|
21744
22208
|
const id = sourceProjectId(input.url);
|
|
21745
22209
|
const row = toSourceProjectRow(input, id, now);
|
|
21746
|
-
this.upsertStmt.run(
|
|
21747
|
-
|
|
21748
|
-
|
|
21749
|
-
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
|
|
21753
|
-
|
|
22210
|
+
this.upsertStmt.run(
|
|
22211
|
+
bindParams({
|
|
22212
|
+
id: row.id,
|
|
22213
|
+
url: row.url,
|
|
22214
|
+
name: row.name,
|
|
22215
|
+
attributes: row.attributes,
|
|
22216
|
+
firstSeen: row.firstSeen,
|
|
22217
|
+
lastSeen: row.lastSeen
|
|
22218
|
+
})
|
|
22219
|
+
);
|
|
21754
22220
|
return id;
|
|
21755
22221
|
}
|
|
21756
22222
|
findById(id) {
|
|
21757
|
-
return
|
|
22223
|
+
return getRow(
|
|
22224
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22225
|
+
{
|
|
22226
|
+
id
|
|
22227
|
+
}
|
|
22228
|
+
);
|
|
21758
22229
|
}
|
|
21759
22230
|
// Distinct project names — a filter facet, served from the source_project
|
|
21760
22231
|
// table, never from the audit fact table.
|
|
21761
22232
|
distinctNames() {
|
|
21762
|
-
const rows =
|
|
21763
|
-
|
|
22233
|
+
const rows = allRows(
|
|
22234
|
+
this.db.prepare(
|
|
22235
|
+
`SELECT DISTINCT name FROM source_project
|
|
21764
22236
|
WHERE name IS NOT NULL
|
|
21765
22237
|
ORDER BY name`
|
|
21766
|
-
|
|
22238
|
+
)
|
|
22239
|
+
);
|
|
21767
22240
|
return rows.map((r) => r.name);
|
|
21768
22241
|
}
|
|
21769
22242
|
};
|
|
@@ -21782,8 +22255,7 @@ function hasLegacySampleRows(db) {
|
|
|
21782
22255
|
function purgeSampleData(db) {
|
|
21783
22256
|
try {
|
|
21784
22257
|
if (!hasLegacySampleRows(db)) return;
|
|
21785
|
-
db
|
|
21786
|
-
try {
|
|
22258
|
+
withTransaction(db, () => {
|
|
21787
22259
|
db.exec(
|
|
21788
22260
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21789
22261
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21828,11 +22300,7 @@ function purgeSampleData(db) {
|
|
|
21828
22300
|
value TEXT NOT NULL
|
|
21829
22301
|
)`);
|
|
21830
22302
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21831
|
-
|
|
21832
|
-
} catch (err) {
|
|
21833
|
-
db.exec("ROLLBACK");
|
|
21834
|
-
throw err;
|
|
21835
|
-
}
|
|
22303
|
+
});
|
|
21836
22304
|
} catch {
|
|
21837
22305
|
}
|
|
21838
22306
|
}
|
|
@@ -21851,7 +22319,7 @@ function openWithPragmas(file2) {
|
|
|
21851
22319
|
function backupLegacyStore(file2) {
|
|
21852
22320
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21853
22321
|
renameSync(file2, backup);
|
|
21854
|
-
for (const sidecar of
|
|
22322
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21855
22323
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21856
22324
|
}
|
|
21857
22325
|
return backup;
|
|
@@ -21864,9 +22332,8 @@ function openLocalDatabase(dir) {
|
|
|
21864
22332
|
db.close();
|
|
21865
22333
|
const backup = backupLegacyStore(file2);
|
|
21866
22334
|
db = openWithPragmas(file2);
|
|
21867
|
-
|
|
21868
|
-
`
|
|
21869
|
-
`
|
|
22335
|
+
akaWarn(
|
|
22336
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21870
22337
|
);
|
|
21871
22338
|
}
|
|
21872
22339
|
applyMigrations(db);
|
|
@@ -21894,96 +22361,77 @@ function openLocalDatabase(dir) {
|
|
|
21894
22361
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21895
22362
|
policies.seedDefaults();
|
|
21896
22363
|
function recordCapture(event, detected) {
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21903
|
-
db.exec("COMMIT");
|
|
21904
|
-
} catch (err) {
|
|
21905
|
-
db.exec("ROLLBACK");
|
|
21906
|
-
throw err;
|
|
21907
|
-
}
|
|
21908
|
-
} catch {
|
|
21909
|
-
}
|
|
22364
|
+
failOpenTransaction(db, () => {
|
|
22365
|
+
events.insertEvent(event);
|
|
22366
|
+
const sessionId = event.metadata?.sessionId;
|
|
22367
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22368
|
+
});
|
|
21910
22369
|
}
|
|
21911
22370
|
function ensureInventory(ctx) {
|
|
21912
22371
|
const resolved = {};
|
|
21913
|
-
|
|
21914
|
-
|
|
21915
|
-
|
|
21916
|
-
|
|
21917
|
-
|
|
21918
|
-
|
|
21919
|
-
|
|
21920
|
-
|
|
21921
|
-
|
|
21922
|
-
|
|
21923
|
-
|
|
21924
|
-
|
|
21925
|
-
|
|
21926
|
-
|
|
21927
|
-
|
|
21928
|
-
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
db.exec("COMMIT");
|
|
21934
|
-
} catch (err) {
|
|
21935
|
-
db.exec("ROLLBACK");
|
|
21936
|
-
throw err;
|
|
21937
|
-
}
|
|
21938
|
-
} catch {
|
|
21939
|
-
return {};
|
|
21940
|
-
}
|
|
21941
|
-
return resolved;
|
|
22372
|
+
const committed = failOpenTransaction(db, () => {
|
|
22373
|
+
const now = Date.now();
|
|
22374
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22375
|
+
if (ctx.harness) {
|
|
22376
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22377
|
+
}
|
|
22378
|
+
resolved.accountId = inventory.upsert(
|
|
22379
|
+
linkHost(
|
|
22380
|
+
{
|
|
22381
|
+
objectType: "user",
|
|
22382
|
+
identityKey: "local",
|
|
22383
|
+
attributes: { source: "local" }
|
|
22384
|
+
},
|
|
22385
|
+
resolved.hostId
|
|
22386
|
+
),
|
|
22387
|
+
now
|
|
22388
|
+
);
|
|
22389
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22390
|
+
});
|
|
22391
|
+
return committed ? resolved : {};
|
|
21942
22392
|
}
|
|
21943
22393
|
function recordConfigScan(record2) {
|
|
21944
|
-
|
|
21945
|
-
|
|
21946
|
-
|
|
21947
|
-
|
|
21948
|
-
|
|
21949
|
-
|
|
21950
|
-
|
|
21951
|
-
|
|
21952
|
-
|
|
21953
|
-
}
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
21963
|
-
|
|
21964
|
-
confidence: finding.confidence
|
|
21965
|
-
});
|
|
21966
|
-
}
|
|
21967
|
-
db.exec("COMMIT");
|
|
21968
|
-
} catch (err) {
|
|
21969
|
-
db.exec("ROLLBACK");
|
|
21970
|
-
throw err;
|
|
22394
|
+
failOpenTransaction(db, () => {
|
|
22395
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22396
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22397
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22398
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22399
|
+
for (const def of record2.definitions ?? []) {
|
|
22400
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22401
|
+
}
|
|
22402
|
+
for (const finding of record2.findings ?? []) {
|
|
22403
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22404
|
+
if (!definitionId) continue;
|
|
22405
|
+
inspectionFindings.insertFinding({
|
|
22406
|
+
id: randomUUID8(),
|
|
22407
|
+
auditEventId: record2.scanEvent.id,
|
|
22408
|
+
inspectionDefinitionId: definitionId,
|
|
22409
|
+
span: finding.span,
|
|
22410
|
+
maskedMatch: finding.maskedMatch,
|
|
22411
|
+
actionTaken: finding.actionTaken,
|
|
22412
|
+
confidence: finding.confidence
|
|
22413
|
+
});
|
|
21971
22414
|
}
|
|
21972
|
-
}
|
|
21973
|
-
}
|
|
22415
|
+
});
|
|
21974
22416
|
}
|
|
21975
22417
|
function recordProjectFiles(projectId, scan2) {
|
|
21976
22418
|
if (scan2.files.length === 0) return;
|
|
22419
|
+
failOpenTransaction(db, () => {
|
|
22420
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22421
|
+
});
|
|
22422
|
+
}
|
|
22423
|
+
async function transaction(fn) {
|
|
22424
|
+
db.exec("BEGIN");
|
|
21977
22425
|
try {
|
|
21978
|
-
|
|
22426
|
+
const result = await fn();
|
|
22427
|
+
db.exec("COMMIT");
|
|
22428
|
+
return result;
|
|
22429
|
+
} catch (err) {
|
|
21979
22430
|
try {
|
|
21980
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21981
|
-
db.exec("COMMIT");
|
|
21982
|
-
} catch (err) {
|
|
21983
22431
|
db.exec("ROLLBACK");
|
|
21984
|
-
|
|
22432
|
+
} catch {
|
|
21985
22433
|
}
|
|
21986
|
-
|
|
22434
|
+
throw err;
|
|
21987
22435
|
}
|
|
21988
22436
|
}
|
|
21989
22437
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -22002,8 +22450,7 @@ function openLocalDatabase(dir) {
|
|
|
22002
22450
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
22003
22451
|
});
|
|
22004
22452
|
if (stale.length === 0) return;
|
|
22005
|
-
db
|
|
22006
|
-
try {
|
|
22453
|
+
withTransaction(db, () => {
|
|
22007
22454
|
for (const { id } of stale) {
|
|
22008
22455
|
db.prepare(
|
|
22009
22456
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -22015,11 +22462,7 @@ function openLocalDatabase(dir) {
|
|
|
22015
22462
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
22016
22463
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
22017
22464
|
}
|
|
22018
|
-
|
|
22019
|
-
} catch (err) {
|
|
22020
|
-
db.exec("ROLLBACK");
|
|
22021
|
-
throw err;
|
|
22022
|
-
}
|
|
22465
|
+
});
|
|
22023
22466
|
} catch {
|
|
22024
22467
|
}
|
|
22025
22468
|
}
|
|
@@ -22061,6 +22504,7 @@ function openLocalDatabase(dir) {
|
|
|
22061
22504
|
purgeSampleData: () => {
|
|
22062
22505
|
purgeSampleData(db);
|
|
22063
22506
|
},
|
|
22507
|
+
transaction,
|
|
22064
22508
|
close: () => {
|
|
22065
22509
|
db.close();
|
|
22066
22510
|
}
|
|
@@ -22153,12 +22597,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22153
22597
|
}
|
|
22154
22598
|
}
|
|
22155
22599
|
function readJson(file2) {
|
|
22600
|
+
let text;
|
|
22156
22601
|
try {
|
|
22157
|
-
|
|
22158
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22602
|
+
text = readFileSync2(file2, "utf8");
|
|
22159
22603
|
} catch {
|
|
22160
22604
|
return null;
|
|
22161
22605
|
}
|
|
22606
|
+
return parseJsonObject(text) ?? null;
|
|
22607
|
+
}
|
|
22608
|
+
|
|
22609
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22610
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22611
|
+
import { join as join5 } from "path";
|
|
22612
|
+
var MARKER = "warn-era-capped";
|
|
22613
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22614
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22615
|
+
const marker = join5(dataDir2, MARKER);
|
|
22616
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22617
|
+
const capped = db.policies.capCategoryActions();
|
|
22618
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22619
|
+
`, { mode: DATA_FILE_MODE });
|
|
22620
|
+
return { capped };
|
|
22162
22621
|
}
|
|
22163
22622
|
|
|
22164
22623
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22233,7 +22692,7 @@ function resolveProviderSafe() {
|
|
|
22233
22692
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22234
22693
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22235
22694
|
import { homedir as homedir2 } from "os";
|
|
22236
|
-
import { basename as basename2, join as
|
|
22695
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22237
22696
|
|
|
22238
22697
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22239
22698
|
var KeywordMatcher2 = class {
|
|
@@ -24367,8 +24826,8 @@ function bundledDetections() {
|
|
|
24367
24826
|
}
|
|
24368
24827
|
|
|
24369
24828
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24370
|
-
import { existsSync as
|
|
24371
|
-
import { basename, dirname, isAbsolute, join as
|
|
24829
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
24830
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24372
24831
|
|
|
24373
24832
|
// ../../packages/plugin-sdk/src/events.ts
|
|
24374
24833
|
import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
|
|
@@ -24380,20 +24839,23 @@ import { createHash as createHash4 } from "crypto";
|
|
|
24380
24839
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
24381
24840
|
|
|
24382
24841
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24383
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24384
|
-
import { join as
|
|
24842
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
24843
|
+
import { join as join8 } from "path";
|
|
24385
24844
|
|
|
24386
24845
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24387
24846
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24388
|
-
import { existsSync as
|
|
24389
|
-
import { basename as basename3, join as
|
|
24847
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
24848
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
24390
24849
|
|
|
24391
24850
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24392
24851
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
24393
24852
|
|
|
24853
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
24854
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
24855
|
+
|
|
24394
24856
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24395
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
24396
|
-
import { join as
|
|
24857
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
24858
|
+
import { join as join10 } from "path";
|
|
24397
24859
|
|
|
24398
24860
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
24399
24861
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -24586,6 +25048,14 @@ var StandaloneDataGateway = class {
|
|
|
24586
25048
|
sweepTerminalExceptions(retentionMs) {
|
|
24587
25049
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
24588
25050
|
}
|
|
25051
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
25052
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
25053
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
25054
|
+
// already-capped one).
|
|
25055
|
+
capWarnEraEnforcement(policyMode) {
|
|
25056
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
25057
|
+
return { capped };
|
|
25058
|
+
}
|
|
24589
25059
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
24590
25060
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
24591
25061
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -24825,6 +25295,7 @@ function shortTime(iso) {
|
|
|
24825
25295
|
function empty(message) {
|
|
24826
25296
|
return message;
|
|
24827
25297
|
}
|
|
25298
|
+
var CATEGORY_ORDER2 = DetectionCategory.options;
|
|
24828
25299
|
function healthScore(summary) {
|
|
24829
25300
|
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
24830
25301
|
const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
|
|
@@ -24841,7 +25312,7 @@ function renderFindings(findings, status, severity) {
|
|
|
24841
25312
|
`${severityGlyph(f.severity)} ${f.severity}`,
|
|
24842
25313
|
f.category,
|
|
24843
25314
|
f.ruleId,
|
|
24844
|
-
f.actionTaken,
|
|
25315
|
+
toApiAction(f.actionTaken),
|
|
24845
25316
|
f.maskedMatch
|
|
24846
25317
|
]);
|
|
24847
25318
|
const heading = severity !== void 0 ? `\u25CF Recent ${severity} findings (${String(findings.length)})` : `\u25CF Recent findings (${String(findings.length)})`;
|
|
@@ -25047,7 +25518,7 @@ function renderAudit(findings) {
|
|
|
25047
25518
|
}
|
|
25048
25519
|
const rows = findings.map((f) => [
|
|
25049
25520
|
shortTime(f.occurredAt),
|
|
25050
|
-
f.actionTaken,
|
|
25521
|
+
toApiAction(f.actionTaken),
|
|
25051
25522
|
f.ruleId,
|
|
25052
25523
|
f.category,
|
|
25053
25524
|
// Join only the parts that are present so a finding missing sourceTool/kind
|