@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/backfill.js
CHANGED
|
@@ -491,6 +491,9 @@ var require_ignore = __commonJS({
|
|
|
491
491
|
}
|
|
492
492
|
});
|
|
493
493
|
|
|
494
|
+
// src/backfill.ts
|
|
495
|
+
import { fileURLToPath } from "url";
|
|
496
|
+
|
|
494
497
|
// ../../packages/persistence/src/database.ts
|
|
495
498
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
499
|
import { existsSync, renameSync, rmSync } from "fs";
|
|
@@ -15430,6 +15433,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15430
15433
|
"prompt",
|
|
15431
15434
|
"response",
|
|
15432
15435
|
"code_change",
|
|
15436
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15437
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15438
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15439
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15440
|
+
"tool_use",
|
|
15433
15441
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15434
15442
|
// fact the posture inspection findings reference (findings require an
|
|
15435
15443
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15821,7 +15829,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15821
15829
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15822
15830
|
|
|
15823
15831
|
// ../../packages/schema/src/zod/event.ts
|
|
15824
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15832
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15825
15833
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15826
15834
|
var EventMetadata = external_exports.object({
|
|
15827
15835
|
sessionId: external_exports.string().optional(),
|
|
@@ -16134,7 +16142,7 @@ var DetectionException = external_exports.object({
|
|
|
16134
16142
|
justification: external_exports.string().min(1),
|
|
16135
16143
|
conditions: ExceptionConditions.nullable(),
|
|
16136
16144
|
createdBy: external_exports.string(),
|
|
16137
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16145
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16138
16146
|
createdAt: external_exports.iso.datetime(),
|
|
16139
16147
|
updatedAt: external_exports.iso.datetime(),
|
|
16140
16148
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16303,20 +16311,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16303
16311
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16304
16312
|
fetchedAt: external_exports.iso.datetime()
|
|
16305
16313
|
}).meta({ id: "PolicyBundle" });
|
|
16306
|
-
var DEFAULT_ACTIONS = {
|
|
16307
|
-
secret: "block",
|
|
16308
|
-
pii: "redact",
|
|
16309
|
-
financial: "redact",
|
|
16310
|
-
phi: "redact",
|
|
16311
|
-
code_context: "warn",
|
|
16312
|
-
code_flaw: "warn",
|
|
16313
|
-
custom: "warn",
|
|
16314
|
-
// Config-posture findings only observe today (they land in
|
|
16315
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16316
|
-
config: "warn"
|
|
16317
|
-
};
|
|
16318
16314
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16319
16315
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16316
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16317
|
+
secret: "critical",
|
|
16318
|
+
financial: "critical",
|
|
16319
|
+
// core-financial/credit-card
|
|
16320
|
+
code_flaw: "critical",
|
|
16321
|
+
pii: "high",
|
|
16322
|
+
phi: "high",
|
|
16323
|
+
custom: "high",
|
|
16324
|
+
// user-defined; conservative
|
|
16325
|
+
code_context: "low",
|
|
16326
|
+
config: "low"
|
|
16327
|
+
// observe-only; floors to monitor regardless
|
|
16328
|
+
};
|
|
16329
|
+
function severityFloorPolicy(category) {
|
|
16330
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16331
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16332
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16333
|
+
}
|
|
16320
16334
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16321
16335
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16322
16336
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16343,6 +16357,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16343
16357
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16344
16358
|
}
|
|
16345
16359
|
};
|
|
16360
|
+
function builtinPolicyToAction(id) {
|
|
16361
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16362
|
+
}
|
|
16363
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16364
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16365
|
+
);
|
|
16346
16366
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16347
16367
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16348
16368
|
);
|
|
@@ -16749,10 +16769,8 @@ function toApiProvider(sourceTool) {
|
|
|
16749
16769
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16750
16770
|
}
|
|
16751
16771
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16752
|
-
function
|
|
16753
|
-
const statuses = new Set(
|
|
16754
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16755
|
-
);
|
|
16772
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16773
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16756
16774
|
if (statuses.size === 0) return void 0;
|
|
16757
16775
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16758
16776
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16770,6 +16788,7 @@ function deriveFindingStatus(row) {
|
|
|
16770
16788
|
function buildFindingGroups(rows, opts = {}) {
|
|
16771
16789
|
const overrides = opts.overrides;
|
|
16772
16790
|
const packNames = opts.packNames;
|
|
16791
|
+
const aggregates = opts.aggregates;
|
|
16773
16792
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16774
16793
|
for (const row of rows) {
|
|
16775
16794
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16791,17 +16810,20 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16791
16810
|
status: r.status
|
|
16792
16811
|
};
|
|
16793
16812
|
});
|
|
16794
|
-
const
|
|
16813
|
+
const agg = aggregates?.get(ruleId);
|
|
16814
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16795
16815
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16796
16816
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16797
16817
|
);
|
|
16798
16818
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16799
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16819
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16800
16820
|
if (seenProviders.has(p)) return false;
|
|
16801
16821
|
seenProviders.add(p);
|
|
16802
16822
|
return true;
|
|
16803
16823
|
});
|
|
16804
|
-
const actionSet = new Set(
|
|
16824
|
+
const actionSet = new Set(
|
|
16825
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16826
|
+
);
|
|
16805
16827
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16806
16828
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16807
16829
|
const detection = {
|
|
@@ -16815,8 +16837,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16815
16837
|
contextPrefix: ""
|
|
16816
16838
|
// empty (pending privacy review)
|
|
16817
16839
|
};
|
|
16818
|
-
const status =
|
|
16819
|
-
|
|
16840
|
+
const status = foldGroupStatus(
|
|
16841
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16842
|
+
);
|
|
16843
|
+
const group = {
|
|
16820
16844
|
id: ruleId,
|
|
16821
16845
|
category: apiCategory,
|
|
16822
16846
|
subtype: ruleId,
|
|
@@ -16825,21 +16849,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16825
16849
|
match,
|
|
16826
16850
|
detection,
|
|
16827
16851
|
policy,
|
|
16828
|
-
instanceCount: instances.length,
|
|
16852
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16829
16853
|
providers,
|
|
16830
16854
|
aggregateAction,
|
|
16831
16855
|
latestDetectedAt,
|
|
16832
16856
|
instances,
|
|
16833
16857
|
status
|
|
16834
|
-
}
|
|
16858
|
+
};
|
|
16859
|
+
if (agg) {
|
|
16860
|
+
actionsCache.set(group, [...actionSet]);
|
|
16861
|
+
if (agg.searchText !== void 0) {
|
|
16862
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16863
|
+
}
|
|
16864
|
+
}
|
|
16865
|
+
groups.push(group);
|
|
16835
16866
|
}
|
|
16836
16867
|
return groups;
|
|
16837
16868
|
}
|
|
16838
16869
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16839
|
-
function
|
|
16840
|
-
|
|
16841
|
-
if (cached2 !== void 0) return cached2;
|
|
16842
|
-
const haystack = [
|
|
16870
|
+
function buildHaystack(g, extra) {
|
|
16871
|
+
return [
|
|
16843
16872
|
g.subtype,
|
|
16844
16873
|
g.category,
|
|
16845
16874
|
g.match.maskedValue,
|
|
@@ -16847,11 +16876,25 @@ function groupHaystack(g) {
|
|
|
16847
16876
|
g.id,
|
|
16848
16877
|
...g.instances.map((i) => i.repo),
|
|
16849
16878
|
...g.instances.map((i) => i.file),
|
|
16850
|
-
...g.instances.map((i) => i.id)
|
|
16879
|
+
...g.instances.map((i) => i.id),
|
|
16880
|
+
...extra === void 0 ? [] : [extra]
|
|
16851
16881
|
].join(" ").toLowerCase();
|
|
16882
|
+
}
|
|
16883
|
+
function groupHaystack(g) {
|
|
16884
|
+
const cached2 = haystackCache.get(g);
|
|
16885
|
+
if (cached2 !== void 0) return cached2;
|
|
16886
|
+
const haystack = buildHaystack(g);
|
|
16852
16887
|
haystackCache.set(g, haystack);
|
|
16853
16888
|
return haystack;
|
|
16854
16889
|
}
|
|
16890
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16891
|
+
function groupActions(g) {
|
|
16892
|
+
const cached2 = actionsCache.get(g);
|
|
16893
|
+
if (cached2 !== void 0) return cached2;
|
|
16894
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16895
|
+
actionsCache.set(g, actions);
|
|
16896
|
+
return actions;
|
|
16897
|
+
}
|
|
16855
16898
|
function applyFindingFilters(groups, opts) {
|
|
16856
16899
|
let filtered = groups;
|
|
16857
16900
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16864,7 +16907,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16864
16907
|
}
|
|
16865
16908
|
if (opts.actions && opts.actions.length > 0) {
|
|
16866
16909
|
const actionSet = new Set(opts.actions);
|
|
16867
|
-
filtered = filtered.filter((g) => g.
|
|
16910
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16868
16911
|
}
|
|
16869
16912
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16870
16913
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16916,8 +16959,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16916
16959
|
});
|
|
16917
16960
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16918
16961
|
for (const g of forAction) {
|
|
16919
|
-
const
|
|
16920
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16962
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16921
16963
|
}
|
|
16922
16964
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16923
16965
|
providers: opts.providers,
|
|
@@ -17479,6 +17521,132 @@ function reviewSeverityRank(reasons) {
|
|
|
17479
17521
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17480
17522
|
}
|
|
17481
17523
|
|
|
17524
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17525
|
+
var TriageHit = external_exports.object({
|
|
17526
|
+
ruleId: external_exports.string(),
|
|
17527
|
+
category: DetectionCategory,
|
|
17528
|
+
severity: Severity,
|
|
17529
|
+
maskedMatch: external_exports.string(),
|
|
17530
|
+
rawMatch: external_exports.string(),
|
|
17531
|
+
context: external_exports.string(),
|
|
17532
|
+
filePath: external_exports.string().optional(),
|
|
17533
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17534
|
+
id: external_exports.string().optional(),
|
|
17535
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17536
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17537
|
+
});
|
|
17538
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17539
|
+
var TriageCategoryRec = external_exports.object({
|
|
17540
|
+
category: DetectionCategory,
|
|
17541
|
+
action: TriagePolicy,
|
|
17542
|
+
reasoning: external_exports.string(),
|
|
17543
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17544
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17545
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17546
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17547
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17548
|
+
});
|
|
17549
|
+
var TriageRecommendation = external_exports.object({
|
|
17550
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17551
|
+
notes: external_exports.string()
|
|
17552
|
+
});
|
|
17553
|
+
|
|
17554
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17555
|
+
function escapeLikePattern(s) {
|
|
17556
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17557
|
+
}
|
|
17558
|
+
function placeholders(n) {
|
|
17559
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17560
|
+
}
|
|
17561
|
+
function containsPattern(q) {
|
|
17562
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17563
|
+
}
|
|
17564
|
+
function likeAny(exprs) {
|
|
17565
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17566
|
+
}
|
|
17567
|
+
|
|
17568
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17569
|
+
var savepointSeq = 0;
|
|
17570
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17571
|
+
if (db.isTransaction) {
|
|
17572
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17573
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17574
|
+
try {
|
|
17575
|
+
fn();
|
|
17576
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17577
|
+
} catch (error51) {
|
|
17578
|
+
try {
|
|
17579
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17580
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17581
|
+
} catch {
|
|
17582
|
+
}
|
|
17583
|
+
throw error51;
|
|
17584
|
+
}
|
|
17585
|
+
return;
|
|
17586
|
+
}
|
|
17587
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17588
|
+
try {
|
|
17589
|
+
fn();
|
|
17590
|
+
db.exec("COMMIT");
|
|
17591
|
+
} catch (error51) {
|
|
17592
|
+
try {
|
|
17593
|
+
db.exec("ROLLBACK");
|
|
17594
|
+
} catch {
|
|
17595
|
+
}
|
|
17596
|
+
throw error51;
|
|
17597
|
+
}
|
|
17598
|
+
}
|
|
17599
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17600
|
+
const nested = db.isTransaction;
|
|
17601
|
+
try {
|
|
17602
|
+
withTransaction(db, fn, mode);
|
|
17603
|
+
return true;
|
|
17604
|
+
} catch (error51) {
|
|
17605
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17606
|
+
return false;
|
|
17607
|
+
}
|
|
17608
|
+
}
|
|
17609
|
+
|
|
17610
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17611
|
+
function akaWarn(message) {
|
|
17612
|
+
process.stderr.write(`[aka] ${message}
|
|
17613
|
+
`);
|
|
17614
|
+
}
|
|
17615
|
+
|
|
17616
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17617
|
+
function evidenceObjects(sql) {
|
|
17618
|
+
const objects = [];
|
|
17619
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17620
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17621
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17622
|
+
}
|
|
17623
|
+
}
|
|
17624
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17625
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17626
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17627
|
+
}
|
|
17628
|
+
}
|
|
17629
|
+
return objects;
|
|
17630
|
+
}
|
|
17631
|
+
function schemaObjectExists(db, kind, name) {
|
|
17632
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17633
|
+
return row !== void 0;
|
|
17634
|
+
}
|
|
17635
|
+
function indexExists(db, name) {
|
|
17636
|
+
return schemaObjectExists(db, "index", name);
|
|
17637
|
+
}
|
|
17638
|
+
function columnNames(db, table, opts) {
|
|
17639
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17640
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
|
|
17641
|
+
return columns.map((c) => c.name);
|
|
17642
|
+
}
|
|
17643
|
+
function evidenceExists(db, object2) {
|
|
17644
|
+
if (object2.kind === "column") {
|
|
17645
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17646
|
+
}
|
|
17647
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17648
|
+
}
|
|
17649
|
+
|
|
17482
17650
|
// ../../packages/persistence/src/ids.ts
|
|
17483
17651
|
import { createHash } from "crypto";
|
|
17484
17652
|
function sha256Hex(input) {
|
|
@@ -17515,28 +17683,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17515
17683
|
}
|
|
17516
17684
|
|
|
17517
17685
|
// ../../packages/persistence/src/migrations.ts
|
|
17518
|
-
function evidenceObjects(sql) {
|
|
17519
|
-
const objects = [];
|
|
17520
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17521
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17522
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17523
|
-
}
|
|
17524
|
-
}
|
|
17525
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17526
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17527
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17528
|
-
}
|
|
17529
|
-
}
|
|
17530
|
-
return objects;
|
|
17531
|
-
}
|
|
17532
|
-
function evidenceExists(db, object2) {
|
|
17533
|
-
if (object2.kind === "column") {
|
|
17534
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17535
|
-
return columns.some((c) => c.name === object2.name);
|
|
17536
|
-
}
|
|
17537
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17538
|
-
return row !== void 0;
|
|
17539
|
-
}
|
|
17540
17686
|
function describeObject(object2) {
|
|
17541
17687
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17542
17688
|
}
|
|
@@ -17547,10 +17693,6 @@ function createdIndexName(statement) {
|
|
|
17547
17693
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17548
17694
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17549
17695
|
}
|
|
17550
|
-
function indexExists(db, name) {
|
|
17551
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17552
|
-
return row !== void 0;
|
|
17553
|
-
}
|
|
17554
17696
|
function applyMigrations(db) {
|
|
17555
17697
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17556
17698
|
db.exec(
|
|
@@ -17569,44 +17711,39 @@ function applyMigrations(db) {
|
|
|
17569
17711
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17570
17712
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17571
17713
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17572
|
-
const message = `
|
|
17573
|
-
|
|
17574
|
-
`);
|
|
17575
|
-
throw new Error(message);
|
|
17714
|
+
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.`;
|
|
17715
|
+
akaWarn(message);
|
|
17716
|
+
throw new Error(`[aka] ${message}`);
|
|
17576
17717
|
}
|
|
17577
17718
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17578
17719
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17579
17720
|
const statements = splitStatements(migration.sql);
|
|
17580
17721
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17581
17722
|
try {
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
const
|
|
17586
|
-
|
|
17587
|
-
if (
|
|
17588
|
-
|
|
17589
|
-
|
|
17723
|
+
withTransaction(
|
|
17724
|
+
db,
|
|
17725
|
+
() => {
|
|
17726
|
+
for (const statement of statements) {
|
|
17727
|
+
const indexName = createdIndexName(statement);
|
|
17728
|
+
if (indexName === void 0) {
|
|
17729
|
+
if (alreadyApplied) continue;
|
|
17730
|
+
} else if (indexExists(db, indexName)) {
|
|
17731
|
+
continue;
|
|
17732
|
+
}
|
|
17733
|
+
db.exec(statement);
|
|
17590
17734
|
}
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
);
|
|
17735
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17736
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17737
|
+
if (violations.length > 0) {
|
|
17738
|
+
throw new Error(
|
|
17739
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17740
|
+
);
|
|
17741
|
+
}
|
|
17599
17742
|
}
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
try {
|
|
17605
|
-
db.exec("ROLLBACK");
|
|
17606
|
-
} catch {
|
|
17607
|
-
}
|
|
17608
|
-
throw error51;
|
|
17609
|
-
}
|
|
17743
|
+
record2.run(migration.tag, Date.now());
|
|
17744
|
+
},
|
|
17745
|
+
"IMMEDIATE"
|
|
17746
|
+
);
|
|
17610
17747
|
} finally {
|
|
17611
17748
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17612
17749
|
}
|
|
@@ -17649,8 +17786,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17649
17786
|
}
|
|
17650
17787
|
];
|
|
17651
17788
|
function ensureTokenUsageColumns(db) {
|
|
17652
|
-
const
|
|
17653
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17789
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17654
17790
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17655
17791
|
if (!existing.has(column.name)) {
|
|
17656
17792
|
db.exec(column.ddl);
|
|
@@ -17688,47 +17824,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17688
17824
|
repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
|
|
17689
17825
|
}));
|
|
17690
17826
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
dropCollisions
|
|
17705
|
-
|
|
17827
|
+
withTransaction(
|
|
17828
|
+
db,
|
|
17829
|
+
() => {
|
|
17830
|
+
for (const { row, canonicalId } of legacy) {
|
|
17831
|
+
foldProject.run(
|
|
17832
|
+
canonicalId,
|
|
17833
|
+
row.url,
|
|
17834
|
+
row.name,
|
|
17835
|
+
row.attributes,
|
|
17836
|
+
row.firstSeen,
|
|
17837
|
+
row.lastSeen
|
|
17838
|
+
);
|
|
17839
|
+
repointAudit.run(canonicalId, row.id);
|
|
17840
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
17841
|
+
dropCollisions.run(row.id, canonicalId);
|
|
17842
|
+
repoint.run(canonicalId, row.id);
|
|
17843
|
+
}
|
|
17844
|
+
repointCallSite.run(canonicalId, row.id);
|
|
17845
|
+
deleteLegacy.run(row.id);
|
|
17706
17846
|
}
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
db.exec("COMMIT");
|
|
17711
|
-
} catch (error51) {
|
|
17712
|
-
try {
|
|
17713
|
-
db.exec("ROLLBACK");
|
|
17714
|
-
} catch {
|
|
17715
|
-
}
|
|
17716
|
-
throw error51;
|
|
17717
|
-
}
|
|
17847
|
+
},
|
|
17848
|
+
"IMMEDIATE"
|
|
17849
|
+
);
|
|
17718
17850
|
} catch (error51) {
|
|
17719
|
-
|
|
17720
|
-
`);
|
|
17851
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17721
17852
|
}
|
|
17722
17853
|
}
|
|
17723
17854
|
function isForeignSqliteLineage(db) {
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17727
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
17855
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
17856
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17728
17857
|
}
|
|
17729
17858
|
function ensureSyncedAtColumn(db, table) {
|
|
17730
|
-
|
|
17731
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
17859
|
+
if (!columnNames(db, table).includes("synced_at")) {
|
|
17732
17860
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
17733
17861
|
}
|
|
17734
17862
|
}
|
|
@@ -17779,8 +17907,11 @@ function ensureDataDirSync(dir) {
|
|
|
17779
17907
|
} catch {
|
|
17780
17908
|
}
|
|
17781
17909
|
}
|
|
17910
|
+
function walSidecars(file2) {
|
|
17911
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
17912
|
+
}
|
|
17782
17913
|
function tightenPerms(file2) {
|
|
17783
|
-
for (const path of [file2,
|
|
17914
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17784
17915
|
try {
|
|
17785
17916
|
chmodSync(path, DATA_FILE_MODE);
|
|
17786
17917
|
} catch {
|
|
@@ -17788,12 +17919,68 @@ function tightenPerms(file2) {
|
|
|
17788
17919
|
}
|
|
17789
17920
|
}
|
|
17790
17921
|
|
|
17791
|
-
// ../../packages/persistence/src/
|
|
17792
|
-
function
|
|
17793
|
-
|
|
17922
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
17923
|
+
function safeJson(s, fallback) {
|
|
17924
|
+
if (s == null) return fallback;
|
|
17925
|
+
try {
|
|
17926
|
+
return JSON.parse(s);
|
|
17927
|
+
} catch {
|
|
17928
|
+
return fallback;
|
|
17929
|
+
}
|
|
17794
17930
|
}
|
|
17795
|
-
function
|
|
17796
|
-
|
|
17931
|
+
function parseJsonObject(s) {
|
|
17932
|
+
if (s == null) return void 0;
|
|
17933
|
+
try {
|
|
17934
|
+
const parsed = JSON.parse(s);
|
|
17935
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
17936
|
+
} catch {
|
|
17937
|
+
}
|
|
17938
|
+
return void 0;
|
|
17939
|
+
}
|
|
17940
|
+
|
|
17941
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
17942
|
+
function allRows(stmt, params) {
|
|
17943
|
+
if (params === void 0) return stmt.all();
|
|
17944
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
17945
|
+
return stmt.all(params);
|
|
17946
|
+
}
|
|
17947
|
+
function getRow(stmt, params) {
|
|
17948
|
+
if (params === void 0) return stmt.get();
|
|
17949
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
17950
|
+
return stmt.get(params);
|
|
17951
|
+
}
|
|
17952
|
+
function intToBool(raw) {
|
|
17953
|
+
return raw === 1 || raw === true;
|
|
17954
|
+
}
|
|
17955
|
+
function boolToInt(b) {
|
|
17956
|
+
return b ? 1 : 0;
|
|
17957
|
+
}
|
|
17958
|
+
function bindParams(row) {
|
|
17959
|
+
const out = {};
|
|
17960
|
+
for (const [key, value] of Object.entries(row)) {
|
|
17961
|
+
out[key] = value === void 0 ? null : value;
|
|
17962
|
+
}
|
|
17963
|
+
return out;
|
|
17964
|
+
}
|
|
17965
|
+
function countScalar(db, sql, params) {
|
|
17966
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
17967
|
+
}
|
|
17968
|
+
function countBy(db, sql, params) {
|
|
17969
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
17970
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
17971
|
+
map2.set(row.k, row.n);
|
|
17972
|
+
}
|
|
17973
|
+
return map2;
|
|
17974
|
+
}
|
|
17975
|
+
function mapRowsTolerant(rows, map2) {
|
|
17976
|
+
const out = [];
|
|
17977
|
+
for (const row of rows) {
|
|
17978
|
+
try {
|
|
17979
|
+
out.push(map2(row));
|
|
17980
|
+
} catch {
|
|
17981
|
+
}
|
|
17982
|
+
}
|
|
17983
|
+
return out;
|
|
17797
17984
|
}
|
|
17798
17985
|
|
|
17799
17986
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17845,15 +18032,11 @@ function encodeCursor(payload) {
|
|
|
17845
18032
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17846
18033
|
}
|
|
17847
18034
|
function decodeCursor(cursor) {
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
return parsed;
|
|
17852
|
-
}
|
|
17853
|
-
return null;
|
|
17854
|
-
} catch {
|
|
17855
|
-
return null;
|
|
18035
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18036
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18037
|
+
return parsed;
|
|
17856
18038
|
}
|
|
18039
|
+
return null;
|
|
17857
18040
|
}
|
|
17858
18041
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17859
18042
|
session: "session",
|
|
@@ -17870,15 +18053,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17870
18053
|
};
|
|
17871
18054
|
function safeParseStringArray(raw) {
|
|
17872
18055
|
if (!raw) return [];
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17876
|
-
} catch {
|
|
17877
|
-
return [];
|
|
17878
|
-
}
|
|
17879
|
-
}
|
|
17880
|
-
function toBool(raw) {
|
|
17881
|
-
return raw === 1 || raw === true;
|
|
18056
|
+
const parsed = safeJson(raw, null);
|
|
18057
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17882
18058
|
}
|
|
17883
18059
|
function toHarness(raw) {
|
|
17884
18060
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17931,8 +18107,8 @@ function buildAuditEvent(row) {
|
|
|
17931
18107
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17932
18108
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17933
18109
|
targetId: row.target_id,
|
|
17934
|
-
internal:
|
|
17935
|
-
flagged:
|
|
18110
|
+
internal: intToBool(row.internal),
|
|
18111
|
+
flagged: intToBool(row.flagged)
|
|
17936
18112
|
};
|
|
17937
18113
|
}
|
|
17938
18114
|
var TIMELINE_COLUMNS = `
|
|
@@ -17958,12 +18134,15 @@ var SqliteActivityRepository = class {
|
|
|
17958
18134
|
stats(tz) {
|
|
17959
18135
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
17960
18136
|
const { startMs, endMs } = window;
|
|
17961
|
-
const sessionsToday =
|
|
18137
|
+
const sessionsToday = countScalar(
|
|
18138
|
+
this.db,
|
|
17962
18139
|
`SELECT count(*) AS n FROM audit_events
|
|
17963
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
17964
|
-
|
|
18140
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
|
|
18141
|
+
[startMs, endMs]
|
|
18142
|
+
);
|
|
17965
18143
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
17966
|
-
const liveNow =
|
|
18144
|
+
const liveNow = countScalar(
|
|
18145
|
+
this.db,
|
|
17967
18146
|
`SELECT count(*) AS n FROM audit_events s
|
|
17968
18147
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
17969
18148
|
AND max(
|
|
@@ -17972,22 +18151,29 @@ var SqliteActivityRepository = class {
|
|
|
17972
18151
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
17973
18152
|
s.started_at
|
|
17974
18153
|
)
|
|
17975
|
-
) >=
|
|
17976
|
-
|
|
17977
|
-
|
|
18154
|
+
) >= ?`,
|
|
18155
|
+
[liveThreshold]
|
|
18156
|
+
);
|
|
18157
|
+
const toolCallsToday = countScalar(
|
|
18158
|
+
this.db,
|
|
17978
18159
|
`SELECT count(*) AS n FROM audit_events
|
|
17979
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
17980
|
-
|
|
17981
|
-
|
|
18160
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18161
|
+
[startMs, endMs]
|
|
18162
|
+
);
|
|
18163
|
+
const findingsToday = countScalar(
|
|
18164
|
+
this.db,
|
|
17982
18165
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
17983
18166
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
17984
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
17985
|
-
|
|
17986
|
-
|
|
18167
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18168
|
+
[startMs, endMs]
|
|
18169
|
+
);
|
|
18170
|
+
const egressToday = countScalar(
|
|
18171
|
+
this.db,
|
|
17987
18172
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
17988
18173
|
FROM audit_events
|
|
17989
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
17990
|
-
|
|
18174
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18175
|
+
[startMs, endMs]
|
|
18176
|
+
);
|
|
17991
18177
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
17992
18178
|
}
|
|
17993
18179
|
listSessions(query) {
|
|
@@ -18009,7 +18195,7 @@ var SqliteActivityRepository = class {
|
|
|
18009
18195
|
conditions.push("started_at <= ?");
|
|
18010
18196
|
params.push(toMs);
|
|
18011
18197
|
if (query.q) {
|
|
18012
|
-
const pattern =
|
|
18198
|
+
const pattern = containsPattern(query.q);
|
|
18013
18199
|
conditions.push(
|
|
18014
18200
|
`(content LIKE ? ESCAPE '\\'
|
|
18015
18201
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18028,8 +18214,9 @@ var SqliteActivityRepository = class {
|
|
|
18028
18214
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18029
18215
|
}
|
|
18030
18216
|
const limit = query.limit;
|
|
18031
|
-
const rows =
|
|
18032
|
-
|
|
18217
|
+
const rows = allRows(
|
|
18218
|
+
this.db.prepare(
|
|
18219
|
+
`SELECT id,
|
|
18033
18220
|
json_extract(attributes, '$.harness') AS harness,
|
|
18034
18221
|
content AS title,
|
|
18035
18222
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18042,7 +18229,9 @@ var SqliteActivityRepository = class {
|
|
|
18042
18229
|
WHERE ${conditions.join(" AND ")}
|
|
18043
18230
|
ORDER BY started_at DESC, id DESC
|
|
18044
18231
|
LIMIT ?`
|
|
18045
|
-
|
|
18232
|
+
),
|
|
18233
|
+
[...params, limit + 1]
|
|
18234
|
+
);
|
|
18046
18235
|
const hasMore = rows.length > limit;
|
|
18047
18236
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18048
18237
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18059,8 +18248,9 @@ var SqliteActivityRepository = class {
|
|
|
18059
18248
|
return Promise.resolve({ items, nextCursor });
|
|
18060
18249
|
}
|
|
18061
18250
|
getSession(sessionId) {
|
|
18062
|
-
const rootRow =
|
|
18063
|
-
|
|
18251
|
+
const rootRow = getRow(
|
|
18252
|
+
this.db.prepare(
|
|
18253
|
+
`SELECT id,
|
|
18064
18254
|
json_extract(attributes, '$.harness') AS harness,
|
|
18065
18255
|
content AS title,
|
|
18066
18256
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18077,47 +18267,66 @@ var SqliteActivityRepository = class {
|
|
|
18077
18267
|
FROM audit_events
|
|
18078
18268
|
WHERE id = ? AND event_type = 'session'
|
|
18079
18269
|
LIMIT 1`
|
|
18080
|
-
|
|
18270
|
+
),
|
|
18271
|
+
[sessionId]
|
|
18272
|
+
);
|
|
18081
18273
|
if (!rootRow) return Promise.resolve(null);
|
|
18082
|
-
const timelineRows =
|
|
18083
|
-
|
|
18274
|
+
const timelineRows = allRows(
|
|
18275
|
+
this.db.prepare(
|
|
18276
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18084
18277
|
FROM audit_events
|
|
18085
18278
|
WHERE id = ? OR root_session_id = ?
|
|
18086
18279
|
ORDER BY started_at ASC, id ASC`
|
|
18087
|
-
|
|
18280
|
+
),
|
|
18281
|
+
[sessionId, sessionId]
|
|
18282
|
+
);
|
|
18088
18283
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18089
|
-
const tokenRow =
|
|
18090
|
-
|
|
18284
|
+
const tokenRow = getRow(
|
|
18285
|
+
this.db.prepare(
|
|
18286
|
+
`SELECT
|
|
18091
18287
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18092
18288
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18093
18289
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18094
18290
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18095
18291
|
FROM audit_events
|
|
18096
18292
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18293
|
+
),
|
|
18294
|
+
[sessionId]
|
|
18295
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18296
|
+
const primaryModel = getRow(
|
|
18297
|
+
this.db.prepare(
|
|
18298
|
+
`SELECT model, provider FROM audit_events
|
|
18100
18299
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18101
18300
|
ORDER BY started_at ASC, id ASC
|
|
18102
18301
|
LIMIT 1`
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18302
|
+
),
|
|
18303
|
+
[sessionId]
|
|
18304
|
+
);
|
|
18305
|
+
const toolRows = allRows(
|
|
18306
|
+
this.db.prepare(
|
|
18307
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18106
18308
|
count(*) AS n
|
|
18107
18309
|
FROM audit_events
|
|
18108
18310
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18109
18311
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18312
|
+
),
|
|
18313
|
+
[sessionId]
|
|
18314
|
+
);
|
|
18315
|
+
const modelRows = allRows(
|
|
18316
|
+
this.db.prepare(
|
|
18317
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18113
18318
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18114
18319
|
ORDER BY model`
|
|
18115
|
-
|
|
18320
|
+
),
|
|
18321
|
+
[sessionId]
|
|
18322
|
+
);
|
|
18116
18323
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18117
|
-
const commits =
|
|
18324
|
+
const commits = countScalar(
|
|
18325
|
+
this.db,
|
|
18118
18326
|
`SELECT count(*) AS n FROM audit_events
|
|
18119
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18120
|
-
|
|
18327
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18328
|
+
[sessionId]
|
|
18329
|
+
);
|
|
18121
18330
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18122
18331
|
turns: 0,
|
|
18123
18332
|
findings: 0,
|
|
@@ -18184,7 +18393,10 @@ var SqliteActivityRepository = class {
|
|
|
18184
18393
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18185
18394
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18186
18395
|
);
|
|
18187
|
-
const rows =
|
|
18396
|
+
const rows = allRows(
|
|
18397
|
+
stmt,
|
|
18398
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18399
|
+
);
|
|
18188
18400
|
const seen = /* @__PURE__ */ new Set();
|
|
18189
18401
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18190
18402
|
return Promise.resolve([...seen]);
|
|
@@ -18207,23 +18419,23 @@ var SqliteActivityRepository = class {
|
|
|
18207
18419
|
conditions.push("started_at >= ?");
|
|
18208
18420
|
params.push(opts.fromMs);
|
|
18209
18421
|
}
|
|
18210
|
-
const rows =
|
|
18211
|
-
|
|
18422
|
+
const rows = allRows(
|
|
18423
|
+
this.db.prepare(
|
|
18424
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18212
18425
|
FROM audit_events
|
|
18213
18426
|
WHERE ${conditions.join(" AND ")}`
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
}
|
|
18225
|
-
|
|
18226
|
-
return leaves;
|
|
18427
|
+
),
|
|
18428
|
+
params
|
|
18429
|
+
);
|
|
18430
|
+
return mapRowsTolerant(
|
|
18431
|
+
rows.filter(
|
|
18432
|
+
(row) => row.sessionId !== null
|
|
18433
|
+
),
|
|
18434
|
+
(row) => ({
|
|
18435
|
+
sessionId: row.sessionId,
|
|
18436
|
+
attributes: JSON.parse(row.attributes)
|
|
18437
|
+
})
|
|
18438
|
+
);
|
|
18227
18439
|
}
|
|
18228
18440
|
/**
|
|
18229
18441
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18237,57 +18449,72 @@ var SqliteActivityRepository = class {
|
|
|
18237
18449
|
);
|
|
18238
18450
|
if (sessionIds.length === 0) return result;
|
|
18239
18451
|
const inClause = placeholders(sessionIds.length);
|
|
18240
|
-
const lastActivityRows =
|
|
18241
|
-
|
|
18452
|
+
const lastActivityRows = allRows(
|
|
18453
|
+
this.db.prepare(
|
|
18454
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18242
18455
|
WHERE root_session_id IN (${inClause})
|
|
18243
18456
|
GROUP BY root_session_id`
|
|
18244
|
-
|
|
18457
|
+
),
|
|
18458
|
+
sessionIds
|
|
18459
|
+
);
|
|
18245
18460
|
for (const row of lastActivityRows) {
|
|
18246
18461
|
if (row.id === null) continue;
|
|
18247
18462
|
const entry = result.get(row.id);
|
|
18248
18463
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18249
18464
|
}
|
|
18250
|
-
const turnsRows =
|
|
18251
|
-
|
|
18465
|
+
const turnsRows = allRows(
|
|
18466
|
+
this.db.prepare(
|
|
18467
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18252
18468
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18253
18469
|
GROUP BY root_session_id`
|
|
18254
|
-
|
|
18470
|
+
),
|
|
18471
|
+
sessionIds
|
|
18472
|
+
);
|
|
18255
18473
|
for (const row of turnsRows) {
|
|
18256
18474
|
if (row.id === null) continue;
|
|
18257
18475
|
const entry = result.get(row.id);
|
|
18258
18476
|
if (entry) entry.turns = row.n;
|
|
18259
18477
|
}
|
|
18260
|
-
const runKeyRows =
|
|
18261
|
-
|
|
18478
|
+
const runKeyRows = allRows(
|
|
18479
|
+
this.db.prepare(
|
|
18480
|
+
`SELECT root_session_id AS id,
|
|
18262
18481
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18263
18482
|
FROM audit_events
|
|
18264
18483
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18265
18484
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18266
18485
|
GROUP BY root_session_id`
|
|
18267
|
-
|
|
18486
|
+
),
|
|
18487
|
+
sessionIds
|
|
18488
|
+
);
|
|
18268
18489
|
for (const row of runKeyRows) {
|
|
18269
18490
|
if (row.id === null) continue;
|
|
18270
18491
|
const entry = result.get(row.id);
|
|
18271
18492
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18272
18493
|
}
|
|
18273
|
-
const findingsRows =
|
|
18274
|
-
|
|
18494
|
+
const findingsRows = allRows(
|
|
18495
|
+
this.db.prepare(
|
|
18496
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18275
18497
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18276
18498
|
WHERE e.root_session_id IN (${inClause})
|
|
18277
18499
|
GROUP BY e.root_session_id`
|
|
18278
|
-
|
|
18500
|
+
),
|
|
18501
|
+
sessionIds
|
|
18502
|
+
);
|
|
18279
18503
|
for (const row of findingsRows) {
|
|
18280
18504
|
if (row.id === null) continue;
|
|
18281
18505
|
const entry = result.get(row.id);
|
|
18282
18506
|
if (entry) entry.findings = row.n;
|
|
18283
18507
|
}
|
|
18284
|
-
const sharesRows =
|
|
18285
|
-
|
|
18508
|
+
const sharesRows = allRows(
|
|
18509
|
+
this.db.prepare(
|
|
18510
|
+
`SELECT root_session_id AS id,
|
|
18286
18511
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18287
18512
|
FROM audit_events
|
|
18288
18513
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18289
18514
|
GROUP BY root_session_id`
|
|
18290
|
-
|
|
18515
|
+
),
|
|
18516
|
+
sessionIds
|
|
18517
|
+
);
|
|
18291
18518
|
for (const row of sharesRows) {
|
|
18292
18519
|
if (row.id === null) continue;
|
|
18293
18520
|
const entry = result.get(row.id);
|
|
@@ -18337,33 +18564,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18337
18564
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18338
18565
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18339
18566
|
runInTransaction(fn) {
|
|
18340
|
-
this.db
|
|
18341
|
-
try {
|
|
18342
|
-
fn();
|
|
18343
|
-
this.db.exec("COMMIT");
|
|
18344
|
-
} catch (err) {
|
|
18345
|
-
this.db.exec("ROLLBACK");
|
|
18346
|
-
throw err;
|
|
18347
|
-
}
|
|
18567
|
+
withTransaction(this.db, fn);
|
|
18348
18568
|
}
|
|
18349
18569
|
insertAuditEvent(input) {
|
|
18350
18570
|
const row = toAuditEventRow(input);
|
|
18351
|
-
this.insertStmt.run(
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18571
|
+
this.insertStmt.run(
|
|
18572
|
+
bindParams({
|
|
18573
|
+
id: row.id,
|
|
18574
|
+
parentId: row.parentId,
|
|
18575
|
+
rootSessionId: row.rootSessionId,
|
|
18576
|
+
eventType: row.eventType,
|
|
18577
|
+
hostId: row.hostId,
|
|
18578
|
+
harnessId: row.harnessId,
|
|
18579
|
+
sourceProjectId: row.sourceProjectId,
|
|
18580
|
+
startedAt: row.startedAt,
|
|
18581
|
+
endedAt: row.endedAt,
|
|
18582
|
+
severity: row.severity,
|
|
18583
|
+
priority: row.priority,
|
|
18584
|
+
content: row.content,
|
|
18585
|
+
contentHash: row.contentHash,
|
|
18586
|
+
attributes: row.attributes
|
|
18587
|
+
})
|
|
18588
|
+
);
|
|
18367
18589
|
}
|
|
18368
18590
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18369
18591
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18377,22 +18599,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18377
18599
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18378
18600
|
if (!Number.isFinite(startedAt)) return;
|
|
18379
18601
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18380
|
-
this.upsertLlmCallStmt.run(
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
|
|
18602
|
+
this.upsertLlmCallStmt.run(
|
|
18603
|
+
bindParams({
|
|
18604
|
+
id,
|
|
18605
|
+
parentId: input.parentId,
|
|
18606
|
+
rootSessionId: input.rootSessionId,
|
|
18607
|
+
eventType: "llm_call",
|
|
18608
|
+
hostId: null,
|
|
18609
|
+
harnessId: null,
|
|
18610
|
+
sourceProjectId: null,
|
|
18611
|
+
startedAt,
|
|
18612
|
+
endedAt: null,
|
|
18613
|
+
severity: null,
|
|
18614
|
+
priority: null,
|
|
18615
|
+
content: null,
|
|
18616
|
+
contentHash: null,
|
|
18617
|
+
attributes: JSON.stringify(input.attributes)
|
|
18618
|
+
})
|
|
18619
|
+
);
|
|
18396
18620
|
}
|
|
18397
18621
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18398
18622
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18416,25 +18640,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18416
18640
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18417
18641
|
if (!Number.isFinite(startedAt)) return;
|
|
18418
18642
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18419
|
-
this.insertStmt.run(
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18643
|
+
this.insertStmt.run(
|
|
18644
|
+
bindParams({
|
|
18645
|
+
id,
|
|
18646
|
+
parentId: input.parentId,
|
|
18647
|
+
rootSessionId: input.rootSessionId,
|
|
18648
|
+
eventType: "tool_call",
|
|
18649
|
+
hostId: null,
|
|
18650
|
+
harnessId: null,
|
|
18651
|
+
sourceProjectId: null,
|
|
18652
|
+
startedAt,
|
|
18653
|
+
endedAt: null,
|
|
18654
|
+
severity: null,
|
|
18655
|
+
priority: null,
|
|
18656
|
+
content: null,
|
|
18657
|
+
contentHash: null,
|
|
18658
|
+
attributes: JSON.stringify(input.attributes)
|
|
18659
|
+
})
|
|
18660
|
+
);
|
|
18435
18661
|
}
|
|
18436
18662
|
findById(id) {
|
|
18437
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18663
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18664
|
+
id
|
|
18665
|
+
});
|
|
18438
18666
|
}
|
|
18439
18667
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18440
18668
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18444,14 +18672,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18444
18672
|
sessionProvider(sessionId) {
|
|
18445
18673
|
const row = this.findById(sessionId);
|
|
18446
18674
|
if (!row?.attributes) return void 0;
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18450
|
-
const provider = parsed.provider;
|
|
18451
|
-
if (typeof provider === "string") return provider;
|
|
18452
|
-
}
|
|
18453
|
-
} catch {
|
|
18454
|
-
}
|
|
18675
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18676
|
+
if (typeof provider === "string") return provider;
|
|
18455
18677
|
return void 0;
|
|
18456
18678
|
}
|
|
18457
18679
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18460,11 +18682,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18460
18682
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18461
18683
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18462
18684
|
llmCallLeaves() {
|
|
18463
|
-
return
|
|
18464
|
-
|
|
18685
|
+
return allRows(
|
|
18686
|
+
this.db.prepare(
|
|
18687
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18465
18688
|
FROM audit_events
|
|
18466
18689
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18467
|
-
|
|
18690
|
+
)
|
|
18691
|
+
);
|
|
18468
18692
|
}
|
|
18469
18693
|
};
|
|
18470
18694
|
|
|
@@ -18483,16 +18707,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18483
18707
|
upsert(input) {
|
|
18484
18708
|
const id = classifiedDataId(input.class);
|
|
18485
18709
|
const row = toClassifiedDataRow(input, id);
|
|
18486
|
-
this.insertStmt.run(
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18710
|
+
this.insertStmt.run(
|
|
18711
|
+
bindParams({
|
|
18712
|
+
id: row.id,
|
|
18713
|
+
class: row.class,
|
|
18714
|
+
label: row.label,
|
|
18715
|
+
attributes: row.attributes
|
|
18716
|
+
})
|
|
18717
|
+
);
|
|
18492
18718
|
return id;
|
|
18493
18719
|
}
|
|
18494
18720
|
};
|
|
18495
18721
|
|
|
18722
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18723
|
+
function latestConfigScan(db) {
|
|
18724
|
+
return getRow(
|
|
18725
|
+
db.prepare(
|
|
18726
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18727
|
+
WHERE event_type = 'config_scan'
|
|
18728
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18729
|
+
)
|
|
18730
|
+
);
|
|
18731
|
+
}
|
|
18732
|
+
|
|
18496
18733
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18497
18734
|
var SqliteConfigInventoryRepository = class {
|
|
18498
18735
|
constructor(db) {
|
|
@@ -18500,7 +18737,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18500
18737
|
}
|
|
18501
18738
|
db;
|
|
18502
18739
|
report() {
|
|
18503
|
-
const scan2 = this.
|
|
18740
|
+
const scan2 = latestConfigScan(this.db);
|
|
18504
18741
|
if (!scan2) {
|
|
18505
18742
|
return {
|
|
18506
18743
|
scannedAt: null,
|
|
@@ -18511,17 +18748,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18511
18748
|
topics: []
|
|
18512
18749
|
};
|
|
18513
18750
|
}
|
|
18514
|
-
const rows =
|
|
18515
|
-
|
|
18751
|
+
const rows = allRows(
|
|
18752
|
+
this.db.prepare(
|
|
18753
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18516
18754
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18517
18755
|
ORDER BY object_type, title`
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18756
|
+
),
|
|
18757
|
+
{ startedAt: scan2.started_at }
|
|
18758
|
+
);
|
|
18759
|
+
const findings = allRows(
|
|
18760
|
+
this.db.prepare(
|
|
18761
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18521
18762
|
FROM inspection_findings f
|
|
18522
18763
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18523
18764
|
WHERE f.audit_event_id = :scanId`
|
|
18524
|
-
|
|
18765
|
+
),
|
|
18766
|
+
{ scanId: scan2.id }
|
|
18767
|
+
);
|
|
18525
18768
|
const skills = [];
|
|
18526
18769
|
const hooks = [];
|
|
18527
18770
|
const mcpServers = [];
|
|
@@ -18550,7 +18793,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18550
18793
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18551
18794
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18552
18795
|
trustOverrides() {
|
|
18553
|
-
const rows =
|
|
18796
|
+
const rows = allRows(
|
|
18797
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18798
|
+
);
|
|
18554
18799
|
const map2 = /* @__PURE__ */ new Map();
|
|
18555
18800
|
for (const row of rows) {
|
|
18556
18801
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18559,13 +18804,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18559
18804
|
}
|
|
18560
18805
|
return map2;
|
|
18561
18806
|
}
|
|
18562
|
-
latestScan() {
|
|
18563
|
-
return this.db.prepare(
|
|
18564
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18565
|
-
WHERE event_type = 'config_scan'
|
|
18566
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18567
|
-
).get();
|
|
18568
|
-
}
|
|
18569
18807
|
};
|
|
18570
18808
|
function toSkillItem(row, bag) {
|
|
18571
18809
|
const item = {
|
|
@@ -18676,22 +18914,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18676
18914
|
return topics;
|
|
18677
18915
|
}
|
|
18678
18916
|
function countScanErrors(attributes) {
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
const parsed = JSON.parse(attributes);
|
|
18682
|
-
const errors = parsed?.errors;
|
|
18683
|
-
return typeof errors === "number" ? errors : 0;
|
|
18684
|
-
} catch {
|
|
18685
|
-
return 0;
|
|
18686
|
-
}
|
|
18917
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
18918
|
+
return typeof errors === "number" ? errors : 0;
|
|
18687
18919
|
}
|
|
18688
18920
|
function parseBag(raw) {
|
|
18689
|
-
|
|
18690
|
-
const parsed = JSON.parse(raw);
|
|
18691
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18692
|
-
} catch {
|
|
18693
|
-
}
|
|
18694
|
-
return void 0;
|
|
18921
|
+
return parseJsonObject(raw);
|
|
18695
18922
|
}
|
|
18696
18923
|
function str(value) {
|
|
18697
18924
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18700,12 +18927,7 @@ function str(value) {
|
|
|
18700
18927
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18701
18928
|
var DAY_MS2 = 864e5;
|
|
18702
18929
|
function parseRules(rulesJson) {
|
|
18703
|
-
|
|
18704
|
-
try {
|
|
18705
|
-
raw = JSON.parse(rulesJson);
|
|
18706
|
-
} catch {
|
|
18707
|
-
return [];
|
|
18708
|
-
}
|
|
18930
|
+
const raw = safeJson(rulesJson, []);
|
|
18709
18931
|
if (!Array.isArray(raw)) return [];
|
|
18710
18932
|
const rules = [];
|
|
18711
18933
|
for (const entry of raw) {
|
|
@@ -18724,11 +18946,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18724
18946
|
db;
|
|
18725
18947
|
now;
|
|
18726
18948
|
listDetections(query) {
|
|
18727
|
-
const rows =
|
|
18728
|
-
|
|
18949
|
+
const rows = allRows(
|
|
18950
|
+
this.db.prepare(
|
|
18951
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18729
18952
|
rules_json AS rulesJson
|
|
18730
18953
|
FROM installed_packs`
|
|
18731
|
-
|
|
18954
|
+
)
|
|
18955
|
+
);
|
|
18732
18956
|
const available = this.availableByPack();
|
|
18733
18957
|
const summaries = rows.map((r) => {
|
|
18734
18958
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18737,7 +18961,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18737
18961
|
packId: r.packId,
|
|
18738
18962
|
version: r.version,
|
|
18739
18963
|
name: r.name,
|
|
18740
|
-
enabled: r.enabled
|
|
18964
|
+
enabled: intToBool(r.enabled),
|
|
18741
18965
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18742
18966
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18743
18967
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18754,19 +18978,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18754
18978
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18755
18979
|
// detail call; the table is a handful of rows).
|
|
18756
18980
|
availableByPack() {
|
|
18757
|
-
const rows =
|
|
18758
|
-
|
|
18981
|
+
const rows = allRows(
|
|
18982
|
+
this.db.prepare(
|
|
18983
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18759
18984
|
FROM available_packs`
|
|
18760
|
-
|
|
18985
|
+
)
|
|
18986
|
+
);
|
|
18761
18987
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18762
18988
|
}
|
|
18763
18989
|
getDetectionStats() {
|
|
18764
|
-
const rows =
|
|
18990
|
+
const rows = allRows(
|
|
18991
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
18992
|
+
);
|
|
18765
18993
|
let rules = 0;
|
|
18766
18994
|
let active = 0;
|
|
18767
18995
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18768
18996
|
for (const r of rows) {
|
|
18769
|
-
if (r.enabled
|
|
18997
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18770
18998
|
const parsed = parseRules(r.rulesJson);
|
|
18771
18999
|
rules += parsed.length;
|
|
18772
19000
|
for (const rule of parsed) {
|
|
@@ -18784,12 +19012,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18784
19012
|
const parts = splitDetectionId(id);
|
|
18785
19013
|
if (!parts) return Promise.resolve(null);
|
|
18786
19014
|
const { namespace, packId } = parts;
|
|
18787
|
-
const row =
|
|
18788
|
-
|
|
19015
|
+
const row = getRow(
|
|
19016
|
+
this.db.prepare(
|
|
19017
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18789
19018
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18790
19019
|
FROM installed_packs
|
|
18791
19020
|
WHERE namespace = ? AND pack_id = ?`
|
|
18792
|
-
|
|
19021
|
+
),
|
|
19022
|
+
[namespace, packId]
|
|
19023
|
+
);
|
|
18793
19024
|
if (!row) return Promise.resolve(null);
|
|
18794
19025
|
const rules = parseRules(row.rulesJson);
|
|
18795
19026
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18807,7 +19038,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18807
19038
|
packId: row.packId,
|
|
18808
19039
|
version: row.version,
|
|
18809
19040
|
name: row.name,
|
|
18810
|
-
enabled: row.enabled
|
|
19041
|
+
enabled: intToBool(row.enabled),
|
|
18811
19042
|
rules,
|
|
18812
19043
|
updatedAt: new Date(row.updatedAt),
|
|
18813
19044
|
policyId: row.policyId
|
|
@@ -18822,13 +19053,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18822
19053
|
countFindingsLast30d(ruleIds) {
|
|
18823
19054
|
if (ruleIds.length === 0) return 0;
|
|
18824
19055
|
const since = this.now() - 30 * DAY_MS2;
|
|
18825
|
-
const
|
|
18826
|
-
|
|
18827
|
-
|
|
19056
|
+
const inClause = placeholders(ruleIds.length);
|
|
19057
|
+
return countScalar(
|
|
19058
|
+
this.db,
|
|
19059
|
+
`SELECT count(*) AS n
|
|
18828
19060
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18829
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18830
|
-
|
|
18831
|
-
|
|
19061
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19062
|
+
[since, ...ruleIds]
|
|
19063
|
+
);
|
|
18832
19064
|
}
|
|
18833
19065
|
};
|
|
18834
19066
|
|
|
@@ -18845,16 +19077,17 @@ var SqliteEventsRepository = class {
|
|
|
18845
19077
|
insertStmt;
|
|
18846
19078
|
insertEvent(event) {
|
|
18847
19079
|
const row = toEventRow(event);
|
|
18848
|
-
this.insertStmt.run(
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
19080
|
+
this.insertStmt.run(
|
|
19081
|
+
bindParams({
|
|
19082
|
+
id: row.id,
|
|
19083
|
+
sourceTool: row.sourceTool,
|
|
19084
|
+
kind: row.kind,
|
|
19085
|
+
occurredAt: row.occurredAt,
|
|
19086
|
+
contentHash: row.contentHash,
|
|
19087
|
+
content: row.content,
|
|
19088
|
+
metadata: row.metadata
|
|
19089
|
+
})
|
|
19090
|
+
);
|
|
18858
19091
|
}
|
|
18859
19092
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18860
19093
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18862,13 +19095,23 @@ var SqliteEventsRepository = class {
|
|
|
18862
19095
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18863
19096
|
// async EventsReadPort contract.
|
|
18864
19097
|
contentHashes() {
|
|
18865
|
-
const rows =
|
|
19098
|
+
const rows = allRows(
|
|
19099
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19100
|
+
);
|
|
18866
19101
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18867
19102
|
}
|
|
18868
19103
|
};
|
|
18869
19104
|
|
|
18870
19105
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18871
19106
|
import { randomUUID } from "crypto";
|
|
19107
|
+
|
|
19108
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19109
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19110
|
+
function isUniqueConstraintError(err) {
|
|
19111
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19112
|
+
}
|
|
19113
|
+
|
|
19114
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18872
19115
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18873
19116
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18874
19117
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18889,10 +19132,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18889
19132
|
this.name = "AmbiguousExceptionIdError";
|
|
18890
19133
|
}
|
|
18891
19134
|
};
|
|
18892
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18893
|
-
function isUniqueConstraintError(err) {
|
|
18894
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18895
|
-
}
|
|
18896
19135
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18897
19136
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18898
19137
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -18948,10 +19187,11 @@ var SqliteExceptionsRepository = class {
|
|
|
18948
19187
|
this.insertExceptionRow(id, input, now);
|
|
18949
19188
|
} catch (err) {
|
|
18950
19189
|
if (!isUniqueConstraintError(err)) throw err;
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
19190
|
+
withTransaction(
|
|
19191
|
+
this.db,
|
|
19192
|
+
() => {
|
|
19193
|
+
const superseded = this.db.prepare(
|
|
19194
|
+
`UPDATE exceptions
|
|
18955
19195
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
18956
19196
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
18957
19197
|
updated_at = :now
|
|
@@ -18959,24 +19199,27 @@ var SqliteExceptionsRepository = class {
|
|
|
18959
19199
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
18960
19200
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
18961
19201
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
19202
|
+
).run({
|
|
19203
|
+
now,
|
|
19204
|
+
revokedBy: input.createdBy,
|
|
19205
|
+
ruleId: input.ruleId,
|
|
19206
|
+
valueFingerprint: input.valueFingerprint,
|
|
19207
|
+
keyVersion: input.keyVersion
|
|
19208
|
+
});
|
|
19209
|
+
if (Number(superseded.changes) !== 1) {
|
|
19210
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19211
|
+
}
|
|
19212
|
+
this.insertExceptionRow(id, input, now);
|
|
19213
|
+
},
|
|
19214
|
+
"IMMEDIATE"
|
|
19215
|
+
);
|
|
19216
|
+
}
|
|
19217
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19218
|
+
id
|
|
19219
|
+
});
|
|
19220
|
+
if (row === void 0) {
|
|
19221
|
+
throw new Error("exception row not found immediately after insert");
|
|
18978
19222
|
}
|
|
18979
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
18980
19223
|
return parseExceptionRow(row);
|
|
18981
19224
|
}
|
|
18982
19225
|
insertExceptionRow(id, input, now) {
|
|
@@ -19014,14 +19257,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19014
19257
|
*/
|
|
19015
19258
|
list(opts) {
|
|
19016
19259
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19017
|
-
const rows =
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
} catch {
|
|
19023
|
-
}
|
|
19024
|
-
}
|
|
19260
|
+
const rows = allRows(
|
|
19261
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19262
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19263
|
+
);
|
|
19264
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19025
19265
|
return Promise.resolve(exceptions);
|
|
19026
19266
|
}
|
|
19027
19267
|
/**
|
|
@@ -19031,7 +19271,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19031
19271
|
*/
|
|
19032
19272
|
getByIdPrefix(prefix) {
|
|
19033
19273
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19034
|
-
const rows =
|
|
19274
|
+
const rows = allRows(
|
|
19275
|
+
this.db.prepare(
|
|
19276
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19277
|
+
),
|
|
19278
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19279
|
+
);
|
|
19035
19280
|
if (rows.length > 1) {
|
|
19036
19281
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19037
19282
|
}
|
|
@@ -19073,30 +19318,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19073
19318
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19074
19319
|
*/
|
|
19075
19320
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19076
|
-
const rows =
|
|
19077
|
-
|
|
19321
|
+
const rows = allRows(
|
|
19322
|
+
this.db.prepare(
|
|
19323
|
+
`SELECT * FROM exceptions
|
|
19078
19324
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19079
19325
|
ORDER BY created_at DESC, rowid DESC`
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
);
|
|
19097
|
-
} catch {
|
|
19098
|
-
}
|
|
19099
|
-
}
|
|
19326
|
+
),
|
|
19327
|
+
{ keyVersion, now }
|
|
19328
|
+
);
|
|
19329
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19330
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19331
|
+
return ExceptionBundleEntry.parse({
|
|
19332
|
+
id: row.id,
|
|
19333
|
+
ruleId: row.rule_id,
|
|
19334
|
+
valueFingerprint: row.value_fingerprint,
|
|
19335
|
+
keyVersion: row.key_version,
|
|
19336
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19337
|
+
maxUses: row.max_uses,
|
|
19338
|
+
useCount: row.use_count,
|
|
19339
|
+
conditions
|
|
19340
|
+
});
|
|
19341
|
+
});
|
|
19100
19342
|
return Promise.resolve(entries);
|
|
19101
19343
|
}
|
|
19102
19344
|
/**
|
|
@@ -19123,11 +19365,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19123
19365
|
}
|
|
19124
19366
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19125
19367
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19126
|
-
const rows =
|
|
19127
|
-
|
|
19368
|
+
const rows = allRows(
|
|
19369
|
+
this.db.prepare(
|
|
19370
|
+
`SELECT * FROM blocked_detections
|
|
19128
19371
|
WHERE blocked_at > :cutoff
|
|
19129
19372
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19130
|
-
|
|
19373
|
+
),
|
|
19374
|
+
{ cutoff: Date.now() - windowMs }
|
|
19375
|
+
);
|
|
19131
19376
|
return Promise.resolve(
|
|
19132
19377
|
rows.map((row) => ({
|
|
19133
19378
|
reference: row.reference,
|
|
@@ -19207,7 +19452,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19207
19452
|
)`;
|
|
19208
19453
|
|
|
19209
19454
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19210
|
-
var
|
|
19455
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19456
|
+
var CONCAT_SEP = ",";
|
|
19457
|
+
var TUPLE_SEP = "|";
|
|
19458
|
+
function splitConcat(value) {
|
|
19459
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19460
|
+
}
|
|
19211
19461
|
function deriveInstanceStatus(row) {
|
|
19212
19462
|
return deriveFindingStatus({
|
|
19213
19463
|
kind: row.kind,
|
|
@@ -19275,13 +19525,16 @@ var SqliteFindingsRepository = class {
|
|
|
19275
19525
|
}
|
|
19276
19526
|
recentFindings(opts) {
|
|
19277
19527
|
const limit = opts?.limit ?? 50;
|
|
19278
|
-
const rows =
|
|
19279
|
-
|
|
19528
|
+
const rows = allRows(
|
|
19529
|
+
this.db.prepare(
|
|
19530
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19280
19531
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19281
19532
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19282
19533
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19283
19534
|
LIMIT :limit`
|
|
19284
|
-
|
|
19535
|
+
),
|
|
19536
|
+
{ limit }
|
|
19537
|
+
);
|
|
19285
19538
|
return Promise.resolve(
|
|
19286
19539
|
rows.map((r) => ({
|
|
19287
19540
|
id: r.id,
|
|
@@ -19304,20 +19557,47 @@ var SqliteFindingsRepository = class {
|
|
|
19304
19557
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19305
19558
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19306
19559
|
* reflect the full filtered set; `items` is the requested
|
|
19307
|
-
* page (default
|
|
19560
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19561
|
+
*
|
|
19562
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19563
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19564
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19565
|
+
* latest, search text);
|
|
19566
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19567
|
+
* populate `instances` for the table's expanded rows.
|
|
19568
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19569
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19570
|
+
* rule is ever restated in SQL.
|
|
19308
19571
|
*/
|
|
19309
19572
|
listGroupedFindings(query) {
|
|
19310
|
-
const
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
19573
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
|
|
19574
|
+
const rows = allRows(
|
|
19575
|
+
this.db.prepare(
|
|
19576
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19577
|
+
occurred_at, source_tool, repo, file, kind, finding_key, latest_status
|
|
19578
|
+
FROM (
|
|
19579
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19580
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19581
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19582
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19583
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19584
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19585
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19586
|
+
latest.status AS latest_status,
|
|
19587
|
+
ROW_NUMBER() OVER (
|
|
19588
|
+
PARTITION BY f.rule_id
|
|
19589
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19590
|
+
) AS rn
|
|
19591
|
+
FROM findings f
|
|
19592
|
+
JOIN events e ON e.id = f.event_id
|
|
19593
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19594
|
+
ON latest.finding_key = f.finding_key
|
|
19595
|
+
)
|
|
19596
|
+
WHERE rn <= :cap
|
|
19597
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19598
|
+
),
|
|
19599
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP }
|
|
19600
|
+
);
|
|
19321
19601
|
const groupable = rows.map((r) => ({
|
|
19322
19602
|
id: r.id,
|
|
19323
19603
|
ruleId: r.rule_id,
|
|
@@ -19332,7 +19612,7 @@ var SqliteFindingsRepository = class {
|
|
|
19332
19612
|
file: r.file ?? "",
|
|
19333
19613
|
status: deriveInstanceStatus(r)
|
|
19334
19614
|
}));
|
|
19335
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19615
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19336
19616
|
const filterOpts = {
|
|
19337
19617
|
severity: query.severity,
|
|
19338
19618
|
providers: query.provider,
|
|
@@ -19350,42 +19630,122 @@ var SqliteFindingsRepository = class {
|
|
|
19350
19630
|
const items = sorted.slice(0, limit);
|
|
19351
19631
|
return Promise.resolve({ totals, facets, items, nextCursor: null });
|
|
19352
19632
|
}
|
|
19633
|
+
/**
|
|
19634
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19635
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19636
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19637
|
+
*
|
|
19638
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19639
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19640
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19641
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19642
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19643
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19644
|
+
* a group's row stays small however many findings it holds.
|
|
19645
|
+
*
|
|
19646
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19647
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19648
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19649
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19650
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19651
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19652
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19653
|
+
* carries a `q`.
|
|
19654
|
+
*/
|
|
19655
|
+
groupAggregates(withSearchText) {
|
|
19656
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19657
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
|
|
19658
|
+
const rows = this.db.prepare(
|
|
19659
|
+
`SELECT f.rule_id AS rule_id,
|
|
19660
|
+
count(*) AS instance_count,
|
|
19661
|
+
max(e.occurred_at) AS latest_at,
|
|
19662
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19663
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19664
|
+
group_concat(DISTINCT (
|
|
19665
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19666
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19667
|
+
coalesce(latest.status, '')
|
|
19668
|
+
)) AS status_inputs
|
|
19669
|
+
${searchTextColumns}
|
|
19670
|
+
FROM findings f
|
|
19671
|
+
JOIN events e ON e.id = f.event_id
|
|
19672
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19673
|
+
ON latest.finding_key = f.finding_key
|
|
19674
|
+
GROUP BY f.rule_id`
|
|
19675
|
+
).all();
|
|
19676
|
+
return new Map(
|
|
19677
|
+
rows.map((r) => [
|
|
19678
|
+
r.rule_id,
|
|
19679
|
+
{
|
|
19680
|
+
instanceCount: r.instance_count,
|
|
19681
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19682
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19683
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19684
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19685
|
+
return {
|
|
19686
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19687
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19688
|
+
kind,
|
|
19689
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19690
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19691
|
+
};
|
|
19692
|
+
}),
|
|
19693
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19694
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19695
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19696
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19697
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19698
|
+
// and skip priming a haystack nothing will read.
|
|
19699
|
+
...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
|
|
19700
|
+
}
|
|
19701
|
+
])
|
|
19702
|
+
);
|
|
19703
|
+
}
|
|
19353
19704
|
healthSummary() {
|
|
19354
|
-
const total = this.db
|
|
19705
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19355
19706
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19356
|
-
const grouped =
|
|
19707
|
+
const grouped = allRows(
|
|
19708
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19709
|
+
);
|
|
19357
19710
|
for (const row of grouped) {
|
|
19358
19711
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19359
19712
|
}
|
|
19360
19713
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19361
|
-
const sevRows =
|
|
19362
|
-
|
|
19714
|
+
const sevRows = allRows(
|
|
19715
|
+
this.db.prepare(
|
|
19716
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19363
19717
|
FROM findings f
|
|
19364
19718
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19365
19719
|
ON latest.finding_key = f.finding_key
|
|
19366
19720
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19367
19721
|
GROUP BY f.severity`
|
|
19368
|
-
|
|
19722
|
+
)
|
|
19723
|
+
);
|
|
19369
19724
|
for (const row of sevRows) {
|
|
19370
19725
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19371
19726
|
}
|
|
19372
19727
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19373
|
-
const enabledRows =
|
|
19374
|
-
|
|
19728
|
+
const enabledRows = allRows(
|
|
19729
|
+
this.db.prepare(
|
|
19730
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19375
19731
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19376
|
-
|
|
19732
|
+
)
|
|
19733
|
+
);
|
|
19377
19734
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19378
19735
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19379
19736
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19380
19737
|
}
|
|
19381
19738
|
activityByDay(days = 7) {
|
|
19382
19739
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19383
|
-
const rows =
|
|
19384
|
-
|
|
19740
|
+
const rows = allRows(
|
|
19741
|
+
this.db.prepare(
|
|
19742
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19385
19743
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19386
19744
|
WHERE e.occurred_at >= :since
|
|
19387
19745
|
GROUP BY day, f.action_taken`
|
|
19388
|
-
|
|
19746
|
+
),
|
|
19747
|
+
{ since }
|
|
19748
|
+
);
|
|
19389
19749
|
const buckets = /* @__PURE__ */ new Map();
|
|
19390
19750
|
for (let i = 0; i < days; i++) {
|
|
19391
19751
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19457,17 +19817,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19457
19817
|
insertStmt;
|
|
19458
19818
|
insertFinding(input) {
|
|
19459
19819
|
const row = toInspectionFindingRow(input);
|
|
19460
|
-
this.insertStmt.run(
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
|
|
19820
|
+
this.insertStmt.run(
|
|
19821
|
+
bindParams({
|
|
19822
|
+
id: row.id,
|
|
19823
|
+
auditEventId: row.auditEventId,
|
|
19824
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
19825
|
+
classifiedDataId: row.classifiedDataId,
|
|
19826
|
+
spanStart: row.spanStart,
|
|
19827
|
+
spanEnd: row.spanEnd,
|
|
19828
|
+
maskedMatch: row.maskedMatch,
|
|
19829
|
+
actionTaken: row.actionTaken,
|
|
19830
|
+
confidence: row.confidence
|
|
19831
|
+
})
|
|
19832
|
+
);
|
|
19471
19833
|
}
|
|
19472
19834
|
};
|
|
19473
19835
|
|
|
@@ -19543,12 +19905,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19543
19905
|
}
|
|
19544
19906
|
function ruleIdsOf(rulesJson) {
|
|
19545
19907
|
const ids = /* @__PURE__ */ new Set();
|
|
19546
|
-
|
|
19547
|
-
try {
|
|
19548
|
-
raw = JSON.parse(rulesJson);
|
|
19549
|
-
} catch {
|
|
19550
|
-
return ids;
|
|
19551
|
-
}
|
|
19908
|
+
const raw = safeJson(rulesJson, []);
|
|
19552
19909
|
if (!Array.isArray(raw)) return ids;
|
|
19553
19910
|
for (const entry of raw) {
|
|
19554
19911
|
if (entry && typeof entry === "object") {
|
|
@@ -19621,47 +19978,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19621
19978
|
}));
|
|
19622
19979
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19623
19980
|
const now = Date.now();
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
const
|
|
19630
|
-
|
|
19631
|
-
namespace: row.namespace,
|
|
19632
|
-
packId: row.packId,
|
|
19633
|
-
version: row.version,
|
|
19634
|
-
name: row.name,
|
|
19635
|
-
rulesJson: row.rulesJson,
|
|
19636
|
-
now
|
|
19637
|
-
};
|
|
19638
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19639
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19640
|
-
this.upsertAvailableStmt.run({
|
|
19641
|
-
...params,
|
|
19981
|
+
withTransaction(
|
|
19982
|
+
this.db,
|
|
19983
|
+
() => {
|
|
19984
|
+
const mirror = this.mirrorState();
|
|
19985
|
+
let behind = false;
|
|
19986
|
+
for (const row of rows) {
|
|
19987
|
+
const params = {
|
|
19642
19988
|
id: randomUUID2(),
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
19989
|
+
namespace: row.namespace,
|
|
19990
|
+
packId: row.packId,
|
|
19991
|
+
version: row.version,
|
|
19992
|
+
name: row.name,
|
|
19993
|
+
rulesJson: row.rulesJson,
|
|
19994
|
+
now
|
|
19995
|
+
};
|
|
19996
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19997
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19998
|
+
this.upsertAvailableStmt.run({
|
|
19999
|
+
...params,
|
|
20000
|
+
id: randomUUID2(),
|
|
20001
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20002
|
+
});
|
|
20003
|
+
} else {
|
|
20004
|
+
behind = true;
|
|
20005
|
+
}
|
|
20006
|
+
this.insertMissingStmt.run(params);
|
|
19647
20007
|
}
|
|
19648
|
-
this.
|
|
19649
|
-
}
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
} catch (err) {
|
|
19653
|
-
this.db.exec("ROLLBACK");
|
|
19654
|
-
throw err;
|
|
19655
|
-
}
|
|
20008
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20009
|
+
},
|
|
20010
|
+
"IMMEDIATE"
|
|
20011
|
+
);
|
|
19656
20012
|
} catch {
|
|
19657
20013
|
}
|
|
19658
20014
|
}
|
|
19659
20015
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19660
20016
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19661
20017
|
mirrorState() {
|
|
19662
|
-
const rows =
|
|
19663
|
-
|
|
19664
|
-
|
|
20018
|
+
const rows = allRows(
|
|
20019
|
+
this.db.prepare(
|
|
20020
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20021
|
+
)
|
|
20022
|
+
);
|
|
19665
20023
|
return new Map(
|
|
19666
20024
|
rows.map((r) => [
|
|
19667
20025
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19673,7 +20031,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19673
20031
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19674
20032
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19675
20033
|
pruneAvailable(keep) {
|
|
19676
|
-
const rows =
|
|
20034
|
+
const rows = allRows(
|
|
20035
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20036
|
+
);
|
|
19677
20037
|
const keepSet = new Set(keep);
|
|
19678
20038
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19679
20039
|
for (const r of rows) {
|
|
@@ -19700,11 +20060,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19700
20060
|
if (this.db.isTransaction) {
|
|
19701
20061
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19702
20062
|
}
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
this.db
|
|
19706
|
-
|
|
19707
|
-
|
|
20063
|
+
let changed = false;
|
|
20064
|
+
withTransaction(
|
|
20065
|
+
this.db,
|
|
20066
|
+
() => {
|
|
20067
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20068
|
+
const res = this.db.prepare(
|
|
20069
|
+
`UPDATE installed_packs SET
|
|
19708
20070
|
version = (SELECT a.version FROM available_packs a
|
|
19709
20071
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19710
20072
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19715,17 +20077,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19715
20077
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19716
20078
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19717
20079
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
} catch {
|
|
19726
|
-
}
|
|
19727
|
-
throw err;
|
|
19728
|
-
}
|
|
20080
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20081
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20082
|
+
changed = Number(res.changes) > 0;
|
|
20083
|
+
},
|
|
20084
|
+
"IMMEDIATE"
|
|
20085
|
+
);
|
|
20086
|
+
return changed;
|
|
19729
20087
|
}
|
|
19730
20088
|
/**
|
|
19731
20089
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19739,9 +20097,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19739
20097
|
* JSON-level failure therefore counts as invalid.
|
|
19740
20098
|
*/
|
|
19741
20099
|
installedRuleset() {
|
|
19742
|
-
const rows =
|
|
19743
|
-
|
|
19744
|
-
|
|
20100
|
+
const rows = allRows(
|
|
20101
|
+
this.db.prepare(
|
|
20102
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20103
|
+
)
|
|
20104
|
+
);
|
|
19745
20105
|
const out = {
|
|
19746
20106
|
installedPacks: rows.length,
|
|
19747
20107
|
enabledPacks: 0,
|
|
@@ -19750,7 +20110,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19750
20110
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19751
20111
|
};
|
|
19752
20112
|
for (const row of rows) {
|
|
19753
|
-
if (row.enabled
|
|
20113
|
+
if (!intToBool(row.enabled)) continue;
|
|
19754
20114
|
out.enabledPacks += 1;
|
|
19755
20115
|
const action = policyIdToAction(row.policyId);
|
|
19756
20116
|
let raw;
|
|
@@ -19785,9 +20145,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19785
20145
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19786
20146
|
*/
|
|
19787
20147
|
newestRecordedBinary() {
|
|
19788
|
-
const rows =
|
|
19789
|
-
|
|
19790
|
-
|
|
20148
|
+
const rows = allRows(
|
|
20149
|
+
this.db.prepare(
|
|
20150
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20151
|
+
)
|
|
20152
|
+
);
|
|
19791
20153
|
let newest = null;
|
|
19792
20154
|
for (const row of rows) {
|
|
19793
20155
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19802,13 +20164,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19802
20164
|
return newest;
|
|
19803
20165
|
}
|
|
19804
20166
|
counts() {
|
|
19805
|
-
const row =
|
|
19806
|
-
|
|
20167
|
+
const row = getRow(
|
|
20168
|
+
this.db.prepare(
|
|
20169
|
+
`SELECT count(*) AS packs,
|
|
19807
20170
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19808
20171
|
coalesce(sum(enabled), 0) AS enabled
|
|
19809
20172
|
FROM installed_packs`
|
|
19810
|
-
|
|
19811
|
-
|
|
20173
|
+
)
|
|
20174
|
+
);
|
|
20175
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19812
20176
|
}
|
|
19813
20177
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19814
20178
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19821,26 +20185,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19821
20185
|
* attributed to Monitor, matching the Detections views.
|
|
19822
20186
|
*/
|
|
19823
20187
|
countsByPolicyId() {
|
|
19824
|
-
|
|
19825
|
-
|
|
20188
|
+
return countBy(
|
|
20189
|
+
this.db,
|
|
20190
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19826
20191
|
FROM installed_packs
|
|
19827
|
-
GROUP BY
|
|
19828
|
-
)
|
|
19829
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20192
|
+
GROUP BY k`
|
|
20193
|
+
);
|
|
19830
20194
|
}
|
|
19831
20195
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19832
20196
|
listByPolicyId(policyId) {
|
|
19833
|
-
const rows =
|
|
19834
|
-
|
|
20197
|
+
const rows = allRows(
|
|
20198
|
+
this.db.prepare(
|
|
20199
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19835
20200
|
FROM installed_packs
|
|
19836
20201
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19837
20202
|
ORDER BY name ASC`
|
|
19838
|
-
|
|
20203
|
+
),
|
|
20204
|
+
[policyId]
|
|
20205
|
+
);
|
|
19839
20206
|
return rows.map((r) => ({
|
|
19840
20207
|
id: `${r.namespace}/${r.packId}`,
|
|
19841
20208
|
name: r.name,
|
|
19842
20209
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19843
|
-
enabled: r.enabled
|
|
20210
|
+
enabled: intToBool(r.enabled)
|
|
19844
20211
|
}));
|
|
19845
20212
|
}
|
|
19846
20213
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19869,14 +20236,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19869
20236
|
const res = this.db.prepare(
|
|
19870
20237
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19871
20238
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19872
|
-
).run({ enabled: enabled
|
|
20239
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19873
20240
|
return Number(res.changes) > 0;
|
|
19874
20241
|
}
|
|
19875
20242
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19876
20243
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19877
20244
|
// binary's inventory hasn't changed since the last record.
|
|
19878
20245
|
storedSignature() {
|
|
19879
|
-
const rows = this.signatureStmt
|
|
20246
|
+
const rows = allRows(this.signatureStmt);
|
|
19880
20247
|
return inventorySignature(rows);
|
|
19881
20248
|
}
|
|
19882
20249
|
};
|
|
@@ -19904,42 +20271,48 @@ var SqliteInventoryRepository = class {
|
|
|
19904
20271
|
upsert(input, now = Date.now()) {
|
|
19905
20272
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19906
20273
|
const row = toInventoryRow(input, id, now);
|
|
19907
|
-
this.upsertStmt.run(
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
20274
|
+
this.upsertStmt.run(
|
|
20275
|
+
bindParams({
|
|
20276
|
+
id: row.id,
|
|
20277
|
+
objectType: row.objectType,
|
|
20278
|
+
location: row.location,
|
|
20279
|
+
title: row.title,
|
|
20280
|
+
hostId: row.hostId,
|
|
20281
|
+
attributes: row.attributes,
|
|
20282
|
+
firstSeen: row.firstSeen,
|
|
20283
|
+
lastSeen: row.lastSeen
|
|
20284
|
+
})
|
|
20285
|
+
);
|
|
19917
20286
|
return id;
|
|
19918
20287
|
}
|
|
19919
20288
|
// The full row, for round-trip assertions.
|
|
19920
20289
|
findById(id) {
|
|
19921
|
-
|
|
19922
|
-
return row;
|
|
20290
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19923
20291
|
}
|
|
19924
20292
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19925
20293
|
// served from the object_type index, never from audit_events.
|
|
19926
20294
|
distinctTitles(objectType) {
|
|
19927
|
-
const rows =
|
|
19928
|
-
|
|
20295
|
+
const rows = allRows(
|
|
20296
|
+
this.db.prepare(
|
|
20297
|
+
`SELECT DISTINCT title FROM inventory
|
|
19929
20298
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19930
20299
|
ORDER BY title`
|
|
19931
|
-
|
|
20300
|
+
),
|
|
20301
|
+
{ objectType }
|
|
20302
|
+
);
|
|
19932
20303
|
return rows.map((r) => r.title);
|
|
19933
20304
|
}
|
|
19934
20305
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19935
20306
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19936
20307
|
// QUERY PLAN).
|
|
19937
20308
|
osVersions() {
|
|
19938
|
-
const rows =
|
|
19939
|
-
|
|
20309
|
+
const rows = allRows(
|
|
20310
|
+
this.db.prepare(
|
|
20311
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19940
20312
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
19941
20313
|
ORDER BY value`
|
|
19942
|
-
|
|
20314
|
+
)
|
|
20315
|
+
);
|
|
19943
20316
|
return rows.map((r) => r.value);
|
|
19944
20317
|
}
|
|
19945
20318
|
};
|
|
@@ -19959,14 +20332,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
19959
20332
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
19960
20333
|
findingsCount: 0
|
|
19961
20334
|
};
|
|
19962
|
-
function safeJson(s, fallback) {
|
|
19963
|
-
if (s == null) return fallback;
|
|
19964
|
-
try {
|
|
19965
|
-
return JSON.parse(s);
|
|
19966
|
-
} catch {
|
|
19967
|
-
return fallback;
|
|
19968
|
-
}
|
|
19969
|
-
}
|
|
19970
20335
|
function resolveHarnessId(attrs, row) {
|
|
19971
20336
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
19972
20337
|
return attrs.provider;
|
|
@@ -20162,30 +20527,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20162
20527
|
configRowsCache;
|
|
20163
20528
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20164
20529
|
getInventoryStats() {
|
|
20165
|
-
const
|
|
20166
|
-
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
byType
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
|
|
20530
|
+
const typeCounts = countBy(
|
|
20531
|
+
this.db,
|
|
20532
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20533
|
+
);
|
|
20534
|
+
const byType = {
|
|
20535
|
+
project: 0,
|
|
20536
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20537
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20538
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20539
|
+
config: typeCounts.get("config") ?? 0
|
|
20540
|
+
};
|
|
20541
|
+
byType.project = countScalar(
|
|
20542
|
+
this.db,
|
|
20543
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20544
|
+
);
|
|
20545
|
+
const mcpTrustCounts = countBy(
|
|
20546
|
+
this.db,
|
|
20547
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20173
20548
|
FROM inventory_asset a
|
|
20174
20549
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20175
20550
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20176
20551
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20177
|
-
)
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20552
|
+
);
|
|
20553
|
+
const mcpTrust = {
|
|
20554
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20555
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20556
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20557
|
+
};
|
|
20558
|
+
const harnesses = countScalar(
|
|
20559
|
+
this.db,
|
|
20181
20560
|
`SELECT count(*) AS n FROM inventory
|
|
20182
20561
|
WHERE object_type = 'harness'
|
|
20183
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20184
|
-
|
|
20185
|
-
|
|
20186
|
-
const
|
|
20562
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20563
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20564
|
+
);
|
|
20565
|
+
const flaggedAssets = countScalar(
|
|
20566
|
+
this.db,
|
|
20567
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20568
|
+
);
|
|
20569
|
+
const flaggedProjects = countScalar(
|
|
20570
|
+
this.db,
|
|
20187
20571
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20188
|
-
)
|
|
20572
|
+
);
|
|
20189
20573
|
const configRows = this.configAssetRows();
|
|
20190
20574
|
for (const r of configRows) {
|
|
20191
20575
|
byType[r.assetType] += 1;
|
|
@@ -20442,12 +20826,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20442
20826
|
}
|
|
20443
20827
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20444
20828
|
fetchHarnessRows() {
|
|
20445
|
-
return
|
|
20446
|
-
|
|
20829
|
+
return allRows(
|
|
20830
|
+
this.db.prepare(
|
|
20831
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20447
20832
|
FROM inventory
|
|
20448
20833
|
WHERE object_type = 'harness'
|
|
20449
20834
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20450
|
-
|
|
20835
|
+
),
|
|
20836
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20837
|
+
);
|
|
20451
20838
|
}
|
|
20452
20839
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20453
20840
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20457,12 +20844,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20457
20844
|
const params = [...harnessInvIds];
|
|
20458
20845
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20459
20846
|
if (q) {
|
|
20460
|
-
const pat =
|
|
20461
|
-
where +=
|
|
20847
|
+
const pat = containsPattern(q);
|
|
20848
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20462
20849
|
params.push(pat, pat);
|
|
20463
20850
|
}
|
|
20464
|
-
const rows =
|
|
20465
|
-
|
|
20851
|
+
const rows = allRows(
|
|
20852
|
+
this.db.prepare(
|
|
20853
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20466
20854
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20467
20855
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20468
20856
|
FROM harness_asset ha
|
|
@@ -20470,7 +20858,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20470
20858
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20471
20859
|
WHERE ${where}
|
|
20472
20860
|
ORDER BY a.name ASC`
|
|
20473
|
-
|
|
20861
|
+
),
|
|
20862
|
+
params
|
|
20863
|
+
);
|
|
20474
20864
|
for (const raw of rows) {
|
|
20475
20865
|
const harnessInvId = raw.harnessInvId;
|
|
20476
20866
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20489,21 +20879,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20489
20879
|
params.push(...types);
|
|
20490
20880
|
}
|
|
20491
20881
|
if (q) {
|
|
20492
|
-
const pat =
|
|
20493
|
-
conditions.push("
|
|
20882
|
+
const pat = containsPattern(q);
|
|
20883
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20494
20884
|
params.push(pat, pat);
|
|
20495
20885
|
}
|
|
20496
20886
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20497
20887
|
const sampleRows = this.mapAssetRows(
|
|
20498
|
-
|
|
20499
|
-
|
|
20888
|
+
allRows(
|
|
20889
|
+
this.db.prepare(
|
|
20890
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20500
20891
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20501
20892
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20502
20893
|
FROM inventory_asset a
|
|
20503
20894
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20504
20895
|
${where}
|
|
20505
20896
|
ORDER BY a.name ASC`
|
|
20506
|
-
|
|
20897
|
+
),
|
|
20898
|
+
params
|
|
20899
|
+
)
|
|
20507
20900
|
);
|
|
20508
20901
|
const configRows = this.configAssetRows(q).filter(
|
|
20509
20902
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20512,14 +20905,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20512
20905
|
}
|
|
20513
20906
|
fetchAssetById(assetId) {
|
|
20514
20907
|
const rows = this.mapAssetRows(
|
|
20515
|
-
|
|
20516
|
-
|
|
20908
|
+
allRows(
|
|
20909
|
+
this.db.prepare(
|
|
20910
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20517
20911
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20518
20912
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20519
20913
|
FROM inventory_asset a
|
|
20520
20914
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20521
20915
|
WHERE a.id = ?`
|
|
20522
|
-
|
|
20916
|
+
),
|
|
20917
|
+
[assetId]
|
|
20918
|
+
)
|
|
20523
20919
|
);
|
|
20524
20920
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20525
20921
|
}
|
|
@@ -20575,37 +20971,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20575
20971
|
return rows;
|
|
20576
20972
|
}
|
|
20577
20973
|
latestConfigScanId() {
|
|
20578
|
-
|
|
20579
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20580
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20581
|
-
).get();
|
|
20582
|
-
return row?.id ?? null;
|
|
20974
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20583
20975
|
}
|
|
20584
20976
|
fetchProjects(q) {
|
|
20585
20977
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20586
20978
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20587
20979
|
const params = [];
|
|
20588
20980
|
if (q) {
|
|
20589
|
-
const pat =
|
|
20590
|
-
sql +=
|
|
20981
|
+
const pat = containsPattern(q);
|
|
20982
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20591
20983
|
params.push(pat, pat);
|
|
20592
20984
|
}
|
|
20593
20985
|
sql += " ORDER BY name ASC";
|
|
20594
|
-
return this.db.prepare(sql)
|
|
20986
|
+
return allRows(this.db.prepare(sql), params);
|
|
20595
20987
|
}
|
|
20596
20988
|
fetchProjectById(projectId) {
|
|
20597
|
-
return
|
|
20598
|
-
|
|
20599
|
-
|
|
20989
|
+
return getRow(
|
|
20990
|
+
this.db.prepare(
|
|
20991
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
20992
|
+
),
|
|
20993
|
+
[projectId]
|
|
20994
|
+
) ?? null;
|
|
20600
20995
|
}
|
|
20601
20996
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20602
20997
|
fetchProjectsByIds(projectIds) {
|
|
20603
20998
|
const map2 = /* @__PURE__ */ new Map();
|
|
20604
20999
|
if (projectIds.length === 0) return map2;
|
|
20605
|
-
const rows =
|
|
20606
|
-
|
|
21000
|
+
const rows = allRows(
|
|
21001
|
+
this.db.prepare(
|
|
21002
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20607
21003
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20608
|
-
|
|
21004
|
+
),
|
|
21005
|
+
projectIds
|
|
21006
|
+
);
|
|
20609
21007
|
for (const r of rows) map2.set(r.id, r);
|
|
20610
21008
|
return map2;
|
|
20611
21009
|
}
|
|
@@ -20616,8 +21014,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20616
21014
|
projectAggregates(projectIds) {
|
|
20617
21015
|
const map2 = /* @__PURE__ */ new Map();
|
|
20618
21016
|
if (projectIds.length === 0) return map2;
|
|
20619
|
-
const rows =
|
|
20620
|
-
|
|
21017
|
+
const rows = allRows(
|
|
21018
|
+
this.db.prepare(
|
|
21019
|
+
`SELECT f.project_id AS projectId,
|
|
20621
21020
|
coalesce(o.access, f.default_access) AS eff,
|
|
20622
21021
|
count(*) AS n,
|
|
20623
21022
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20625,7 +21024,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20625
21024
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20626
21025
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20627
21026
|
GROUP BY f.project_id, eff`
|
|
20628
|
-
|
|
21027
|
+
),
|
|
21028
|
+
projectIds
|
|
21029
|
+
);
|
|
20629
21030
|
for (const r of rows) {
|
|
20630
21031
|
let agg = map2.get(r.projectId);
|
|
20631
21032
|
if (!agg) {
|
|
@@ -20663,37 +21064,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20663
21064
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20664
21065
|
if (prefix === "") {
|
|
20665
21066
|
return this.mapFileRows(
|
|
20666
|
-
|
|
21067
|
+
allRows(
|
|
21068
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21069
|
+
[projectId]
|
|
21070
|
+
)
|
|
20667
21071
|
);
|
|
20668
21072
|
}
|
|
20669
21073
|
return this.mapFileRows(
|
|
20670
|
-
|
|
20671
|
-
this.
|
|
20672
|
-
|
|
21074
|
+
allRows(
|
|
21075
|
+
this.db.prepare(
|
|
21076
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21077
|
+
),
|
|
21078
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21079
|
+
)
|
|
20673
21080
|
);
|
|
20674
21081
|
}
|
|
20675
21082
|
fetchProjectFilesSearch(projectId, q) {
|
|
20676
|
-
const pat =
|
|
21083
|
+
const pat = containsPattern(q);
|
|
20677
21084
|
return this.mapFileRows(
|
|
20678
|
-
|
|
20679
|
-
this.
|
|
20680
|
-
|
|
20681
|
-
|
|
20682
|
-
|
|
21085
|
+
allRows(
|
|
21086
|
+
this.db.prepare(
|
|
21087
|
+
this.fileSelect(
|
|
21088
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21089
|
+
)
|
|
21090
|
+
),
|
|
21091
|
+
[projectId, pat, pat]
|
|
21092
|
+
)
|
|
20683
21093
|
);
|
|
20684
21094
|
}
|
|
20685
21095
|
fetchProjectFilesBlocked(projectId) {
|
|
20686
21096
|
return this.mapFileRows(
|
|
20687
|
-
|
|
20688
|
-
this.
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
21097
|
+
allRows(
|
|
21098
|
+
this.db.prepare(
|
|
21099
|
+
this.fileSelect(
|
|
21100
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21101
|
+
)
|
|
21102
|
+
),
|
|
21103
|
+
[projectId]
|
|
21104
|
+
)
|
|
20692
21105
|
);
|
|
20693
21106
|
}
|
|
20694
21107
|
fetchProjectFile(projectId, path) {
|
|
20695
21108
|
const rows = this.mapFileRows(
|
|
20696
|
-
|
|
21109
|
+
allRows(
|
|
21110
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21111
|
+
[projectId, path]
|
|
21112
|
+
)
|
|
20697
21113
|
);
|
|
20698
21114
|
return rows[0] ?? null;
|
|
20699
21115
|
}
|
|
@@ -20707,39 +21123,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20707
21123
|
}
|
|
20708
21124
|
db;
|
|
20709
21125
|
readPolicies() {
|
|
20710
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20711
|
-
const policies =
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
customKeywords
|
|
20724
|
-
})
|
|
20725
|
-
);
|
|
20726
|
-
} catch {
|
|
20727
|
-
}
|
|
20728
|
-
}
|
|
21126
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21127
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21128
|
+
const target = JSON.parse(row.target);
|
|
21129
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21130
|
+
return Policy.parse({
|
|
21131
|
+
id: row.id,
|
|
21132
|
+
scope: row.scope,
|
|
21133
|
+
target,
|
|
21134
|
+
action: row.action,
|
|
21135
|
+
enabled: intToBool(row.enabled),
|
|
21136
|
+
customKeywords
|
|
21137
|
+
});
|
|
21138
|
+
});
|
|
20729
21139
|
return Promise.resolve(policies);
|
|
20730
21140
|
}
|
|
20731
21141
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20732
21142
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20733
21143
|
// so a user's edits are never clobbered.
|
|
20734
21144
|
seedDefaults() {
|
|
20735
|
-
const count = this.db
|
|
21145
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20736
21146
|
if (count > 0) return;
|
|
20737
21147
|
const stmt = this.db.prepare(
|
|
20738
21148
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20739
21149
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20740
21150
|
);
|
|
20741
|
-
this.db
|
|
20742
|
-
try {
|
|
21151
|
+
failOpenTransaction(this.db, () => {
|
|
20743
21152
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20744
21153
|
stmt.run({
|
|
20745
21154
|
id: randomUUID4(),
|
|
@@ -20748,10 +21157,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20748
21157
|
now: Date.now()
|
|
20749
21158
|
});
|
|
20750
21159
|
}
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
|
|
20754
|
-
|
|
21160
|
+
});
|
|
21161
|
+
}
|
|
21162
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21163
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21164
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21165
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21166
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21167
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21168
|
+
upsertCategoryAction(category, action) {
|
|
21169
|
+
const now = Date.now();
|
|
21170
|
+
this.db.prepare(
|
|
21171
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21172
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21173
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21174
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21175
|
+
}
|
|
21176
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21177
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21178
|
+
// Returns the number of rows changed.
|
|
21179
|
+
capCategoryActions() {
|
|
21180
|
+
const info = this.db.prepare(
|
|
21181
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21182
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21183
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21184
|
+
).run({ now: Date.now() });
|
|
21185
|
+
return Number(info.changes);
|
|
21186
|
+
}
|
|
21187
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21188
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21189
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21190
|
+
getCategoryAction(category) {
|
|
21191
|
+
const row = this.db.prepare(
|
|
21192
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21193
|
+
).get({ category });
|
|
21194
|
+
return row?.action;
|
|
20755
21195
|
}
|
|
20756
21196
|
};
|
|
20757
21197
|
|
|
@@ -20827,7 +21267,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20827
21267
|
maxStampStmt;
|
|
20828
21268
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20829
21269
|
replaceForProject(projectId, scan2, now) {
|
|
20830
|
-
const
|
|
21270
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20831
21271
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20832
21272
|
for (const file2 of scan2.files) {
|
|
20833
21273
|
this.upsertStmt.run({
|
|
@@ -20910,7 +21350,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20910
21350
|
}
|
|
20911
21351
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20912
21352
|
latestByKey(key) {
|
|
20913
|
-
const row = this.latestStmt
|
|
21353
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20914
21354
|
if (!row) return void 0;
|
|
20915
21355
|
return {
|
|
20916
21356
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20927,7 +21367,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20927
21367
|
* the CLI) surfaces for that file.
|
|
20928
21368
|
*/
|
|
20929
21369
|
openAtRestKeysForPath(path) {
|
|
20930
|
-
const rows = this.openAtRestStmt
|
|
21370
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20931
21371
|
return rows.map((r) => r.finding_key);
|
|
20932
21372
|
}
|
|
20933
21373
|
/**
|
|
@@ -20938,7 +21378,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20938
21378
|
* resolution row (see scan.ts).
|
|
20939
21379
|
*/
|
|
20940
21380
|
resolvedAtRestKeysForPath(path) {
|
|
20941
|
-
const rows = this.resolvedAtRestStmt
|
|
21381
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
20942
21382
|
return rows.map((r) => r.finding_key);
|
|
20943
21383
|
}
|
|
20944
21384
|
};
|
|
@@ -20967,31 +21407,25 @@ var SqliteScanLedgerRepository = class {
|
|
|
20967
21407
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
20968
21408
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
20969
21409
|
entriesForRuleset(rulesetHash) {
|
|
20970
|
-
const rows = this.readStmt
|
|
21410
|
+
const rows = allRows(this.readStmt, {
|
|
21411
|
+
rulesetHash
|
|
21412
|
+
});
|
|
20971
21413
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
20972
21414
|
}
|
|
20973
21415
|
upsertEntries(entries) {
|
|
20974
21416
|
if (entries.length === 0) return;
|
|
20975
21417
|
const scannedAt = Date.now();
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
20983
|
-
|
|
20984
|
-
|
|
20985
|
-
scannedAt
|
|
20986
|
-
});
|
|
20987
|
-
}
|
|
20988
|
-
this.db.exec("COMMIT");
|
|
20989
|
-
} catch (err) {
|
|
20990
|
-
this.db.exec("ROLLBACK");
|
|
20991
|
-
throw err;
|
|
21418
|
+
failOpenTransaction(this.db, () => {
|
|
21419
|
+
for (const entry of entries) {
|
|
21420
|
+
this.upsertStmt.run({
|
|
21421
|
+
path: entry.path,
|
|
21422
|
+
mtime: entry.mtime,
|
|
21423
|
+
contentHash: entry.contentHash,
|
|
21424
|
+
rulesetHash: entry.rulesetHash,
|
|
21425
|
+
scannedAt
|
|
21426
|
+
});
|
|
20992
21427
|
}
|
|
20993
|
-
}
|
|
20994
|
-
}
|
|
21428
|
+
});
|
|
20995
21429
|
}
|
|
20996
21430
|
};
|
|
20997
21431
|
|
|
@@ -21068,8 +21502,9 @@ var SqliteSecurityRepository = class {
|
|
|
21068
21502
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21069
21503
|
// double-counting a key that accumulated several append-only rows.
|
|
21070
21504
|
severitySummary() {
|
|
21071
|
-
const rows =
|
|
21072
|
-
|
|
21505
|
+
const rows = allRows(
|
|
21506
|
+
this.db.prepare(
|
|
21507
|
+
`SELECT f.severity AS severity,
|
|
21073
21508
|
COUNT(*) AS count,
|
|
21074
21509
|
SUM(CASE
|
|
21075
21510
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21088,7 +21523,8 @@ var SqliteSecurityRepository = class {
|
|
|
21088
21523
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21089
21524
|
ON latest.finding_key = f.finding_key
|
|
21090
21525
|
GROUP BY f.severity`
|
|
21091
|
-
|
|
21526
|
+
)
|
|
21527
|
+
);
|
|
21092
21528
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21093
21529
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21094
21530
|
severity,
|
|
@@ -21172,14 +21608,15 @@ var SqliteSecurityRepository = class {
|
|
|
21172
21608
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21173
21609
|
const now = this.now();
|
|
21174
21610
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21175
|
-
const rows =
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21180
|
-
|
|
21181
|
-
|
|
21182
|
-
|
|
21611
|
+
const rows = allRows(
|
|
21612
|
+
this.db.prepare(
|
|
21613
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21614
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21615
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21616
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21617
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21618
|
+
// backfill left null.
|
|
21619
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21183
21620
|
(
|
|
21184
21621
|
SELECT fr.status FROM finding_resolution fr
|
|
21185
21622
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21205,14 +21642,16 @@ var SqliteSecurityRepository = class {
|
|
|
21205
21642
|
WHERE fr.finding_key = f.finding_key
|
|
21206
21643
|
AND fr.resolved_at >= :windowStart
|
|
21207
21644
|
)`
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
|
|
21645
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21646
|
+
// any resolution activity at/after the window start — a row this method
|
|
21647
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21648
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21649
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21650
|
+
// dialect-agnostic. Without this, a
|
|
21651
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21652
|
+
),
|
|
21653
|
+
{ windowStart }
|
|
21654
|
+
);
|
|
21216
21655
|
const sums = /* @__PURE__ */ new Map();
|
|
21217
21656
|
const counts = /* @__PURE__ */ new Map();
|
|
21218
21657
|
for (const r of rows) {
|
|
@@ -21242,8 +21681,9 @@ var SqliteSecurityRepository = class {
|
|
|
21242
21681
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21243
21682
|
const now = this.now();
|
|
21244
21683
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21245
|
-
const rows =
|
|
21246
|
-
|
|
21684
|
+
const rows = allRows(
|
|
21685
|
+
this.db.prepare(
|
|
21686
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21247
21687
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21248
21688
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21249
21689
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21251,7 +21691,9 @@ var SqliteSecurityRepository = class {
|
|
|
21251
21691
|
GROUP BY repo
|
|
21252
21692
|
ORDER BY c DESC, repo
|
|
21253
21693
|
LIMIT :limit`
|
|
21254
|
-
|
|
21694
|
+
),
|
|
21695
|
+
{ from, to: now, limit }
|
|
21696
|
+
);
|
|
21255
21697
|
const items = rows.map((r) => ({
|
|
21256
21698
|
id: `repo_${r.repo}`,
|
|
21257
21699
|
name: r.repo,
|
|
@@ -21273,8 +21715,9 @@ var SqliteSecurityRepository = class {
|
|
|
21273
21715
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21274
21716
|
// capped at `limit`.
|
|
21275
21717
|
recentlyResolved(limit = 20) {
|
|
21276
|
-
const rows =
|
|
21277
|
-
|
|
21718
|
+
const rows = allRows(
|
|
21719
|
+
this.db.prepare(
|
|
21720
|
+
`SELECT f.finding_key AS finding_key,
|
|
21278
21721
|
f.rule_id AS rule_id,
|
|
21279
21722
|
f.severity AS severity,
|
|
21280
21723
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21308,7 +21751,9 @@ var SqliteSecurityRepository = class {
|
|
|
21308
21751
|
) IS NOT NULL
|
|
21309
21752
|
ORDER BY latest_resolved_at DESC
|
|
21310
21753
|
LIMIT :limit`
|
|
21311
|
-
|
|
21754
|
+
),
|
|
21755
|
+
{ limit }
|
|
21756
|
+
);
|
|
21312
21757
|
const items = rows.map((r) => ({
|
|
21313
21758
|
findingKey: r.finding_key,
|
|
21314
21759
|
ruleId: r.rule_id,
|
|
@@ -21325,12 +21770,15 @@ var SqliteSecurityRepository = class {
|
|
|
21325
21770
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21326
21771
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21327
21772
|
findingsInRange(fromMs, toMs) {
|
|
21328
|
-
const rows =
|
|
21329
|
-
|
|
21773
|
+
const rows = allRows(
|
|
21774
|
+
this.db.prepare(
|
|
21775
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21330
21776
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21331
21777
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21332
21778
|
ORDER BY e.occurred_at`
|
|
21333
|
-
|
|
21779
|
+
),
|
|
21780
|
+
{ from: fromMs, to: toMs }
|
|
21781
|
+
);
|
|
21334
21782
|
return rows.map((r) => ({
|
|
21335
21783
|
occurredAt: r.occurred_at,
|
|
21336
21784
|
severity: r.severity,
|
|
@@ -21344,12 +21792,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21344
21792
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21345
21793
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21346
21794
|
function parseNetwork(networkJson) {
|
|
21347
|
-
|
|
21348
|
-
try {
|
|
21349
|
-
return JSON.parse(networkJson);
|
|
21350
|
-
} catch {
|
|
21351
|
-
return null;
|
|
21352
|
-
}
|
|
21795
|
+
return safeJson(networkJson, null);
|
|
21353
21796
|
}
|
|
21354
21797
|
function toEndpointSummary(row) {
|
|
21355
21798
|
return {
|
|
@@ -21436,27 +21879,39 @@ var SqliteSharesRepository = class {
|
|
|
21436
21879
|
}
|
|
21437
21880
|
db;
|
|
21438
21881
|
stats() {
|
|
21439
|
-
const
|
|
21440
|
-
const
|
|
21441
|
-
const
|
|
21442
|
-
const
|
|
21443
|
-
|
|
21882
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
21883
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
21884
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
21885
|
+
const insecure = countScalar(
|
|
21886
|
+
this.db,
|
|
21444
21887
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21445
21888
|
);
|
|
21446
|
-
const needsReview =
|
|
21889
|
+
const needsReview = countScalar(
|
|
21890
|
+
this.db,
|
|
21447
21891
|
`SELECT count(DISTINCT d.id) AS n
|
|
21448
21892
|
FROM share_destination d
|
|
21449
21893
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21450
21894
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21451
21895
|
);
|
|
21452
|
-
const
|
|
21453
|
-
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
const
|
|
21457
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
21896
|
+
const kindCounts = countBy(
|
|
21897
|
+
this.db,
|
|
21898
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
21899
|
+
);
|
|
21900
|
+
const byKind = {
|
|
21901
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
21902
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
21903
|
+
ip: kindCounts.get("ip") ?? 0
|
|
21904
|
+
};
|
|
21905
|
+
const trustCounts = countBy(
|
|
21906
|
+
this.db,
|
|
21907
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
21908
|
+
);
|
|
21909
|
+
const byTrust = {
|
|
21910
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
21911
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
21912
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
21913
|
+
ip: trustCounts.get("ip") ?? 0
|
|
21914
|
+
};
|
|
21460
21915
|
return Promise.resolve({
|
|
21461
21916
|
destinations,
|
|
21462
21917
|
endpoints,
|
|
@@ -21572,7 +22027,7 @@ var SqliteSharesRepository = class {
|
|
|
21572
22027
|
}
|
|
21573
22028
|
let sql;
|
|
21574
22029
|
if (q) {
|
|
21575
|
-
const pattern =
|
|
22030
|
+
const pattern = containsPattern(q);
|
|
21576
22031
|
conditions.push(
|
|
21577
22032
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21578
22033
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21592,24 +22047,31 @@ var SqliteSharesRepository = class {
|
|
|
21592
22047
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21593
22048
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21594
22049
|
}
|
|
21595
|
-
const rows =
|
|
22050
|
+
const rows = allRows(
|
|
22051
|
+
this.db.prepare(sql),
|
|
22052
|
+
params
|
|
22053
|
+
);
|
|
21596
22054
|
return rows.map((r) => this.mapDestRow(r));
|
|
21597
22055
|
}
|
|
21598
22056
|
fetchDestinationById(destinationId) {
|
|
21599
|
-
const row =
|
|
21600
|
-
|
|
22057
|
+
const row = getRow(
|
|
22058
|
+
this.db.prepare(
|
|
22059
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21601
22060
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21602
22061
|
o.decision AS overrideDecision
|
|
21603
22062
|
FROM share_destination d
|
|
21604
22063
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21605
22064
|
WHERE d.id = ?`
|
|
21606
|
-
|
|
22065
|
+
),
|
|
22066
|
+
[destinationId]
|
|
22067
|
+
);
|
|
21607
22068
|
return row ? this.mapDestRow(row) : null;
|
|
21608
22069
|
}
|
|
21609
22070
|
fetchEndpoints(destinationIds) {
|
|
21610
22071
|
if (destinationIds.length === 0) return [];
|
|
21611
|
-
const rows =
|
|
21612
|
-
|
|
22072
|
+
const rows = allRows(
|
|
22073
|
+
this.db.prepare(
|
|
22074
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21613
22075
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21614
22076
|
count(c.id) AS callSiteCount
|
|
21615
22077
|
FROM share_endpoint e
|
|
@@ -21617,7 +22079,9 @@ var SqliteSharesRepository = class {
|
|
|
21617
22079
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21618
22080
|
GROUP BY e.id
|
|
21619
22081
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21620
|
-
|
|
22082
|
+
),
|
|
22083
|
+
destinationIds
|
|
22084
|
+
);
|
|
21621
22085
|
return rows.map((r) => ({
|
|
21622
22086
|
id: r.id,
|
|
21623
22087
|
destinationId: r.destinationId,
|
|
@@ -21642,13 +22106,16 @@ var SqliteSharesRepository = class {
|
|
|
21642
22106
|
}
|
|
21643
22107
|
fetchCallSites(endpointIds) {
|
|
21644
22108
|
if (endpointIds.length === 0) return [];
|
|
21645
|
-
const rows =
|
|
21646
|
-
|
|
22109
|
+
const rows = allRows(
|
|
22110
|
+
this.db.prepare(
|
|
22111
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21647
22112
|
project_id AS projectId
|
|
21648
22113
|
FROM share_call_site
|
|
21649
22114
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21650
22115
|
ORDER BY created_at ASC, id ASC`
|
|
21651
|
-
|
|
22116
|
+
),
|
|
22117
|
+
endpointIds
|
|
22118
|
+
);
|
|
21652
22119
|
return rows.map((r) => ({
|
|
21653
22120
|
id: r.id,
|
|
21654
22121
|
endpointId: r.endpointId,
|
|
@@ -21684,27 +22151,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21684
22151
|
upsert(input, now = Date.now()) {
|
|
21685
22152
|
const id = sourceProjectId(input.url);
|
|
21686
22153
|
const row = toSourceProjectRow(input, id, now);
|
|
21687
|
-
this.upsertStmt.run(
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
21692
|
-
|
|
21693
|
-
|
|
21694
|
-
|
|
22154
|
+
this.upsertStmt.run(
|
|
22155
|
+
bindParams({
|
|
22156
|
+
id: row.id,
|
|
22157
|
+
url: row.url,
|
|
22158
|
+
name: row.name,
|
|
22159
|
+
attributes: row.attributes,
|
|
22160
|
+
firstSeen: row.firstSeen,
|
|
22161
|
+
lastSeen: row.lastSeen
|
|
22162
|
+
})
|
|
22163
|
+
);
|
|
21695
22164
|
return id;
|
|
21696
22165
|
}
|
|
21697
22166
|
findById(id) {
|
|
21698
|
-
return
|
|
22167
|
+
return getRow(
|
|
22168
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22169
|
+
{
|
|
22170
|
+
id
|
|
22171
|
+
}
|
|
22172
|
+
);
|
|
21699
22173
|
}
|
|
21700
22174
|
// Distinct project names — a filter facet, served from the source_project
|
|
21701
22175
|
// table, never from the audit fact table.
|
|
21702
22176
|
distinctNames() {
|
|
21703
|
-
const rows =
|
|
21704
|
-
|
|
22177
|
+
const rows = allRows(
|
|
22178
|
+
this.db.prepare(
|
|
22179
|
+
`SELECT DISTINCT name FROM source_project
|
|
21705
22180
|
WHERE name IS NOT NULL
|
|
21706
22181
|
ORDER BY name`
|
|
21707
|
-
|
|
22182
|
+
)
|
|
22183
|
+
);
|
|
21708
22184
|
return rows.map((r) => r.name);
|
|
21709
22185
|
}
|
|
21710
22186
|
};
|
|
@@ -21723,8 +22199,7 @@ function hasLegacySampleRows(db) {
|
|
|
21723
22199
|
function purgeSampleData(db) {
|
|
21724
22200
|
try {
|
|
21725
22201
|
if (!hasLegacySampleRows(db)) return;
|
|
21726
|
-
db
|
|
21727
|
-
try {
|
|
22202
|
+
withTransaction(db, () => {
|
|
21728
22203
|
db.exec(
|
|
21729
22204
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21730
22205
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21769,11 +22244,7 @@ function purgeSampleData(db) {
|
|
|
21769
22244
|
value TEXT NOT NULL
|
|
21770
22245
|
)`);
|
|
21771
22246
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21772
|
-
|
|
21773
|
-
} catch (err) {
|
|
21774
|
-
db.exec("ROLLBACK");
|
|
21775
|
-
throw err;
|
|
21776
|
-
}
|
|
22247
|
+
});
|
|
21777
22248
|
} catch {
|
|
21778
22249
|
}
|
|
21779
22250
|
}
|
|
@@ -21792,7 +22263,7 @@ function openWithPragmas(file2) {
|
|
|
21792
22263
|
function backupLegacyStore(file2) {
|
|
21793
22264
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21794
22265
|
renameSync(file2, backup);
|
|
21795
|
-
for (const sidecar of
|
|
22266
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21796
22267
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21797
22268
|
}
|
|
21798
22269
|
return backup;
|
|
@@ -21805,9 +22276,8 @@ function openLocalDatabase(dir) {
|
|
|
21805
22276
|
db.close();
|
|
21806
22277
|
const backup = backupLegacyStore(file2);
|
|
21807
22278
|
db = openWithPragmas(file2);
|
|
21808
|
-
|
|
21809
|
-
`
|
|
21810
|
-
`
|
|
22279
|
+
akaWarn(
|
|
22280
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21811
22281
|
);
|
|
21812
22282
|
}
|
|
21813
22283
|
applyMigrations(db);
|
|
@@ -21835,96 +22305,77 @@ function openLocalDatabase(dir) {
|
|
|
21835
22305
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21836
22306
|
policies.seedDefaults();
|
|
21837
22307
|
function recordCapture(event, detected) {
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21843
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21844
|
-
db.exec("COMMIT");
|
|
21845
|
-
} catch (err) {
|
|
21846
|
-
db.exec("ROLLBACK");
|
|
21847
|
-
throw err;
|
|
21848
|
-
}
|
|
21849
|
-
} catch {
|
|
21850
|
-
}
|
|
22308
|
+
failOpenTransaction(db, () => {
|
|
22309
|
+
events.insertEvent(event);
|
|
22310
|
+
const sessionId = event.metadata?.sessionId;
|
|
22311
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22312
|
+
});
|
|
21851
22313
|
}
|
|
21852
22314
|
function ensureInventory(ctx) {
|
|
21853
22315
|
const resolved = {};
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
db.exec("COMMIT");
|
|
21875
|
-
} catch (err) {
|
|
21876
|
-
db.exec("ROLLBACK");
|
|
21877
|
-
throw err;
|
|
21878
|
-
}
|
|
21879
|
-
} catch {
|
|
21880
|
-
return {};
|
|
21881
|
-
}
|
|
21882
|
-
return resolved;
|
|
22316
|
+
const committed = failOpenTransaction(db, () => {
|
|
22317
|
+
const now = Date.now();
|
|
22318
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22319
|
+
if (ctx.harness) {
|
|
22320
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22321
|
+
}
|
|
22322
|
+
resolved.accountId = inventory.upsert(
|
|
22323
|
+
linkHost(
|
|
22324
|
+
{
|
|
22325
|
+
objectType: "user",
|
|
22326
|
+
identityKey: "local",
|
|
22327
|
+
attributes: { source: "local" }
|
|
22328
|
+
},
|
|
22329
|
+
resolved.hostId
|
|
22330
|
+
),
|
|
22331
|
+
now
|
|
22332
|
+
);
|
|
22333
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22334
|
+
});
|
|
22335
|
+
return committed ? resolved : {};
|
|
21883
22336
|
}
|
|
21884
22337
|
function recordConfigScan(record2) {
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
}
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
|
|
21903
|
-
|
|
21904
|
-
|
|
21905
|
-
confidence: finding.confidence
|
|
21906
|
-
});
|
|
21907
|
-
}
|
|
21908
|
-
db.exec("COMMIT");
|
|
21909
|
-
} catch (err) {
|
|
21910
|
-
db.exec("ROLLBACK");
|
|
21911
|
-
throw err;
|
|
22338
|
+
failOpenTransaction(db, () => {
|
|
22339
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22340
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22341
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22342
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22343
|
+
for (const def of record2.definitions ?? []) {
|
|
22344
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22345
|
+
}
|
|
22346
|
+
for (const finding of record2.findings ?? []) {
|
|
22347
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22348
|
+
if (!definitionId) continue;
|
|
22349
|
+
inspectionFindings.insertFinding({
|
|
22350
|
+
id: randomUUID8(),
|
|
22351
|
+
auditEventId: record2.scanEvent.id,
|
|
22352
|
+
inspectionDefinitionId: definitionId,
|
|
22353
|
+
span: finding.span,
|
|
22354
|
+
maskedMatch: finding.maskedMatch,
|
|
22355
|
+
actionTaken: finding.actionTaken,
|
|
22356
|
+
confidence: finding.confidence
|
|
22357
|
+
});
|
|
21912
22358
|
}
|
|
21913
|
-
}
|
|
21914
|
-
}
|
|
22359
|
+
});
|
|
21915
22360
|
}
|
|
21916
22361
|
function recordProjectFiles(projectId, scan2) {
|
|
21917
22362
|
if (scan2.files.length === 0) return;
|
|
22363
|
+
failOpenTransaction(db, () => {
|
|
22364
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22365
|
+
});
|
|
22366
|
+
}
|
|
22367
|
+
async function transaction(fn) {
|
|
22368
|
+
db.exec("BEGIN");
|
|
21918
22369
|
try {
|
|
21919
|
-
|
|
22370
|
+
const result = await fn();
|
|
22371
|
+
db.exec("COMMIT");
|
|
22372
|
+
return result;
|
|
22373
|
+
} catch (err) {
|
|
21920
22374
|
try {
|
|
21921
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21922
|
-
db.exec("COMMIT");
|
|
21923
|
-
} catch (err) {
|
|
21924
22375
|
db.exec("ROLLBACK");
|
|
21925
|
-
|
|
22376
|
+
} catch {
|
|
21926
22377
|
}
|
|
21927
|
-
|
|
22378
|
+
throw err;
|
|
21928
22379
|
}
|
|
21929
22380
|
}
|
|
21930
22381
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -21943,8 +22394,7 @@ function openLocalDatabase(dir) {
|
|
|
21943
22394
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
21944
22395
|
});
|
|
21945
22396
|
if (stale.length === 0) return;
|
|
21946
|
-
db
|
|
21947
|
-
try {
|
|
22397
|
+
withTransaction(db, () => {
|
|
21948
22398
|
for (const { id } of stale) {
|
|
21949
22399
|
db.prepare(
|
|
21950
22400
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -21956,11 +22406,7 @@ function openLocalDatabase(dir) {
|
|
|
21956
22406
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
21957
22407
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
21958
22408
|
}
|
|
21959
|
-
|
|
21960
|
-
} catch (err) {
|
|
21961
|
-
db.exec("ROLLBACK");
|
|
21962
|
-
throw err;
|
|
21963
|
-
}
|
|
22409
|
+
});
|
|
21964
22410
|
} catch {
|
|
21965
22411
|
}
|
|
21966
22412
|
}
|
|
@@ -22002,6 +22448,7 @@ function openLocalDatabase(dir) {
|
|
|
22002
22448
|
purgeSampleData: () => {
|
|
22003
22449
|
purgeSampleData(db);
|
|
22004
22450
|
},
|
|
22451
|
+
transaction,
|
|
22005
22452
|
close: () => {
|
|
22006
22453
|
db.close();
|
|
22007
22454
|
}
|
|
@@ -22122,12 +22569,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22122
22569
|
}
|
|
22123
22570
|
}
|
|
22124
22571
|
function readJson(file2) {
|
|
22572
|
+
let text;
|
|
22125
22573
|
try {
|
|
22126
|
-
|
|
22127
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22574
|
+
text = readFileSync2(file2, "utf8");
|
|
22128
22575
|
} catch {
|
|
22129
22576
|
return null;
|
|
22130
22577
|
}
|
|
22578
|
+
return parseJsonObject(text) ?? null;
|
|
22579
|
+
}
|
|
22580
|
+
|
|
22581
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22582
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22583
|
+
import { join as join5 } from "path";
|
|
22584
|
+
var MARKER = "warn-era-capped";
|
|
22585
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22586
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22587
|
+
const marker = join5(dataDir2, MARKER);
|
|
22588
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22589
|
+
const capped = db.policies.capCategoryActions();
|
|
22590
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22591
|
+
`, { mode: DATA_FILE_MODE });
|
|
22592
|
+
return { capped };
|
|
22131
22593
|
}
|
|
22132
22594
|
|
|
22133
22595
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22212,7 +22674,7 @@ function resolveProviderSafe() {
|
|
|
22212
22674
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22213
22675
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22214
22676
|
import { homedir as homedir2 } from "os";
|
|
22215
|
-
import { basename as basename2, join as
|
|
22677
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22216
22678
|
|
|
22217
22679
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22218
22680
|
var KeywordMatcher2 = class {
|
|
@@ -24591,8 +25053,8 @@ function scanText(text) {
|
|
|
24591
25053
|
}
|
|
24592
25054
|
|
|
24593
25055
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24594
|
-
import { existsSync as
|
|
24595
|
-
import { basename, dirname, isAbsolute, join as
|
|
25056
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25057
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24596
25058
|
function resolveRepoIdentity(cwd) {
|
|
24597
25059
|
try {
|
|
24598
25060
|
const root = findGitRoot(cwd);
|
|
@@ -24625,32 +25087,32 @@ function resolveRepoNwo(cwd) {
|
|
|
24625
25087
|
function findGitRoot(start) {
|
|
24626
25088
|
let dir = start;
|
|
24627
25089
|
for (; ; ) {
|
|
24628
|
-
if (
|
|
25090
|
+
if (existsSync3(join6(dir, ".git"))) return dir;
|
|
24629
25091
|
const parent = dirname(dir);
|
|
24630
25092
|
if (parent === dir) return void 0;
|
|
24631
25093
|
dir = parent;
|
|
24632
25094
|
}
|
|
24633
25095
|
}
|
|
24634
25096
|
function resolveGitContext(root) {
|
|
24635
|
-
const dotGit =
|
|
25097
|
+
const dotGit = join6(root, ".git");
|
|
24636
25098
|
try {
|
|
24637
25099
|
if (statSync(dotGit).isDirectory()) {
|
|
24638
|
-
return { configPath:
|
|
25100
|
+
return { configPath: join6(dotGit, "config"), headRoot: root };
|
|
24639
25101
|
}
|
|
24640
25102
|
} catch {
|
|
24641
25103
|
return void 0;
|
|
24642
25104
|
}
|
|
24643
25105
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
24644
25106
|
if (!target) return void 0;
|
|
24645
|
-
const gitdir = isAbsolute(target) ? target :
|
|
24646
|
-
if (
|
|
24647
|
-
return { configPath:
|
|
25107
|
+
const gitdir = isAbsolute(target) ? target : join6(root, target);
|
|
25108
|
+
if (existsSync3(join6(gitdir, "config"))) {
|
|
25109
|
+
return { configPath: join6(gitdir, "config"), headRoot: root };
|
|
24648
25110
|
}
|
|
24649
|
-
const commonRaw = safeRead(
|
|
25111
|
+
const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
|
|
24650
25112
|
if (!commonRaw) return void 0;
|
|
24651
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
25113
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
|
|
24652
25114
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
24653
|
-
return { configPath:
|
|
25115
|
+
return { configPath: join6(commonGitDir, "config"), headRoot };
|
|
24654
25116
|
}
|
|
24655
25117
|
function safeRead(path) {
|
|
24656
25118
|
try {
|
|
@@ -24767,16 +25229,57 @@ function resolveInventoryContext(input) {
|
|
|
24767
25229
|
}
|
|
24768
25230
|
|
|
24769
25231
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24770
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24771
|
-
import { join as
|
|
25232
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25233
|
+
import { join as join8 } from "path";
|
|
24772
25234
|
|
|
24773
25235
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24774
25236
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24775
|
-
import { existsSync as
|
|
24776
|
-
import { basename as basename3, join as
|
|
25237
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
25238
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
25239
|
+
|
|
25240
|
+
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
25241
|
+
var RawEgressError = class extends Error {
|
|
25242
|
+
constructor(message) {
|
|
25243
|
+
super(message);
|
|
25244
|
+
this.name = "RawEgressError";
|
|
25245
|
+
}
|
|
25246
|
+
};
|
|
25247
|
+
var MIN_RAW_LEN = 4;
|
|
25248
|
+
function maskContextSlice(slice, sliceStart, hits) {
|
|
25249
|
+
const findings = [];
|
|
25250
|
+
for (const h of hits) {
|
|
25251
|
+
const start = Math.max(0, h.span.start - sliceStart);
|
|
25252
|
+
const end = Math.min(slice.length, h.span.end - sliceStart);
|
|
25253
|
+
if (end > start) {
|
|
25254
|
+
findings.push({
|
|
25255
|
+
ruleId: "raw-egress",
|
|
25256
|
+
category: "secret",
|
|
25257
|
+
severity: "critical",
|
|
25258
|
+
span: { start, end },
|
|
25259
|
+
rawMatch: "",
|
|
25260
|
+
confidence: 1
|
|
25261
|
+
});
|
|
25262
|
+
}
|
|
25263
|
+
}
|
|
25264
|
+
const masked = findings.length > 0 ? redact(slice, findings) : slice;
|
|
25265
|
+
for (const h of hits) {
|
|
25266
|
+
if (h.rawMatch.length >= MIN_RAW_LEN && masked.includes(h.rawMatch)) {
|
|
25267
|
+
throw new RawEgressError("raw match survived context masking");
|
|
25268
|
+
}
|
|
25269
|
+
}
|
|
25270
|
+
return masked;
|
|
25271
|
+
}
|
|
25272
|
+
function safeMaskedMatch(rawMatch) {
|
|
25273
|
+
const masked = maskMatch(rawMatch);
|
|
25274
|
+
if (masked === rawMatch || rawMatch.length >= MIN_RAW_LEN && masked.includes(rawMatch)) {
|
|
25275
|
+
return "***";
|
|
25276
|
+
}
|
|
25277
|
+
return masked;
|
|
25278
|
+
}
|
|
24777
25279
|
|
|
24778
25280
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24779
25281
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
25282
|
+
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
24780
25283
|
var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
|
|
24781
25284
|
function entryIsActive(entry, now) {
|
|
24782
25285
|
if (entry.expiresAt !== null && Date.parse(entry.expiresAt) <= now) return false;
|
|
@@ -24857,7 +25360,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24857
25360
|
const action = actionFor(finding);
|
|
24858
25361
|
if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
|
|
24859
25362
|
}
|
|
24860
|
-
if (policyMode === "warn" && (worst === "block" || worst === "redact")) {
|
|
25363
|
+
if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (worst === "block" || worst === "redact")) {
|
|
24861
25364
|
return { action: "warn", text, findings };
|
|
24862
25365
|
}
|
|
24863
25366
|
if (worst === "block") return { action: "block", text: null, findings };
|
|
@@ -24879,7 +25382,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24879
25382
|
const excepted = /* @__PURE__ */ new Set();
|
|
24880
25383
|
const exceptionIds = [];
|
|
24881
25384
|
try {
|
|
24882
|
-
if (policyMode === "warn") return { excepted, exceptionIds };
|
|
25385
|
+
if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn") return { excepted, exceptionIds };
|
|
24883
25386
|
const enforced = findings.filter((f) => {
|
|
24884
25387
|
const action = resolveAction(f.ruleId, f.category);
|
|
24885
25388
|
return action === "block" || action === "redact";
|
|
@@ -25050,9 +25553,12 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25050
25553
|
return { processText, capture, rulesetFingerprint, close };
|
|
25051
25554
|
}
|
|
25052
25555
|
|
|
25556
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
25557
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25558
|
+
|
|
25053
25559
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25054
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
25055
|
-
import { join as
|
|
25560
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
25561
|
+
import { join as join10 } from "path";
|
|
25056
25562
|
|
|
25057
25563
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25058
25564
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -25245,6 +25751,14 @@ var StandaloneDataGateway = class {
|
|
|
25245
25751
|
sweepTerminalExceptions(retentionMs) {
|
|
25246
25752
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
25247
25753
|
}
|
|
25754
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
25755
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
25756
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
25757
|
+
// already-capped one).
|
|
25758
|
+
capWarnEraEnforcement(policyMode) {
|
|
25759
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
25760
|
+
return { capped };
|
|
25761
|
+
}
|
|
25248
25762
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
25249
25763
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
25250
25764
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -25344,9 +25858,9 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
|
25344
25858
|
// src/history/transcripts.ts
|
|
25345
25859
|
import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
25346
25860
|
import { homedir as homedir3 } from "os";
|
|
25347
|
-
import { join as
|
|
25861
|
+
import { join as join11 } from "path";
|
|
25348
25862
|
function transcriptsDir() {
|
|
25349
|
-
return
|
|
25863
|
+
return join11(homedir3(), ".claude", "projects");
|
|
25350
25864
|
}
|
|
25351
25865
|
function isRecord(value) {
|
|
25352
25866
|
return typeof value === "object" && value !== null;
|
|
@@ -25362,7 +25876,7 @@ function extractText(content) {
|
|
|
25362
25876
|
}
|
|
25363
25877
|
return parts.join("\n");
|
|
25364
25878
|
}
|
|
25365
|
-
function parseTranscript(jsonl, sinceMs = 0) {
|
|
25879
|
+
function parseTranscript(jsonl, sinceMs = 0, beforeMs = Infinity) {
|
|
25366
25880
|
const out = [];
|
|
25367
25881
|
for (const line of jsonl.split("\n")) {
|
|
25368
25882
|
const trimmed = line.trim();
|
|
@@ -25380,6 +25894,7 @@ function parseTranscript(jsonl, sinceMs = 0) {
|
|
|
25380
25894
|
const occurredMs = Date.parse(occurredAt);
|
|
25381
25895
|
if (Number.isNaN(occurredMs)) continue;
|
|
25382
25896
|
if (sinceMs > 0 && occurredMs < sinceMs) continue;
|
|
25897
|
+
if (occurredMs >= beforeMs) continue;
|
|
25383
25898
|
const message = rec.message;
|
|
25384
25899
|
if (!isRecord(message)) continue;
|
|
25385
25900
|
const text = extractText(message.content);
|
|
@@ -25585,7 +26100,7 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
|
|
|
25585
26100
|
return out;
|
|
25586
26101
|
}
|
|
25587
26102
|
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
25588
|
-
function* iterateFileContents(dir) {
|
|
26103
|
+
function* iterateFileContents(dir, excludeSessionId) {
|
|
25589
26104
|
let projects;
|
|
25590
26105
|
try {
|
|
25591
26106
|
projects = readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
@@ -25593,7 +26108,7 @@ function* iterateFileContents(dir) {
|
|
|
25593
26108
|
return;
|
|
25594
26109
|
}
|
|
25595
26110
|
for (const project of projects) {
|
|
25596
|
-
const projectDir =
|
|
26111
|
+
const projectDir = join11(dir, project);
|
|
25597
26112
|
let files;
|
|
25598
26113
|
try {
|
|
25599
26114
|
files = readdirSync3(projectDir).filter((name) => name.endsWith(".jsonl"));
|
|
@@ -25601,9 +26116,11 @@ function* iterateFileContents(dir) {
|
|
|
25601
26116
|
continue;
|
|
25602
26117
|
}
|
|
25603
26118
|
for (const file2 of files) {
|
|
26119
|
+
if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
|
|
26120
|
+
continue;
|
|
25604
26121
|
let content;
|
|
25605
26122
|
try {
|
|
25606
|
-
content = readFileSync7(
|
|
26123
|
+
content = readFileSync7(join11(projectDir, file2), "utf8");
|
|
25607
26124
|
} catch {
|
|
25608
26125
|
continue;
|
|
25609
26126
|
}
|
|
@@ -25617,8 +26134,9 @@ function windowStartMs(opts) {
|
|
|
25617
26134
|
}
|
|
25618
26135
|
function* iterateHistory(opts = {}) {
|
|
25619
26136
|
const sinceMs = windowStartMs(opts);
|
|
25620
|
-
|
|
25621
|
-
|
|
26137
|
+
const beforeMs = opts.beforeMs ?? Infinity;
|
|
26138
|
+
for (const content of iterateFileContents(opts.dir ?? transcriptsDir(), opts.excludeSessionId))
|
|
26139
|
+
yield* parseTranscript(content, sinceMs, beforeMs);
|
|
25622
26140
|
}
|
|
25623
26141
|
function* iterateUsageAndToolCalls(opts = {}) {
|
|
25624
26142
|
const sinceMs = windowStartMs(opts);
|
|
@@ -25631,7 +26149,35 @@ function* iterateUsageAndToolCalls(opts = {}) {
|
|
|
25631
26149
|
}
|
|
25632
26150
|
|
|
25633
26151
|
// src/history/scan.ts
|
|
25634
|
-
|
|
26152
|
+
var CONTEXT_RADIUS = 120;
|
|
26153
|
+
function redactOverlapping(rawContext, contextStart, others) {
|
|
26154
|
+
if (others.length === 0) return rawContext;
|
|
26155
|
+
try {
|
|
26156
|
+
return maskContextSlice(rawContext, contextStart, others);
|
|
26157
|
+
} catch {
|
|
26158
|
+
let safe = rawContext;
|
|
26159
|
+
for (const other of others) {
|
|
26160
|
+
if (other.rawMatch.length > 0) safe = safe.split(other.rawMatch).join("[REDACTED]");
|
|
26161
|
+
}
|
|
26162
|
+
return safe;
|
|
26163
|
+
}
|
|
26164
|
+
}
|
|
26165
|
+
function buildTriageHit(text, f, otherFindings = []) {
|
|
26166
|
+
const start = Math.max(0, f.span.start - CONTEXT_RADIUS);
|
|
26167
|
+
const end = Math.min(text.length, f.span.end + CONTEXT_RADIUS);
|
|
26168
|
+
const rawContext = text.slice(start, end);
|
|
26169
|
+
const overlapping = otherFindings.filter((o) => o.span.start < end && o.span.end > start);
|
|
26170
|
+
return {
|
|
26171
|
+
ruleId: f.ruleId,
|
|
26172
|
+
category: f.category,
|
|
26173
|
+
severity: f.severity,
|
|
26174
|
+
maskedMatch: safeMaskedMatch(f.rawMatch),
|
|
26175
|
+
rawMatch: f.rawMatch,
|
|
26176
|
+
context: redactOverlapping(rawContext, start, overlapping),
|
|
26177
|
+
confidence: f.confidence
|
|
26178
|
+
};
|
|
26179
|
+
}
|
|
26180
|
+
async function scanHistory(config2, opts = {}, onHit) {
|
|
25635
26181
|
const windowDays = opts.windowDays ?? 30;
|
|
25636
26182
|
if (config2.settings.historicalAccess !== "full") {
|
|
25637
26183
|
return { consented: false, scanned: 0, skipped: 0, findings: 0, bySeverity: {}, windowDays };
|
|
@@ -25667,6 +26213,14 @@ async function scanHistory(config2, opts = {}) {
|
|
|
25667
26213
|
for (const finding of result.findings) {
|
|
25668
26214
|
findings++;
|
|
25669
26215
|
bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1;
|
|
26216
|
+
if (onHit) {
|
|
26217
|
+
const otherFindings = result.findings.filter((other) => other !== finding);
|
|
26218
|
+
const hit = buildTriageHit(message.text, finding, otherFindings);
|
|
26219
|
+
try {
|
|
26220
|
+
onHit(hit);
|
|
26221
|
+
} catch {
|
|
26222
|
+
}
|
|
26223
|
+
}
|
|
25670
26224
|
}
|
|
25671
26225
|
}
|
|
25672
26226
|
} finally {
|
|
@@ -25684,9 +26238,9 @@ import {
|
|
|
25684
26238
|
openSync,
|
|
25685
26239
|
readFileSync as readFileSync8,
|
|
25686
26240
|
readSync,
|
|
25687
|
-
writeFileSync as
|
|
26241
|
+
writeFileSync as writeFileSync6
|
|
25688
26242
|
} from "fs";
|
|
25689
|
-
import { join as
|
|
26243
|
+
import { join as join12 } from "path";
|
|
25690
26244
|
|
|
25691
26245
|
// src/history/usage.ts
|
|
25692
26246
|
var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
|
|
@@ -25929,25 +26483,113 @@ function fenced(body) {
|
|
|
25929
26483
|
}
|
|
25930
26484
|
|
|
25931
26485
|
// src/backfill.ts
|
|
25932
|
-
|
|
25933
|
-
|
|
25934
|
-
|
|
25935
|
-
|
|
25936
|
-
|
|
25937
|
-
}
|
|
25938
|
-
const summary = await scanHistory(cfg);
|
|
26486
|
+
function triageSentinel(count, status) {
|
|
26487
|
+
return JSON.stringify({ done: true, count, status }) + "\n";
|
|
26488
|
+
}
|
|
26489
|
+
async function runBackfill(deps) {
|
|
26490
|
+
const { triage, io } = deps;
|
|
25939
26491
|
try {
|
|
25940
|
-
|
|
25941
|
-
|
|
25942
|
-
|
|
25943
|
-
|
|
25944
|
-
|
|
25945
|
-
|
|
25946
|
-
|
|
26492
|
+
const cfg = deps.loadConfig();
|
|
26493
|
+
if (cfg.settings.historicalAccess !== "full") {
|
|
26494
|
+
if (triage) {
|
|
26495
|
+
io.stdout(triageSentinel(0, "skipped:no-consent"));
|
|
26496
|
+
} else {
|
|
26497
|
+
io.stdout("Historical scan skipped \u2014 full review was not granted.\n");
|
|
26498
|
+
}
|
|
26499
|
+
return;
|
|
26500
|
+
}
|
|
26501
|
+
let fpKey = null;
|
|
26502
|
+
if (triage) {
|
|
26503
|
+
try {
|
|
26504
|
+
fpKey = loadOrCreateFingerprintKey(cfg.dataDir);
|
|
26505
|
+
} catch {
|
|
26506
|
+
fpKey = null;
|
|
26507
|
+
}
|
|
26508
|
+
}
|
|
26509
|
+
let count = 0;
|
|
26510
|
+
let onHitError;
|
|
26511
|
+
const summary = await deps.scanHistory(
|
|
26512
|
+
cfg,
|
|
26513
|
+
deps.guard ?? {},
|
|
26514
|
+
triage ? (hit) => {
|
|
26515
|
+
if (onHitError !== void 0) return;
|
|
26516
|
+
try {
|
|
26517
|
+
const enriched = {
|
|
26518
|
+
...hit,
|
|
26519
|
+
id: String(count),
|
|
26520
|
+
valueFingerprint: fpKey ? fingerprintValue(fpKey, hit.rawMatch) : void 0,
|
|
26521
|
+
keyVersion: fpKey?.version
|
|
26522
|
+
};
|
|
26523
|
+
const validated = TriageHit.safeParse(enriched);
|
|
26524
|
+
if (!validated.success) {
|
|
26525
|
+
throw new Error("enriched triage hit failed TriageHit validation");
|
|
26526
|
+
}
|
|
26527
|
+
io.stdout(JSON.stringify(validated.data) + "\n");
|
|
26528
|
+
count += 1;
|
|
26529
|
+
} catch (err) {
|
|
26530
|
+
onHitError = err;
|
|
26531
|
+
}
|
|
26532
|
+
} : void 0
|
|
26533
|
+
);
|
|
26534
|
+
if (onHitError !== void 0) {
|
|
26535
|
+
throw onHitError instanceof Error ? onHitError : new Error(typeof onHitError === "string" ? onHitError : "triage stream write failed");
|
|
26536
|
+
}
|
|
26537
|
+
try {
|
|
26538
|
+
await deps.reconcileHistory(cfg);
|
|
26539
|
+
} catch {
|
|
26540
|
+
}
|
|
26541
|
+
if (triage) {
|
|
26542
|
+
io.stdout(triageSentinel(count, "complete"));
|
|
26543
|
+
} else {
|
|
26544
|
+
const heading = "\u2713 Historical scan complete";
|
|
26545
|
+
const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
|
|
26546
|
+
const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
|
|
26547
|
+
io.stdout(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
|
|
25947
26548
|
`);
|
|
25948
|
-
}
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
26549
|
+
}
|
|
26550
|
+
} catch (err) {
|
|
26551
|
+
if (triage) {
|
|
26552
|
+
io.stderr(`aka backfill --triage: history scan failed: ${String(err)}
|
|
26553
|
+
`);
|
|
26554
|
+
io.fail();
|
|
26555
|
+
} else {
|
|
26556
|
+
io.stdout(
|
|
26557
|
+
"AKA could not scan your history right now. It will still protect everything from here on.\n"
|
|
26558
|
+
);
|
|
26559
|
+
}
|
|
26560
|
+
}
|
|
25952
26561
|
}
|
|
25953
|
-
process.
|
|
26562
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
26563
|
+
const triage = process.argv.includes("--triage");
|
|
26564
|
+
const startedAt = Date.now();
|
|
26565
|
+
const sessionId = process.env.CLAUDE_CODE_BRIDGE_SESSION_ID;
|
|
26566
|
+
await runBackfill({
|
|
26567
|
+
triage,
|
|
26568
|
+
io: {
|
|
26569
|
+
stdout: (chunk) => process.stdout.write(chunk),
|
|
26570
|
+
stderr: (chunk) => process.stderr.write(chunk),
|
|
26571
|
+
fail: () => {
|
|
26572
|
+
process.exitCode = 1;
|
|
26573
|
+
}
|
|
26574
|
+
},
|
|
26575
|
+
loadConfig,
|
|
26576
|
+
scanHistory,
|
|
26577
|
+
reconcileHistory,
|
|
26578
|
+
guard: {
|
|
26579
|
+
beforeMs: startedAt,
|
|
26580
|
+
...sessionId ? { excludeSessionId: sessionId } : {}
|
|
26581
|
+
}
|
|
26582
|
+
});
|
|
26583
|
+
if (process.stdout.writableLength > 0) {
|
|
26584
|
+
await new Promise((resolve) => {
|
|
26585
|
+
process.stdout.write("", () => {
|
|
26586
|
+
resolve();
|
|
26587
|
+
});
|
|
26588
|
+
});
|
|
26589
|
+
}
|
|
26590
|
+
process.exit(process.exitCode ?? 0);
|
|
26591
|
+
}
|
|
26592
|
+
export {
|
|
26593
|
+
runBackfill,
|
|
26594
|
+
triageSentinel
|
|
26595
|
+
};
|