@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/firstrun.js
CHANGED
|
@@ -15427,6 +15427,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15427
15427
|
"prompt",
|
|
15428
15428
|
"response",
|
|
15429
15429
|
"code_change",
|
|
15430
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15431
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15432
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15433
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15434
|
+
"tool_use",
|
|
15430
15435
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15431
15436
|
// fact the posture inspection findings reference (findings require an
|
|
15432
15437
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15818,7 +15823,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15818
15823
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15819
15824
|
|
|
15820
15825
|
// ../../packages/schema/src/zod/event.ts
|
|
15821
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15826
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15822
15827
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15823
15828
|
var EventMetadata = external_exports.object({
|
|
15824
15829
|
sessionId: external_exports.string().optional(),
|
|
@@ -16131,7 +16136,7 @@ var DetectionException = external_exports.object({
|
|
|
16131
16136
|
justification: external_exports.string().min(1),
|
|
16132
16137
|
conditions: ExceptionConditions.nullable(),
|
|
16133
16138
|
createdBy: external_exports.string(),
|
|
16134
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16139
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16135
16140
|
createdAt: external_exports.iso.datetime(),
|
|
16136
16141
|
updatedAt: external_exports.iso.datetime(),
|
|
16137
16142
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16300,20 +16305,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16300
16305
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16301
16306
|
fetchedAt: external_exports.iso.datetime()
|
|
16302
16307
|
}).meta({ id: "PolicyBundle" });
|
|
16303
|
-
var DEFAULT_ACTIONS = {
|
|
16304
|
-
secret: "block",
|
|
16305
|
-
pii: "redact",
|
|
16306
|
-
financial: "redact",
|
|
16307
|
-
phi: "redact",
|
|
16308
|
-
code_context: "warn",
|
|
16309
|
-
code_flaw: "warn",
|
|
16310
|
-
custom: "warn",
|
|
16311
|
-
// Config-posture findings only observe today (they land in
|
|
16312
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16313
|
-
config: "warn"
|
|
16314
|
-
};
|
|
16315
16308
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16316
16309
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16310
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16311
|
+
secret: "critical",
|
|
16312
|
+
financial: "critical",
|
|
16313
|
+
// core-financial/credit-card
|
|
16314
|
+
code_flaw: "critical",
|
|
16315
|
+
pii: "high",
|
|
16316
|
+
phi: "high",
|
|
16317
|
+
custom: "high",
|
|
16318
|
+
// user-defined; conservative
|
|
16319
|
+
code_context: "low",
|
|
16320
|
+
config: "low"
|
|
16321
|
+
// observe-only; floors to monitor regardless
|
|
16322
|
+
};
|
|
16323
|
+
function severityFloorPolicy(category) {
|
|
16324
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16325
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16326
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16327
|
+
}
|
|
16317
16328
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16318
16329
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16319
16330
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16340,6 +16351,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16340
16351
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16341
16352
|
}
|
|
16342
16353
|
};
|
|
16354
|
+
function builtinPolicyToAction(id) {
|
|
16355
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16356
|
+
}
|
|
16357
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16358
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16359
|
+
);
|
|
16343
16360
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16344
16361
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16345
16362
|
);
|
|
@@ -16746,10 +16763,8 @@ function toApiProvider(sourceTool) {
|
|
|
16746
16763
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16747
16764
|
}
|
|
16748
16765
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16749
|
-
function
|
|
16750
|
-
const statuses = new Set(
|
|
16751
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16752
|
-
);
|
|
16766
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16767
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16753
16768
|
if (statuses.size === 0) return void 0;
|
|
16754
16769
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16755
16770
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16767,6 +16782,7 @@ function deriveFindingStatus(row) {
|
|
|
16767
16782
|
function buildFindingGroups(rows, opts = {}) {
|
|
16768
16783
|
const overrides = opts.overrides;
|
|
16769
16784
|
const packNames = opts.packNames;
|
|
16785
|
+
const aggregates = opts.aggregates;
|
|
16770
16786
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16771
16787
|
for (const row of rows) {
|
|
16772
16788
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16788,17 +16804,20 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16788
16804
|
status: r.status
|
|
16789
16805
|
};
|
|
16790
16806
|
});
|
|
16791
|
-
const
|
|
16807
|
+
const agg = aggregates?.get(ruleId);
|
|
16808
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16792
16809
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16793
16810
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16794
16811
|
);
|
|
16795
16812
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16796
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16813
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16797
16814
|
if (seenProviders.has(p)) return false;
|
|
16798
16815
|
seenProviders.add(p);
|
|
16799
16816
|
return true;
|
|
16800
16817
|
});
|
|
16801
|
-
const actionSet = new Set(
|
|
16818
|
+
const actionSet = new Set(
|
|
16819
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16820
|
+
);
|
|
16802
16821
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16803
16822
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16804
16823
|
const detection = {
|
|
@@ -16812,8 +16831,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16812
16831
|
contextPrefix: ""
|
|
16813
16832
|
// empty (pending privacy review)
|
|
16814
16833
|
};
|
|
16815
|
-
const status =
|
|
16816
|
-
|
|
16834
|
+
const status = foldGroupStatus(
|
|
16835
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16836
|
+
);
|
|
16837
|
+
const group = {
|
|
16817
16838
|
id: ruleId,
|
|
16818
16839
|
category: apiCategory,
|
|
16819
16840
|
subtype: ruleId,
|
|
@@ -16822,21 +16843,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16822
16843
|
match,
|
|
16823
16844
|
detection,
|
|
16824
16845
|
policy,
|
|
16825
|
-
instanceCount: instances.length,
|
|
16846
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16826
16847
|
providers,
|
|
16827
16848
|
aggregateAction,
|
|
16828
16849
|
latestDetectedAt,
|
|
16829
16850
|
instances,
|
|
16830
16851
|
status
|
|
16831
|
-
}
|
|
16852
|
+
};
|
|
16853
|
+
if (agg) {
|
|
16854
|
+
actionsCache.set(group, [...actionSet]);
|
|
16855
|
+
if (agg.searchText !== void 0) {
|
|
16856
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16857
|
+
}
|
|
16858
|
+
}
|
|
16859
|
+
groups.push(group);
|
|
16832
16860
|
}
|
|
16833
16861
|
return groups;
|
|
16834
16862
|
}
|
|
16835
16863
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16836
|
-
function
|
|
16837
|
-
|
|
16838
|
-
if (cached2 !== void 0) return cached2;
|
|
16839
|
-
const haystack = [
|
|
16864
|
+
function buildHaystack(g, extra) {
|
|
16865
|
+
return [
|
|
16840
16866
|
g.subtype,
|
|
16841
16867
|
g.category,
|
|
16842
16868
|
g.match.maskedValue,
|
|
@@ -16844,11 +16870,25 @@ function groupHaystack(g) {
|
|
|
16844
16870
|
g.id,
|
|
16845
16871
|
...g.instances.map((i) => i.repo),
|
|
16846
16872
|
...g.instances.map((i) => i.file),
|
|
16847
|
-
...g.instances.map((i) => i.id)
|
|
16873
|
+
...g.instances.map((i) => i.id),
|
|
16874
|
+
...extra === void 0 ? [] : [extra]
|
|
16848
16875
|
].join(" ").toLowerCase();
|
|
16876
|
+
}
|
|
16877
|
+
function groupHaystack(g) {
|
|
16878
|
+
const cached2 = haystackCache.get(g);
|
|
16879
|
+
if (cached2 !== void 0) return cached2;
|
|
16880
|
+
const haystack = buildHaystack(g);
|
|
16849
16881
|
haystackCache.set(g, haystack);
|
|
16850
16882
|
return haystack;
|
|
16851
16883
|
}
|
|
16884
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16885
|
+
function groupActions(g) {
|
|
16886
|
+
const cached2 = actionsCache.get(g);
|
|
16887
|
+
if (cached2 !== void 0) return cached2;
|
|
16888
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16889
|
+
actionsCache.set(g, actions);
|
|
16890
|
+
return actions;
|
|
16891
|
+
}
|
|
16852
16892
|
function applyFindingFilters(groups, opts) {
|
|
16853
16893
|
let filtered = groups;
|
|
16854
16894
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16861,7 +16901,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16861
16901
|
}
|
|
16862
16902
|
if (opts.actions && opts.actions.length > 0) {
|
|
16863
16903
|
const actionSet = new Set(opts.actions);
|
|
16864
|
-
filtered = filtered.filter((g) => g.
|
|
16904
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16865
16905
|
}
|
|
16866
16906
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16867
16907
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16913,8 +16953,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16913
16953
|
});
|
|
16914
16954
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16915
16955
|
for (const g of forAction) {
|
|
16916
|
-
const
|
|
16917
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16956
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16918
16957
|
}
|
|
16919
16958
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16920
16959
|
providers: opts.providers,
|
|
@@ -17476,6 +17515,132 @@ function reviewSeverityRank(reasons) {
|
|
|
17476
17515
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17477
17516
|
}
|
|
17478
17517
|
|
|
17518
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17519
|
+
var TriageHit = external_exports.object({
|
|
17520
|
+
ruleId: external_exports.string(),
|
|
17521
|
+
category: DetectionCategory,
|
|
17522
|
+
severity: Severity,
|
|
17523
|
+
maskedMatch: external_exports.string(),
|
|
17524
|
+
rawMatch: external_exports.string(),
|
|
17525
|
+
context: external_exports.string(),
|
|
17526
|
+
filePath: external_exports.string().optional(),
|
|
17527
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17528
|
+
id: external_exports.string().optional(),
|
|
17529
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17530
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17531
|
+
});
|
|
17532
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17533
|
+
var TriageCategoryRec = external_exports.object({
|
|
17534
|
+
category: DetectionCategory,
|
|
17535
|
+
action: TriagePolicy,
|
|
17536
|
+
reasoning: external_exports.string(),
|
|
17537
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17538
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17539
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17540
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17541
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17542
|
+
});
|
|
17543
|
+
var TriageRecommendation = external_exports.object({
|
|
17544
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17545
|
+
notes: external_exports.string()
|
|
17546
|
+
});
|
|
17547
|
+
|
|
17548
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17549
|
+
function escapeLikePattern(s) {
|
|
17550
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17551
|
+
}
|
|
17552
|
+
function placeholders(n) {
|
|
17553
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17554
|
+
}
|
|
17555
|
+
function containsPattern(q) {
|
|
17556
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17557
|
+
}
|
|
17558
|
+
function likeAny(exprs) {
|
|
17559
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17560
|
+
}
|
|
17561
|
+
|
|
17562
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17563
|
+
var savepointSeq = 0;
|
|
17564
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17565
|
+
if (db.isTransaction) {
|
|
17566
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17567
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17568
|
+
try {
|
|
17569
|
+
fn();
|
|
17570
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17571
|
+
} catch (error51) {
|
|
17572
|
+
try {
|
|
17573
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17574
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17575
|
+
} catch {
|
|
17576
|
+
}
|
|
17577
|
+
throw error51;
|
|
17578
|
+
}
|
|
17579
|
+
return;
|
|
17580
|
+
}
|
|
17581
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17582
|
+
try {
|
|
17583
|
+
fn();
|
|
17584
|
+
db.exec("COMMIT");
|
|
17585
|
+
} catch (error51) {
|
|
17586
|
+
try {
|
|
17587
|
+
db.exec("ROLLBACK");
|
|
17588
|
+
} catch {
|
|
17589
|
+
}
|
|
17590
|
+
throw error51;
|
|
17591
|
+
}
|
|
17592
|
+
}
|
|
17593
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17594
|
+
const nested = db.isTransaction;
|
|
17595
|
+
try {
|
|
17596
|
+
withTransaction(db, fn, mode);
|
|
17597
|
+
return true;
|
|
17598
|
+
} catch (error51) {
|
|
17599
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17600
|
+
return false;
|
|
17601
|
+
}
|
|
17602
|
+
}
|
|
17603
|
+
|
|
17604
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17605
|
+
function akaWarn(message) {
|
|
17606
|
+
process.stderr.write(`[aka] ${message}
|
|
17607
|
+
`);
|
|
17608
|
+
}
|
|
17609
|
+
|
|
17610
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17611
|
+
function evidenceObjects(sql) {
|
|
17612
|
+
const objects = [];
|
|
17613
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17614
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17615
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17616
|
+
}
|
|
17617
|
+
}
|
|
17618
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17619
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17620
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17621
|
+
}
|
|
17622
|
+
}
|
|
17623
|
+
return objects;
|
|
17624
|
+
}
|
|
17625
|
+
function schemaObjectExists(db, kind, name) {
|
|
17626
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17627
|
+
return row !== void 0;
|
|
17628
|
+
}
|
|
17629
|
+
function indexExists(db, name) {
|
|
17630
|
+
return schemaObjectExists(db, "index", name);
|
|
17631
|
+
}
|
|
17632
|
+
function columnNames(db, table2, opts) {
|
|
17633
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17634
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
|
|
17635
|
+
return columns.map((c) => c.name);
|
|
17636
|
+
}
|
|
17637
|
+
function evidenceExists(db, object2) {
|
|
17638
|
+
if (object2.kind === "column") {
|
|
17639
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17640
|
+
}
|
|
17641
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17642
|
+
}
|
|
17643
|
+
|
|
17479
17644
|
// ../../packages/persistence/src/ids.ts
|
|
17480
17645
|
import { createHash } from "crypto";
|
|
17481
17646
|
function sha256Hex(input) {
|
|
@@ -17512,28 +17677,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17512
17677
|
}
|
|
17513
17678
|
|
|
17514
17679
|
// ../../packages/persistence/src/migrations.ts
|
|
17515
|
-
function evidenceObjects(sql) {
|
|
17516
|
-
const objects = [];
|
|
17517
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17518
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17519
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17520
|
-
}
|
|
17521
|
-
}
|
|
17522
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17523
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17524
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17525
|
-
}
|
|
17526
|
-
}
|
|
17527
|
-
return objects;
|
|
17528
|
-
}
|
|
17529
|
-
function evidenceExists(db, object2) {
|
|
17530
|
-
if (object2.kind === "column") {
|
|
17531
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17532
|
-
return columns.some((c) => c.name === object2.name);
|
|
17533
|
-
}
|
|
17534
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17535
|
-
return row !== void 0;
|
|
17536
|
-
}
|
|
17537
17680
|
function describeObject(object2) {
|
|
17538
17681
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17539
17682
|
}
|
|
@@ -17544,10 +17687,6 @@ function createdIndexName(statement) {
|
|
|
17544
17687
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17545
17688
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17546
17689
|
}
|
|
17547
|
-
function indexExists(db, name) {
|
|
17548
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17549
|
-
return row !== void 0;
|
|
17550
|
-
}
|
|
17551
17690
|
function applyMigrations(db) {
|
|
17552
17691
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17553
17692
|
db.exec(
|
|
@@ -17566,44 +17705,39 @@ function applyMigrations(db) {
|
|
|
17566
17705
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17567
17706
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17568
17707
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17569
|
-
const message = `
|
|
17570
|
-
|
|
17571
|
-
`);
|
|
17572
|
-
throw new Error(message);
|
|
17708
|
+
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.`;
|
|
17709
|
+
akaWarn(message);
|
|
17710
|
+
throw new Error(`[aka] ${message}`);
|
|
17573
17711
|
}
|
|
17574
17712
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17575
17713
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17576
17714
|
const statements = splitStatements(migration.sql);
|
|
17577
17715
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17578
17716
|
try {
|
|
17579
|
-
|
|
17580
|
-
|
|
17581
|
-
|
|
17582
|
-
const
|
|
17583
|
-
|
|
17584
|
-
if (
|
|
17585
|
-
|
|
17586
|
-
|
|
17717
|
+
withTransaction(
|
|
17718
|
+
db,
|
|
17719
|
+
() => {
|
|
17720
|
+
for (const statement of statements) {
|
|
17721
|
+
const indexName = createdIndexName(statement);
|
|
17722
|
+
if (indexName === void 0) {
|
|
17723
|
+
if (alreadyApplied) continue;
|
|
17724
|
+
} else if (indexExists(db, indexName)) {
|
|
17725
|
+
continue;
|
|
17726
|
+
}
|
|
17727
|
+
db.exec(statement);
|
|
17587
17728
|
}
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
);
|
|
17729
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17730
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17731
|
+
if (violations.length > 0) {
|
|
17732
|
+
throw new Error(
|
|
17733
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17734
|
+
);
|
|
17735
|
+
}
|
|
17596
17736
|
}
|
|
17597
|
-
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
try {
|
|
17602
|
-
db.exec("ROLLBACK");
|
|
17603
|
-
} catch {
|
|
17604
|
-
}
|
|
17605
|
-
throw error51;
|
|
17606
|
-
}
|
|
17737
|
+
record2.run(migration.tag, Date.now());
|
|
17738
|
+
},
|
|
17739
|
+
"IMMEDIATE"
|
|
17740
|
+
);
|
|
17607
17741
|
} finally {
|
|
17608
17742
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17609
17743
|
}
|
|
@@ -17646,8 +17780,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17646
17780
|
}
|
|
17647
17781
|
];
|
|
17648
17782
|
function ensureTokenUsageColumns(db) {
|
|
17649
|
-
const
|
|
17650
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17783
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17651
17784
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17652
17785
|
if (!existing.has(column.name)) {
|
|
17653
17786
|
db.exec(column.ddl);
|
|
@@ -17685,47 +17818,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17685
17818
|
repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
|
|
17686
17819
|
}));
|
|
17687
17820
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
dropCollisions
|
|
17702
|
-
|
|
17821
|
+
withTransaction(
|
|
17822
|
+
db,
|
|
17823
|
+
() => {
|
|
17824
|
+
for (const { row, canonicalId } of legacy) {
|
|
17825
|
+
foldProject.run(
|
|
17826
|
+
canonicalId,
|
|
17827
|
+
row.url,
|
|
17828
|
+
row.name,
|
|
17829
|
+
row.attributes,
|
|
17830
|
+
row.firstSeen,
|
|
17831
|
+
row.lastSeen
|
|
17832
|
+
);
|
|
17833
|
+
repointAudit.run(canonicalId, row.id);
|
|
17834
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
17835
|
+
dropCollisions.run(row.id, canonicalId);
|
|
17836
|
+
repoint.run(canonicalId, row.id);
|
|
17837
|
+
}
|
|
17838
|
+
repointCallSite.run(canonicalId, row.id);
|
|
17839
|
+
deleteLegacy.run(row.id);
|
|
17703
17840
|
}
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
db.exec("COMMIT");
|
|
17708
|
-
} catch (error51) {
|
|
17709
|
-
try {
|
|
17710
|
-
db.exec("ROLLBACK");
|
|
17711
|
-
} catch {
|
|
17712
|
-
}
|
|
17713
|
-
throw error51;
|
|
17714
|
-
}
|
|
17841
|
+
},
|
|
17842
|
+
"IMMEDIATE"
|
|
17843
|
+
);
|
|
17715
17844
|
} catch (error51) {
|
|
17716
|
-
|
|
17717
|
-
`);
|
|
17845
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17718
17846
|
}
|
|
17719
17847
|
}
|
|
17720
17848
|
function isForeignSqliteLineage(db) {
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17724
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
17849
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
17850
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17725
17851
|
}
|
|
17726
17852
|
function ensureSyncedAtColumn(db, table2) {
|
|
17727
|
-
|
|
17728
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
17853
|
+
if (!columnNames(db, table2).includes("synced_at")) {
|
|
17729
17854
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
|
|
17730
17855
|
}
|
|
17731
17856
|
}
|
|
@@ -17776,8 +17901,11 @@ function ensureDataDirSync(dir) {
|
|
|
17776
17901
|
} catch {
|
|
17777
17902
|
}
|
|
17778
17903
|
}
|
|
17904
|
+
function walSidecars(file2) {
|
|
17905
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
17906
|
+
}
|
|
17779
17907
|
function tightenPerms(file2) {
|
|
17780
|
-
for (const path of [file2,
|
|
17908
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17781
17909
|
try {
|
|
17782
17910
|
chmodSync(path, DATA_FILE_MODE);
|
|
17783
17911
|
} catch {
|
|
@@ -17785,12 +17913,68 @@ function tightenPerms(file2) {
|
|
|
17785
17913
|
}
|
|
17786
17914
|
}
|
|
17787
17915
|
|
|
17788
|
-
// ../../packages/persistence/src/
|
|
17789
|
-
function
|
|
17790
|
-
|
|
17916
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
17917
|
+
function safeJson(s, fallback) {
|
|
17918
|
+
if (s == null) return fallback;
|
|
17919
|
+
try {
|
|
17920
|
+
return JSON.parse(s);
|
|
17921
|
+
} catch {
|
|
17922
|
+
return fallback;
|
|
17923
|
+
}
|
|
17791
17924
|
}
|
|
17792
|
-
function
|
|
17793
|
-
|
|
17925
|
+
function parseJsonObject(s) {
|
|
17926
|
+
if (s == null) return void 0;
|
|
17927
|
+
try {
|
|
17928
|
+
const parsed = JSON.parse(s);
|
|
17929
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
17930
|
+
} catch {
|
|
17931
|
+
}
|
|
17932
|
+
return void 0;
|
|
17933
|
+
}
|
|
17934
|
+
|
|
17935
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
17936
|
+
function allRows(stmt, params) {
|
|
17937
|
+
if (params === void 0) return stmt.all();
|
|
17938
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
17939
|
+
return stmt.all(params);
|
|
17940
|
+
}
|
|
17941
|
+
function getRow(stmt, params) {
|
|
17942
|
+
if (params === void 0) return stmt.get();
|
|
17943
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
17944
|
+
return stmt.get(params);
|
|
17945
|
+
}
|
|
17946
|
+
function intToBool(raw) {
|
|
17947
|
+
return raw === 1 || raw === true;
|
|
17948
|
+
}
|
|
17949
|
+
function boolToInt(b) {
|
|
17950
|
+
return b ? 1 : 0;
|
|
17951
|
+
}
|
|
17952
|
+
function bindParams(row) {
|
|
17953
|
+
const out = {};
|
|
17954
|
+
for (const [key, value] of Object.entries(row)) {
|
|
17955
|
+
out[key] = value === void 0 ? null : value;
|
|
17956
|
+
}
|
|
17957
|
+
return out;
|
|
17958
|
+
}
|
|
17959
|
+
function countScalar(db, sql, params) {
|
|
17960
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17961
|
+
}
|
|
17962
|
+
function countBy(db, sql, params) {
|
|
17963
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
17964
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
17965
|
+
map2.set(row.k, row.n);
|
|
17966
|
+
}
|
|
17967
|
+
return map2;
|
|
17968
|
+
}
|
|
17969
|
+
function mapRowsTolerant(rows, map2) {
|
|
17970
|
+
const out = [];
|
|
17971
|
+
for (const row of rows) {
|
|
17972
|
+
try {
|
|
17973
|
+
out.push(map2(row));
|
|
17974
|
+
} catch {
|
|
17975
|
+
}
|
|
17976
|
+
}
|
|
17977
|
+
return out;
|
|
17794
17978
|
}
|
|
17795
17979
|
|
|
17796
17980
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17842,15 +18026,11 @@ function encodeCursor(payload) {
|
|
|
17842
18026
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17843
18027
|
}
|
|
17844
18028
|
function decodeCursor(cursor) {
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
return parsed;
|
|
17849
|
-
}
|
|
17850
|
-
return null;
|
|
17851
|
-
} catch {
|
|
17852
|
-
return null;
|
|
18029
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18030
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18031
|
+
return parsed;
|
|
17853
18032
|
}
|
|
18033
|
+
return null;
|
|
17854
18034
|
}
|
|
17855
18035
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17856
18036
|
session: "session",
|
|
@@ -17867,15 +18047,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17867
18047
|
};
|
|
17868
18048
|
function safeParseStringArray(raw) {
|
|
17869
18049
|
if (!raw) return [];
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17873
|
-
} catch {
|
|
17874
|
-
return [];
|
|
17875
|
-
}
|
|
17876
|
-
}
|
|
17877
|
-
function toBool(raw) {
|
|
17878
|
-
return raw === 1 || raw === true;
|
|
18050
|
+
const parsed = safeJson(raw, null);
|
|
18051
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17879
18052
|
}
|
|
17880
18053
|
function toHarness(raw) {
|
|
17881
18054
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17928,8 +18101,8 @@ function buildAuditEvent(row) {
|
|
|
17928
18101
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17929
18102
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17930
18103
|
targetId: row.target_id,
|
|
17931
|
-
internal:
|
|
17932
|
-
flagged:
|
|
18104
|
+
internal: intToBool(row.internal),
|
|
18105
|
+
flagged: intToBool(row.flagged)
|
|
17933
18106
|
};
|
|
17934
18107
|
}
|
|
17935
18108
|
var TIMELINE_COLUMNS = `
|
|
@@ -17955,12 +18128,15 @@ var SqliteActivityRepository = class {
|
|
|
17955
18128
|
stats(tz) {
|
|
17956
18129
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
17957
18130
|
const { startMs, endMs } = window;
|
|
17958
|
-
const sessionsToday =
|
|
18131
|
+
const sessionsToday = countScalar(
|
|
18132
|
+
this.db,
|
|
17959
18133
|
`SELECT count(*) AS n FROM audit_events
|
|
17960
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
17961
|
-
|
|
18134
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
|
|
18135
|
+
[startMs, endMs]
|
|
18136
|
+
);
|
|
17962
18137
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
17963
|
-
const liveNow =
|
|
18138
|
+
const liveNow = countScalar(
|
|
18139
|
+
this.db,
|
|
17964
18140
|
`SELECT count(*) AS n FROM audit_events s
|
|
17965
18141
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
17966
18142
|
AND max(
|
|
@@ -17969,22 +18145,29 @@ var SqliteActivityRepository = class {
|
|
|
17969
18145
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
17970
18146
|
s.started_at
|
|
17971
18147
|
)
|
|
17972
|
-
) >=
|
|
17973
|
-
|
|
17974
|
-
|
|
18148
|
+
) >= ?`,
|
|
18149
|
+
[liveThreshold]
|
|
18150
|
+
);
|
|
18151
|
+
const toolCallsToday = countScalar(
|
|
18152
|
+
this.db,
|
|
17975
18153
|
`SELECT count(*) AS n FROM audit_events
|
|
17976
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
17977
|
-
|
|
17978
|
-
|
|
18154
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18155
|
+
[startMs, endMs]
|
|
18156
|
+
);
|
|
18157
|
+
const findingsToday = countScalar(
|
|
18158
|
+
this.db,
|
|
17979
18159
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
17980
18160
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
17981
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
17982
|
-
|
|
17983
|
-
|
|
18161
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18162
|
+
[startMs, endMs]
|
|
18163
|
+
);
|
|
18164
|
+
const egressToday = countScalar(
|
|
18165
|
+
this.db,
|
|
17984
18166
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
17985
18167
|
FROM audit_events
|
|
17986
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
17987
|
-
|
|
18168
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18169
|
+
[startMs, endMs]
|
|
18170
|
+
);
|
|
17988
18171
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
17989
18172
|
}
|
|
17990
18173
|
listSessions(query) {
|
|
@@ -18006,7 +18189,7 @@ var SqliteActivityRepository = class {
|
|
|
18006
18189
|
conditions.push("started_at <= ?");
|
|
18007
18190
|
params.push(toMs);
|
|
18008
18191
|
if (query.q) {
|
|
18009
|
-
const pattern =
|
|
18192
|
+
const pattern = containsPattern(query.q);
|
|
18010
18193
|
conditions.push(
|
|
18011
18194
|
`(content LIKE ? ESCAPE '\\'
|
|
18012
18195
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18025,8 +18208,9 @@ var SqliteActivityRepository = class {
|
|
|
18025
18208
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18026
18209
|
}
|
|
18027
18210
|
const limit = query.limit;
|
|
18028
|
-
const rows =
|
|
18029
|
-
|
|
18211
|
+
const rows = allRows(
|
|
18212
|
+
this.db.prepare(
|
|
18213
|
+
`SELECT id,
|
|
18030
18214
|
json_extract(attributes, '$.harness') AS harness,
|
|
18031
18215
|
content AS title,
|
|
18032
18216
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18039,7 +18223,9 @@ var SqliteActivityRepository = class {
|
|
|
18039
18223
|
WHERE ${conditions.join(" AND ")}
|
|
18040
18224
|
ORDER BY started_at DESC, id DESC
|
|
18041
18225
|
LIMIT ?`
|
|
18042
|
-
|
|
18226
|
+
),
|
|
18227
|
+
[...params, limit + 1]
|
|
18228
|
+
);
|
|
18043
18229
|
const hasMore = rows.length > limit;
|
|
18044
18230
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18045
18231
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18056,8 +18242,9 @@ var SqliteActivityRepository = class {
|
|
|
18056
18242
|
return Promise.resolve({ items, nextCursor });
|
|
18057
18243
|
}
|
|
18058
18244
|
getSession(sessionId) {
|
|
18059
|
-
const rootRow =
|
|
18060
|
-
|
|
18245
|
+
const rootRow = getRow(
|
|
18246
|
+
this.db.prepare(
|
|
18247
|
+
`SELECT id,
|
|
18061
18248
|
json_extract(attributes, '$.harness') AS harness,
|
|
18062
18249
|
content AS title,
|
|
18063
18250
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18074,47 +18261,66 @@ var SqliteActivityRepository = class {
|
|
|
18074
18261
|
FROM audit_events
|
|
18075
18262
|
WHERE id = ? AND event_type = 'session'
|
|
18076
18263
|
LIMIT 1`
|
|
18077
|
-
|
|
18264
|
+
),
|
|
18265
|
+
[sessionId]
|
|
18266
|
+
);
|
|
18078
18267
|
if (!rootRow) return Promise.resolve(null);
|
|
18079
|
-
const timelineRows =
|
|
18080
|
-
|
|
18268
|
+
const timelineRows = allRows(
|
|
18269
|
+
this.db.prepare(
|
|
18270
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18081
18271
|
FROM audit_events
|
|
18082
18272
|
WHERE id = ? OR root_session_id = ?
|
|
18083
18273
|
ORDER BY started_at ASC, id ASC`
|
|
18084
|
-
|
|
18274
|
+
),
|
|
18275
|
+
[sessionId, sessionId]
|
|
18276
|
+
);
|
|
18085
18277
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18086
|
-
const tokenRow =
|
|
18087
|
-
|
|
18278
|
+
const tokenRow = getRow(
|
|
18279
|
+
this.db.prepare(
|
|
18280
|
+
`SELECT
|
|
18088
18281
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18089
18282
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18090
18283
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18091
18284
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18092
18285
|
FROM audit_events
|
|
18093
18286
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18094
|
-
|
|
18095
|
-
|
|
18096
|
-
|
|
18287
|
+
),
|
|
18288
|
+
[sessionId]
|
|
18289
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18290
|
+
const primaryModel = getRow(
|
|
18291
|
+
this.db.prepare(
|
|
18292
|
+
`SELECT model, provider FROM audit_events
|
|
18097
18293
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18098
18294
|
ORDER BY started_at ASC, id ASC
|
|
18099
18295
|
LIMIT 1`
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18296
|
+
),
|
|
18297
|
+
[sessionId]
|
|
18298
|
+
);
|
|
18299
|
+
const toolRows = allRows(
|
|
18300
|
+
this.db.prepare(
|
|
18301
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18103
18302
|
count(*) AS n
|
|
18104
18303
|
FROM audit_events
|
|
18105
18304
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18106
18305
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
|
|
18306
|
+
),
|
|
18307
|
+
[sessionId]
|
|
18308
|
+
);
|
|
18309
|
+
const modelRows = allRows(
|
|
18310
|
+
this.db.prepare(
|
|
18311
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18110
18312
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18111
18313
|
ORDER BY model`
|
|
18112
|
-
|
|
18314
|
+
),
|
|
18315
|
+
[sessionId]
|
|
18316
|
+
);
|
|
18113
18317
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18114
|
-
const commits =
|
|
18318
|
+
const commits = countScalar(
|
|
18319
|
+
this.db,
|
|
18115
18320
|
`SELECT count(*) AS n FROM audit_events
|
|
18116
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18117
|
-
|
|
18321
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18322
|
+
[sessionId]
|
|
18323
|
+
);
|
|
18118
18324
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18119
18325
|
turns: 0,
|
|
18120
18326
|
findings: 0,
|
|
@@ -18181,7 +18387,10 @@ var SqliteActivityRepository = class {
|
|
|
18181
18387
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18182
18388
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18183
18389
|
);
|
|
18184
|
-
const rows =
|
|
18390
|
+
const rows = allRows(
|
|
18391
|
+
stmt,
|
|
18392
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18393
|
+
);
|
|
18185
18394
|
const seen = /* @__PURE__ */ new Set();
|
|
18186
18395
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18187
18396
|
return Promise.resolve([...seen]);
|
|
@@ -18204,23 +18413,23 @@ var SqliteActivityRepository = class {
|
|
|
18204
18413
|
conditions.push("started_at >= ?");
|
|
18205
18414
|
params.push(opts.fromMs);
|
|
18206
18415
|
}
|
|
18207
|
-
const rows =
|
|
18208
|
-
|
|
18416
|
+
const rows = allRows(
|
|
18417
|
+
this.db.prepare(
|
|
18418
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18209
18419
|
FROM audit_events
|
|
18210
18420
|
WHERE ${conditions.join(" AND ")}`
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
}
|
|
18222
|
-
|
|
18223
|
-
return leaves;
|
|
18421
|
+
),
|
|
18422
|
+
params
|
|
18423
|
+
);
|
|
18424
|
+
return mapRowsTolerant(
|
|
18425
|
+
rows.filter(
|
|
18426
|
+
(row) => row.sessionId !== null
|
|
18427
|
+
),
|
|
18428
|
+
(row) => ({
|
|
18429
|
+
sessionId: row.sessionId,
|
|
18430
|
+
attributes: JSON.parse(row.attributes)
|
|
18431
|
+
})
|
|
18432
|
+
);
|
|
18224
18433
|
}
|
|
18225
18434
|
/**
|
|
18226
18435
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18234,57 +18443,72 @@ var SqliteActivityRepository = class {
|
|
|
18234
18443
|
);
|
|
18235
18444
|
if (sessionIds.length === 0) return result;
|
|
18236
18445
|
const inClause = placeholders(sessionIds.length);
|
|
18237
|
-
const lastActivityRows =
|
|
18238
|
-
|
|
18446
|
+
const lastActivityRows = allRows(
|
|
18447
|
+
this.db.prepare(
|
|
18448
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18239
18449
|
WHERE root_session_id IN (${inClause})
|
|
18240
18450
|
GROUP BY root_session_id`
|
|
18241
|
-
|
|
18451
|
+
),
|
|
18452
|
+
sessionIds
|
|
18453
|
+
);
|
|
18242
18454
|
for (const row of lastActivityRows) {
|
|
18243
18455
|
if (row.id === null) continue;
|
|
18244
18456
|
const entry = result.get(row.id);
|
|
18245
18457
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18246
18458
|
}
|
|
18247
|
-
const turnsRows =
|
|
18248
|
-
|
|
18459
|
+
const turnsRows = allRows(
|
|
18460
|
+
this.db.prepare(
|
|
18461
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18249
18462
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18250
18463
|
GROUP BY root_session_id`
|
|
18251
|
-
|
|
18464
|
+
),
|
|
18465
|
+
sessionIds
|
|
18466
|
+
);
|
|
18252
18467
|
for (const row of turnsRows) {
|
|
18253
18468
|
if (row.id === null) continue;
|
|
18254
18469
|
const entry = result.get(row.id);
|
|
18255
18470
|
if (entry) entry.turns = row.n;
|
|
18256
18471
|
}
|
|
18257
|
-
const runKeyRows =
|
|
18258
|
-
|
|
18472
|
+
const runKeyRows = allRows(
|
|
18473
|
+
this.db.prepare(
|
|
18474
|
+
`SELECT root_session_id AS id,
|
|
18259
18475
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18260
18476
|
FROM audit_events
|
|
18261
18477
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18262
18478
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18263
18479
|
GROUP BY root_session_id`
|
|
18264
|
-
|
|
18480
|
+
),
|
|
18481
|
+
sessionIds
|
|
18482
|
+
);
|
|
18265
18483
|
for (const row of runKeyRows) {
|
|
18266
18484
|
if (row.id === null) continue;
|
|
18267
18485
|
const entry = result.get(row.id);
|
|
18268
18486
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18269
18487
|
}
|
|
18270
|
-
const findingsRows =
|
|
18271
|
-
|
|
18488
|
+
const findingsRows = allRows(
|
|
18489
|
+
this.db.prepare(
|
|
18490
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18272
18491
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18273
18492
|
WHERE e.root_session_id IN (${inClause})
|
|
18274
18493
|
GROUP BY e.root_session_id`
|
|
18275
|
-
|
|
18494
|
+
),
|
|
18495
|
+
sessionIds
|
|
18496
|
+
);
|
|
18276
18497
|
for (const row of findingsRows) {
|
|
18277
18498
|
if (row.id === null) continue;
|
|
18278
18499
|
const entry = result.get(row.id);
|
|
18279
18500
|
if (entry) entry.findings = row.n;
|
|
18280
18501
|
}
|
|
18281
|
-
const sharesRows =
|
|
18282
|
-
|
|
18502
|
+
const sharesRows = allRows(
|
|
18503
|
+
this.db.prepare(
|
|
18504
|
+
`SELECT root_session_id AS id,
|
|
18283
18505
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18284
18506
|
FROM audit_events
|
|
18285
18507
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18286
18508
|
GROUP BY root_session_id`
|
|
18287
|
-
|
|
18509
|
+
),
|
|
18510
|
+
sessionIds
|
|
18511
|
+
);
|
|
18288
18512
|
for (const row of sharesRows) {
|
|
18289
18513
|
if (row.id === null) continue;
|
|
18290
18514
|
const entry = result.get(row.id);
|
|
@@ -18334,33 +18558,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18334
18558
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18335
18559
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18336
18560
|
runInTransaction(fn) {
|
|
18337
|
-
this.db
|
|
18338
|
-
try {
|
|
18339
|
-
fn();
|
|
18340
|
-
this.db.exec("COMMIT");
|
|
18341
|
-
} catch (err) {
|
|
18342
|
-
this.db.exec("ROLLBACK");
|
|
18343
|
-
throw err;
|
|
18344
|
-
}
|
|
18561
|
+
withTransaction(this.db, fn);
|
|
18345
18562
|
}
|
|
18346
18563
|
insertAuditEvent(input) {
|
|
18347
18564
|
const row = toAuditEventRow(input);
|
|
18348
|
-
this.insertStmt.run(
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18565
|
+
this.insertStmt.run(
|
|
18566
|
+
bindParams({
|
|
18567
|
+
id: row.id,
|
|
18568
|
+
parentId: row.parentId,
|
|
18569
|
+
rootSessionId: row.rootSessionId,
|
|
18570
|
+
eventType: row.eventType,
|
|
18571
|
+
hostId: row.hostId,
|
|
18572
|
+
harnessId: row.harnessId,
|
|
18573
|
+
sourceProjectId: row.sourceProjectId,
|
|
18574
|
+
startedAt: row.startedAt,
|
|
18575
|
+
endedAt: row.endedAt,
|
|
18576
|
+
severity: row.severity,
|
|
18577
|
+
priority: row.priority,
|
|
18578
|
+
content: row.content,
|
|
18579
|
+
contentHash: row.contentHash,
|
|
18580
|
+
attributes: row.attributes
|
|
18581
|
+
})
|
|
18582
|
+
);
|
|
18364
18583
|
}
|
|
18365
18584
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18366
18585
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18374,22 +18593,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18374
18593
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18375
18594
|
if (!Number.isFinite(startedAt)) return;
|
|
18376
18595
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18377
|
-
this.upsertLlmCallStmt.run(
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18596
|
+
this.upsertLlmCallStmt.run(
|
|
18597
|
+
bindParams({
|
|
18598
|
+
id,
|
|
18599
|
+
parentId: input.parentId,
|
|
18600
|
+
rootSessionId: input.rootSessionId,
|
|
18601
|
+
eventType: "llm_call",
|
|
18602
|
+
hostId: null,
|
|
18603
|
+
harnessId: null,
|
|
18604
|
+
sourceProjectId: null,
|
|
18605
|
+
startedAt,
|
|
18606
|
+
endedAt: null,
|
|
18607
|
+
severity: null,
|
|
18608
|
+
priority: null,
|
|
18609
|
+
content: null,
|
|
18610
|
+
contentHash: null,
|
|
18611
|
+
attributes: JSON.stringify(input.attributes)
|
|
18612
|
+
})
|
|
18613
|
+
);
|
|
18393
18614
|
}
|
|
18394
18615
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18395
18616
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18413,25 +18634,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18413
18634
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18414
18635
|
if (!Number.isFinite(startedAt)) return;
|
|
18415
18636
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18416
|
-
this.insertStmt.run(
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18637
|
+
this.insertStmt.run(
|
|
18638
|
+
bindParams({
|
|
18639
|
+
id,
|
|
18640
|
+
parentId: input.parentId,
|
|
18641
|
+
rootSessionId: input.rootSessionId,
|
|
18642
|
+
eventType: "tool_call",
|
|
18643
|
+
hostId: null,
|
|
18644
|
+
harnessId: null,
|
|
18645
|
+
sourceProjectId: null,
|
|
18646
|
+
startedAt,
|
|
18647
|
+
endedAt: null,
|
|
18648
|
+
severity: null,
|
|
18649
|
+
priority: null,
|
|
18650
|
+
content: null,
|
|
18651
|
+
contentHash: null,
|
|
18652
|
+
attributes: JSON.stringify(input.attributes)
|
|
18653
|
+
})
|
|
18654
|
+
);
|
|
18432
18655
|
}
|
|
18433
18656
|
findById(id) {
|
|
18434
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18657
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18658
|
+
id
|
|
18659
|
+
});
|
|
18435
18660
|
}
|
|
18436
18661
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18437
18662
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18441,14 +18666,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18441
18666
|
sessionProvider(sessionId) {
|
|
18442
18667
|
const row = this.findById(sessionId);
|
|
18443
18668
|
if (!row?.attributes) return void 0;
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18447
|
-
const provider = parsed.provider;
|
|
18448
|
-
if (typeof provider === "string") return provider;
|
|
18449
|
-
}
|
|
18450
|
-
} catch {
|
|
18451
|
-
}
|
|
18669
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18670
|
+
if (typeof provider === "string") return provider;
|
|
18452
18671
|
return void 0;
|
|
18453
18672
|
}
|
|
18454
18673
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18457,11 +18676,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18457
18676
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18458
18677
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18459
18678
|
llmCallLeaves() {
|
|
18460
|
-
return
|
|
18461
|
-
|
|
18679
|
+
return allRows(
|
|
18680
|
+
this.db.prepare(
|
|
18681
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18462
18682
|
FROM audit_events
|
|
18463
18683
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18464
|
-
|
|
18684
|
+
)
|
|
18685
|
+
);
|
|
18465
18686
|
}
|
|
18466
18687
|
};
|
|
18467
18688
|
|
|
@@ -18480,16 +18701,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18480
18701
|
upsert(input) {
|
|
18481
18702
|
const id = classifiedDataId(input.class);
|
|
18482
18703
|
const row = toClassifiedDataRow(input, id);
|
|
18483
|
-
this.insertStmt.run(
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18704
|
+
this.insertStmt.run(
|
|
18705
|
+
bindParams({
|
|
18706
|
+
id: row.id,
|
|
18707
|
+
class: row.class,
|
|
18708
|
+
label: row.label,
|
|
18709
|
+
attributes: row.attributes
|
|
18710
|
+
})
|
|
18711
|
+
);
|
|
18489
18712
|
return id;
|
|
18490
18713
|
}
|
|
18491
18714
|
};
|
|
18492
18715
|
|
|
18716
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18717
|
+
function latestConfigScan(db) {
|
|
18718
|
+
return getRow(
|
|
18719
|
+
db.prepare(
|
|
18720
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18721
|
+
WHERE event_type = 'config_scan'
|
|
18722
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18723
|
+
)
|
|
18724
|
+
);
|
|
18725
|
+
}
|
|
18726
|
+
|
|
18493
18727
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18494
18728
|
var SqliteConfigInventoryRepository = class {
|
|
18495
18729
|
constructor(db) {
|
|
@@ -18497,7 +18731,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18497
18731
|
}
|
|
18498
18732
|
db;
|
|
18499
18733
|
report() {
|
|
18500
|
-
const scan2 = this.
|
|
18734
|
+
const scan2 = latestConfigScan(this.db);
|
|
18501
18735
|
if (!scan2) {
|
|
18502
18736
|
return {
|
|
18503
18737
|
scannedAt: null,
|
|
@@ -18508,17 +18742,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18508
18742
|
topics: []
|
|
18509
18743
|
};
|
|
18510
18744
|
}
|
|
18511
|
-
const rows =
|
|
18512
|
-
|
|
18745
|
+
const rows = allRows(
|
|
18746
|
+
this.db.prepare(
|
|
18747
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18513
18748
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18514
18749
|
ORDER BY object_type, title`
|
|
18515
|
-
|
|
18516
|
-
|
|
18517
|
-
|
|
18750
|
+
),
|
|
18751
|
+
{ startedAt: scan2.started_at }
|
|
18752
|
+
);
|
|
18753
|
+
const findings = allRows(
|
|
18754
|
+
this.db.prepare(
|
|
18755
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18518
18756
|
FROM inspection_findings f
|
|
18519
18757
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18520
18758
|
WHERE f.audit_event_id = :scanId`
|
|
18521
|
-
|
|
18759
|
+
),
|
|
18760
|
+
{ scanId: scan2.id }
|
|
18761
|
+
);
|
|
18522
18762
|
const skills = [];
|
|
18523
18763
|
const hooks = [];
|
|
18524
18764
|
const mcpServers = [];
|
|
@@ -18547,7 +18787,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18547
18787
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18548
18788
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18549
18789
|
trustOverrides() {
|
|
18550
|
-
const rows =
|
|
18790
|
+
const rows = allRows(
|
|
18791
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18792
|
+
);
|
|
18551
18793
|
const map2 = /* @__PURE__ */ new Map();
|
|
18552
18794
|
for (const row of rows) {
|
|
18553
18795
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18556,13 +18798,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18556
18798
|
}
|
|
18557
18799
|
return map2;
|
|
18558
18800
|
}
|
|
18559
|
-
latestScan() {
|
|
18560
|
-
return this.db.prepare(
|
|
18561
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18562
|
-
WHERE event_type = 'config_scan'
|
|
18563
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18564
|
-
).get();
|
|
18565
|
-
}
|
|
18566
18801
|
};
|
|
18567
18802
|
function toSkillItem(row, bag) {
|
|
18568
18803
|
const item = {
|
|
@@ -18673,22 +18908,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18673
18908
|
return topics;
|
|
18674
18909
|
}
|
|
18675
18910
|
function countScanErrors(attributes) {
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
const parsed = JSON.parse(attributes);
|
|
18679
|
-
const errors = parsed?.errors;
|
|
18680
|
-
return typeof errors === "number" ? errors : 0;
|
|
18681
|
-
} catch {
|
|
18682
|
-
return 0;
|
|
18683
|
-
}
|
|
18911
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
18912
|
+
return typeof errors === "number" ? errors : 0;
|
|
18684
18913
|
}
|
|
18685
18914
|
function parseBag(raw) {
|
|
18686
|
-
|
|
18687
|
-
const parsed = JSON.parse(raw);
|
|
18688
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18689
|
-
} catch {
|
|
18690
|
-
}
|
|
18691
|
-
return void 0;
|
|
18915
|
+
return parseJsonObject(raw);
|
|
18692
18916
|
}
|
|
18693
18917
|
function str(value) {
|
|
18694
18918
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18697,12 +18921,7 @@ function str(value) {
|
|
|
18697
18921
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18698
18922
|
var DAY_MS2 = 864e5;
|
|
18699
18923
|
function parseRules(rulesJson) {
|
|
18700
|
-
|
|
18701
|
-
try {
|
|
18702
|
-
raw = JSON.parse(rulesJson);
|
|
18703
|
-
} catch {
|
|
18704
|
-
return [];
|
|
18705
|
-
}
|
|
18924
|
+
const raw = safeJson(rulesJson, []);
|
|
18706
18925
|
if (!Array.isArray(raw)) return [];
|
|
18707
18926
|
const rules = [];
|
|
18708
18927
|
for (const entry of raw) {
|
|
@@ -18721,11 +18940,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18721
18940
|
db;
|
|
18722
18941
|
now;
|
|
18723
18942
|
listDetections(query) {
|
|
18724
|
-
const rows =
|
|
18725
|
-
|
|
18943
|
+
const rows = allRows(
|
|
18944
|
+
this.db.prepare(
|
|
18945
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18726
18946
|
rules_json AS rulesJson
|
|
18727
18947
|
FROM installed_packs`
|
|
18728
|
-
|
|
18948
|
+
)
|
|
18949
|
+
);
|
|
18729
18950
|
const available = this.availableByPack();
|
|
18730
18951
|
const summaries = rows.map((r) => {
|
|
18731
18952
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18734,7 +18955,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18734
18955
|
packId: r.packId,
|
|
18735
18956
|
version: r.version,
|
|
18736
18957
|
name: r.name,
|
|
18737
|
-
enabled: r.enabled
|
|
18958
|
+
enabled: intToBool(r.enabled),
|
|
18738
18959
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18739
18960
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18740
18961
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18751,19 +18972,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18751
18972
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18752
18973
|
// detail call; the table is a handful of rows).
|
|
18753
18974
|
availableByPack() {
|
|
18754
|
-
const rows =
|
|
18755
|
-
|
|
18975
|
+
const rows = allRows(
|
|
18976
|
+
this.db.prepare(
|
|
18977
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18756
18978
|
FROM available_packs`
|
|
18757
|
-
|
|
18979
|
+
)
|
|
18980
|
+
);
|
|
18758
18981
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18759
18982
|
}
|
|
18760
18983
|
getDetectionStats() {
|
|
18761
|
-
const rows =
|
|
18984
|
+
const rows = allRows(
|
|
18985
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
18986
|
+
);
|
|
18762
18987
|
let rules = 0;
|
|
18763
18988
|
let active = 0;
|
|
18764
18989
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18765
18990
|
for (const r of rows) {
|
|
18766
|
-
if (r.enabled
|
|
18991
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18767
18992
|
const parsed = parseRules(r.rulesJson);
|
|
18768
18993
|
rules += parsed.length;
|
|
18769
18994
|
for (const rule of parsed) {
|
|
@@ -18781,12 +19006,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18781
19006
|
const parts = splitDetectionId(id);
|
|
18782
19007
|
if (!parts) return Promise.resolve(null);
|
|
18783
19008
|
const { namespace, packId } = parts;
|
|
18784
|
-
const row =
|
|
18785
|
-
|
|
19009
|
+
const row = getRow(
|
|
19010
|
+
this.db.prepare(
|
|
19011
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18786
19012
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18787
19013
|
FROM installed_packs
|
|
18788
19014
|
WHERE namespace = ? AND pack_id = ?`
|
|
18789
|
-
|
|
19015
|
+
),
|
|
19016
|
+
[namespace, packId]
|
|
19017
|
+
);
|
|
18790
19018
|
if (!row) return Promise.resolve(null);
|
|
18791
19019
|
const rules = parseRules(row.rulesJson);
|
|
18792
19020
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18804,7 +19032,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18804
19032
|
packId: row.packId,
|
|
18805
19033
|
version: row.version,
|
|
18806
19034
|
name: row.name,
|
|
18807
|
-
enabled: row.enabled
|
|
19035
|
+
enabled: intToBool(row.enabled),
|
|
18808
19036
|
rules,
|
|
18809
19037
|
updatedAt: new Date(row.updatedAt),
|
|
18810
19038
|
policyId: row.policyId
|
|
@@ -18819,13 +19047,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18819
19047
|
countFindingsLast30d(ruleIds) {
|
|
18820
19048
|
if (ruleIds.length === 0) return 0;
|
|
18821
19049
|
const since = this.now() - 30 * DAY_MS2;
|
|
18822
|
-
const
|
|
18823
|
-
|
|
18824
|
-
|
|
19050
|
+
const inClause = placeholders(ruleIds.length);
|
|
19051
|
+
return countScalar(
|
|
19052
|
+
this.db,
|
|
19053
|
+
`SELECT count(*) AS n
|
|
18825
19054
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18826
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18827
|
-
|
|
18828
|
-
|
|
19055
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19056
|
+
[since, ...ruleIds]
|
|
19057
|
+
);
|
|
18829
19058
|
}
|
|
18830
19059
|
};
|
|
18831
19060
|
|
|
@@ -18842,16 +19071,17 @@ var SqliteEventsRepository = class {
|
|
|
18842
19071
|
insertStmt;
|
|
18843
19072
|
insertEvent(event) {
|
|
18844
19073
|
const row = toEventRow(event);
|
|
18845
|
-
this.insertStmt.run(
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
19074
|
+
this.insertStmt.run(
|
|
19075
|
+
bindParams({
|
|
19076
|
+
id: row.id,
|
|
19077
|
+
sourceTool: row.sourceTool,
|
|
19078
|
+
kind: row.kind,
|
|
19079
|
+
occurredAt: row.occurredAt,
|
|
19080
|
+
contentHash: row.contentHash,
|
|
19081
|
+
content: row.content,
|
|
19082
|
+
metadata: row.metadata
|
|
19083
|
+
})
|
|
19084
|
+
);
|
|
18855
19085
|
}
|
|
18856
19086
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18857
19087
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18859,13 +19089,23 @@ var SqliteEventsRepository = class {
|
|
|
18859
19089
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18860
19090
|
// async EventsReadPort contract.
|
|
18861
19091
|
contentHashes() {
|
|
18862
|
-
const rows =
|
|
19092
|
+
const rows = allRows(
|
|
19093
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19094
|
+
);
|
|
18863
19095
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18864
19096
|
}
|
|
18865
19097
|
};
|
|
18866
19098
|
|
|
18867
19099
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18868
19100
|
import { randomUUID } from "crypto";
|
|
19101
|
+
|
|
19102
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19103
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19104
|
+
function isUniqueConstraintError(err) {
|
|
19105
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19106
|
+
}
|
|
19107
|
+
|
|
19108
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18869
19109
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18870
19110
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18871
19111
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18886,10 +19126,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18886
19126
|
this.name = "AmbiguousExceptionIdError";
|
|
18887
19127
|
}
|
|
18888
19128
|
};
|
|
18889
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18890
|
-
function isUniqueConstraintError(err) {
|
|
18891
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18892
|
-
}
|
|
18893
19129
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18894
19130
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18895
19131
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -18945,10 +19181,11 @@ var SqliteExceptionsRepository = class {
|
|
|
18945
19181
|
this.insertExceptionRow(id, input, now);
|
|
18946
19182
|
} catch (err) {
|
|
18947
19183
|
if (!isUniqueConstraintError(err)) throw err;
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
19184
|
+
withTransaction(
|
|
19185
|
+
this.db,
|
|
19186
|
+
() => {
|
|
19187
|
+
const superseded = this.db.prepare(
|
|
19188
|
+
`UPDATE exceptions
|
|
18952
19189
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
18953
19190
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
18954
19191
|
updated_at = :now
|
|
@@ -18956,24 +19193,27 @@ var SqliteExceptionsRepository = class {
|
|
|
18956
19193
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
18957
19194
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
18958
19195
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
19196
|
+
).run({
|
|
19197
|
+
now,
|
|
19198
|
+
revokedBy: input.createdBy,
|
|
19199
|
+
ruleId: input.ruleId,
|
|
19200
|
+
valueFingerprint: input.valueFingerprint,
|
|
19201
|
+
keyVersion: input.keyVersion
|
|
19202
|
+
});
|
|
19203
|
+
if (Number(superseded.changes) !== 1) {
|
|
19204
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19205
|
+
}
|
|
19206
|
+
this.insertExceptionRow(id, input, now);
|
|
19207
|
+
},
|
|
19208
|
+
"IMMEDIATE"
|
|
19209
|
+
);
|
|
19210
|
+
}
|
|
19211
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19212
|
+
id
|
|
19213
|
+
});
|
|
19214
|
+
if (row === void 0) {
|
|
19215
|
+
throw new Error("exception row not found immediately after insert");
|
|
18975
19216
|
}
|
|
18976
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
18977
19217
|
return parseExceptionRow(row);
|
|
18978
19218
|
}
|
|
18979
19219
|
insertExceptionRow(id, input, now) {
|
|
@@ -19011,14 +19251,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19011
19251
|
*/
|
|
19012
19252
|
list(opts) {
|
|
19013
19253
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19014
|
-
const rows =
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
} catch {
|
|
19020
|
-
}
|
|
19021
|
-
}
|
|
19254
|
+
const rows = allRows(
|
|
19255
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19256
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19257
|
+
);
|
|
19258
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19022
19259
|
return Promise.resolve(exceptions);
|
|
19023
19260
|
}
|
|
19024
19261
|
/**
|
|
@@ -19028,7 +19265,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19028
19265
|
*/
|
|
19029
19266
|
getByIdPrefix(prefix) {
|
|
19030
19267
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19031
|
-
const rows =
|
|
19268
|
+
const rows = allRows(
|
|
19269
|
+
this.db.prepare(
|
|
19270
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19271
|
+
),
|
|
19272
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19273
|
+
);
|
|
19032
19274
|
if (rows.length > 1) {
|
|
19033
19275
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19034
19276
|
}
|
|
@@ -19070,30 +19312,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19070
19312
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19071
19313
|
*/
|
|
19072
19314
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19073
|
-
const rows =
|
|
19074
|
-
|
|
19315
|
+
const rows = allRows(
|
|
19316
|
+
this.db.prepare(
|
|
19317
|
+
`SELECT * FROM exceptions
|
|
19075
19318
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19076
19319
|
ORDER BY created_at DESC, rowid DESC`
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
);
|
|
19094
|
-
} catch {
|
|
19095
|
-
}
|
|
19096
|
-
}
|
|
19320
|
+
),
|
|
19321
|
+
{ keyVersion, now }
|
|
19322
|
+
);
|
|
19323
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19324
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19325
|
+
return ExceptionBundleEntry.parse({
|
|
19326
|
+
id: row.id,
|
|
19327
|
+
ruleId: row.rule_id,
|
|
19328
|
+
valueFingerprint: row.value_fingerprint,
|
|
19329
|
+
keyVersion: row.key_version,
|
|
19330
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19331
|
+
maxUses: row.max_uses,
|
|
19332
|
+
useCount: row.use_count,
|
|
19333
|
+
conditions
|
|
19334
|
+
});
|
|
19335
|
+
});
|
|
19097
19336
|
return Promise.resolve(entries);
|
|
19098
19337
|
}
|
|
19099
19338
|
/**
|
|
@@ -19120,11 +19359,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19120
19359
|
}
|
|
19121
19360
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19122
19361
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19123
|
-
const rows =
|
|
19124
|
-
|
|
19362
|
+
const rows = allRows(
|
|
19363
|
+
this.db.prepare(
|
|
19364
|
+
`SELECT * FROM blocked_detections
|
|
19125
19365
|
WHERE blocked_at > :cutoff
|
|
19126
19366
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19127
|
-
|
|
19367
|
+
),
|
|
19368
|
+
{ cutoff: Date.now() - windowMs }
|
|
19369
|
+
);
|
|
19128
19370
|
return Promise.resolve(
|
|
19129
19371
|
rows.map((row) => ({
|
|
19130
19372
|
reference: row.reference,
|
|
@@ -19204,7 +19446,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19204
19446
|
)`;
|
|
19205
19447
|
|
|
19206
19448
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19207
|
-
var
|
|
19449
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19450
|
+
var CONCAT_SEP = ",";
|
|
19451
|
+
var TUPLE_SEP = "|";
|
|
19452
|
+
function splitConcat(value) {
|
|
19453
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19454
|
+
}
|
|
19208
19455
|
function deriveInstanceStatus(row) {
|
|
19209
19456
|
return deriveFindingStatus({
|
|
19210
19457
|
kind: row.kind,
|
|
@@ -19272,13 +19519,16 @@ var SqliteFindingsRepository = class {
|
|
|
19272
19519
|
}
|
|
19273
19520
|
recentFindings(opts) {
|
|
19274
19521
|
const limit = opts?.limit ?? 50;
|
|
19275
|
-
const rows =
|
|
19276
|
-
|
|
19522
|
+
const rows = allRows(
|
|
19523
|
+
this.db.prepare(
|
|
19524
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19277
19525
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19278
19526
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19279
19527
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19280
19528
|
LIMIT :limit`
|
|
19281
|
-
|
|
19529
|
+
),
|
|
19530
|
+
{ limit }
|
|
19531
|
+
);
|
|
19282
19532
|
return Promise.resolve(
|
|
19283
19533
|
rows.map((r) => ({
|
|
19284
19534
|
id: r.id,
|
|
@@ -19301,20 +19551,47 @@ var SqliteFindingsRepository = class {
|
|
|
19301
19551
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19302
19552
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19303
19553
|
* reflect the full filtered set; `items` is the requested
|
|
19304
|
-
* page (default
|
|
19554
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19555
|
+
*
|
|
19556
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19557
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19558
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19559
|
+
* latest, search text);
|
|
19560
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19561
|
+
* populate `instances` for the table's expanded rows.
|
|
19562
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19563
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19564
|
+
* rule is ever restated in SQL.
|
|
19305
19565
|
*/
|
|
19306
19566
|
listGroupedFindings(query) {
|
|
19307
|
-
const
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19567
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
|
|
19568
|
+
const rows = allRows(
|
|
19569
|
+
this.db.prepare(
|
|
19570
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19571
|
+
occurred_at, source_tool, repo, file, kind, finding_key, latest_status
|
|
19572
|
+
FROM (
|
|
19573
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19574
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19575
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19576
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19577
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19578
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19579
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19580
|
+
latest.status AS latest_status,
|
|
19581
|
+
ROW_NUMBER() OVER (
|
|
19582
|
+
PARTITION BY f.rule_id
|
|
19583
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19584
|
+
) AS rn
|
|
19585
|
+
FROM findings f
|
|
19586
|
+
JOIN events e ON e.id = f.event_id
|
|
19587
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19588
|
+
ON latest.finding_key = f.finding_key
|
|
19589
|
+
)
|
|
19590
|
+
WHERE rn <= :cap
|
|
19591
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19592
|
+
),
|
|
19593
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP }
|
|
19594
|
+
);
|
|
19318
19595
|
const groupable = rows.map((r) => ({
|
|
19319
19596
|
id: r.id,
|
|
19320
19597
|
ruleId: r.rule_id,
|
|
@@ -19329,7 +19606,7 @@ var SqliteFindingsRepository = class {
|
|
|
19329
19606
|
file: r.file ?? "",
|
|
19330
19607
|
status: deriveInstanceStatus(r)
|
|
19331
19608
|
}));
|
|
19332
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19609
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19333
19610
|
const filterOpts = {
|
|
19334
19611
|
severity: query.severity,
|
|
19335
19612
|
providers: query.provider,
|
|
@@ -19347,42 +19624,122 @@ var SqliteFindingsRepository = class {
|
|
|
19347
19624
|
const items = sorted.slice(0, limit);
|
|
19348
19625
|
return Promise.resolve({ totals, facets, items, nextCursor: null });
|
|
19349
19626
|
}
|
|
19627
|
+
/**
|
|
19628
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19629
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19630
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19631
|
+
*
|
|
19632
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19633
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19634
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19635
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19636
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19637
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19638
|
+
* a group's row stays small however many findings it holds.
|
|
19639
|
+
*
|
|
19640
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19641
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19642
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19643
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19644
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19645
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19646
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19647
|
+
* carries a `q`.
|
|
19648
|
+
*/
|
|
19649
|
+
groupAggregates(withSearchText) {
|
|
19650
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19651
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
|
|
19652
|
+
const rows = this.db.prepare(
|
|
19653
|
+
`SELECT f.rule_id AS rule_id,
|
|
19654
|
+
count(*) AS instance_count,
|
|
19655
|
+
max(e.occurred_at) AS latest_at,
|
|
19656
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19657
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19658
|
+
group_concat(DISTINCT (
|
|
19659
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19660
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19661
|
+
coalesce(latest.status, '')
|
|
19662
|
+
)) AS status_inputs
|
|
19663
|
+
${searchTextColumns}
|
|
19664
|
+
FROM findings f
|
|
19665
|
+
JOIN events e ON e.id = f.event_id
|
|
19666
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19667
|
+
ON latest.finding_key = f.finding_key
|
|
19668
|
+
GROUP BY f.rule_id`
|
|
19669
|
+
).all();
|
|
19670
|
+
return new Map(
|
|
19671
|
+
rows.map((r) => [
|
|
19672
|
+
r.rule_id,
|
|
19673
|
+
{
|
|
19674
|
+
instanceCount: r.instance_count,
|
|
19675
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19676
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19677
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19678
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19679
|
+
return {
|
|
19680
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19681
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19682
|
+
kind,
|
|
19683
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19684
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19685
|
+
};
|
|
19686
|
+
}),
|
|
19687
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19688
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19689
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19690
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19691
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19692
|
+
// and skip priming a haystack nothing will read.
|
|
19693
|
+
...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
|
|
19694
|
+
}
|
|
19695
|
+
])
|
|
19696
|
+
);
|
|
19697
|
+
}
|
|
19350
19698
|
healthSummary() {
|
|
19351
|
-
const total = this.db
|
|
19699
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19352
19700
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19353
|
-
const grouped =
|
|
19701
|
+
const grouped = allRows(
|
|
19702
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19703
|
+
);
|
|
19354
19704
|
for (const row of grouped) {
|
|
19355
19705
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19356
19706
|
}
|
|
19357
19707
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19358
|
-
const sevRows =
|
|
19359
|
-
|
|
19708
|
+
const sevRows = allRows(
|
|
19709
|
+
this.db.prepare(
|
|
19710
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19360
19711
|
FROM findings f
|
|
19361
19712
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19362
19713
|
ON latest.finding_key = f.finding_key
|
|
19363
19714
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19364
19715
|
GROUP BY f.severity`
|
|
19365
|
-
|
|
19716
|
+
)
|
|
19717
|
+
);
|
|
19366
19718
|
for (const row of sevRows) {
|
|
19367
19719
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19368
19720
|
}
|
|
19369
19721
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19370
|
-
const enabledRows =
|
|
19371
|
-
|
|
19722
|
+
const enabledRows = allRows(
|
|
19723
|
+
this.db.prepare(
|
|
19724
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19372
19725
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19373
|
-
|
|
19726
|
+
)
|
|
19727
|
+
);
|
|
19374
19728
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19375
19729
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19376
19730
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19377
19731
|
}
|
|
19378
19732
|
activityByDay(days = 7) {
|
|
19379
19733
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19380
|
-
const rows =
|
|
19381
|
-
|
|
19734
|
+
const rows = allRows(
|
|
19735
|
+
this.db.prepare(
|
|
19736
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19382
19737
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19383
19738
|
WHERE e.occurred_at >= :since
|
|
19384
19739
|
GROUP BY day, f.action_taken`
|
|
19385
|
-
|
|
19740
|
+
),
|
|
19741
|
+
{ since }
|
|
19742
|
+
);
|
|
19386
19743
|
const buckets = /* @__PURE__ */ new Map();
|
|
19387
19744
|
for (let i = 0; i < days; i++) {
|
|
19388
19745
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19454,17 +19811,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19454
19811
|
insertStmt;
|
|
19455
19812
|
insertFinding(input) {
|
|
19456
19813
|
const row = toInspectionFindingRow(input);
|
|
19457
|
-
this.insertStmt.run(
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19814
|
+
this.insertStmt.run(
|
|
19815
|
+
bindParams({
|
|
19816
|
+
id: row.id,
|
|
19817
|
+
auditEventId: row.auditEventId,
|
|
19818
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
19819
|
+
classifiedDataId: row.classifiedDataId,
|
|
19820
|
+
spanStart: row.spanStart,
|
|
19821
|
+
spanEnd: row.spanEnd,
|
|
19822
|
+
maskedMatch: row.maskedMatch,
|
|
19823
|
+
actionTaken: row.actionTaken,
|
|
19824
|
+
confidence: row.confidence
|
|
19825
|
+
})
|
|
19826
|
+
);
|
|
19468
19827
|
}
|
|
19469
19828
|
};
|
|
19470
19829
|
|
|
@@ -19540,12 +19899,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19540
19899
|
}
|
|
19541
19900
|
function ruleIdsOf(rulesJson) {
|
|
19542
19901
|
const ids = /* @__PURE__ */ new Set();
|
|
19543
|
-
|
|
19544
|
-
try {
|
|
19545
|
-
raw = JSON.parse(rulesJson);
|
|
19546
|
-
} catch {
|
|
19547
|
-
return ids;
|
|
19548
|
-
}
|
|
19902
|
+
const raw = safeJson(rulesJson, []);
|
|
19549
19903
|
if (!Array.isArray(raw)) return ids;
|
|
19550
19904
|
for (const entry of raw) {
|
|
19551
19905
|
if (entry && typeof entry === "object") {
|
|
@@ -19618,47 +19972,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19618
19972
|
}));
|
|
19619
19973
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19620
19974
|
const now = Date.now();
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
const
|
|
19627
|
-
|
|
19628
|
-
namespace: row.namespace,
|
|
19629
|
-
packId: row.packId,
|
|
19630
|
-
version: row.version,
|
|
19631
|
-
name: row.name,
|
|
19632
|
-
rulesJson: row.rulesJson,
|
|
19633
|
-
now
|
|
19634
|
-
};
|
|
19635
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19636
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19637
|
-
this.upsertAvailableStmt.run({
|
|
19638
|
-
...params,
|
|
19975
|
+
withTransaction(
|
|
19976
|
+
this.db,
|
|
19977
|
+
() => {
|
|
19978
|
+
const mirror = this.mirrorState();
|
|
19979
|
+
let behind = false;
|
|
19980
|
+
for (const row of rows) {
|
|
19981
|
+
const params = {
|
|
19639
19982
|
id: randomUUID2(),
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19983
|
+
namespace: row.namespace,
|
|
19984
|
+
packId: row.packId,
|
|
19985
|
+
version: row.version,
|
|
19986
|
+
name: row.name,
|
|
19987
|
+
rulesJson: row.rulesJson,
|
|
19988
|
+
now
|
|
19989
|
+
};
|
|
19990
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19991
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19992
|
+
this.upsertAvailableStmt.run({
|
|
19993
|
+
...params,
|
|
19994
|
+
id: randomUUID2(),
|
|
19995
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
19996
|
+
});
|
|
19997
|
+
} else {
|
|
19998
|
+
behind = true;
|
|
19999
|
+
}
|
|
20000
|
+
this.insertMissingStmt.run(params);
|
|
19644
20001
|
}
|
|
19645
|
-
this.
|
|
19646
|
-
}
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
} catch (err) {
|
|
19650
|
-
this.db.exec("ROLLBACK");
|
|
19651
|
-
throw err;
|
|
19652
|
-
}
|
|
20002
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20003
|
+
},
|
|
20004
|
+
"IMMEDIATE"
|
|
20005
|
+
);
|
|
19653
20006
|
} catch {
|
|
19654
20007
|
}
|
|
19655
20008
|
}
|
|
19656
20009
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19657
20010
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19658
20011
|
mirrorState() {
|
|
19659
|
-
const rows =
|
|
19660
|
-
|
|
19661
|
-
|
|
20012
|
+
const rows = allRows(
|
|
20013
|
+
this.db.prepare(
|
|
20014
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20015
|
+
)
|
|
20016
|
+
);
|
|
19662
20017
|
return new Map(
|
|
19663
20018
|
rows.map((r) => [
|
|
19664
20019
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19670,7 +20025,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19670
20025
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19671
20026
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19672
20027
|
pruneAvailable(keep) {
|
|
19673
|
-
const rows =
|
|
20028
|
+
const rows = allRows(
|
|
20029
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20030
|
+
);
|
|
19674
20031
|
const keepSet = new Set(keep);
|
|
19675
20032
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19676
20033
|
for (const r of rows) {
|
|
@@ -19697,11 +20054,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19697
20054
|
if (this.db.isTransaction) {
|
|
19698
20055
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19699
20056
|
}
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
this.db
|
|
19703
|
-
|
|
19704
|
-
|
|
20057
|
+
let changed = false;
|
|
20058
|
+
withTransaction(
|
|
20059
|
+
this.db,
|
|
20060
|
+
() => {
|
|
20061
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20062
|
+
const res = this.db.prepare(
|
|
20063
|
+
`UPDATE installed_packs SET
|
|
19705
20064
|
version = (SELECT a.version FROM available_packs a
|
|
19706
20065
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19707
20066
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19712,17 +20071,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19712
20071
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19713
20072
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19714
20073
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
} catch {
|
|
19723
|
-
}
|
|
19724
|
-
throw err;
|
|
19725
|
-
}
|
|
20074
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20075
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20076
|
+
changed = Number(res.changes) > 0;
|
|
20077
|
+
},
|
|
20078
|
+
"IMMEDIATE"
|
|
20079
|
+
);
|
|
20080
|
+
return changed;
|
|
19726
20081
|
}
|
|
19727
20082
|
/**
|
|
19728
20083
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19736,9 +20091,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19736
20091
|
* JSON-level failure therefore counts as invalid.
|
|
19737
20092
|
*/
|
|
19738
20093
|
installedRuleset() {
|
|
19739
|
-
const rows =
|
|
19740
|
-
|
|
19741
|
-
|
|
20094
|
+
const rows = allRows(
|
|
20095
|
+
this.db.prepare(
|
|
20096
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20097
|
+
)
|
|
20098
|
+
);
|
|
19742
20099
|
const out = {
|
|
19743
20100
|
installedPacks: rows.length,
|
|
19744
20101
|
enabledPacks: 0,
|
|
@@ -19747,7 +20104,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19747
20104
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19748
20105
|
};
|
|
19749
20106
|
for (const row of rows) {
|
|
19750
|
-
if (row.enabled
|
|
20107
|
+
if (!intToBool(row.enabled)) continue;
|
|
19751
20108
|
out.enabledPacks += 1;
|
|
19752
20109
|
const action = policyIdToAction(row.policyId);
|
|
19753
20110
|
let raw;
|
|
@@ -19782,9 +20139,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19782
20139
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19783
20140
|
*/
|
|
19784
20141
|
newestRecordedBinary() {
|
|
19785
|
-
const rows =
|
|
19786
|
-
|
|
19787
|
-
|
|
20142
|
+
const rows = allRows(
|
|
20143
|
+
this.db.prepare(
|
|
20144
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20145
|
+
)
|
|
20146
|
+
);
|
|
19788
20147
|
let newest = null;
|
|
19789
20148
|
for (const row of rows) {
|
|
19790
20149
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19799,13 +20158,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19799
20158
|
return newest;
|
|
19800
20159
|
}
|
|
19801
20160
|
counts() {
|
|
19802
|
-
const row =
|
|
19803
|
-
|
|
20161
|
+
const row = getRow(
|
|
20162
|
+
this.db.prepare(
|
|
20163
|
+
`SELECT count(*) AS packs,
|
|
19804
20164
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19805
20165
|
coalesce(sum(enabled), 0) AS enabled
|
|
19806
20166
|
FROM installed_packs`
|
|
19807
|
-
|
|
19808
|
-
|
|
20167
|
+
)
|
|
20168
|
+
);
|
|
20169
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19809
20170
|
}
|
|
19810
20171
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19811
20172
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19818,26 +20179,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19818
20179
|
* attributed to Monitor, matching the Detections views.
|
|
19819
20180
|
*/
|
|
19820
20181
|
countsByPolicyId() {
|
|
19821
|
-
|
|
19822
|
-
|
|
20182
|
+
return countBy(
|
|
20183
|
+
this.db,
|
|
20184
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19823
20185
|
FROM installed_packs
|
|
19824
|
-
GROUP BY
|
|
19825
|
-
)
|
|
19826
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20186
|
+
GROUP BY k`
|
|
20187
|
+
);
|
|
19827
20188
|
}
|
|
19828
20189
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19829
20190
|
listByPolicyId(policyId) {
|
|
19830
|
-
const rows =
|
|
19831
|
-
|
|
20191
|
+
const rows = allRows(
|
|
20192
|
+
this.db.prepare(
|
|
20193
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19832
20194
|
FROM installed_packs
|
|
19833
20195
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19834
20196
|
ORDER BY name ASC`
|
|
19835
|
-
|
|
20197
|
+
),
|
|
20198
|
+
[policyId]
|
|
20199
|
+
);
|
|
19836
20200
|
return rows.map((r) => ({
|
|
19837
20201
|
id: `${r.namespace}/${r.packId}`,
|
|
19838
20202
|
name: r.name,
|
|
19839
20203
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19840
|
-
enabled: r.enabled
|
|
20204
|
+
enabled: intToBool(r.enabled)
|
|
19841
20205
|
}));
|
|
19842
20206
|
}
|
|
19843
20207
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19866,14 +20230,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19866
20230
|
const res = this.db.prepare(
|
|
19867
20231
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19868
20232
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19869
|
-
).run({ enabled: enabled
|
|
20233
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19870
20234
|
return Number(res.changes) > 0;
|
|
19871
20235
|
}
|
|
19872
20236
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19873
20237
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19874
20238
|
// binary's inventory hasn't changed since the last record.
|
|
19875
20239
|
storedSignature() {
|
|
19876
|
-
const rows = this.signatureStmt
|
|
20240
|
+
const rows = allRows(this.signatureStmt);
|
|
19877
20241
|
return inventorySignature(rows);
|
|
19878
20242
|
}
|
|
19879
20243
|
};
|
|
@@ -19901,42 +20265,48 @@ var SqliteInventoryRepository = class {
|
|
|
19901
20265
|
upsert(input, now = Date.now()) {
|
|
19902
20266
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19903
20267
|
const row = toInventoryRow(input, id, now);
|
|
19904
|
-
this.upsertStmt.run(
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
20268
|
+
this.upsertStmt.run(
|
|
20269
|
+
bindParams({
|
|
20270
|
+
id: row.id,
|
|
20271
|
+
objectType: row.objectType,
|
|
20272
|
+
location: row.location,
|
|
20273
|
+
title: row.title,
|
|
20274
|
+
hostId: row.hostId,
|
|
20275
|
+
attributes: row.attributes,
|
|
20276
|
+
firstSeen: row.firstSeen,
|
|
20277
|
+
lastSeen: row.lastSeen
|
|
20278
|
+
})
|
|
20279
|
+
);
|
|
19914
20280
|
return id;
|
|
19915
20281
|
}
|
|
19916
20282
|
// The full row, for round-trip assertions.
|
|
19917
20283
|
findById(id) {
|
|
19918
|
-
|
|
19919
|
-
return row;
|
|
20284
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19920
20285
|
}
|
|
19921
20286
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19922
20287
|
// served from the object_type index, never from audit_events.
|
|
19923
20288
|
distinctTitles(objectType) {
|
|
19924
|
-
const rows =
|
|
19925
|
-
|
|
20289
|
+
const rows = allRows(
|
|
20290
|
+
this.db.prepare(
|
|
20291
|
+
`SELECT DISTINCT title FROM inventory
|
|
19926
20292
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19927
20293
|
ORDER BY title`
|
|
19928
|
-
|
|
20294
|
+
),
|
|
20295
|
+
{ objectType }
|
|
20296
|
+
);
|
|
19929
20297
|
return rows.map((r) => r.title);
|
|
19930
20298
|
}
|
|
19931
20299
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19932
20300
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19933
20301
|
// QUERY PLAN).
|
|
19934
20302
|
osVersions() {
|
|
19935
|
-
const rows =
|
|
19936
|
-
|
|
20303
|
+
const rows = allRows(
|
|
20304
|
+
this.db.prepare(
|
|
20305
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19937
20306
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
19938
20307
|
ORDER BY value`
|
|
19939
|
-
|
|
20308
|
+
)
|
|
20309
|
+
);
|
|
19940
20310
|
return rows.map((r) => r.value);
|
|
19941
20311
|
}
|
|
19942
20312
|
};
|
|
@@ -19956,14 +20326,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
19956
20326
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
19957
20327
|
findingsCount: 0
|
|
19958
20328
|
};
|
|
19959
|
-
function safeJson(s, fallback) {
|
|
19960
|
-
if (s == null) return fallback;
|
|
19961
|
-
try {
|
|
19962
|
-
return JSON.parse(s);
|
|
19963
|
-
} catch {
|
|
19964
|
-
return fallback;
|
|
19965
|
-
}
|
|
19966
|
-
}
|
|
19967
20329
|
function resolveHarnessId(attrs, row) {
|
|
19968
20330
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
19969
20331
|
return attrs.provider;
|
|
@@ -20159,30 +20521,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20159
20521
|
configRowsCache;
|
|
20160
20522
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20161
20523
|
getInventoryStats() {
|
|
20162
|
-
const
|
|
20163
|
-
|
|
20164
|
-
|
|
20165
|
-
|
|
20166
|
-
byType
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
|
|
20524
|
+
const typeCounts = countBy(
|
|
20525
|
+
this.db,
|
|
20526
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20527
|
+
);
|
|
20528
|
+
const byType = {
|
|
20529
|
+
project: 0,
|
|
20530
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20531
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20532
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20533
|
+
config: typeCounts.get("config") ?? 0
|
|
20534
|
+
};
|
|
20535
|
+
byType.project = countScalar(
|
|
20536
|
+
this.db,
|
|
20537
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20538
|
+
);
|
|
20539
|
+
const mcpTrustCounts = countBy(
|
|
20540
|
+
this.db,
|
|
20541
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20170
20542
|
FROM inventory_asset a
|
|
20171
20543
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20172
20544
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20173
20545
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20174
|
-
)
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20546
|
+
);
|
|
20547
|
+
const mcpTrust = {
|
|
20548
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20549
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20550
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20551
|
+
};
|
|
20552
|
+
const harnesses = countScalar(
|
|
20553
|
+
this.db,
|
|
20178
20554
|
`SELECT count(*) AS n FROM inventory
|
|
20179
20555
|
WHERE object_type = 'harness'
|
|
20180
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
const
|
|
20556
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20557
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20558
|
+
);
|
|
20559
|
+
const flaggedAssets = countScalar(
|
|
20560
|
+
this.db,
|
|
20561
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20562
|
+
);
|
|
20563
|
+
const flaggedProjects = countScalar(
|
|
20564
|
+
this.db,
|
|
20184
20565
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20185
|
-
)
|
|
20566
|
+
);
|
|
20186
20567
|
const configRows = this.configAssetRows();
|
|
20187
20568
|
for (const r of configRows) {
|
|
20188
20569
|
byType[r.assetType] += 1;
|
|
@@ -20439,12 +20820,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20439
20820
|
}
|
|
20440
20821
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20441
20822
|
fetchHarnessRows() {
|
|
20442
|
-
return
|
|
20443
|
-
|
|
20823
|
+
return allRows(
|
|
20824
|
+
this.db.prepare(
|
|
20825
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20444
20826
|
FROM inventory
|
|
20445
20827
|
WHERE object_type = 'harness'
|
|
20446
20828
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20447
|
-
|
|
20829
|
+
),
|
|
20830
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20831
|
+
);
|
|
20448
20832
|
}
|
|
20449
20833
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20450
20834
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20454,12 +20838,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20454
20838
|
const params = [...harnessInvIds];
|
|
20455
20839
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20456
20840
|
if (q) {
|
|
20457
|
-
const pat =
|
|
20458
|
-
where +=
|
|
20841
|
+
const pat = containsPattern(q);
|
|
20842
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20459
20843
|
params.push(pat, pat);
|
|
20460
20844
|
}
|
|
20461
|
-
const rows =
|
|
20462
|
-
|
|
20845
|
+
const rows = allRows(
|
|
20846
|
+
this.db.prepare(
|
|
20847
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20463
20848
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20464
20849
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20465
20850
|
FROM harness_asset ha
|
|
@@ -20467,7 +20852,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20467
20852
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20468
20853
|
WHERE ${where}
|
|
20469
20854
|
ORDER BY a.name ASC`
|
|
20470
|
-
|
|
20855
|
+
),
|
|
20856
|
+
params
|
|
20857
|
+
);
|
|
20471
20858
|
for (const raw of rows) {
|
|
20472
20859
|
const harnessInvId = raw.harnessInvId;
|
|
20473
20860
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20486,21 +20873,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20486
20873
|
params.push(...types);
|
|
20487
20874
|
}
|
|
20488
20875
|
if (q) {
|
|
20489
|
-
const pat =
|
|
20490
|
-
conditions.push("
|
|
20876
|
+
const pat = containsPattern(q);
|
|
20877
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20491
20878
|
params.push(pat, pat);
|
|
20492
20879
|
}
|
|
20493
20880
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20494
20881
|
const sampleRows = this.mapAssetRows(
|
|
20495
|
-
|
|
20496
|
-
|
|
20882
|
+
allRows(
|
|
20883
|
+
this.db.prepare(
|
|
20884
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20497
20885
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20498
20886
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20499
20887
|
FROM inventory_asset a
|
|
20500
20888
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20501
20889
|
${where}
|
|
20502
20890
|
ORDER BY a.name ASC`
|
|
20503
|
-
|
|
20891
|
+
),
|
|
20892
|
+
params
|
|
20893
|
+
)
|
|
20504
20894
|
);
|
|
20505
20895
|
const configRows = this.configAssetRows(q).filter(
|
|
20506
20896
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20509,14 +20899,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20509
20899
|
}
|
|
20510
20900
|
fetchAssetById(assetId) {
|
|
20511
20901
|
const rows = this.mapAssetRows(
|
|
20512
|
-
|
|
20513
|
-
|
|
20902
|
+
allRows(
|
|
20903
|
+
this.db.prepare(
|
|
20904
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20514
20905
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20515
20906
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20516
20907
|
FROM inventory_asset a
|
|
20517
20908
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20518
20909
|
WHERE a.id = ?`
|
|
20519
|
-
|
|
20910
|
+
),
|
|
20911
|
+
[assetId]
|
|
20912
|
+
)
|
|
20520
20913
|
);
|
|
20521
20914
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20522
20915
|
}
|
|
@@ -20572,37 +20965,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20572
20965
|
return rows;
|
|
20573
20966
|
}
|
|
20574
20967
|
latestConfigScanId() {
|
|
20575
|
-
|
|
20576
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20577
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20578
|
-
).get();
|
|
20579
|
-
return row?.id ?? null;
|
|
20968
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20580
20969
|
}
|
|
20581
20970
|
fetchProjects(q) {
|
|
20582
20971
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20583
20972
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20584
20973
|
const params = [];
|
|
20585
20974
|
if (q) {
|
|
20586
|
-
const pat =
|
|
20587
|
-
sql +=
|
|
20975
|
+
const pat = containsPattern(q);
|
|
20976
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20588
20977
|
params.push(pat, pat);
|
|
20589
20978
|
}
|
|
20590
20979
|
sql += " ORDER BY name ASC";
|
|
20591
|
-
return this.db.prepare(sql)
|
|
20980
|
+
return allRows(this.db.prepare(sql), params);
|
|
20592
20981
|
}
|
|
20593
20982
|
fetchProjectById(projectId) {
|
|
20594
|
-
return
|
|
20595
|
-
|
|
20596
|
-
|
|
20983
|
+
return getRow(
|
|
20984
|
+
this.db.prepare(
|
|
20985
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
20986
|
+
),
|
|
20987
|
+
[projectId]
|
|
20988
|
+
) ?? null;
|
|
20597
20989
|
}
|
|
20598
20990
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20599
20991
|
fetchProjectsByIds(projectIds) {
|
|
20600
20992
|
const map2 = /* @__PURE__ */ new Map();
|
|
20601
20993
|
if (projectIds.length === 0) return map2;
|
|
20602
|
-
const rows =
|
|
20603
|
-
|
|
20994
|
+
const rows = allRows(
|
|
20995
|
+
this.db.prepare(
|
|
20996
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20604
20997
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20605
|
-
|
|
20998
|
+
),
|
|
20999
|
+
projectIds
|
|
21000
|
+
);
|
|
20606
21001
|
for (const r of rows) map2.set(r.id, r);
|
|
20607
21002
|
return map2;
|
|
20608
21003
|
}
|
|
@@ -20613,8 +21008,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20613
21008
|
projectAggregates(projectIds) {
|
|
20614
21009
|
const map2 = /* @__PURE__ */ new Map();
|
|
20615
21010
|
if (projectIds.length === 0) return map2;
|
|
20616
|
-
const rows =
|
|
20617
|
-
|
|
21011
|
+
const rows = allRows(
|
|
21012
|
+
this.db.prepare(
|
|
21013
|
+
`SELECT f.project_id AS projectId,
|
|
20618
21014
|
coalesce(o.access, f.default_access) AS eff,
|
|
20619
21015
|
count(*) AS n,
|
|
20620
21016
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20622,7 +21018,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20622
21018
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20623
21019
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20624
21020
|
GROUP BY f.project_id, eff`
|
|
20625
|
-
|
|
21021
|
+
),
|
|
21022
|
+
projectIds
|
|
21023
|
+
);
|
|
20626
21024
|
for (const r of rows) {
|
|
20627
21025
|
let agg = map2.get(r.projectId);
|
|
20628
21026
|
if (!agg) {
|
|
@@ -20660,37 +21058,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20660
21058
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20661
21059
|
if (prefix === "") {
|
|
20662
21060
|
return this.mapFileRows(
|
|
20663
|
-
|
|
21061
|
+
allRows(
|
|
21062
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21063
|
+
[projectId]
|
|
21064
|
+
)
|
|
20664
21065
|
);
|
|
20665
21066
|
}
|
|
20666
21067
|
return this.mapFileRows(
|
|
20667
|
-
|
|
20668
|
-
this.
|
|
20669
|
-
|
|
21068
|
+
allRows(
|
|
21069
|
+
this.db.prepare(
|
|
21070
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21071
|
+
),
|
|
21072
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21073
|
+
)
|
|
20670
21074
|
);
|
|
20671
21075
|
}
|
|
20672
21076
|
fetchProjectFilesSearch(projectId, q) {
|
|
20673
|
-
const pat =
|
|
21077
|
+
const pat = containsPattern(q);
|
|
20674
21078
|
return this.mapFileRows(
|
|
20675
|
-
|
|
20676
|
-
this.
|
|
20677
|
-
|
|
20678
|
-
|
|
20679
|
-
|
|
21079
|
+
allRows(
|
|
21080
|
+
this.db.prepare(
|
|
21081
|
+
this.fileSelect(
|
|
21082
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21083
|
+
)
|
|
21084
|
+
),
|
|
21085
|
+
[projectId, pat, pat]
|
|
21086
|
+
)
|
|
20680
21087
|
);
|
|
20681
21088
|
}
|
|
20682
21089
|
fetchProjectFilesBlocked(projectId) {
|
|
20683
21090
|
return this.mapFileRows(
|
|
20684
|
-
|
|
20685
|
-
this.
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
21091
|
+
allRows(
|
|
21092
|
+
this.db.prepare(
|
|
21093
|
+
this.fileSelect(
|
|
21094
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21095
|
+
)
|
|
21096
|
+
),
|
|
21097
|
+
[projectId]
|
|
21098
|
+
)
|
|
20689
21099
|
);
|
|
20690
21100
|
}
|
|
20691
21101
|
fetchProjectFile(projectId, path) {
|
|
20692
21102
|
const rows = this.mapFileRows(
|
|
20693
|
-
|
|
21103
|
+
allRows(
|
|
21104
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21105
|
+
[projectId, path]
|
|
21106
|
+
)
|
|
20694
21107
|
);
|
|
20695
21108
|
return rows[0] ?? null;
|
|
20696
21109
|
}
|
|
@@ -20704,39 +21117,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20704
21117
|
}
|
|
20705
21118
|
db;
|
|
20706
21119
|
readPolicies() {
|
|
20707
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20708
|
-
const policies =
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
customKeywords
|
|
20721
|
-
})
|
|
20722
|
-
);
|
|
20723
|
-
} catch {
|
|
20724
|
-
}
|
|
20725
|
-
}
|
|
21120
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21121
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21122
|
+
const target = JSON.parse(row.target);
|
|
21123
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21124
|
+
return Policy.parse({
|
|
21125
|
+
id: row.id,
|
|
21126
|
+
scope: row.scope,
|
|
21127
|
+
target,
|
|
21128
|
+
action: row.action,
|
|
21129
|
+
enabled: intToBool(row.enabled),
|
|
21130
|
+
customKeywords
|
|
21131
|
+
});
|
|
21132
|
+
});
|
|
20726
21133
|
return Promise.resolve(policies);
|
|
20727
21134
|
}
|
|
20728
21135
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20729
21136
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20730
21137
|
// so a user's edits are never clobbered.
|
|
20731
21138
|
seedDefaults() {
|
|
20732
|
-
const count = this.db
|
|
21139
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20733
21140
|
if (count > 0) return;
|
|
20734
21141
|
const stmt = this.db.prepare(
|
|
20735
21142
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20736
21143
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20737
21144
|
);
|
|
20738
|
-
this.db
|
|
20739
|
-
try {
|
|
21145
|
+
failOpenTransaction(this.db, () => {
|
|
20740
21146
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20741
21147
|
stmt.run({
|
|
20742
21148
|
id: randomUUID4(),
|
|
@@ -20745,10 +21151,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20745
21151
|
now: Date.now()
|
|
20746
21152
|
});
|
|
20747
21153
|
}
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
20751
|
-
|
|
21154
|
+
});
|
|
21155
|
+
}
|
|
21156
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21157
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21158
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21159
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21160
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21161
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21162
|
+
upsertCategoryAction(category, action) {
|
|
21163
|
+
const now = Date.now();
|
|
21164
|
+
this.db.prepare(
|
|
21165
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21166
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21167
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21168
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21169
|
+
}
|
|
21170
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21171
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21172
|
+
// Returns the number of rows changed.
|
|
21173
|
+
capCategoryActions() {
|
|
21174
|
+
const info = this.db.prepare(
|
|
21175
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21176
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21177
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21178
|
+
).run({ now: Date.now() });
|
|
21179
|
+
return Number(info.changes);
|
|
21180
|
+
}
|
|
21181
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21182
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21183
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21184
|
+
getCategoryAction(category) {
|
|
21185
|
+
const row = this.db.prepare(
|
|
21186
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21187
|
+
).get({ category });
|
|
21188
|
+
return row?.action;
|
|
20752
21189
|
}
|
|
20753
21190
|
};
|
|
20754
21191
|
|
|
@@ -20824,7 +21261,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20824
21261
|
maxStampStmt;
|
|
20825
21262
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20826
21263
|
replaceForProject(projectId, scan2, now) {
|
|
20827
|
-
const
|
|
21264
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20828
21265
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20829
21266
|
for (const file2 of scan2.files) {
|
|
20830
21267
|
this.upsertStmt.run({
|
|
@@ -20907,7 +21344,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20907
21344
|
}
|
|
20908
21345
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20909
21346
|
latestByKey(key) {
|
|
20910
|
-
const row = this.latestStmt
|
|
21347
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20911
21348
|
if (!row) return void 0;
|
|
20912
21349
|
return {
|
|
20913
21350
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20924,7 +21361,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20924
21361
|
* the CLI) surfaces for that file.
|
|
20925
21362
|
*/
|
|
20926
21363
|
openAtRestKeysForPath(path) {
|
|
20927
|
-
const rows = this.openAtRestStmt
|
|
21364
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20928
21365
|
return rows.map((r) => r.finding_key);
|
|
20929
21366
|
}
|
|
20930
21367
|
/**
|
|
@@ -20935,7 +21372,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20935
21372
|
* resolution row (see scan.ts).
|
|
20936
21373
|
*/
|
|
20937
21374
|
resolvedAtRestKeysForPath(path) {
|
|
20938
|
-
const rows = this.resolvedAtRestStmt
|
|
21375
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
20939
21376
|
return rows.map((r) => r.finding_key);
|
|
20940
21377
|
}
|
|
20941
21378
|
};
|
|
@@ -20964,31 +21401,25 @@ var SqliteScanLedgerRepository = class {
|
|
|
20964
21401
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
20965
21402
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
20966
21403
|
entriesForRuleset(rulesetHash) {
|
|
20967
|
-
const rows = this.readStmt
|
|
21404
|
+
const rows = allRows(this.readStmt, {
|
|
21405
|
+
rulesetHash
|
|
21406
|
+
});
|
|
20968
21407
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
20969
21408
|
}
|
|
20970
21409
|
upsertEntries(entries) {
|
|
20971
21410
|
if (entries.length === 0) return;
|
|
20972
21411
|
const scannedAt = Date.now();
|
|
20973
|
-
|
|
20974
|
-
|
|
20975
|
-
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
scannedAt
|
|
20983
|
-
});
|
|
20984
|
-
}
|
|
20985
|
-
this.db.exec("COMMIT");
|
|
20986
|
-
} catch (err) {
|
|
20987
|
-
this.db.exec("ROLLBACK");
|
|
20988
|
-
throw err;
|
|
21412
|
+
failOpenTransaction(this.db, () => {
|
|
21413
|
+
for (const entry of entries) {
|
|
21414
|
+
this.upsertStmt.run({
|
|
21415
|
+
path: entry.path,
|
|
21416
|
+
mtime: entry.mtime,
|
|
21417
|
+
contentHash: entry.contentHash,
|
|
21418
|
+
rulesetHash: entry.rulesetHash,
|
|
21419
|
+
scannedAt
|
|
21420
|
+
});
|
|
20989
21421
|
}
|
|
20990
|
-
}
|
|
20991
|
-
}
|
|
21422
|
+
});
|
|
20992
21423
|
}
|
|
20993
21424
|
};
|
|
20994
21425
|
|
|
@@ -21065,8 +21496,9 @@ var SqliteSecurityRepository = class {
|
|
|
21065
21496
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21066
21497
|
// double-counting a key that accumulated several append-only rows.
|
|
21067
21498
|
severitySummary() {
|
|
21068
|
-
const rows =
|
|
21069
|
-
|
|
21499
|
+
const rows = allRows(
|
|
21500
|
+
this.db.prepare(
|
|
21501
|
+
`SELECT f.severity AS severity,
|
|
21070
21502
|
COUNT(*) AS count,
|
|
21071
21503
|
SUM(CASE
|
|
21072
21504
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21085,7 +21517,8 @@ var SqliteSecurityRepository = class {
|
|
|
21085
21517
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21086
21518
|
ON latest.finding_key = f.finding_key
|
|
21087
21519
|
GROUP BY f.severity`
|
|
21088
|
-
|
|
21520
|
+
)
|
|
21521
|
+
);
|
|
21089
21522
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21090
21523
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21091
21524
|
severity,
|
|
@@ -21169,14 +21602,15 @@ var SqliteSecurityRepository = class {
|
|
|
21169
21602
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21170
21603
|
const now = this.now();
|
|
21171
21604
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21172
|
-
const rows =
|
|
21173
|
-
|
|
21174
|
-
|
|
21175
|
-
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21605
|
+
const rows = allRows(
|
|
21606
|
+
this.db.prepare(
|
|
21607
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21608
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21609
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21610
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21611
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21612
|
+
// backfill left null.
|
|
21613
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21180
21614
|
(
|
|
21181
21615
|
SELECT fr.status FROM finding_resolution fr
|
|
21182
21616
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21202,14 +21636,16 @@ var SqliteSecurityRepository = class {
|
|
|
21202
21636
|
WHERE fr.finding_key = f.finding_key
|
|
21203
21637
|
AND fr.resolved_at >= :windowStart
|
|
21204
21638
|
)`
|
|
21205
|
-
|
|
21206
|
-
|
|
21207
|
-
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21639
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21640
|
+
// any resolution activity at/after the window start — a row this method
|
|
21641
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21642
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21643
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21644
|
+
// dialect-agnostic. Without this, a
|
|
21645
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21646
|
+
),
|
|
21647
|
+
{ windowStart }
|
|
21648
|
+
);
|
|
21213
21649
|
const sums = /* @__PURE__ */ new Map();
|
|
21214
21650
|
const counts = /* @__PURE__ */ new Map();
|
|
21215
21651
|
for (const r of rows) {
|
|
@@ -21239,8 +21675,9 @@ var SqliteSecurityRepository = class {
|
|
|
21239
21675
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21240
21676
|
const now = this.now();
|
|
21241
21677
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21242
|
-
const rows =
|
|
21243
|
-
|
|
21678
|
+
const rows = allRows(
|
|
21679
|
+
this.db.prepare(
|
|
21680
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21244
21681
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21245
21682
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21246
21683
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21248,7 +21685,9 @@ var SqliteSecurityRepository = class {
|
|
|
21248
21685
|
GROUP BY repo
|
|
21249
21686
|
ORDER BY c DESC, repo
|
|
21250
21687
|
LIMIT :limit`
|
|
21251
|
-
|
|
21688
|
+
),
|
|
21689
|
+
{ from, to: now, limit }
|
|
21690
|
+
);
|
|
21252
21691
|
const items = rows.map((r) => ({
|
|
21253
21692
|
id: `repo_${r.repo}`,
|
|
21254
21693
|
name: r.repo,
|
|
@@ -21270,8 +21709,9 @@ var SqliteSecurityRepository = class {
|
|
|
21270
21709
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21271
21710
|
// capped at `limit`.
|
|
21272
21711
|
recentlyResolved(limit = 20) {
|
|
21273
|
-
const rows =
|
|
21274
|
-
|
|
21712
|
+
const rows = allRows(
|
|
21713
|
+
this.db.prepare(
|
|
21714
|
+
`SELECT f.finding_key AS finding_key,
|
|
21275
21715
|
f.rule_id AS rule_id,
|
|
21276
21716
|
f.severity AS severity,
|
|
21277
21717
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21305,7 +21745,9 @@ var SqliteSecurityRepository = class {
|
|
|
21305
21745
|
) IS NOT NULL
|
|
21306
21746
|
ORDER BY latest_resolved_at DESC
|
|
21307
21747
|
LIMIT :limit`
|
|
21308
|
-
|
|
21748
|
+
),
|
|
21749
|
+
{ limit }
|
|
21750
|
+
);
|
|
21309
21751
|
const items = rows.map((r) => ({
|
|
21310
21752
|
findingKey: r.finding_key,
|
|
21311
21753
|
ruleId: r.rule_id,
|
|
@@ -21322,12 +21764,15 @@ var SqliteSecurityRepository = class {
|
|
|
21322
21764
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21323
21765
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21324
21766
|
findingsInRange(fromMs, toMs) {
|
|
21325
|
-
const rows =
|
|
21326
|
-
|
|
21767
|
+
const rows = allRows(
|
|
21768
|
+
this.db.prepare(
|
|
21769
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21327
21770
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21328
21771
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21329
21772
|
ORDER BY e.occurred_at`
|
|
21330
|
-
|
|
21773
|
+
),
|
|
21774
|
+
{ from: fromMs, to: toMs }
|
|
21775
|
+
);
|
|
21331
21776
|
return rows.map((r) => ({
|
|
21332
21777
|
occurredAt: r.occurred_at,
|
|
21333
21778
|
severity: r.severity,
|
|
@@ -21341,12 +21786,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21341
21786
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21342
21787
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21343
21788
|
function parseNetwork(networkJson) {
|
|
21344
|
-
|
|
21345
|
-
try {
|
|
21346
|
-
return JSON.parse(networkJson);
|
|
21347
|
-
} catch {
|
|
21348
|
-
return null;
|
|
21349
|
-
}
|
|
21789
|
+
return safeJson(networkJson, null);
|
|
21350
21790
|
}
|
|
21351
21791
|
function toEndpointSummary(row) {
|
|
21352
21792
|
return {
|
|
@@ -21433,27 +21873,39 @@ var SqliteSharesRepository = class {
|
|
|
21433
21873
|
}
|
|
21434
21874
|
db;
|
|
21435
21875
|
stats() {
|
|
21436
|
-
const
|
|
21437
|
-
const
|
|
21438
|
-
const
|
|
21439
|
-
const
|
|
21440
|
-
|
|
21876
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
21877
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
21878
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
21879
|
+
const insecure = countScalar(
|
|
21880
|
+
this.db,
|
|
21441
21881
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21442
21882
|
);
|
|
21443
|
-
const needsReview =
|
|
21883
|
+
const needsReview = countScalar(
|
|
21884
|
+
this.db,
|
|
21444
21885
|
`SELECT count(DISTINCT d.id) AS n
|
|
21445
21886
|
FROM share_destination d
|
|
21446
21887
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21447
21888
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21448
21889
|
);
|
|
21449
|
-
const
|
|
21450
|
-
|
|
21451
|
-
|
|
21452
|
-
|
|
21453
|
-
const
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
|
|
21890
|
+
const kindCounts = countBy(
|
|
21891
|
+
this.db,
|
|
21892
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
21893
|
+
);
|
|
21894
|
+
const byKind = {
|
|
21895
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
21896
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
21897
|
+
ip: kindCounts.get("ip") ?? 0
|
|
21898
|
+
};
|
|
21899
|
+
const trustCounts = countBy(
|
|
21900
|
+
this.db,
|
|
21901
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
21902
|
+
);
|
|
21903
|
+
const byTrust = {
|
|
21904
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
21905
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
21906
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
21907
|
+
ip: trustCounts.get("ip") ?? 0
|
|
21908
|
+
};
|
|
21457
21909
|
return Promise.resolve({
|
|
21458
21910
|
destinations,
|
|
21459
21911
|
endpoints,
|
|
@@ -21569,7 +22021,7 @@ var SqliteSharesRepository = class {
|
|
|
21569
22021
|
}
|
|
21570
22022
|
let sql;
|
|
21571
22023
|
if (q) {
|
|
21572
|
-
const pattern =
|
|
22024
|
+
const pattern = containsPattern(q);
|
|
21573
22025
|
conditions.push(
|
|
21574
22026
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21575
22027
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21589,24 +22041,31 @@ var SqliteSharesRepository = class {
|
|
|
21589
22041
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21590
22042
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21591
22043
|
}
|
|
21592
|
-
const rows =
|
|
22044
|
+
const rows = allRows(
|
|
22045
|
+
this.db.prepare(sql),
|
|
22046
|
+
params
|
|
22047
|
+
);
|
|
21593
22048
|
return rows.map((r) => this.mapDestRow(r));
|
|
21594
22049
|
}
|
|
21595
22050
|
fetchDestinationById(destinationId) {
|
|
21596
|
-
const row =
|
|
21597
|
-
|
|
22051
|
+
const row = getRow(
|
|
22052
|
+
this.db.prepare(
|
|
22053
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21598
22054
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21599
22055
|
o.decision AS overrideDecision
|
|
21600
22056
|
FROM share_destination d
|
|
21601
22057
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21602
22058
|
WHERE d.id = ?`
|
|
21603
|
-
|
|
22059
|
+
),
|
|
22060
|
+
[destinationId]
|
|
22061
|
+
);
|
|
21604
22062
|
return row ? this.mapDestRow(row) : null;
|
|
21605
22063
|
}
|
|
21606
22064
|
fetchEndpoints(destinationIds) {
|
|
21607
22065
|
if (destinationIds.length === 0) return [];
|
|
21608
|
-
const rows =
|
|
21609
|
-
|
|
22066
|
+
const rows = allRows(
|
|
22067
|
+
this.db.prepare(
|
|
22068
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21610
22069
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21611
22070
|
count(c.id) AS callSiteCount
|
|
21612
22071
|
FROM share_endpoint e
|
|
@@ -21614,7 +22073,9 @@ var SqliteSharesRepository = class {
|
|
|
21614
22073
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21615
22074
|
GROUP BY e.id
|
|
21616
22075
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21617
|
-
|
|
22076
|
+
),
|
|
22077
|
+
destinationIds
|
|
22078
|
+
);
|
|
21618
22079
|
return rows.map((r) => ({
|
|
21619
22080
|
id: r.id,
|
|
21620
22081
|
destinationId: r.destinationId,
|
|
@@ -21639,13 +22100,16 @@ var SqliteSharesRepository = class {
|
|
|
21639
22100
|
}
|
|
21640
22101
|
fetchCallSites(endpointIds) {
|
|
21641
22102
|
if (endpointIds.length === 0) return [];
|
|
21642
|
-
const rows =
|
|
21643
|
-
|
|
22103
|
+
const rows = allRows(
|
|
22104
|
+
this.db.prepare(
|
|
22105
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21644
22106
|
project_id AS projectId
|
|
21645
22107
|
FROM share_call_site
|
|
21646
22108
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21647
22109
|
ORDER BY created_at ASC, id ASC`
|
|
21648
|
-
|
|
22110
|
+
),
|
|
22111
|
+
endpointIds
|
|
22112
|
+
);
|
|
21649
22113
|
return rows.map((r) => ({
|
|
21650
22114
|
id: r.id,
|
|
21651
22115
|
endpointId: r.endpointId,
|
|
@@ -21681,27 +22145,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21681
22145
|
upsert(input, now = Date.now()) {
|
|
21682
22146
|
const id = sourceProjectId(input.url);
|
|
21683
22147
|
const row = toSourceProjectRow(input, id, now);
|
|
21684
|
-
this.upsertStmt.run(
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
22148
|
+
this.upsertStmt.run(
|
|
22149
|
+
bindParams({
|
|
22150
|
+
id: row.id,
|
|
22151
|
+
url: row.url,
|
|
22152
|
+
name: row.name,
|
|
22153
|
+
attributes: row.attributes,
|
|
22154
|
+
firstSeen: row.firstSeen,
|
|
22155
|
+
lastSeen: row.lastSeen
|
|
22156
|
+
})
|
|
22157
|
+
);
|
|
21692
22158
|
return id;
|
|
21693
22159
|
}
|
|
21694
22160
|
findById(id) {
|
|
21695
|
-
return
|
|
22161
|
+
return getRow(
|
|
22162
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22163
|
+
{
|
|
22164
|
+
id
|
|
22165
|
+
}
|
|
22166
|
+
);
|
|
21696
22167
|
}
|
|
21697
22168
|
// Distinct project names — a filter facet, served from the source_project
|
|
21698
22169
|
// table, never from the audit fact table.
|
|
21699
22170
|
distinctNames() {
|
|
21700
|
-
const rows =
|
|
21701
|
-
|
|
22171
|
+
const rows = allRows(
|
|
22172
|
+
this.db.prepare(
|
|
22173
|
+
`SELECT DISTINCT name FROM source_project
|
|
21702
22174
|
WHERE name IS NOT NULL
|
|
21703
22175
|
ORDER BY name`
|
|
21704
|
-
|
|
22176
|
+
)
|
|
22177
|
+
);
|
|
21705
22178
|
return rows.map((r) => r.name);
|
|
21706
22179
|
}
|
|
21707
22180
|
};
|
|
@@ -21720,8 +22193,7 @@ function hasLegacySampleRows(db) {
|
|
|
21720
22193
|
function purgeSampleData(db) {
|
|
21721
22194
|
try {
|
|
21722
22195
|
if (!hasLegacySampleRows(db)) return;
|
|
21723
|
-
db
|
|
21724
|
-
try {
|
|
22196
|
+
withTransaction(db, () => {
|
|
21725
22197
|
db.exec(
|
|
21726
22198
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21727
22199
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21766,11 +22238,7 @@ function purgeSampleData(db) {
|
|
|
21766
22238
|
value TEXT NOT NULL
|
|
21767
22239
|
)`);
|
|
21768
22240
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21769
|
-
|
|
21770
|
-
} catch (err) {
|
|
21771
|
-
db.exec("ROLLBACK");
|
|
21772
|
-
throw err;
|
|
21773
|
-
}
|
|
22241
|
+
});
|
|
21774
22242
|
} catch {
|
|
21775
22243
|
}
|
|
21776
22244
|
}
|
|
@@ -21789,7 +22257,7 @@ function openWithPragmas(file2) {
|
|
|
21789
22257
|
function backupLegacyStore(file2) {
|
|
21790
22258
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21791
22259
|
renameSync(file2, backup);
|
|
21792
|
-
for (const sidecar of
|
|
22260
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21793
22261
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21794
22262
|
}
|
|
21795
22263
|
return backup;
|
|
@@ -21802,9 +22270,8 @@ function openLocalDatabase(dir) {
|
|
|
21802
22270
|
db.close();
|
|
21803
22271
|
const backup = backupLegacyStore(file2);
|
|
21804
22272
|
db = openWithPragmas(file2);
|
|
21805
|
-
|
|
21806
|
-
`
|
|
21807
|
-
`
|
|
22273
|
+
akaWarn(
|
|
22274
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21808
22275
|
);
|
|
21809
22276
|
}
|
|
21810
22277
|
applyMigrations(db);
|
|
@@ -21832,96 +22299,77 @@ function openLocalDatabase(dir) {
|
|
|
21832
22299
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21833
22300
|
policies.seedDefaults();
|
|
21834
22301
|
function recordCapture(event, detected) {
|
|
21835
|
-
|
|
21836
|
-
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21841
|
-
db.exec("COMMIT");
|
|
21842
|
-
} catch (err) {
|
|
21843
|
-
db.exec("ROLLBACK");
|
|
21844
|
-
throw err;
|
|
21845
|
-
}
|
|
21846
|
-
} catch {
|
|
21847
|
-
}
|
|
22302
|
+
failOpenTransaction(db, () => {
|
|
22303
|
+
events.insertEvent(event);
|
|
22304
|
+
const sessionId = event.metadata?.sessionId;
|
|
22305
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22306
|
+
});
|
|
21848
22307
|
}
|
|
21849
22308
|
function ensureInventory(ctx) {
|
|
21850
22309
|
const resolved = {};
|
|
21851
|
-
|
|
21852
|
-
|
|
21853
|
-
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
db.exec("COMMIT");
|
|
21872
|
-
} catch (err) {
|
|
21873
|
-
db.exec("ROLLBACK");
|
|
21874
|
-
throw err;
|
|
21875
|
-
}
|
|
21876
|
-
} catch {
|
|
21877
|
-
return {};
|
|
21878
|
-
}
|
|
21879
|
-
return resolved;
|
|
22310
|
+
const committed = failOpenTransaction(db, () => {
|
|
22311
|
+
const now = Date.now();
|
|
22312
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22313
|
+
if (ctx.harness) {
|
|
22314
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22315
|
+
}
|
|
22316
|
+
resolved.accountId = inventory.upsert(
|
|
22317
|
+
linkHost(
|
|
22318
|
+
{
|
|
22319
|
+
objectType: "user",
|
|
22320
|
+
identityKey: "local",
|
|
22321
|
+
attributes: { source: "local" }
|
|
22322
|
+
},
|
|
22323
|
+
resolved.hostId
|
|
22324
|
+
),
|
|
22325
|
+
now
|
|
22326
|
+
);
|
|
22327
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22328
|
+
});
|
|
22329
|
+
return committed ? resolved : {};
|
|
21880
22330
|
}
|
|
21881
22331
|
function recordConfigScan(record2) {
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
}
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
confidence: finding.confidence
|
|
21903
|
-
});
|
|
21904
|
-
}
|
|
21905
|
-
db.exec("COMMIT");
|
|
21906
|
-
} catch (err) {
|
|
21907
|
-
db.exec("ROLLBACK");
|
|
21908
|
-
throw err;
|
|
22332
|
+
failOpenTransaction(db, () => {
|
|
22333
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22334
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22335
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22336
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22337
|
+
for (const def of record2.definitions ?? []) {
|
|
22338
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22339
|
+
}
|
|
22340
|
+
for (const finding of record2.findings ?? []) {
|
|
22341
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22342
|
+
if (!definitionId) continue;
|
|
22343
|
+
inspectionFindings.insertFinding({
|
|
22344
|
+
id: randomUUID8(),
|
|
22345
|
+
auditEventId: record2.scanEvent.id,
|
|
22346
|
+
inspectionDefinitionId: definitionId,
|
|
22347
|
+
span: finding.span,
|
|
22348
|
+
maskedMatch: finding.maskedMatch,
|
|
22349
|
+
actionTaken: finding.actionTaken,
|
|
22350
|
+
confidence: finding.confidence
|
|
22351
|
+
});
|
|
21909
22352
|
}
|
|
21910
|
-
}
|
|
21911
|
-
}
|
|
22353
|
+
});
|
|
21912
22354
|
}
|
|
21913
22355
|
function recordProjectFiles(projectId, scan2) {
|
|
21914
22356
|
if (scan2.files.length === 0) return;
|
|
22357
|
+
failOpenTransaction(db, () => {
|
|
22358
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22359
|
+
});
|
|
22360
|
+
}
|
|
22361
|
+
async function transaction(fn) {
|
|
22362
|
+
db.exec("BEGIN");
|
|
21915
22363
|
try {
|
|
21916
|
-
|
|
22364
|
+
const result = await fn();
|
|
22365
|
+
db.exec("COMMIT");
|
|
22366
|
+
return result;
|
|
22367
|
+
} catch (err) {
|
|
21917
22368
|
try {
|
|
21918
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21919
|
-
db.exec("COMMIT");
|
|
21920
|
-
} catch (err) {
|
|
21921
22369
|
db.exec("ROLLBACK");
|
|
21922
|
-
|
|
22370
|
+
} catch {
|
|
21923
22371
|
}
|
|
21924
|
-
|
|
22372
|
+
throw err;
|
|
21925
22373
|
}
|
|
21926
22374
|
}
|
|
21927
22375
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -21940,8 +22388,7 @@ function openLocalDatabase(dir) {
|
|
|
21940
22388
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
21941
22389
|
});
|
|
21942
22390
|
if (stale.length === 0) return;
|
|
21943
|
-
db
|
|
21944
|
-
try {
|
|
22391
|
+
withTransaction(db, () => {
|
|
21945
22392
|
for (const { id } of stale) {
|
|
21946
22393
|
db.prepare(
|
|
21947
22394
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -21953,11 +22400,7 @@ function openLocalDatabase(dir) {
|
|
|
21953
22400
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
21954
22401
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
21955
22402
|
}
|
|
21956
|
-
|
|
21957
|
-
} catch (err) {
|
|
21958
|
-
db.exec("ROLLBACK");
|
|
21959
|
-
throw err;
|
|
21960
|
-
}
|
|
22403
|
+
});
|
|
21961
22404
|
} catch {
|
|
21962
22405
|
}
|
|
21963
22406
|
}
|
|
@@ -21999,6 +22442,7 @@ function openLocalDatabase(dir) {
|
|
|
21999
22442
|
purgeSampleData: () => {
|
|
22000
22443
|
purgeSampleData(db);
|
|
22001
22444
|
},
|
|
22445
|
+
transaction,
|
|
22002
22446
|
close: () => {
|
|
22003
22447
|
db.close();
|
|
22004
22448
|
}
|
|
@@ -22091,12 +22535,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22091
22535
|
}
|
|
22092
22536
|
}
|
|
22093
22537
|
function readJson(file2) {
|
|
22538
|
+
let text;
|
|
22094
22539
|
try {
|
|
22095
|
-
|
|
22096
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22540
|
+
text = readFileSync2(file2, "utf8");
|
|
22097
22541
|
} catch {
|
|
22098
22542
|
return null;
|
|
22099
22543
|
}
|
|
22544
|
+
return parseJsonObject(text) ?? null;
|
|
22545
|
+
}
|
|
22546
|
+
|
|
22547
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22548
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22549
|
+
import { join as join5 } from "path";
|
|
22550
|
+
var MARKER = "warn-era-capped";
|
|
22551
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22552
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22553
|
+
const marker = join5(dataDir2, MARKER);
|
|
22554
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22555
|
+
const capped = db.policies.capCategoryActions();
|
|
22556
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22557
|
+
`, { mode: DATA_FILE_MODE });
|
|
22558
|
+
return { capped };
|
|
22100
22559
|
}
|
|
22101
22560
|
|
|
22102
22561
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22171,7 +22630,7 @@ function resolveProviderSafe() {
|
|
|
22171
22630
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22172
22631
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22173
22632
|
import { homedir as homedir2 } from "os";
|
|
22174
|
-
import { basename as basename2, join as
|
|
22633
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22175
22634
|
|
|
22176
22635
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22177
22636
|
var KeywordMatcher2 = class {
|
|
@@ -24305,8 +24764,8 @@ function bundledDetections() {
|
|
|
24305
24764
|
}
|
|
24306
24765
|
|
|
24307
24766
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24308
|
-
import { existsSync as
|
|
24309
|
-
import { basename, dirname, isAbsolute, join as
|
|
24767
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
24768
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24310
24769
|
|
|
24311
24770
|
// ../../packages/plugin-sdk/src/events.ts
|
|
24312
24771
|
import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
|
|
@@ -24318,20 +24777,23 @@ import { createHash as createHash4 } from "crypto";
|
|
|
24318
24777
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
24319
24778
|
|
|
24320
24779
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24321
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24322
|
-
import { join as
|
|
24780
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
24781
|
+
import { join as join8 } from "path";
|
|
24323
24782
|
|
|
24324
24783
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24325
24784
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24326
|
-
import { existsSync as
|
|
24327
|
-
import { basename as basename3, join as
|
|
24785
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
24786
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
24328
24787
|
|
|
24329
24788
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24330
24789
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
24331
24790
|
|
|
24791
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
24792
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
24793
|
+
|
|
24332
24794
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
24333
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
24334
|
-
import { join as
|
|
24795
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
24796
|
+
import { join as join10 } from "path";
|
|
24335
24797
|
|
|
24336
24798
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
24337
24799
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -24524,6 +24986,14 @@ var StandaloneDataGateway = class {
|
|
|
24524
24986
|
sweepTerminalExceptions(retentionMs) {
|
|
24525
24987
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
24526
24988
|
}
|
|
24989
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
24990
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
24991
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
24992
|
+
// already-capped one).
|
|
24993
|
+
capWarnEraEnforcement(policyMode) {
|
|
24994
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
24995
|
+
return { capped };
|
|
24996
|
+
}
|
|
24527
24997
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
24528
24998
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
24529
24999
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -24714,6 +25184,22 @@ var ADVICE = {
|
|
|
24714
25184
|
code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
|
|
24715
25185
|
custom: "Review against your organization\u2019s custom policy."
|
|
24716
25186
|
};
|
|
25187
|
+
var ACTION_LABEL = {
|
|
25188
|
+
log: "monitor",
|
|
25189
|
+
warn: "warn",
|
|
25190
|
+
redact: "redact",
|
|
25191
|
+
block: "block",
|
|
25192
|
+
allow: "allow"
|
|
25193
|
+
};
|
|
25194
|
+
var CATEGORY_ORDER2 = DetectionCategory.options;
|
|
25195
|
+
function categoryRank(category) {
|
|
25196
|
+
const i = CATEGORY_ORDER2.indexOf(category);
|
|
25197
|
+
return i === -1 ? CATEGORY_ORDER2.length : i;
|
|
25198
|
+
}
|
|
25199
|
+
function renderPosture(rows) {
|
|
25200
|
+
const width = Math.max(0, ...rows.map((r) => r.category.length));
|
|
25201
|
+
return [...rows].sort((a, b) => categoryRank(a.category) - categoryRank(b.category)).map((r) => ` ${r.category.padEnd(width)} ${ACTION_LABEL[r.action] ?? r.action}`).join("\n");
|
|
25202
|
+
}
|
|
24717
25203
|
var RULE_WIDTH = 64;
|
|
24718
25204
|
function healthScore(summary) {
|
|
24719
25205
|
const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
|
|
@@ -24728,29 +25214,27 @@ function topFindings(findings, limit = 10) {
|
|
|
24728
25214
|
}
|
|
24729
25215
|
function renderFirstRun(s) {
|
|
24730
25216
|
const heading = "\u2713 AKA Security installed";
|
|
24731
|
-
const details = defList([
|
|
24732
|
-
["Commands", s.commands.join(" \xB7 ")],
|
|
24733
|
-
["Handling", s.handling]
|
|
24734
|
-
]);
|
|
25217
|
+
const details = defList([["Commands", s.commands.join(" \xB7 ")]]);
|
|
24735
25218
|
const stats = `Health ${String(s.health)}/100 Findings ${String(s.findings)} Recommendations ${String(s.recommendations)}`;
|
|
24736
|
-
const lines = [
|
|
24737
|
-
|
|
24738
|
-
"",
|
|
24739
|
-
|
|
25219
|
+
const lines = [heading, "", indent(details)];
|
|
25220
|
+
if (s.posture !== void 0 && s.posture.length > 0) {
|
|
25221
|
+
lines.push("", indent("Posture"), "", indent(s.posture));
|
|
25222
|
+
}
|
|
25223
|
+
lines.push(
|
|
24740
25224
|
"",
|
|
24741
25225
|
indent("\u2500".repeat(RULE_WIDTH)),
|
|
24742
25226
|
"",
|
|
24743
25227
|
indent("First scan complete"),
|
|
24744
25228
|
"",
|
|
24745
25229
|
indent(stats)
|
|
24746
|
-
|
|
25230
|
+
);
|
|
24747
25231
|
const top = s.topFindings ?? [];
|
|
24748
25232
|
if (top.length > 0) {
|
|
24749
25233
|
const rows = top.map((f) => [
|
|
24750
25234
|
`${severityGlyph(f.severity)} ${f.severity}`,
|
|
24751
25235
|
f.category,
|
|
24752
25236
|
f.ruleId,
|
|
24753
|
-
f.actionTaken,
|
|
25237
|
+
toApiAction(f.actionTaken),
|
|
24754
25238
|
f.maskedMatch
|
|
24755
25239
|
]);
|
|
24756
25240
|
lines.push(
|
|
@@ -24808,11 +25292,24 @@ function buildRecommendations(findings) {
|
|
|
24808
25292
|
});
|
|
24809
25293
|
}
|
|
24810
25294
|
|
|
25295
|
+
// src/posture.ts
|
|
25296
|
+
async function readPostureBlock(db) {
|
|
25297
|
+
try {
|
|
25298
|
+
const policies = await db.policies.readPolicies();
|
|
25299
|
+
return renderPosture(
|
|
25300
|
+
policies.map((p) => ({
|
|
25301
|
+
category: p.target.category ?? "",
|
|
25302
|
+
action: p.action
|
|
25303
|
+
})).filter((r) => r.category !== "")
|
|
25304
|
+
);
|
|
25305
|
+
} catch {
|
|
25306
|
+
return "";
|
|
25307
|
+
} finally {
|
|
25308
|
+
db.close();
|
|
25309
|
+
}
|
|
25310
|
+
}
|
|
25311
|
+
|
|
24811
25312
|
// src/firstrun.ts
|
|
24812
|
-
var HANDLING = {
|
|
24813
|
-
redact: "Active redaction enabled",
|
|
24814
|
-
warn: "Warn-only enabled"
|
|
24815
|
-
};
|
|
24816
25313
|
var COMMANDS = ["/health", "/recommend", "/findings", "/audit"];
|
|
24817
25314
|
try {
|
|
24818
25315
|
const cfg = loadConfig();
|
|
@@ -24823,11 +25320,12 @@ try {
|
|
|
24823
25320
|
gateway.recentFindings({ limit: 500 })
|
|
24824
25321
|
]);
|
|
24825
25322
|
const recommendations = buildRecommendations(findings).length;
|
|
25323
|
+
const postureBlock = await readPostureBlock(openLocalDatabase(cfg.dataDir));
|
|
24826
25324
|
process.stdout.write(
|
|
24827
25325
|
`${fenced(
|
|
24828
25326
|
renderFirstRun({
|
|
24829
25327
|
commands: COMMANDS,
|
|
24830
|
-
|
|
25328
|
+
posture: postureBlock,
|
|
24831
25329
|
health: healthScore(summary),
|
|
24832
25330
|
findings: summary.findings,
|
|
24833
25331
|
recommendations,
|