@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/session-start.js
CHANGED
|
@@ -15433,6 +15433,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15433
15433
|
"prompt",
|
|
15434
15434
|
"response",
|
|
15435
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",
|
|
15436
15441
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15437
15442
|
// fact the posture inspection findings reference (findings require an
|
|
15438
15443
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15824,7 +15829,7 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15824
15829
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15825
15830
|
|
|
15826
15831
|
// ../../packages/schema/src/zod/event.ts
|
|
15827
|
-
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" });
|
|
15828
15833
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15829
15834
|
var EventMetadata = external_exports.object({
|
|
15830
15835
|
sessionId: external_exports.string().optional(),
|
|
@@ -16137,7 +16142,7 @@ var DetectionException = external_exports.object({
|
|
|
16137
16142
|
justification: external_exports.string().min(1),
|
|
16138
16143
|
conditions: ExceptionConditions.nullable(),
|
|
16139
16144
|
createdBy: external_exports.string(),
|
|
16140
|
-
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"]),
|
|
16141
16146
|
createdAt: external_exports.iso.datetime(),
|
|
16142
16147
|
updatedAt: external_exports.iso.datetime(),
|
|
16143
16148
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16306,20 +16311,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16306
16311
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16307
16312
|
fetchedAt: external_exports.iso.datetime()
|
|
16308
16313
|
}).meta({ id: "PolicyBundle" });
|
|
16309
|
-
var DEFAULT_ACTIONS = {
|
|
16310
|
-
secret: "block",
|
|
16311
|
-
pii: "redact",
|
|
16312
|
-
financial: "redact",
|
|
16313
|
-
phi: "redact",
|
|
16314
|
-
code_context: "warn",
|
|
16315
|
-
code_flaw: "warn",
|
|
16316
|
-
custom: "warn",
|
|
16317
|
-
// Config-posture findings only observe today (they land in
|
|
16318
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16319
|
-
config: "warn"
|
|
16320
|
-
};
|
|
16321
16314
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16322
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
|
+
}
|
|
16323
16334
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16324
16335
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16325
16336
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16346,6 +16357,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16346
16357
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16347
16358
|
}
|
|
16348
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
|
+
);
|
|
16349
16366
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16350
16367
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16351
16368
|
);
|
|
@@ -16866,10 +16883,8 @@ function toApiProvider(sourceTool) {
|
|
|
16866
16883
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16867
16884
|
}
|
|
16868
16885
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16869
|
-
function
|
|
16870
|
-
const statuses = new Set(
|
|
16871
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16872
|
-
);
|
|
16886
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16887
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16873
16888
|
if (statuses.size === 0) return void 0;
|
|
16874
16889
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16875
16890
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16887,6 +16902,7 @@ function deriveFindingStatus(row) {
|
|
|
16887
16902
|
function buildFindingGroups(rows, opts = {}) {
|
|
16888
16903
|
const overrides = opts.overrides;
|
|
16889
16904
|
const packNames = opts.packNames;
|
|
16905
|
+
const aggregates = opts.aggregates;
|
|
16890
16906
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16891
16907
|
for (const row of rows) {
|
|
16892
16908
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16908,17 +16924,20 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16908
16924
|
status: r.status
|
|
16909
16925
|
};
|
|
16910
16926
|
});
|
|
16911
|
-
const
|
|
16927
|
+
const agg = aggregates?.get(ruleId);
|
|
16928
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16912
16929
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16913
16930
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16914
16931
|
);
|
|
16915
16932
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16916
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16933
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16917
16934
|
if (seenProviders.has(p)) return false;
|
|
16918
16935
|
seenProviders.add(p);
|
|
16919
16936
|
return true;
|
|
16920
16937
|
});
|
|
16921
|
-
const actionSet = new Set(
|
|
16938
|
+
const actionSet = new Set(
|
|
16939
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16940
|
+
);
|
|
16922
16941
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16923
16942
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16924
16943
|
const detection = {
|
|
@@ -16932,8 +16951,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16932
16951
|
contextPrefix: ""
|
|
16933
16952
|
// empty (pending privacy review)
|
|
16934
16953
|
};
|
|
16935
|
-
const status =
|
|
16936
|
-
|
|
16954
|
+
const status = foldGroupStatus(
|
|
16955
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16956
|
+
);
|
|
16957
|
+
const group = {
|
|
16937
16958
|
id: ruleId,
|
|
16938
16959
|
category: apiCategory,
|
|
16939
16960
|
subtype: ruleId,
|
|
@@ -16942,21 +16963,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16942
16963
|
match,
|
|
16943
16964
|
detection,
|
|
16944
16965
|
policy,
|
|
16945
|
-
instanceCount: instances.length,
|
|
16966
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16946
16967
|
providers,
|
|
16947
16968
|
aggregateAction,
|
|
16948
16969
|
latestDetectedAt,
|
|
16949
16970
|
instances,
|
|
16950
16971
|
status
|
|
16951
|
-
}
|
|
16972
|
+
};
|
|
16973
|
+
if (agg) {
|
|
16974
|
+
actionsCache.set(group, [...actionSet]);
|
|
16975
|
+
if (agg.searchText !== void 0) {
|
|
16976
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16977
|
+
}
|
|
16978
|
+
}
|
|
16979
|
+
groups.push(group);
|
|
16952
16980
|
}
|
|
16953
16981
|
return groups;
|
|
16954
16982
|
}
|
|
16955
16983
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16956
|
-
function
|
|
16957
|
-
|
|
16958
|
-
if (cached2 !== void 0) return cached2;
|
|
16959
|
-
const haystack = [
|
|
16984
|
+
function buildHaystack(g, extra) {
|
|
16985
|
+
return [
|
|
16960
16986
|
g.subtype,
|
|
16961
16987
|
g.category,
|
|
16962
16988
|
g.match.maskedValue,
|
|
@@ -16964,11 +16990,25 @@ function groupHaystack(g) {
|
|
|
16964
16990
|
g.id,
|
|
16965
16991
|
...g.instances.map((i) => i.repo),
|
|
16966
16992
|
...g.instances.map((i) => i.file),
|
|
16967
|
-
...g.instances.map((i) => i.id)
|
|
16993
|
+
...g.instances.map((i) => i.id),
|
|
16994
|
+
...extra === void 0 ? [] : [extra]
|
|
16968
16995
|
].join(" ").toLowerCase();
|
|
16996
|
+
}
|
|
16997
|
+
function groupHaystack(g) {
|
|
16998
|
+
const cached2 = haystackCache.get(g);
|
|
16999
|
+
if (cached2 !== void 0) return cached2;
|
|
17000
|
+
const haystack = buildHaystack(g);
|
|
16969
17001
|
haystackCache.set(g, haystack);
|
|
16970
17002
|
return haystack;
|
|
16971
17003
|
}
|
|
17004
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
17005
|
+
function groupActions(g) {
|
|
17006
|
+
const cached2 = actionsCache.get(g);
|
|
17007
|
+
if (cached2 !== void 0) return cached2;
|
|
17008
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
17009
|
+
actionsCache.set(g, actions);
|
|
17010
|
+
return actions;
|
|
17011
|
+
}
|
|
16972
17012
|
function applyFindingFilters(groups, opts) {
|
|
16973
17013
|
let filtered = groups;
|
|
16974
17014
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16981,7 +17021,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16981
17021
|
}
|
|
16982
17022
|
if (opts.actions && opts.actions.length > 0) {
|
|
16983
17023
|
const actionSet = new Set(opts.actions);
|
|
16984
|
-
filtered = filtered.filter((g) => g.
|
|
17024
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16985
17025
|
}
|
|
16986
17026
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16987
17027
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -17033,8 +17073,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17033
17073
|
});
|
|
17034
17074
|
const actionMap = /* @__PURE__ */ new Map();
|
|
17035
17075
|
for (const g of forAction) {
|
|
17036
|
-
const
|
|
17037
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17076
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17038
17077
|
}
|
|
17039
17078
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
17040
17079
|
providers: opts.providers,
|
|
@@ -17596,6 +17635,132 @@ function reviewSeverityRank(reasons) {
|
|
|
17596
17635
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17597
17636
|
}
|
|
17598
17637
|
|
|
17638
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17639
|
+
var TriageHit = external_exports.object({
|
|
17640
|
+
ruleId: external_exports.string(),
|
|
17641
|
+
category: DetectionCategory,
|
|
17642
|
+
severity: Severity,
|
|
17643
|
+
maskedMatch: external_exports.string(),
|
|
17644
|
+
rawMatch: external_exports.string(),
|
|
17645
|
+
context: external_exports.string(),
|
|
17646
|
+
filePath: external_exports.string().optional(),
|
|
17647
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17648
|
+
id: external_exports.string().optional(),
|
|
17649
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17650
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17651
|
+
});
|
|
17652
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17653
|
+
var TriageCategoryRec = external_exports.object({
|
|
17654
|
+
category: DetectionCategory,
|
|
17655
|
+
action: TriagePolicy,
|
|
17656
|
+
reasoning: external_exports.string(),
|
|
17657
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17658
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17659
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17660
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17661
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17662
|
+
});
|
|
17663
|
+
var TriageRecommendation = external_exports.object({
|
|
17664
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17665
|
+
notes: external_exports.string()
|
|
17666
|
+
});
|
|
17667
|
+
|
|
17668
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17669
|
+
function escapeLikePattern(s) {
|
|
17670
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17671
|
+
}
|
|
17672
|
+
function placeholders(n) {
|
|
17673
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17674
|
+
}
|
|
17675
|
+
function containsPattern(q) {
|
|
17676
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17677
|
+
}
|
|
17678
|
+
function likeAny(exprs) {
|
|
17679
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17680
|
+
}
|
|
17681
|
+
|
|
17682
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17683
|
+
var savepointSeq = 0;
|
|
17684
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17685
|
+
if (db.isTransaction) {
|
|
17686
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17687
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17688
|
+
try {
|
|
17689
|
+
fn();
|
|
17690
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17691
|
+
} catch (error51) {
|
|
17692
|
+
try {
|
|
17693
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17694
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17695
|
+
} catch {
|
|
17696
|
+
}
|
|
17697
|
+
throw error51;
|
|
17698
|
+
}
|
|
17699
|
+
return;
|
|
17700
|
+
}
|
|
17701
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17702
|
+
try {
|
|
17703
|
+
fn();
|
|
17704
|
+
db.exec("COMMIT");
|
|
17705
|
+
} catch (error51) {
|
|
17706
|
+
try {
|
|
17707
|
+
db.exec("ROLLBACK");
|
|
17708
|
+
} catch {
|
|
17709
|
+
}
|
|
17710
|
+
throw error51;
|
|
17711
|
+
}
|
|
17712
|
+
}
|
|
17713
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17714
|
+
const nested = db.isTransaction;
|
|
17715
|
+
try {
|
|
17716
|
+
withTransaction(db, fn, mode);
|
|
17717
|
+
return true;
|
|
17718
|
+
} catch (error51) {
|
|
17719
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17720
|
+
return false;
|
|
17721
|
+
}
|
|
17722
|
+
}
|
|
17723
|
+
|
|
17724
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17725
|
+
function akaWarn(message2) {
|
|
17726
|
+
process.stderr.write(`[aka] ${message2}
|
|
17727
|
+
`);
|
|
17728
|
+
}
|
|
17729
|
+
|
|
17730
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17731
|
+
function evidenceObjects(sql) {
|
|
17732
|
+
const objects = [];
|
|
17733
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17734
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17735
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17736
|
+
}
|
|
17737
|
+
}
|
|
17738
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17739
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17740
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17741
|
+
}
|
|
17742
|
+
}
|
|
17743
|
+
return objects;
|
|
17744
|
+
}
|
|
17745
|
+
function schemaObjectExists(db, kind, name) {
|
|
17746
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17747
|
+
return row !== void 0;
|
|
17748
|
+
}
|
|
17749
|
+
function indexExists(db, name) {
|
|
17750
|
+
return schemaObjectExists(db, "index", name);
|
|
17751
|
+
}
|
|
17752
|
+
function columnNames(db, table, opts) {
|
|
17753
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17754
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
|
|
17755
|
+
return columns.map((c) => c.name);
|
|
17756
|
+
}
|
|
17757
|
+
function evidenceExists(db, object2) {
|
|
17758
|
+
if (object2.kind === "column") {
|
|
17759
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17760
|
+
}
|
|
17761
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17762
|
+
}
|
|
17763
|
+
|
|
17599
17764
|
// ../../packages/persistence/src/ids.ts
|
|
17600
17765
|
import { createHash } from "crypto";
|
|
17601
17766
|
function sha256Hex(input) {
|
|
@@ -17632,28 +17797,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17632
17797
|
}
|
|
17633
17798
|
|
|
17634
17799
|
// ../../packages/persistence/src/migrations.ts
|
|
17635
|
-
function evidenceObjects(sql) {
|
|
17636
|
-
const objects = [];
|
|
17637
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17638
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17639
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17640
|
-
}
|
|
17641
|
-
}
|
|
17642
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17643
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17644
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17645
|
-
}
|
|
17646
|
-
}
|
|
17647
|
-
return objects;
|
|
17648
|
-
}
|
|
17649
|
-
function evidenceExists(db, object2) {
|
|
17650
|
-
if (object2.kind === "column") {
|
|
17651
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17652
|
-
return columns.some((c) => c.name === object2.name);
|
|
17653
|
-
}
|
|
17654
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17655
|
-
return row !== void 0;
|
|
17656
|
-
}
|
|
17657
17800
|
function describeObject(object2) {
|
|
17658
17801
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17659
17802
|
}
|
|
@@ -17664,10 +17807,6 @@ function createdIndexName(statement) {
|
|
|
17664
17807
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17665
17808
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17666
17809
|
}
|
|
17667
|
-
function indexExists(db, name) {
|
|
17668
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17669
|
-
return row !== void 0;
|
|
17670
|
-
}
|
|
17671
17810
|
function applyMigrations(db) {
|
|
17672
17811
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17673
17812
|
db.exec(
|
|
@@ -17686,44 +17825,39 @@ function applyMigrations(db) {
|
|
|
17686
17825
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17687
17826
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17688
17827
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17689
|
-
const message2 = `
|
|
17690
|
-
|
|
17691
|
-
`);
|
|
17692
|
-
throw new Error(message2);
|
|
17828
|
+
const message2 = `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.`;
|
|
17829
|
+
akaWarn(message2);
|
|
17830
|
+
throw new Error(`[aka] ${message2}`);
|
|
17693
17831
|
}
|
|
17694
17832
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17695
17833
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17696
17834
|
const statements = splitStatements(migration.sql);
|
|
17697
17835
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17698
17836
|
try {
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
const
|
|
17703
|
-
|
|
17704
|
-
if (
|
|
17705
|
-
|
|
17706
|
-
|
|
17837
|
+
withTransaction(
|
|
17838
|
+
db,
|
|
17839
|
+
() => {
|
|
17840
|
+
for (const statement of statements) {
|
|
17841
|
+
const indexName = createdIndexName(statement);
|
|
17842
|
+
if (indexName === void 0) {
|
|
17843
|
+
if (alreadyApplied) continue;
|
|
17844
|
+
} else if (indexExists(db, indexName)) {
|
|
17845
|
+
continue;
|
|
17846
|
+
}
|
|
17847
|
+
db.exec(statement);
|
|
17707
17848
|
}
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
);
|
|
17849
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17850
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17851
|
+
if (violations.length > 0) {
|
|
17852
|
+
throw new Error(
|
|
17853
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17854
|
+
);
|
|
17855
|
+
}
|
|
17716
17856
|
}
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
try {
|
|
17722
|
-
db.exec("ROLLBACK");
|
|
17723
|
-
} catch {
|
|
17724
|
-
}
|
|
17725
|
-
throw error51;
|
|
17726
|
-
}
|
|
17857
|
+
record2.run(migration.tag, Date.now());
|
|
17858
|
+
},
|
|
17859
|
+
"IMMEDIATE"
|
|
17860
|
+
);
|
|
17727
17861
|
} finally {
|
|
17728
17862
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17729
17863
|
}
|
|
@@ -17766,8 +17900,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17766
17900
|
}
|
|
17767
17901
|
];
|
|
17768
17902
|
function ensureTokenUsageColumns(db) {
|
|
17769
|
-
const
|
|
17770
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17903
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17771
17904
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17772
17905
|
if (!existing.has(column.name)) {
|
|
17773
17906
|
db.exec(column.ddl);
|
|
@@ -17805,47 +17938,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17805
17938
|
repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
|
|
17806
17939
|
}));
|
|
17807
17940
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
dropCollisions
|
|
17822
|
-
|
|
17941
|
+
withTransaction(
|
|
17942
|
+
db,
|
|
17943
|
+
() => {
|
|
17944
|
+
for (const { row, canonicalId } of legacy) {
|
|
17945
|
+
foldProject.run(
|
|
17946
|
+
canonicalId,
|
|
17947
|
+
row.url,
|
|
17948
|
+
row.name,
|
|
17949
|
+
row.attributes,
|
|
17950
|
+
row.firstSeen,
|
|
17951
|
+
row.lastSeen
|
|
17952
|
+
);
|
|
17953
|
+
repointAudit.run(canonicalId, row.id);
|
|
17954
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
17955
|
+
dropCollisions.run(row.id, canonicalId);
|
|
17956
|
+
repoint.run(canonicalId, row.id);
|
|
17957
|
+
}
|
|
17958
|
+
repointCallSite.run(canonicalId, row.id);
|
|
17959
|
+
deleteLegacy.run(row.id);
|
|
17823
17960
|
}
|
|
17824
|
-
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
db.exec("COMMIT");
|
|
17828
|
-
} catch (error51) {
|
|
17829
|
-
try {
|
|
17830
|
-
db.exec("ROLLBACK");
|
|
17831
|
-
} catch {
|
|
17832
|
-
}
|
|
17833
|
-
throw error51;
|
|
17834
|
-
}
|
|
17961
|
+
},
|
|
17962
|
+
"IMMEDIATE"
|
|
17963
|
+
);
|
|
17835
17964
|
} catch (error51) {
|
|
17836
|
-
|
|
17837
|
-
`);
|
|
17965
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17838
17966
|
}
|
|
17839
17967
|
}
|
|
17840
17968
|
function isForeignSqliteLineage(db) {
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17844
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
17969
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
17970
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17845
17971
|
}
|
|
17846
17972
|
function ensureSyncedAtColumn(db, table) {
|
|
17847
|
-
|
|
17848
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
17973
|
+
if (!columnNames(db, table).includes("synced_at")) {
|
|
17849
17974
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
17850
17975
|
}
|
|
17851
17976
|
}
|
|
@@ -17896,8 +18021,11 @@ function ensureDataDirSync(dir) {
|
|
|
17896
18021
|
} catch {
|
|
17897
18022
|
}
|
|
17898
18023
|
}
|
|
18024
|
+
function walSidecars(file2) {
|
|
18025
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
18026
|
+
}
|
|
17899
18027
|
function tightenPerms(file2) {
|
|
17900
|
-
for (const path of [file2,
|
|
18028
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17901
18029
|
try {
|
|
17902
18030
|
chmodSync(path, DATA_FILE_MODE);
|
|
17903
18031
|
} catch {
|
|
@@ -17905,12 +18033,68 @@ function tightenPerms(file2) {
|
|
|
17905
18033
|
}
|
|
17906
18034
|
}
|
|
17907
18035
|
|
|
17908
|
-
// ../../packages/persistence/src/
|
|
17909
|
-
function
|
|
17910
|
-
|
|
18036
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
18037
|
+
function safeJson(s, fallback) {
|
|
18038
|
+
if (s == null) return fallback;
|
|
18039
|
+
try {
|
|
18040
|
+
return JSON.parse(s);
|
|
18041
|
+
} catch {
|
|
18042
|
+
return fallback;
|
|
18043
|
+
}
|
|
17911
18044
|
}
|
|
17912
|
-
function
|
|
17913
|
-
|
|
18045
|
+
function parseJsonObject(s) {
|
|
18046
|
+
if (s == null) return void 0;
|
|
18047
|
+
try {
|
|
18048
|
+
const parsed = JSON.parse(s);
|
|
18049
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18050
|
+
} catch {
|
|
18051
|
+
}
|
|
18052
|
+
return void 0;
|
|
18053
|
+
}
|
|
18054
|
+
|
|
18055
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18056
|
+
function allRows(stmt, params) {
|
|
18057
|
+
if (params === void 0) return stmt.all();
|
|
18058
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18059
|
+
return stmt.all(params);
|
|
18060
|
+
}
|
|
18061
|
+
function getRow(stmt, params) {
|
|
18062
|
+
if (params === void 0) return stmt.get();
|
|
18063
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18064
|
+
return stmt.get(params);
|
|
18065
|
+
}
|
|
18066
|
+
function intToBool(raw) {
|
|
18067
|
+
return raw === 1 || raw === true;
|
|
18068
|
+
}
|
|
18069
|
+
function boolToInt(b) {
|
|
18070
|
+
return b ? 1 : 0;
|
|
18071
|
+
}
|
|
18072
|
+
function bindParams(row) {
|
|
18073
|
+
const out = {};
|
|
18074
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18075
|
+
out[key] = value === void 0 ? null : value;
|
|
18076
|
+
}
|
|
18077
|
+
return out;
|
|
18078
|
+
}
|
|
18079
|
+
function countScalar(db, sql, params) {
|
|
18080
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18081
|
+
}
|
|
18082
|
+
function countBy(db, sql, params) {
|
|
18083
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18084
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18085
|
+
map2.set(row.k, row.n);
|
|
18086
|
+
}
|
|
18087
|
+
return map2;
|
|
18088
|
+
}
|
|
18089
|
+
function mapRowsTolerant(rows, map2) {
|
|
18090
|
+
const out = [];
|
|
18091
|
+
for (const row of rows) {
|
|
18092
|
+
try {
|
|
18093
|
+
out.push(map2(row));
|
|
18094
|
+
} catch {
|
|
18095
|
+
}
|
|
18096
|
+
}
|
|
18097
|
+
return out;
|
|
17914
18098
|
}
|
|
17915
18099
|
|
|
17916
18100
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17962,15 +18146,11 @@ function encodeCursor(payload) {
|
|
|
17962
18146
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17963
18147
|
}
|
|
17964
18148
|
function decodeCursor(cursor) {
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
return parsed;
|
|
17969
|
-
}
|
|
17970
|
-
return null;
|
|
17971
|
-
} catch {
|
|
17972
|
-
return null;
|
|
18149
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18150
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18151
|
+
return parsed;
|
|
17973
18152
|
}
|
|
18153
|
+
return null;
|
|
17974
18154
|
}
|
|
17975
18155
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17976
18156
|
session: "session",
|
|
@@ -17987,15 +18167,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17987
18167
|
};
|
|
17988
18168
|
function safeParseStringArray(raw) {
|
|
17989
18169
|
if (!raw) return [];
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17993
|
-
} catch {
|
|
17994
|
-
return [];
|
|
17995
|
-
}
|
|
17996
|
-
}
|
|
17997
|
-
function toBool(raw) {
|
|
17998
|
-
return raw === 1 || raw === true;
|
|
18170
|
+
const parsed = safeJson(raw, null);
|
|
18171
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17999
18172
|
}
|
|
18000
18173
|
function toHarness(raw) {
|
|
18001
18174
|
const parsed = Harness.safeParse(raw);
|
|
@@ -18048,8 +18221,8 @@ function buildAuditEvent(row) {
|
|
|
18048
18221
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
18049
18222
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
18050
18223
|
targetId: row.target_id,
|
|
18051
|
-
internal:
|
|
18052
|
-
flagged:
|
|
18224
|
+
internal: intToBool(row.internal),
|
|
18225
|
+
flagged: intToBool(row.flagged)
|
|
18053
18226
|
};
|
|
18054
18227
|
}
|
|
18055
18228
|
var TIMELINE_COLUMNS = `
|
|
@@ -18075,12 +18248,15 @@ var SqliteActivityRepository = class {
|
|
|
18075
18248
|
stats(tz) {
|
|
18076
18249
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
18077
18250
|
const { startMs, endMs } = window;
|
|
18078
|
-
const sessionsToday =
|
|
18251
|
+
const sessionsToday = countScalar(
|
|
18252
|
+
this.db,
|
|
18079
18253
|
`SELECT count(*) AS n FROM audit_events
|
|
18080
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
18081
|
-
|
|
18254
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
|
|
18255
|
+
[startMs, endMs]
|
|
18256
|
+
);
|
|
18082
18257
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
18083
|
-
const liveNow =
|
|
18258
|
+
const liveNow = countScalar(
|
|
18259
|
+
this.db,
|
|
18084
18260
|
`SELECT count(*) AS n FROM audit_events s
|
|
18085
18261
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
18086
18262
|
AND max(
|
|
@@ -18089,22 +18265,29 @@ var SqliteActivityRepository = class {
|
|
|
18089
18265
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
18090
18266
|
s.started_at
|
|
18091
18267
|
)
|
|
18092
|
-
) >=
|
|
18093
|
-
|
|
18094
|
-
|
|
18268
|
+
) >= ?`,
|
|
18269
|
+
[liveThreshold]
|
|
18270
|
+
);
|
|
18271
|
+
const toolCallsToday = countScalar(
|
|
18272
|
+
this.db,
|
|
18095
18273
|
`SELECT count(*) AS n FROM audit_events
|
|
18096
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
18097
|
-
|
|
18098
|
-
|
|
18274
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18275
|
+
[startMs, endMs]
|
|
18276
|
+
);
|
|
18277
|
+
const findingsToday = countScalar(
|
|
18278
|
+
this.db,
|
|
18099
18279
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
18100
18280
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18101
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
18102
|
-
|
|
18103
|
-
|
|
18281
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18282
|
+
[startMs, endMs]
|
|
18283
|
+
);
|
|
18284
|
+
const egressToday = countScalar(
|
|
18285
|
+
this.db,
|
|
18104
18286
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18105
18287
|
FROM audit_events
|
|
18106
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
18107
|
-
|
|
18288
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18289
|
+
[startMs, endMs]
|
|
18290
|
+
);
|
|
18108
18291
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
18109
18292
|
}
|
|
18110
18293
|
listSessions(query) {
|
|
@@ -18126,7 +18309,7 @@ var SqliteActivityRepository = class {
|
|
|
18126
18309
|
conditions.push("started_at <= ?");
|
|
18127
18310
|
params.push(toMs);
|
|
18128
18311
|
if (query.q) {
|
|
18129
|
-
const pattern =
|
|
18312
|
+
const pattern = containsPattern(query.q);
|
|
18130
18313
|
conditions.push(
|
|
18131
18314
|
`(content LIKE ? ESCAPE '\\'
|
|
18132
18315
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18145,8 +18328,9 @@ var SqliteActivityRepository = class {
|
|
|
18145
18328
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18146
18329
|
}
|
|
18147
18330
|
const limit = query.limit;
|
|
18148
|
-
const rows =
|
|
18149
|
-
|
|
18331
|
+
const rows = allRows(
|
|
18332
|
+
this.db.prepare(
|
|
18333
|
+
`SELECT id,
|
|
18150
18334
|
json_extract(attributes, '$.harness') AS harness,
|
|
18151
18335
|
content AS title,
|
|
18152
18336
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18159,7 +18343,9 @@ var SqliteActivityRepository = class {
|
|
|
18159
18343
|
WHERE ${conditions.join(" AND ")}
|
|
18160
18344
|
ORDER BY started_at DESC, id DESC
|
|
18161
18345
|
LIMIT ?`
|
|
18162
|
-
|
|
18346
|
+
),
|
|
18347
|
+
[...params, limit + 1]
|
|
18348
|
+
);
|
|
18163
18349
|
const hasMore = rows.length > limit;
|
|
18164
18350
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18165
18351
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18176,8 +18362,9 @@ var SqliteActivityRepository = class {
|
|
|
18176
18362
|
return Promise.resolve({ items, nextCursor });
|
|
18177
18363
|
}
|
|
18178
18364
|
getSession(sessionId) {
|
|
18179
|
-
const rootRow =
|
|
18180
|
-
|
|
18365
|
+
const rootRow = getRow(
|
|
18366
|
+
this.db.prepare(
|
|
18367
|
+
`SELECT id,
|
|
18181
18368
|
json_extract(attributes, '$.harness') AS harness,
|
|
18182
18369
|
content AS title,
|
|
18183
18370
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18194,47 +18381,66 @@ var SqliteActivityRepository = class {
|
|
|
18194
18381
|
FROM audit_events
|
|
18195
18382
|
WHERE id = ? AND event_type = 'session'
|
|
18196
18383
|
LIMIT 1`
|
|
18197
|
-
|
|
18384
|
+
),
|
|
18385
|
+
[sessionId]
|
|
18386
|
+
);
|
|
18198
18387
|
if (!rootRow) return Promise.resolve(null);
|
|
18199
|
-
const timelineRows =
|
|
18200
|
-
|
|
18388
|
+
const timelineRows = allRows(
|
|
18389
|
+
this.db.prepare(
|
|
18390
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18201
18391
|
FROM audit_events
|
|
18202
18392
|
WHERE id = ? OR root_session_id = ?
|
|
18203
18393
|
ORDER BY started_at ASC, id ASC`
|
|
18204
|
-
|
|
18394
|
+
),
|
|
18395
|
+
[sessionId, sessionId]
|
|
18396
|
+
);
|
|
18205
18397
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18206
|
-
const tokenRow =
|
|
18207
|
-
|
|
18398
|
+
const tokenRow = getRow(
|
|
18399
|
+
this.db.prepare(
|
|
18400
|
+
`SELECT
|
|
18208
18401
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18209
18402
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18210
18403
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18211
18404
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18212
18405
|
FROM audit_events
|
|
18213
18406
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18407
|
+
),
|
|
18408
|
+
[sessionId]
|
|
18409
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18410
|
+
const primaryModel = getRow(
|
|
18411
|
+
this.db.prepare(
|
|
18412
|
+
`SELECT model, provider FROM audit_events
|
|
18217
18413
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18218
18414
|
ORDER BY started_at ASC, id ASC
|
|
18219
18415
|
LIMIT 1`
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18416
|
+
),
|
|
18417
|
+
[sessionId]
|
|
18418
|
+
);
|
|
18419
|
+
const toolRows = allRows(
|
|
18420
|
+
this.db.prepare(
|
|
18421
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18223
18422
|
count(*) AS n
|
|
18224
18423
|
FROM audit_events
|
|
18225
18424
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18226
18425
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18426
|
+
),
|
|
18427
|
+
[sessionId]
|
|
18428
|
+
);
|
|
18429
|
+
const modelRows = allRows(
|
|
18430
|
+
this.db.prepare(
|
|
18431
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18230
18432
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18231
18433
|
ORDER BY model`
|
|
18232
|
-
|
|
18434
|
+
),
|
|
18435
|
+
[sessionId]
|
|
18436
|
+
);
|
|
18233
18437
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18234
|
-
const commits =
|
|
18438
|
+
const commits = countScalar(
|
|
18439
|
+
this.db,
|
|
18235
18440
|
`SELECT count(*) AS n FROM audit_events
|
|
18236
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18237
|
-
|
|
18441
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18442
|
+
[sessionId]
|
|
18443
|
+
);
|
|
18238
18444
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18239
18445
|
turns: 0,
|
|
18240
18446
|
findings: 0,
|
|
@@ -18301,7 +18507,10 @@ var SqliteActivityRepository = class {
|
|
|
18301
18507
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18302
18508
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18303
18509
|
);
|
|
18304
|
-
const rows =
|
|
18510
|
+
const rows = allRows(
|
|
18511
|
+
stmt,
|
|
18512
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18513
|
+
);
|
|
18305
18514
|
const seen = /* @__PURE__ */ new Set();
|
|
18306
18515
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18307
18516
|
return Promise.resolve([...seen]);
|
|
@@ -18324,23 +18533,23 @@ var SqliteActivityRepository = class {
|
|
|
18324
18533
|
conditions.push("started_at >= ?");
|
|
18325
18534
|
params.push(opts.fromMs);
|
|
18326
18535
|
}
|
|
18327
|
-
const rows =
|
|
18328
|
-
|
|
18536
|
+
const rows = allRows(
|
|
18537
|
+
this.db.prepare(
|
|
18538
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18329
18539
|
FROM audit_events
|
|
18330
18540
|
WHERE ${conditions.join(" AND ")}`
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18336
|
-
|
|
18337
|
-
|
|
18338
|
-
|
|
18339
|
-
|
|
18340
|
-
|
|
18341
|
-
}
|
|
18342
|
-
|
|
18343
|
-
return leaves;
|
|
18541
|
+
),
|
|
18542
|
+
params
|
|
18543
|
+
);
|
|
18544
|
+
return mapRowsTolerant(
|
|
18545
|
+
rows.filter(
|
|
18546
|
+
(row) => row.sessionId !== null
|
|
18547
|
+
),
|
|
18548
|
+
(row) => ({
|
|
18549
|
+
sessionId: row.sessionId,
|
|
18550
|
+
attributes: JSON.parse(row.attributes)
|
|
18551
|
+
})
|
|
18552
|
+
);
|
|
18344
18553
|
}
|
|
18345
18554
|
/**
|
|
18346
18555
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18354,57 +18563,72 @@ var SqliteActivityRepository = class {
|
|
|
18354
18563
|
);
|
|
18355
18564
|
if (sessionIds.length === 0) return result;
|
|
18356
18565
|
const inClause = placeholders(sessionIds.length);
|
|
18357
|
-
const lastActivityRows =
|
|
18358
|
-
|
|
18566
|
+
const lastActivityRows = allRows(
|
|
18567
|
+
this.db.prepare(
|
|
18568
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18359
18569
|
WHERE root_session_id IN (${inClause})
|
|
18360
18570
|
GROUP BY root_session_id`
|
|
18361
|
-
|
|
18571
|
+
),
|
|
18572
|
+
sessionIds
|
|
18573
|
+
);
|
|
18362
18574
|
for (const row of lastActivityRows) {
|
|
18363
18575
|
if (row.id === null) continue;
|
|
18364
18576
|
const entry = result.get(row.id);
|
|
18365
18577
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18366
18578
|
}
|
|
18367
|
-
const turnsRows =
|
|
18368
|
-
|
|
18579
|
+
const turnsRows = allRows(
|
|
18580
|
+
this.db.prepare(
|
|
18581
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18369
18582
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18370
18583
|
GROUP BY root_session_id`
|
|
18371
|
-
|
|
18584
|
+
),
|
|
18585
|
+
sessionIds
|
|
18586
|
+
);
|
|
18372
18587
|
for (const row of turnsRows) {
|
|
18373
18588
|
if (row.id === null) continue;
|
|
18374
18589
|
const entry = result.get(row.id);
|
|
18375
18590
|
if (entry) entry.turns = row.n;
|
|
18376
18591
|
}
|
|
18377
|
-
const runKeyRows =
|
|
18378
|
-
|
|
18592
|
+
const runKeyRows = allRows(
|
|
18593
|
+
this.db.prepare(
|
|
18594
|
+
`SELECT root_session_id AS id,
|
|
18379
18595
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18380
18596
|
FROM audit_events
|
|
18381
18597
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18382
18598
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18383
18599
|
GROUP BY root_session_id`
|
|
18384
|
-
|
|
18600
|
+
),
|
|
18601
|
+
sessionIds
|
|
18602
|
+
);
|
|
18385
18603
|
for (const row of runKeyRows) {
|
|
18386
18604
|
if (row.id === null) continue;
|
|
18387
18605
|
const entry = result.get(row.id);
|
|
18388
18606
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18389
18607
|
}
|
|
18390
|
-
const findingsRows =
|
|
18391
|
-
|
|
18608
|
+
const findingsRows = allRows(
|
|
18609
|
+
this.db.prepare(
|
|
18610
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18392
18611
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18393
18612
|
WHERE e.root_session_id IN (${inClause})
|
|
18394
18613
|
GROUP BY e.root_session_id`
|
|
18395
|
-
|
|
18614
|
+
),
|
|
18615
|
+
sessionIds
|
|
18616
|
+
);
|
|
18396
18617
|
for (const row of findingsRows) {
|
|
18397
18618
|
if (row.id === null) continue;
|
|
18398
18619
|
const entry = result.get(row.id);
|
|
18399
18620
|
if (entry) entry.findings = row.n;
|
|
18400
18621
|
}
|
|
18401
|
-
const sharesRows =
|
|
18402
|
-
|
|
18622
|
+
const sharesRows = allRows(
|
|
18623
|
+
this.db.prepare(
|
|
18624
|
+
`SELECT root_session_id AS id,
|
|
18403
18625
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18404
18626
|
FROM audit_events
|
|
18405
18627
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18406
18628
|
GROUP BY root_session_id`
|
|
18407
|
-
|
|
18629
|
+
),
|
|
18630
|
+
sessionIds
|
|
18631
|
+
);
|
|
18408
18632
|
for (const row of sharesRows) {
|
|
18409
18633
|
if (row.id === null) continue;
|
|
18410
18634
|
const entry = result.get(row.id);
|
|
@@ -18454,33 +18678,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18454
18678
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18455
18679
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18456
18680
|
runInTransaction(fn) {
|
|
18457
|
-
this.db
|
|
18458
|
-
try {
|
|
18459
|
-
fn();
|
|
18460
|
-
this.db.exec("COMMIT");
|
|
18461
|
-
} catch (err) {
|
|
18462
|
-
this.db.exec("ROLLBACK");
|
|
18463
|
-
throw err;
|
|
18464
|
-
}
|
|
18681
|
+
withTransaction(this.db, fn);
|
|
18465
18682
|
}
|
|
18466
18683
|
insertAuditEvent(input) {
|
|
18467
18684
|
const row = toAuditEventRow(input);
|
|
18468
|
-
this.insertStmt.run(
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
|
|
18685
|
+
this.insertStmt.run(
|
|
18686
|
+
bindParams({
|
|
18687
|
+
id: row.id,
|
|
18688
|
+
parentId: row.parentId,
|
|
18689
|
+
rootSessionId: row.rootSessionId,
|
|
18690
|
+
eventType: row.eventType,
|
|
18691
|
+
hostId: row.hostId,
|
|
18692
|
+
harnessId: row.harnessId,
|
|
18693
|
+
sourceProjectId: row.sourceProjectId,
|
|
18694
|
+
startedAt: row.startedAt,
|
|
18695
|
+
endedAt: row.endedAt,
|
|
18696
|
+
severity: row.severity,
|
|
18697
|
+
priority: row.priority,
|
|
18698
|
+
content: row.content,
|
|
18699
|
+
contentHash: row.contentHash,
|
|
18700
|
+
attributes: row.attributes
|
|
18701
|
+
})
|
|
18702
|
+
);
|
|
18484
18703
|
}
|
|
18485
18704
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18486
18705
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18494,22 +18713,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18494
18713
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18495
18714
|
if (!Number.isFinite(startedAt)) return;
|
|
18496
18715
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18497
|
-
this.upsertLlmCallStmt.run(
|
|
18498
|
-
|
|
18499
|
-
|
|
18500
|
-
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
|
|
18716
|
+
this.upsertLlmCallStmt.run(
|
|
18717
|
+
bindParams({
|
|
18718
|
+
id,
|
|
18719
|
+
parentId: input.parentId,
|
|
18720
|
+
rootSessionId: input.rootSessionId,
|
|
18721
|
+
eventType: "llm_call",
|
|
18722
|
+
hostId: null,
|
|
18723
|
+
harnessId: null,
|
|
18724
|
+
sourceProjectId: null,
|
|
18725
|
+
startedAt,
|
|
18726
|
+
endedAt: null,
|
|
18727
|
+
severity: null,
|
|
18728
|
+
priority: null,
|
|
18729
|
+
content: null,
|
|
18730
|
+
contentHash: null,
|
|
18731
|
+
attributes: JSON.stringify(input.attributes)
|
|
18732
|
+
})
|
|
18733
|
+
);
|
|
18513
18734
|
}
|
|
18514
18735
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18515
18736
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18533,25 +18754,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18533
18754
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18534
18755
|
if (!Number.isFinite(startedAt)) return;
|
|
18535
18756
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18536
|
-
this.insertStmt.run(
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18757
|
+
this.insertStmt.run(
|
|
18758
|
+
bindParams({
|
|
18759
|
+
id,
|
|
18760
|
+
parentId: input.parentId,
|
|
18761
|
+
rootSessionId: input.rootSessionId,
|
|
18762
|
+
eventType: "tool_call",
|
|
18763
|
+
hostId: null,
|
|
18764
|
+
harnessId: null,
|
|
18765
|
+
sourceProjectId: null,
|
|
18766
|
+
startedAt,
|
|
18767
|
+
endedAt: null,
|
|
18768
|
+
severity: null,
|
|
18769
|
+
priority: null,
|
|
18770
|
+
content: null,
|
|
18771
|
+
contentHash: null,
|
|
18772
|
+
attributes: JSON.stringify(input.attributes)
|
|
18773
|
+
})
|
|
18774
|
+
);
|
|
18552
18775
|
}
|
|
18553
18776
|
findById(id) {
|
|
18554
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18777
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18778
|
+
id
|
|
18779
|
+
});
|
|
18555
18780
|
}
|
|
18556
18781
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18557
18782
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18561,14 +18786,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18561
18786
|
sessionProvider(sessionId) {
|
|
18562
18787
|
const row = this.findById(sessionId);
|
|
18563
18788
|
if (!row?.attributes) return void 0;
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18567
|
-
const provider = parsed.provider;
|
|
18568
|
-
if (typeof provider === "string") return provider;
|
|
18569
|
-
}
|
|
18570
|
-
} catch {
|
|
18571
|
-
}
|
|
18789
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18790
|
+
if (typeof provider === "string") return provider;
|
|
18572
18791
|
return void 0;
|
|
18573
18792
|
}
|
|
18574
18793
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18577,11 +18796,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18577
18796
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18578
18797
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18579
18798
|
llmCallLeaves() {
|
|
18580
|
-
return
|
|
18581
|
-
|
|
18799
|
+
return allRows(
|
|
18800
|
+
this.db.prepare(
|
|
18801
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18582
18802
|
FROM audit_events
|
|
18583
18803
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18584
|
-
|
|
18804
|
+
)
|
|
18805
|
+
);
|
|
18585
18806
|
}
|
|
18586
18807
|
};
|
|
18587
18808
|
|
|
@@ -18600,16 +18821,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18600
18821
|
upsert(input) {
|
|
18601
18822
|
const id = classifiedDataId(input.class);
|
|
18602
18823
|
const row = toClassifiedDataRow(input, id);
|
|
18603
|
-
this.insertStmt.run(
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18608
|
-
|
|
18824
|
+
this.insertStmt.run(
|
|
18825
|
+
bindParams({
|
|
18826
|
+
id: row.id,
|
|
18827
|
+
class: row.class,
|
|
18828
|
+
label: row.label,
|
|
18829
|
+
attributes: row.attributes
|
|
18830
|
+
})
|
|
18831
|
+
);
|
|
18609
18832
|
return id;
|
|
18610
18833
|
}
|
|
18611
18834
|
};
|
|
18612
18835
|
|
|
18836
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18837
|
+
function latestConfigScan(db) {
|
|
18838
|
+
return getRow(
|
|
18839
|
+
db.prepare(
|
|
18840
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18841
|
+
WHERE event_type = 'config_scan'
|
|
18842
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18843
|
+
)
|
|
18844
|
+
);
|
|
18845
|
+
}
|
|
18846
|
+
|
|
18613
18847
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18614
18848
|
var SqliteConfigInventoryRepository = class {
|
|
18615
18849
|
constructor(db) {
|
|
@@ -18617,7 +18851,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18617
18851
|
}
|
|
18618
18852
|
db;
|
|
18619
18853
|
report() {
|
|
18620
|
-
const scan2 = this.
|
|
18854
|
+
const scan2 = latestConfigScan(this.db);
|
|
18621
18855
|
if (!scan2) {
|
|
18622
18856
|
return {
|
|
18623
18857
|
scannedAt: null,
|
|
@@ -18628,17 +18862,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18628
18862
|
topics: []
|
|
18629
18863
|
};
|
|
18630
18864
|
}
|
|
18631
|
-
const rows =
|
|
18632
|
-
|
|
18865
|
+
const rows = allRows(
|
|
18866
|
+
this.db.prepare(
|
|
18867
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18633
18868
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18634
18869
|
ORDER BY object_type, title`
|
|
18635
|
-
|
|
18636
|
-
|
|
18637
|
-
|
|
18870
|
+
),
|
|
18871
|
+
{ startedAt: scan2.started_at }
|
|
18872
|
+
);
|
|
18873
|
+
const findings = allRows(
|
|
18874
|
+
this.db.prepare(
|
|
18875
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18638
18876
|
FROM inspection_findings f
|
|
18639
18877
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18640
18878
|
WHERE f.audit_event_id = :scanId`
|
|
18641
|
-
|
|
18879
|
+
),
|
|
18880
|
+
{ scanId: scan2.id }
|
|
18881
|
+
);
|
|
18642
18882
|
const skills = [];
|
|
18643
18883
|
const hooks = [];
|
|
18644
18884
|
const mcpServers = [];
|
|
@@ -18667,7 +18907,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18667
18907
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18668
18908
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18669
18909
|
trustOverrides() {
|
|
18670
|
-
const rows =
|
|
18910
|
+
const rows = allRows(
|
|
18911
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18912
|
+
);
|
|
18671
18913
|
const map2 = /* @__PURE__ */ new Map();
|
|
18672
18914
|
for (const row of rows) {
|
|
18673
18915
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18676,13 +18918,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18676
18918
|
}
|
|
18677
18919
|
return map2;
|
|
18678
18920
|
}
|
|
18679
|
-
latestScan() {
|
|
18680
|
-
return this.db.prepare(
|
|
18681
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18682
|
-
WHERE event_type = 'config_scan'
|
|
18683
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18684
|
-
).get();
|
|
18685
|
-
}
|
|
18686
18921
|
};
|
|
18687
18922
|
function toSkillItem(row, bag) {
|
|
18688
18923
|
const item = {
|
|
@@ -18793,22 +19028,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18793
19028
|
return topics;
|
|
18794
19029
|
}
|
|
18795
19030
|
function countScanErrors(attributes) {
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
const parsed = JSON.parse(attributes);
|
|
18799
|
-
const errors = parsed?.errors;
|
|
18800
|
-
return typeof errors === "number" ? errors : 0;
|
|
18801
|
-
} catch {
|
|
18802
|
-
return 0;
|
|
18803
|
-
}
|
|
19031
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
19032
|
+
return typeof errors === "number" ? errors : 0;
|
|
18804
19033
|
}
|
|
18805
19034
|
function parseBag(raw) {
|
|
18806
|
-
|
|
18807
|
-
const parsed = JSON.parse(raw);
|
|
18808
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18809
|
-
} catch {
|
|
18810
|
-
}
|
|
18811
|
-
return void 0;
|
|
19035
|
+
return parseJsonObject(raw);
|
|
18812
19036
|
}
|
|
18813
19037
|
function str(value) {
|
|
18814
19038
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18817,12 +19041,7 @@ function str(value) {
|
|
|
18817
19041
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18818
19042
|
var DAY_MS2 = 864e5;
|
|
18819
19043
|
function parseRules(rulesJson) {
|
|
18820
|
-
|
|
18821
|
-
try {
|
|
18822
|
-
raw = JSON.parse(rulesJson);
|
|
18823
|
-
} catch {
|
|
18824
|
-
return [];
|
|
18825
|
-
}
|
|
19044
|
+
const raw = safeJson(rulesJson, []);
|
|
18826
19045
|
if (!Array.isArray(raw)) return [];
|
|
18827
19046
|
const rules = [];
|
|
18828
19047
|
for (const entry of raw) {
|
|
@@ -18841,11 +19060,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18841
19060
|
db;
|
|
18842
19061
|
now;
|
|
18843
19062
|
listDetections(query) {
|
|
18844
|
-
const rows =
|
|
18845
|
-
|
|
19063
|
+
const rows = allRows(
|
|
19064
|
+
this.db.prepare(
|
|
19065
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18846
19066
|
rules_json AS rulesJson
|
|
18847
19067
|
FROM installed_packs`
|
|
18848
|
-
|
|
19068
|
+
)
|
|
19069
|
+
);
|
|
18849
19070
|
const available = this.availableByPack();
|
|
18850
19071
|
const summaries = rows.map((r) => {
|
|
18851
19072
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18854,7 +19075,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18854
19075
|
packId: r.packId,
|
|
18855
19076
|
version: r.version,
|
|
18856
19077
|
name: r.name,
|
|
18857
|
-
enabled: r.enabled
|
|
19078
|
+
enabled: intToBool(r.enabled),
|
|
18858
19079
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18859
19080
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18860
19081
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18871,19 +19092,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18871
19092
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18872
19093
|
// detail call; the table is a handful of rows).
|
|
18873
19094
|
availableByPack() {
|
|
18874
|
-
const rows =
|
|
18875
|
-
|
|
19095
|
+
const rows = allRows(
|
|
19096
|
+
this.db.prepare(
|
|
19097
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18876
19098
|
FROM available_packs`
|
|
18877
|
-
|
|
19099
|
+
)
|
|
19100
|
+
);
|
|
18878
19101
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18879
19102
|
}
|
|
18880
19103
|
getDetectionStats() {
|
|
18881
|
-
const rows =
|
|
19104
|
+
const rows = allRows(
|
|
19105
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19106
|
+
);
|
|
18882
19107
|
let rules = 0;
|
|
18883
19108
|
let active = 0;
|
|
18884
19109
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18885
19110
|
for (const r of rows) {
|
|
18886
|
-
if (r.enabled
|
|
19111
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18887
19112
|
const parsed = parseRules(r.rulesJson);
|
|
18888
19113
|
rules += parsed.length;
|
|
18889
19114
|
for (const rule of parsed) {
|
|
@@ -18901,12 +19126,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18901
19126
|
const parts = splitDetectionId(id);
|
|
18902
19127
|
if (!parts) return Promise.resolve(null);
|
|
18903
19128
|
const { namespace, packId } = parts;
|
|
18904
|
-
const row =
|
|
18905
|
-
|
|
19129
|
+
const row = getRow(
|
|
19130
|
+
this.db.prepare(
|
|
19131
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18906
19132
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18907
19133
|
FROM installed_packs
|
|
18908
19134
|
WHERE namespace = ? AND pack_id = ?`
|
|
18909
|
-
|
|
19135
|
+
),
|
|
19136
|
+
[namespace, packId]
|
|
19137
|
+
);
|
|
18910
19138
|
if (!row) return Promise.resolve(null);
|
|
18911
19139
|
const rules = parseRules(row.rulesJson);
|
|
18912
19140
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18924,7 +19152,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18924
19152
|
packId: row.packId,
|
|
18925
19153
|
version: row.version,
|
|
18926
19154
|
name: row.name,
|
|
18927
|
-
enabled: row.enabled
|
|
19155
|
+
enabled: intToBool(row.enabled),
|
|
18928
19156
|
rules,
|
|
18929
19157
|
updatedAt: new Date(row.updatedAt),
|
|
18930
19158
|
policyId: row.policyId
|
|
@@ -18939,13 +19167,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18939
19167
|
countFindingsLast30d(ruleIds) {
|
|
18940
19168
|
if (ruleIds.length === 0) return 0;
|
|
18941
19169
|
const since = this.now() - 30 * DAY_MS2;
|
|
18942
|
-
const
|
|
18943
|
-
|
|
18944
|
-
|
|
19170
|
+
const inClause = placeholders(ruleIds.length);
|
|
19171
|
+
return countScalar(
|
|
19172
|
+
this.db,
|
|
19173
|
+
`SELECT count(*) AS n
|
|
18945
19174
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18946
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18947
|
-
|
|
18948
|
-
|
|
19175
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19176
|
+
[since, ...ruleIds]
|
|
19177
|
+
);
|
|
18949
19178
|
}
|
|
18950
19179
|
};
|
|
18951
19180
|
|
|
@@ -18962,16 +19191,17 @@ var SqliteEventsRepository = class {
|
|
|
18962
19191
|
insertStmt;
|
|
18963
19192
|
insertEvent(event) {
|
|
18964
19193
|
const row = toEventRow(event);
|
|
18965
|
-
this.insertStmt.run(
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
19194
|
+
this.insertStmt.run(
|
|
19195
|
+
bindParams({
|
|
19196
|
+
id: row.id,
|
|
19197
|
+
sourceTool: row.sourceTool,
|
|
19198
|
+
kind: row.kind,
|
|
19199
|
+
occurredAt: row.occurredAt,
|
|
19200
|
+
contentHash: row.contentHash,
|
|
19201
|
+
content: row.content,
|
|
19202
|
+
metadata: row.metadata
|
|
19203
|
+
})
|
|
19204
|
+
);
|
|
18975
19205
|
}
|
|
18976
19206
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18977
19207
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18979,13 +19209,23 @@ var SqliteEventsRepository = class {
|
|
|
18979
19209
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18980
19210
|
// async EventsReadPort contract.
|
|
18981
19211
|
contentHashes() {
|
|
18982
|
-
const rows =
|
|
19212
|
+
const rows = allRows(
|
|
19213
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19214
|
+
);
|
|
18983
19215
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18984
19216
|
}
|
|
18985
19217
|
};
|
|
18986
19218
|
|
|
18987
19219
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18988
19220
|
import { randomUUID } from "crypto";
|
|
19221
|
+
|
|
19222
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19223
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19224
|
+
function isUniqueConstraintError(err) {
|
|
19225
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19226
|
+
}
|
|
19227
|
+
|
|
19228
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18989
19229
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18990
19230
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18991
19231
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -19006,10 +19246,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
19006
19246
|
this.name = "AmbiguousExceptionIdError";
|
|
19007
19247
|
}
|
|
19008
19248
|
};
|
|
19009
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19010
|
-
function isUniqueConstraintError(err) {
|
|
19011
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19012
|
-
}
|
|
19013
19249
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
19014
19250
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
19015
19251
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -19065,10 +19301,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19065
19301
|
this.insertExceptionRow(id, input, now);
|
|
19066
19302
|
} catch (err) {
|
|
19067
19303
|
if (!isUniqueConstraintError(err)) throw err;
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19304
|
+
withTransaction(
|
|
19305
|
+
this.db,
|
|
19306
|
+
() => {
|
|
19307
|
+
const superseded = this.db.prepare(
|
|
19308
|
+
`UPDATE exceptions
|
|
19072
19309
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
19073
19310
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
19074
19311
|
updated_at = :now
|
|
@@ -19076,24 +19313,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19076
19313
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
19077
19314
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
19078
19315
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19316
|
+
).run({
|
|
19317
|
+
now,
|
|
19318
|
+
revokedBy: input.createdBy,
|
|
19319
|
+
ruleId: input.ruleId,
|
|
19320
|
+
valueFingerprint: input.valueFingerprint,
|
|
19321
|
+
keyVersion: input.keyVersion
|
|
19322
|
+
});
|
|
19323
|
+
if (Number(superseded.changes) !== 1) {
|
|
19324
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19325
|
+
}
|
|
19326
|
+
this.insertExceptionRow(id, input, now);
|
|
19327
|
+
},
|
|
19328
|
+
"IMMEDIATE"
|
|
19329
|
+
);
|
|
19330
|
+
}
|
|
19331
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19332
|
+
id
|
|
19333
|
+
});
|
|
19334
|
+
if (row === void 0) {
|
|
19335
|
+
throw new Error("exception row not found immediately after insert");
|
|
19095
19336
|
}
|
|
19096
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
19097
19337
|
return parseExceptionRow(row);
|
|
19098
19338
|
}
|
|
19099
19339
|
insertExceptionRow(id, input, now) {
|
|
@@ -19131,14 +19371,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19131
19371
|
*/
|
|
19132
19372
|
list(opts) {
|
|
19133
19373
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19134
|
-
const rows =
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
19139
|
-
} catch {
|
|
19140
|
-
}
|
|
19141
|
-
}
|
|
19374
|
+
const rows = allRows(
|
|
19375
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19376
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19377
|
+
);
|
|
19378
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19142
19379
|
return Promise.resolve(exceptions);
|
|
19143
19380
|
}
|
|
19144
19381
|
/**
|
|
@@ -19148,7 +19385,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19148
19385
|
*/
|
|
19149
19386
|
getByIdPrefix(prefix) {
|
|
19150
19387
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19151
|
-
const rows =
|
|
19388
|
+
const rows = allRows(
|
|
19389
|
+
this.db.prepare(
|
|
19390
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19391
|
+
),
|
|
19392
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19393
|
+
);
|
|
19152
19394
|
if (rows.length > 1) {
|
|
19153
19395
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19154
19396
|
}
|
|
@@ -19190,30 +19432,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19190
19432
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19191
19433
|
*/
|
|
19192
19434
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19193
|
-
const rows =
|
|
19194
|
-
|
|
19435
|
+
const rows = allRows(
|
|
19436
|
+
this.db.prepare(
|
|
19437
|
+
`SELECT * FROM exceptions
|
|
19195
19438
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19196
19439
|
ORDER BY created_at DESC, rowid DESC`
|
|
19197
|
-
|
|
19198
|
-
|
|
19199
|
-
|
|
19200
|
-
|
|
19201
|
-
|
|
19202
|
-
|
|
19203
|
-
|
|
19204
|
-
|
|
19205
|
-
|
|
19206
|
-
|
|
19207
|
-
|
|
19208
|
-
|
|
19209
|
-
|
|
19210
|
-
|
|
19211
|
-
|
|
19212
|
-
|
|
19213
|
-
);
|
|
19214
|
-
} catch {
|
|
19215
|
-
}
|
|
19216
|
-
}
|
|
19440
|
+
),
|
|
19441
|
+
{ keyVersion, now }
|
|
19442
|
+
);
|
|
19443
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19444
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19445
|
+
return ExceptionBundleEntry.parse({
|
|
19446
|
+
id: row.id,
|
|
19447
|
+
ruleId: row.rule_id,
|
|
19448
|
+
valueFingerprint: row.value_fingerprint,
|
|
19449
|
+
keyVersion: row.key_version,
|
|
19450
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19451
|
+
maxUses: row.max_uses,
|
|
19452
|
+
useCount: row.use_count,
|
|
19453
|
+
conditions
|
|
19454
|
+
});
|
|
19455
|
+
});
|
|
19217
19456
|
return Promise.resolve(entries);
|
|
19218
19457
|
}
|
|
19219
19458
|
/**
|
|
@@ -19240,11 +19479,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19240
19479
|
}
|
|
19241
19480
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19242
19481
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19243
|
-
const rows =
|
|
19244
|
-
|
|
19482
|
+
const rows = allRows(
|
|
19483
|
+
this.db.prepare(
|
|
19484
|
+
`SELECT * FROM blocked_detections
|
|
19245
19485
|
WHERE blocked_at > :cutoff
|
|
19246
19486
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19247
|
-
|
|
19487
|
+
),
|
|
19488
|
+
{ cutoff: Date.now() - windowMs }
|
|
19489
|
+
);
|
|
19248
19490
|
return Promise.resolve(
|
|
19249
19491
|
rows.map((row) => ({
|
|
19250
19492
|
reference: row.reference,
|
|
@@ -19324,7 +19566,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19324
19566
|
)`;
|
|
19325
19567
|
|
|
19326
19568
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19327
|
-
var
|
|
19569
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19570
|
+
var CONCAT_SEP = ",";
|
|
19571
|
+
var TUPLE_SEP = "|";
|
|
19572
|
+
function splitConcat(value) {
|
|
19573
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19574
|
+
}
|
|
19328
19575
|
function deriveInstanceStatus(row) {
|
|
19329
19576
|
return deriveFindingStatus({
|
|
19330
19577
|
kind: row.kind,
|
|
@@ -19392,13 +19639,16 @@ var SqliteFindingsRepository = class {
|
|
|
19392
19639
|
}
|
|
19393
19640
|
recentFindings(opts) {
|
|
19394
19641
|
const limit = opts?.limit ?? 50;
|
|
19395
|
-
const rows =
|
|
19396
|
-
|
|
19642
|
+
const rows = allRows(
|
|
19643
|
+
this.db.prepare(
|
|
19644
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19397
19645
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19398
19646
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19399
19647
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19400
19648
|
LIMIT :limit`
|
|
19401
|
-
|
|
19649
|
+
),
|
|
19650
|
+
{ limit }
|
|
19651
|
+
);
|
|
19402
19652
|
return Promise.resolve(
|
|
19403
19653
|
rows.map((r) => ({
|
|
19404
19654
|
id: r.id,
|
|
@@ -19421,20 +19671,47 @@ var SqliteFindingsRepository = class {
|
|
|
19421
19671
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19422
19672
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19423
19673
|
* reflect the full filtered set; `items` is the requested
|
|
19424
|
-
* page (default
|
|
19674
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19675
|
+
*
|
|
19676
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19677
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19678
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19679
|
+
* latest, search text);
|
|
19680
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19681
|
+
* populate `instances` for the table's expanded rows.
|
|
19682
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19683
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19684
|
+
* rule is ever restated in SQL.
|
|
19425
19685
|
*/
|
|
19426
19686
|
listGroupedFindings(query) {
|
|
19427
|
-
const
|
|
19428
|
-
|
|
19429
|
-
|
|
19430
|
-
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19687
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
|
|
19688
|
+
const rows = allRows(
|
|
19689
|
+
this.db.prepare(
|
|
19690
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19691
|
+
occurred_at, source_tool, repo, file, kind, finding_key, latest_status
|
|
19692
|
+
FROM (
|
|
19693
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19694
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19695
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19696
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19697
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19698
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19699
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19700
|
+
latest.status AS latest_status,
|
|
19701
|
+
ROW_NUMBER() OVER (
|
|
19702
|
+
PARTITION BY f.rule_id
|
|
19703
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19704
|
+
) AS rn
|
|
19705
|
+
FROM findings f
|
|
19706
|
+
JOIN events e ON e.id = f.event_id
|
|
19707
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19708
|
+
ON latest.finding_key = f.finding_key
|
|
19709
|
+
)
|
|
19710
|
+
WHERE rn <= :cap
|
|
19711
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19712
|
+
),
|
|
19713
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP }
|
|
19714
|
+
);
|
|
19438
19715
|
const groupable = rows.map((r) => ({
|
|
19439
19716
|
id: r.id,
|
|
19440
19717
|
ruleId: r.rule_id,
|
|
@@ -19449,7 +19726,7 @@ var SqliteFindingsRepository = class {
|
|
|
19449
19726
|
file: r.file ?? "",
|
|
19450
19727
|
status: deriveInstanceStatus(r)
|
|
19451
19728
|
}));
|
|
19452
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19729
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19453
19730
|
const filterOpts = {
|
|
19454
19731
|
severity: query.severity,
|
|
19455
19732
|
providers: query.provider,
|
|
@@ -19467,42 +19744,122 @@ var SqliteFindingsRepository = class {
|
|
|
19467
19744
|
const items = sorted.slice(0, limit);
|
|
19468
19745
|
return Promise.resolve({ totals, facets, items, nextCursor: null });
|
|
19469
19746
|
}
|
|
19747
|
+
/**
|
|
19748
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19749
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19750
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19751
|
+
*
|
|
19752
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19753
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19754
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19755
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19756
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19757
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19758
|
+
* a group's row stays small however many findings it holds.
|
|
19759
|
+
*
|
|
19760
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19761
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19762
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19763
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19764
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19765
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19766
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19767
|
+
* carries a `q`.
|
|
19768
|
+
*/
|
|
19769
|
+
groupAggregates(withSearchText) {
|
|
19770
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19771
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
|
|
19772
|
+
const rows = this.db.prepare(
|
|
19773
|
+
`SELECT f.rule_id AS rule_id,
|
|
19774
|
+
count(*) AS instance_count,
|
|
19775
|
+
max(e.occurred_at) AS latest_at,
|
|
19776
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19777
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19778
|
+
group_concat(DISTINCT (
|
|
19779
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19780
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19781
|
+
coalesce(latest.status, '')
|
|
19782
|
+
)) AS status_inputs
|
|
19783
|
+
${searchTextColumns}
|
|
19784
|
+
FROM findings f
|
|
19785
|
+
JOIN events e ON e.id = f.event_id
|
|
19786
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19787
|
+
ON latest.finding_key = f.finding_key
|
|
19788
|
+
GROUP BY f.rule_id`
|
|
19789
|
+
).all();
|
|
19790
|
+
return new Map(
|
|
19791
|
+
rows.map((r) => [
|
|
19792
|
+
r.rule_id,
|
|
19793
|
+
{
|
|
19794
|
+
instanceCount: r.instance_count,
|
|
19795
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19796
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19797
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19798
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19799
|
+
return {
|
|
19800
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19801
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19802
|
+
kind,
|
|
19803
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19804
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19805
|
+
};
|
|
19806
|
+
}),
|
|
19807
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19808
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19809
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19810
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19811
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19812
|
+
// and skip priming a haystack nothing will read.
|
|
19813
|
+
...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
|
|
19814
|
+
}
|
|
19815
|
+
])
|
|
19816
|
+
);
|
|
19817
|
+
}
|
|
19470
19818
|
healthSummary() {
|
|
19471
|
-
const total = this.db
|
|
19819
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19472
19820
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19473
|
-
const grouped =
|
|
19821
|
+
const grouped = allRows(
|
|
19822
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19823
|
+
);
|
|
19474
19824
|
for (const row of grouped) {
|
|
19475
19825
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19476
19826
|
}
|
|
19477
19827
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19478
|
-
const sevRows =
|
|
19479
|
-
|
|
19828
|
+
const sevRows = allRows(
|
|
19829
|
+
this.db.prepare(
|
|
19830
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19480
19831
|
FROM findings f
|
|
19481
19832
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19482
19833
|
ON latest.finding_key = f.finding_key
|
|
19483
19834
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19484
19835
|
GROUP BY f.severity`
|
|
19485
|
-
|
|
19836
|
+
)
|
|
19837
|
+
);
|
|
19486
19838
|
for (const row of sevRows) {
|
|
19487
19839
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19488
19840
|
}
|
|
19489
19841
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19490
|
-
const enabledRows =
|
|
19491
|
-
|
|
19842
|
+
const enabledRows = allRows(
|
|
19843
|
+
this.db.prepare(
|
|
19844
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19492
19845
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19493
|
-
|
|
19846
|
+
)
|
|
19847
|
+
);
|
|
19494
19848
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19495
19849
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19496
19850
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19497
19851
|
}
|
|
19498
19852
|
activityByDay(days = 7) {
|
|
19499
19853
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19500
|
-
const rows =
|
|
19501
|
-
|
|
19854
|
+
const rows = allRows(
|
|
19855
|
+
this.db.prepare(
|
|
19856
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19502
19857
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19503
19858
|
WHERE e.occurred_at >= :since
|
|
19504
19859
|
GROUP BY day, f.action_taken`
|
|
19505
|
-
|
|
19860
|
+
),
|
|
19861
|
+
{ since }
|
|
19862
|
+
);
|
|
19506
19863
|
const buckets = /* @__PURE__ */ new Map();
|
|
19507
19864
|
for (let i = 0; i < days; i++) {
|
|
19508
19865
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19574,17 +19931,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19574
19931
|
insertStmt;
|
|
19575
19932
|
insertFinding(input) {
|
|
19576
19933
|
const row = toInspectionFindingRow(input);
|
|
19577
|
-
this.insertStmt.run(
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
19934
|
+
this.insertStmt.run(
|
|
19935
|
+
bindParams({
|
|
19936
|
+
id: row.id,
|
|
19937
|
+
auditEventId: row.auditEventId,
|
|
19938
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
19939
|
+
classifiedDataId: row.classifiedDataId,
|
|
19940
|
+
spanStart: row.spanStart,
|
|
19941
|
+
spanEnd: row.spanEnd,
|
|
19942
|
+
maskedMatch: row.maskedMatch,
|
|
19943
|
+
actionTaken: row.actionTaken,
|
|
19944
|
+
confidence: row.confidence
|
|
19945
|
+
})
|
|
19946
|
+
);
|
|
19588
19947
|
}
|
|
19589
19948
|
};
|
|
19590
19949
|
|
|
@@ -19660,12 +20019,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19660
20019
|
}
|
|
19661
20020
|
function ruleIdsOf(rulesJson) {
|
|
19662
20021
|
const ids = /* @__PURE__ */ new Set();
|
|
19663
|
-
|
|
19664
|
-
try {
|
|
19665
|
-
raw = JSON.parse(rulesJson);
|
|
19666
|
-
} catch {
|
|
19667
|
-
return ids;
|
|
19668
|
-
}
|
|
20022
|
+
const raw = safeJson(rulesJson, []);
|
|
19669
20023
|
if (!Array.isArray(raw)) return ids;
|
|
19670
20024
|
for (const entry of raw) {
|
|
19671
20025
|
if (entry && typeof entry === "object") {
|
|
@@ -19738,47 +20092,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19738
20092
|
}));
|
|
19739
20093
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19740
20094
|
const now = Date.now();
|
|
19741
|
-
|
|
19742
|
-
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
const
|
|
19747
|
-
|
|
19748
|
-
namespace: row.namespace,
|
|
19749
|
-
packId: row.packId,
|
|
19750
|
-
version: row.version,
|
|
19751
|
-
name: row.name,
|
|
19752
|
-
rulesJson: row.rulesJson,
|
|
19753
|
-
now
|
|
19754
|
-
};
|
|
19755
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19756
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19757
|
-
this.upsertAvailableStmt.run({
|
|
19758
|
-
...params,
|
|
20095
|
+
withTransaction(
|
|
20096
|
+
this.db,
|
|
20097
|
+
() => {
|
|
20098
|
+
const mirror = this.mirrorState();
|
|
20099
|
+
let behind = false;
|
|
20100
|
+
for (const row of rows) {
|
|
20101
|
+
const params = {
|
|
19759
20102
|
id: randomUUID2(),
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
20103
|
+
namespace: row.namespace,
|
|
20104
|
+
packId: row.packId,
|
|
20105
|
+
version: row.version,
|
|
20106
|
+
name: row.name,
|
|
20107
|
+
rulesJson: row.rulesJson,
|
|
20108
|
+
now
|
|
20109
|
+
};
|
|
20110
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20111
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20112
|
+
this.upsertAvailableStmt.run({
|
|
20113
|
+
...params,
|
|
20114
|
+
id: randomUUID2(),
|
|
20115
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20116
|
+
});
|
|
20117
|
+
} else {
|
|
20118
|
+
behind = true;
|
|
20119
|
+
}
|
|
20120
|
+
this.insertMissingStmt.run(params);
|
|
20121
|
+
}
|
|
20122
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20123
|
+
},
|
|
20124
|
+
"IMMEDIATE"
|
|
20125
|
+
);
|
|
19773
20126
|
} catch {
|
|
19774
20127
|
}
|
|
19775
20128
|
}
|
|
19776
20129
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19777
20130
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19778
20131
|
mirrorState() {
|
|
19779
|
-
const rows =
|
|
19780
|
-
|
|
19781
|
-
|
|
20132
|
+
const rows = allRows(
|
|
20133
|
+
this.db.prepare(
|
|
20134
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20135
|
+
)
|
|
20136
|
+
);
|
|
19782
20137
|
return new Map(
|
|
19783
20138
|
rows.map((r) => [
|
|
19784
20139
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19790,7 +20145,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19790
20145
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19791
20146
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19792
20147
|
pruneAvailable(keep) {
|
|
19793
|
-
const rows =
|
|
20148
|
+
const rows = allRows(
|
|
20149
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20150
|
+
);
|
|
19794
20151
|
const keepSet = new Set(keep);
|
|
19795
20152
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19796
20153
|
for (const r of rows) {
|
|
@@ -19817,11 +20174,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19817
20174
|
if (this.db.isTransaction) {
|
|
19818
20175
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19819
20176
|
}
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
this.db
|
|
19823
|
-
|
|
19824
|
-
|
|
20177
|
+
let changed = false;
|
|
20178
|
+
withTransaction(
|
|
20179
|
+
this.db,
|
|
20180
|
+
() => {
|
|
20181
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20182
|
+
const res = this.db.prepare(
|
|
20183
|
+
`UPDATE installed_packs SET
|
|
19825
20184
|
version = (SELECT a.version FROM available_packs a
|
|
19826
20185
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19827
20186
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19832,17 +20191,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19832
20191
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19833
20192
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19834
20193
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
} catch {
|
|
19843
|
-
}
|
|
19844
|
-
throw err;
|
|
19845
|
-
}
|
|
20194
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20195
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20196
|
+
changed = Number(res.changes) > 0;
|
|
20197
|
+
},
|
|
20198
|
+
"IMMEDIATE"
|
|
20199
|
+
);
|
|
20200
|
+
return changed;
|
|
19846
20201
|
}
|
|
19847
20202
|
/**
|
|
19848
20203
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19856,9 +20211,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19856
20211
|
* JSON-level failure therefore counts as invalid.
|
|
19857
20212
|
*/
|
|
19858
20213
|
installedRuleset() {
|
|
19859
|
-
const rows =
|
|
19860
|
-
|
|
19861
|
-
|
|
20214
|
+
const rows = allRows(
|
|
20215
|
+
this.db.prepare(
|
|
20216
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20217
|
+
)
|
|
20218
|
+
);
|
|
19862
20219
|
const out = {
|
|
19863
20220
|
installedPacks: rows.length,
|
|
19864
20221
|
enabledPacks: 0,
|
|
@@ -19867,7 +20224,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19867
20224
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19868
20225
|
};
|
|
19869
20226
|
for (const row of rows) {
|
|
19870
|
-
if (row.enabled
|
|
20227
|
+
if (!intToBool(row.enabled)) continue;
|
|
19871
20228
|
out.enabledPacks += 1;
|
|
19872
20229
|
const action = policyIdToAction(row.policyId);
|
|
19873
20230
|
let raw;
|
|
@@ -19902,9 +20259,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19902
20259
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19903
20260
|
*/
|
|
19904
20261
|
newestRecordedBinary() {
|
|
19905
|
-
const rows =
|
|
19906
|
-
|
|
19907
|
-
|
|
20262
|
+
const rows = allRows(
|
|
20263
|
+
this.db.prepare(
|
|
20264
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20265
|
+
)
|
|
20266
|
+
);
|
|
19908
20267
|
let newest = null;
|
|
19909
20268
|
for (const row of rows) {
|
|
19910
20269
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19919,13 +20278,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19919
20278
|
return newest;
|
|
19920
20279
|
}
|
|
19921
20280
|
counts() {
|
|
19922
|
-
const row =
|
|
19923
|
-
|
|
20281
|
+
const row = getRow(
|
|
20282
|
+
this.db.prepare(
|
|
20283
|
+
`SELECT count(*) AS packs,
|
|
19924
20284
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19925
20285
|
coalesce(sum(enabled), 0) AS enabled
|
|
19926
20286
|
FROM installed_packs`
|
|
19927
|
-
|
|
19928
|
-
|
|
20287
|
+
)
|
|
20288
|
+
);
|
|
20289
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19929
20290
|
}
|
|
19930
20291
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19931
20292
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19938,26 +20299,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19938
20299
|
* attributed to Monitor, matching the Detections views.
|
|
19939
20300
|
*/
|
|
19940
20301
|
countsByPolicyId() {
|
|
19941
|
-
|
|
19942
|
-
|
|
20302
|
+
return countBy(
|
|
20303
|
+
this.db,
|
|
20304
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19943
20305
|
FROM installed_packs
|
|
19944
|
-
GROUP BY
|
|
19945
|
-
)
|
|
19946
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20306
|
+
GROUP BY k`
|
|
20307
|
+
);
|
|
19947
20308
|
}
|
|
19948
20309
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19949
20310
|
listByPolicyId(policyId) {
|
|
19950
|
-
const rows =
|
|
19951
|
-
|
|
20311
|
+
const rows = allRows(
|
|
20312
|
+
this.db.prepare(
|
|
20313
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19952
20314
|
FROM installed_packs
|
|
19953
20315
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19954
20316
|
ORDER BY name ASC`
|
|
19955
|
-
|
|
20317
|
+
),
|
|
20318
|
+
[policyId]
|
|
20319
|
+
);
|
|
19956
20320
|
return rows.map((r) => ({
|
|
19957
20321
|
id: `${r.namespace}/${r.packId}`,
|
|
19958
20322
|
name: r.name,
|
|
19959
20323
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19960
|
-
enabled: r.enabled
|
|
20324
|
+
enabled: intToBool(r.enabled)
|
|
19961
20325
|
}));
|
|
19962
20326
|
}
|
|
19963
20327
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19986,14 +20350,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19986
20350
|
const res = this.db.prepare(
|
|
19987
20351
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19988
20352
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19989
|
-
).run({ enabled: enabled
|
|
20353
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19990
20354
|
return Number(res.changes) > 0;
|
|
19991
20355
|
}
|
|
19992
20356
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19993
20357
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19994
20358
|
// binary's inventory hasn't changed since the last record.
|
|
19995
20359
|
storedSignature() {
|
|
19996
|
-
const rows = this.signatureStmt
|
|
20360
|
+
const rows = allRows(this.signatureStmt);
|
|
19997
20361
|
return inventorySignature(rows);
|
|
19998
20362
|
}
|
|
19999
20363
|
};
|
|
@@ -20021,42 +20385,48 @@ var SqliteInventoryRepository = class {
|
|
|
20021
20385
|
upsert(input, now = Date.now()) {
|
|
20022
20386
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
20023
20387
|
const row = toInventoryRow(input, id, now);
|
|
20024
|
-
this.upsertStmt.run(
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20388
|
+
this.upsertStmt.run(
|
|
20389
|
+
bindParams({
|
|
20390
|
+
id: row.id,
|
|
20391
|
+
objectType: row.objectType,
|
|
20392
|
+
location: row.location,
|
|
20393
|
+
title: row.title,
|
|
20394
|
+
hostId: row.hostId,
|
|
20395
|
+
attributes: row.attributes,
|
|
20396
|
+
firstSeen: row.firstSeen,
|
|
20397
|
+
lastSeen: row.lastSeen
|
|
20398
|
+
})
|
|
20399
|
+
);
|
|
20034
20400
|
return id;
|
|
20035
20401
|
}
|
|
20036
20402
|
// The full row, for round-trip assertions.
|
|
20037
20403
|
findById(id) {
|
|
20038
|
-
|
|
20039
|
-
return row;
|
|
20404
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
20040
20405
|
}
|
|
20041
20406
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
20042
20407
|
// served from the object_type index, never from audit_events.
|
|
20043
20408
|
distinctTitles(objectType) {
|
|
20044
|
-
const rows =
|
|
20045
|
-
|
|
20409
|
+
const rows = allRows(
|
|
20410
|
+
this.db.prepare(
|
|
20411
|
+
`SELECT DISTINCT title FROM inventory
|
|
20046
20412
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
20047
20413
|
ORDER BY title`
|
|
20048
|
-
|
|
20414
|
+
),
|
|
20415
|
+
{ objectType }
|
|
20416
|
+
);
|
|
20049
20417
|
return rows.map((r) => r.title);
|
|
20050
20418
|
}
|
|
20051
20419
|
// Distinct host os_version values — a facet served from an inventory index
|
|
20052
20420
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
20053
20421
|
// QUERY PLAN).
|
|
20054
20422
|
osVersions() {
|
|
20055
|
-
const rows =
|
|
20056
|
-
|
|
20423
|
+
const rows = allRows(
|
|
20424
|
+
this.db.prepare(
|
|
20425
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
20057
20426
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
20058
20427
|
ORDER BY value`
|
|
20059
|
-
|
|
20428
|
+
)
|
|
20429
|
+
);
|
|
20060
20430
|
return rows.map((r) => r.value);
|
|
20061
20431
|
}
|
|
20062
20432
|
};
|
|
@@ -20076,14 +20446,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
20076
20446
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
20077
20447
|
findingsCount: 0
|
|
20078
20448
|
};
|
|
20079
|
-
function safeJson(s, fallback) {
|
|
20080
|
-
if (s == null) return fallback;
|
|
20081
|
-
try {
|
|
20082
|
-
return JSON.parse(s);
|
|
20083
|
-
} catch {
|
|
20084
|
-
return fallback;
|
|
20085
|
-
}
|
|
20086
|
-
}
|
|
20087
20449
|
function resolveHarnessId(attrs, row) {
|
|
20088
20450
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
20089
20451
|
return attrs.provider;
|
|
@@ -20279,30 +20641,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20279
20641
|
configRowsCache;
|
|
20280
20642
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20281
20643
|
getInventoryStats() {
|
|
20282
|
-
const
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
byType
|
|
20287
|
-
|
|
20288
|
-
|
|
20289
|
-
|
|
20644
|
+
const typeCounts = countBy(
|
|
20645
|
+
this.db,
|
|
20646
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20647
|
+
);
|
|
20648
|
+
const byType = {
|
|
20649
|
+
project: 0,
|
|
20650
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20651
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20652
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20653
|
+
config: typeCounts.get("config") ?? 0
|
|
20654
|
+
};
|
|
20655
|
+
byType.project = countScalar(
|
|
20656
|
+
this.db,
|
|
20657
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20658
|
+
);
|
|
20659
|
+
const mcpTrustCounts = countBy(
|
|
20660
|
+
this.db,
|
|
20661
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20290
20662
|
FROM inventory_asset a
|
|
20291
20663
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20292
20664
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20293
20665
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20294
|
-
)
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20666
|
+
);
|
|
20667
|
+
const mcpTrust = {
|
|
20668
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20669
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20670
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20671
|
+
};
|
|
20672
|
+
const harnesses = countScalar(
|
|
20673
|
+
this.db,
|
|
20298
20674
|
`SELECT count(*) AS n FROM inventory
|
|
20299
20675
|
WHERE object_type = 'harness'
|
|
20300
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
const
|
|
20676
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20677
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20678
|
+
);
|
|
20679
|
+
const flaggedAssets = countScalar(
|
|
20680
|
+
this.db,
|
|
20681
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20682
|
+
);
|
|
20683
|
+
const flaggedProjects = countScalar(
|
|
20684
|
+
this.db,
|
|
20304
20685
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20305
|
-
)
|
|
20686
|
+
);
|
|
20306
20687
|
const configRows = this.configAssetRows();
|
|
20307
20688
|
for (const r of configRows) {
|
|
20308
20689
|
byType[r.assetType] += 1;
|
|
@@ -20559,12 +20940,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20559
20940
|
}
|
|
20560
20941
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20561
20942
|
fetchHarnessRows() {
|
|
20562
|
-
return
|
|
20563
|
-
|
|
20943
|
+
return allRows(
|
|
20944
|
+
this.db.prepare(
|
|
20945
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20564
20946
|
FROM inventory
|
|
20565
20947
|
WHERE object_type = 'harness'
|
|
20566
20948
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20567
|
-
|
|
20949
|
+
),
|
|
20950
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20951
|
+
);
|
|
20568
20952
|
}
|
|
20569
20953
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20570
20954
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20574,12 +20958,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20574
20958
|
const params = [...harnessInvIds];
|
|
20575
20959
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20576
20960
|
if (q) {
|
|
20577
|
-
const pat =
|
|
20578
|
-
where +=
|
|
20961
|
+
const pat = containsPattern(q);
|
|
20962
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20579
20963
|
params.push(pat, pat);
|
|
20580
20964
|
}
|
|
20581
|
-
const rows =
|
|
20582
|
-
|
|
20965
|
+
const rows = allRows(
|
|
20966
|
+
this.db.prepare(
|
|
20967
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20583
20968
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20584
20969
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20585
20970
|
FROM harness_asset ha
|
|
@@ -20587,7 +20972,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20587
20972
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20588
20973
|
WHERE ${where}
|
|
20589
20974
|
ORDER BY a.name ASC`
|
|
20590
|
-
|
|
20975
|
+
),
|
|
20976
|
+
params
|
|
20977
|
+
);
|
|
20591
20978
|
for (const raw of rows) {
|
|
20592
20979
|
const harnessInvId = raw.harnessInvId;
|
|
20593
20980
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20606,21 +20993,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20606
20993
|
params.push(...types);
|
|
20607
20994
|
}
|
|
20608
20995
|
if (q) {
|
|
20609
|
-
const pat =
|
|
20610
|
-
conditions.push("
|
|
20996
|
+
const pat = containsPattern(q);
|
|
20997
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20611
20998
|
params.push(pat, pat);
|
|
20612
20999
|
}
|
|
20613
21000
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20614
21001
|
const sampleRows = this.mapAssetRows(
|
|
20615
|
-
|
|
20616
|
-
|
|
21002
|
+
allRows(
|
|
21003
|
+
this.db.prepare(
|
|
21004
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20617
21005
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20618
21006
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20619
21007
|
FROM inventory_asset a
|
|
20620
21008
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20621
21009
|
${where}
|
|
20622
21010
|
ORDER BY a.name ASC`
|
|
20623
|
-
|
|
21011
|
+
),
|
|
21012
|
+
params
|
|
21013
|
+
)
|
|
20624
21014
|
);
|
|
20625
21015
|
const configRows = this.configAssetRows(q).filter(
|
|
20626
21016
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20629,14 +21019,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20629
21019
|
}
|
|
20630
21020
|
fetchAssetById(assetId) {
|
|
20631
21021
|
const rows = this.mapAssetRows(
|
|
20632
|
-
|
|
20633
|
-
|
|
21022
|
+
allRows(
|
|
21023
|
+
this.db.prepare(
|
|
21024
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20634
21025
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20635
21026
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20636
21027
|
FROM inventory_asset a
|
|
20637
21028
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20638
21029
|
WHERE a.id = ?`
|
|
20639
|
-
|
|
21030
|
+
),
|
|
21031
|
+
[assetId]
|
|
21032
|
+
)
|
|
20640
21033
|
);
|
|
20641
21034
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20642
21035
|
}
|
|
@@ -20692,37 +21085,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20692
21085
|
return rows;
|
|
20693
21086
|
}
|
|
20694
21087
|
latestConfigScanId() {
|
|
20695
|
-
|
|
20696
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20697
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20698
|
-
).get();
|
|
20699
|
-
return row?.id ?? null;
|
|
21088
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20700
21089
|
}
|
|
20701
21090
|
fetchProjects(q) {
|
|
20702
21091
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20703
21092
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20704
21093
|
const params = [];
|
|
20705
21094
|
if (q) {
|
|
20706
|
-
const pat =
|
|
20707
|
-
sql +=
|
|
21095
|
+
const pat = containsPattern(q);
|
|
21096
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20708
21097
|
params.push(pat, pat);
|
|
20709
21098
|
}
|
|
20710
21099
|
sql += " ORDER BY name ASC";
|
|
20711
|
-
return this.db.prepare(sql)
|
|
21100
|
+
return allRows(this.db.prepare(sql), params);
|
|
20712
21101
|
}
|
|
20713
21102
|
fetchProjectById(projectId) {
|
|
20714
|
-
return
|
|
20715
|
-
|
|
20716
|
-
|
|
21103
|
+
return getRow(
|
|
21104
|
+
this.db.prepare(
|
|
21105
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21106
|
+
),
|
|
21107
|
+
[projectId]
|
|
21108
|
+
) ?? null;
|
|
20717
21109
|
}
|
|
20718
21110
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20719
21111
|
fetchProjectsByIds(projectIds) {
|
|
20720
21112
|
const map2 = /* @__PURE__ */ new Map();
|
|
20721
21113
|
if (projectIds.length === 0) return map2;
|
|
20722
|
-
const rows =
|
|
20723
|
-
|
|
21114
|
+
const rows = allRows(
|
|
21115
|
+
this.db.prepare(
|
|
21116
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20724
21117
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20725
|
-
|
|
21118
|
+
),
|
|
21119
|
+
projectIds
|
|
21120
|
+
);
|
|
20726
21121
|
for (const r of rows) map2.set(r.id, r);
|
|
20727
21122
|
return map2;
|
|
20728
21123
|
}
|
|
@@ -20733,8 +21128,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20733
21128
|
projectAggregates(projectIds) {
|
|
20734
21129
|
const map2 = /* @__PURE__ */ new Map();
|
|
20735
21130
|
if (projectIds.length === 0) return map2;
|
|
20736
|
-
const rows =
|
|
20737
|
-
|
|
21131
|
+
const rows = allRows(
|
|
21132
|
+
this.db.prepare(
|
|
21133
|
+
`SELECT f.project_id AS projectId,
|
|
20738
21134
|
coalesce(o.access, f.default_access) AS eff,
|
|
20739
21135
|
count(*) AS n,
|
|
20740
21136
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20742,7 +21138,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20742
21138
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20743
21139
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20744
21140
|
GROUP BY f.project_id, eff`
|
|
20745
|
-
|
|
21141
|
+
),
|
|
21142
|
+
projectIds
|
|
21143
|
+
);
|
|
20746
21144
|
for (const r of rows) {
|
|
20747
21145
|
let agg = map2.get(r.projectId);
|
|
20748
21146
|
if (!agg) {
|
|
@@ -20780,37 +21178,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20780
21178
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20781
21179
|
if (prefix === "") {
|
|
20782
21180
|
return this.mapFileRows(
|
|
20783
|
-
|
|
21181
|
+
allRows(
|
|
21182
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21183
|
+
[projectId]
|
|
21184
|
+
)
|
|
20784
21185
|
);
|
|
20785
21186
|
}
|
|
20786
21187
|
return this.mapFileRows(
|
|
20787
|
-
|
|
20788
|
-
this.
|
|
20789
|
-
|
|
21188
|
+
allRows(
|
|
21189
|
+
this.db.prepare(
|
|
21190
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21191
|
+
),
|
|
21192
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21193
|
+
)
|
|
20790
21194
|
);
|
|
20791
21195
|
}
|
|
20792
21196
|
fetchProjectFilesSearch(projectId, q) {
|
|
20793
|
-
const pat =
|
|
21197
|
+
const pat = containsPattern(q);
|
|
20794
21198
|
return this.mapFileRows(
|
|
20795
|
-
|
|
20796
|
-
this.
|
|
20797
|
-
|
|
20798
|
-
|
|
20799
|
-
|
|
21199
|
+
allRows(
|
|
21200
|
+
this.db.prepare(
|
|
21201
|
+
this.fileSelect(
|
|
21202
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21203
|
+
)
|
|
21204
|
+
),
|
|
21205
|
+
[projectId, pat, pat]
|
|
21206
|
+
)
|
|
20800
21207
|
);
|
|
20801
21208
|
}
|
|
20802
21209
|
fetchProjectFilesBlocked(projectId) {
|
|
20803
21210
|
return this.mapFileRows(
|
|
20804
|
-
|
|
20805
|
-
this.
|
|
20806
|
-
|
|
20807
|
-
|
|
20808
|
-
|
|
21211
|
+
allRows(
|
|
21212
|
+
this.db.prepare(
|
|
21213
|
+
this.fileSelect(
|
|
21214
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21215
|
+
)
|
|
21216
|
+
),
|
|
21217
|
+
[projectId]
|
|
21218
|
+
)
|
|
20809
21219
|
);
|
|
20810
21220
|
}
|
|
20811
21221
|
fetchProjectFile(projectId, path) {
|
|
20812
21222
|
const rows = this.mapFileRows(
|
|
20813
|
-
|
|
21223
|
+
allRows(
|
|
21224
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21225
|
+
[projectId, path]
|
|
21226
|
+
)
|
|
20814
21227
|
);
|
|
20815
21228
|
return rows[0] ?? null;
|
|
20816
21229
|
}
|
|
@@ -20824,39 +21237,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20824
21237
|
}
|
|
20825
21238
|
db;
|
|
20826
21239
|
readPolicies() {
|
|
20827
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20828
|
-
const policies =
|
|
20829
|
-
|
|
20830
|
-
|
|
20831
|
-
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
|
|
20838
|
-
|
|
20839
|
-
|
|
20840
|
-
customKeywords
|
|
20841
|
-
})
|
|
20842
|
-
);
|
|
20843
|
-
} catch {
|
|
20844
|
-
}
|
|
20845
|
-
}
|
|
21240
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21241
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21242
|
+
const target = JSON.parse(row.target);
|
|
21243
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21244
|
+
return Policy.parse({
|
|
21245
|
+
id: row.id,
|
|
21246
|
+
scope: row.scope,
|
|
21247
|
+
target,
|
|
21248
|
+
action: row.action,
|
|
21249
|
+
enabled: intToBool(row.enabled),
|
|
21250
|
+
customKeywords
|
|
21251
|
+
});
|
|
21252
|
+
});
|
|
20846
21253
|
return Promise.resolve(policies);
|
|
20847
21254
|
}
|
|
20848
21255
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20849
21256
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20850
21257
|
// so a user's edits are never clobbered.
|
|
20851
21258
|
seedDefaults() {
|
|
20852
|
-
const count = this.db
|
|
21259
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20853
21260
|
if (count > 0) return;
|
|
20854
21261
|
const stmt = this.db.prepare(
|
|
20855
21262
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20856
21263
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20857
21264
|
);
|
|
20858
|
-
this.db
|
|
20859
|
-
try {
|
|
21265
|
+
failOpenTransaction(this.db, () => {
|
|
20860
21266
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20861
21267
|
stmt.run({
|
|
20862
21268
|
id: randomUUID4(),
|
|
@@ -20865,10 +21271,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20865
21271
|
now: Date.now()
|
|
20866
21272
|
});
|
|
20867
21273
|
}
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
21274
|
+
});
|
|
21275
|
+
}
|
|
21276
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21277
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21278
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21279
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21280
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21281
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21282
|
+
upsertCategoryAction(category, action) {
|
|
21283
|
+
const now = Date.now();
|
|
21284
|
+
this.db.prepare(
|
|
21285
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21286
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21287
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21288
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21289
|
+
}
|
|
21290
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21291
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21292
|
+
// Returns the number of rows changed.
|
|
21293
|
+
capCategoryActions() {
|
|
21294
|
+
const info = this.db.prepare(
|
|
21295
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21296
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21297
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21298
|
+
).run({ now: Date.now() });
|
|
21299
|
+
return Number(info.changes);
|
|
21300
|
+
}
|
|
21301
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21302
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21303
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21304
|
+
getCategoryAction(category) {
|
|
21305
|
+
const row = this.db.prepare(
|
|
21306
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21307
|
+
).get({ category });
|
|
21308
|
+
return row?.action;
|
|
20872
21309
|
}
|
|
20873
21310
|
};
|
|
20874
21311
|
|
|
@@ -20944,7 +21381,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20944
21381
|
maxStampStmt;
|
|
20945
21382
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20946
21383
|
replaceForProject(projectId, scan2, now) {
|
|
20947
|
-
const
|
|
21384
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20948
21385
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20949
21386
|
for (const file2 of scan2.files) {
|
|
20950
21387
|
this.upsertStmt.run({
|
|
@@ -21027,7 +21464,7 @@ var SqliteResolutionsRepository = class {
|
|
|
21027
21464
|
}
|
|
21028
21465
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
21029
21466
|
latestByKey(key) {
|
|
21030
|
-
const row = this.latestStmt
|
|
21467
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
21031
21468
|
if (!row) return void 0;
|
|
21032
21469
|
return {
|
|
21033
21470
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -21044,7 +21481,7 @@ var SqliteResolutionsRepository = class {
|
|
|
21044
21481
|
* the CLI) surfaces for that file.
|
|
21045
21482
|
*/
|
|
21046
21483
|
openAtRestKeysForPath(path) {
|
|
21047
|
-
const rows = this.openAtRestStmt
|
|
21484
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
21048
21485
|
return rows.map((r) => r.finding_key);
|
|
21049
21486
|
}
|
|
21050
21487
|
/**
|
|
@@ -21055,7 +21492,7 @@ var SqliteResolutionsRepository = class {
|
|
|
21055
21492
|
* resolution row (see scan.ts).
|
|
21056
21493
|
*/
|
|
21057
21494
|
resolvedAtRestKeysForPath(path) {
|
|
21058
|
-
const rows = this.resolvedAtRestStmt
|
|
21495
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
21059
21496
|
return rows.map((r) => r.finding_key);
|
|
21060
21497
|
}
|
|
21061
21498
|
};
|
|
@@ -21084,31 +21521,25 @@ var SqliteScanLedgerRepository = class {
|
|
|
21084
21521
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
21085
21522
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
21086
21523
|
entriesForRuleset(rulesetHash) {
|
|
21087
|
-
const rows = this.readStmt
|
|
21524
|
+
const rows = allRows(this.readStmt, {
|
|
21525
|
+
rulesetHash
|
|
21526
|
+
});
|
|
21088
21527
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
21089
21528
|
}
|
|
21090
21529
|
upsertEntries(entries) {
|
|
21091
21530
|
if (entries.length === 0) return;
|
|
21092
21531
|
const scannedAt = Date.now();
|
|
21093
|
-
|
|
21094
|
-
|
|
21095
|
-
|
|
21096
|
-
|
|
21097
|
-
|
|
21098
|
-
|
|
21099
|
-
|
|
21100
|
-
|
|
21101
|
-
|
|
21102
|
-
scannedAt
|
|
21103
|
-
});
|
|
21104
|
-
}
|
|
21105
|
-
this.db.exec("COMMIT");
|
|
21106
|
-
} catch (err) {
|
|
21107
|
-
this.db.exec("ROLLBACK");
|
|
21108
|
-
throw err;
|
|
21532
|
+
failOpenTransaction(this.db, () => {
|
|
21533
|
+
for (const entry of entries) {
|
|
21534
|
+
this.upsertStmt.run({
|
|
21535
|
+
path: entry.path,
|
|
21536
|
+
mtime: entry.mtime,
|
|
21537
|
+
contentHash: entry.contentHash,
|
|
21538
|
+
rulesetHash: entry.rulesetHash,
|
|
21539
|
+
scannedAt
|
|
21540
|
+
});
|
|
21109
21541
|
}
|
|
21110
|
-
}
|
|
21111
|
-
}
|
|
21542
|
+
});
|
|
21112
21543
|
}
|
|
21113
21544
|
};
|
|
21114
21545
|
|
|
@@ -21185,8 +21616,9 @@ var SqliteSecurityRepository = class {
|
|
|
21185
21616
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21186
21617
|
// double-counting a key that accumulated several append-only rows.
|
|
21187
21618
|
severitySummary() {
|
|
21188
|
-
const rows =
|
|
21189
|
-
|
|
21619
|
+
const rows = allRows(
|
|
21620
|
+
this.db.prepare(
|
|
21621
|
+
`SELECT f.severity AS severity,
|
|
21190
21622
|
COUNT(*) AS count,
|
|
21191
21623
|
SUM(CASE
|
|
21192
21624
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21205,7 +21637,8 @@ var SqliteSecurityRepository = class {
|
|
|
21205
21637
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21206
21638
|
ON latest.finding_key = f.finding_key
|
|
21207
21639
|
GROUP BY f.severity`
|
|
21208
|
-
|
|
21640
|
+
)
|
|
21641
|
+
);
|
|
21209
21642
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21210
21643
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21211
21644
|
severity,
|
|
@@ -21289,14 +21722,15 @@ var SqliteSecurityRepository = class {
|
|
|
21289
21722
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21290
21723
|
const now = this.now();
|
|
21291
21724
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21292
|
-
const rows =
|
|
21293
|
-
|
|
21294
|
-
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
|
|
21725
|
+
const rows = allRows(
|
|
21726
|
+
this.db.prepare(
|
|
21727
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21728
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21729
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21730
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21731
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21732
|
+
// backfill left null.
|
|
21733
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21300
21734
|
(
|
|
21301
21735
|
SELECT fr.status FROM finding_resolution fr
|
|
21302
21736
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21322,14 +21756,16 @@ var SqliteSecurityRepository = class {
|
|
|
21322
21756
|
WHERE fr.finding_key = f.finding_key
|
|
21323
21757
|
AND fr.resolved_at >= :windowStart
|
|
21324
21758
|
)`
|
|
21325
|
-
|
|
21326
|
-
|
|
21327
|
-
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21759
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21760
|
+
// any resolution activity at/after the window start — a row this method
|
|
21761
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21762
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21763
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21764
|
+
// dialect-agnostic. Without this, a
|
|
21765
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21766
|
+
),
|
|
21767
|
+
{ windowStart }
|
|
21768
|
+
);
|
|
21333
21769
|
const sums = /* @__PURE__ */ new Map();
|
|
21334
21770
|
const counts = /* @__PURE__ */ new Map();
|
|
21335
21771
|
for (const r of rows) {
|
|
@@ -21359,8 +21795,9 @@ var SqliteSecurityRepository = class {
|
|
|
21359
21795
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21360
21796
|
const now = this.now();
|
|
21361
21797
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21362
|
-
const rows =
|
|
21363
|
-
|
|
21798
|
+
const rows = allRows(
|
|
21799
|
+
this.db.prepare(
|
|
21800
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21364
21801
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21365
21802
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21366
21803
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21368,7 +21805,9 @@ var SqliteSecurityRepository = class {
|
|
|
21368
21805
|
GROUP BY repo
|
|
21369
21806
|
ORDER BY c DESC, repo
|
|
21370
21807
|
LIMIT :limit`
|
|
21371
|
-
|
|
21808
|
+
),
|
|
21809
|
+
{ from, to: now, limit }
|
|
21810
|
+
);
|
|
21372
21811
|
const items = rows.map((r) => ({
|
|
21373
21812
|
id: `repo_${r.repo}`,
|
|
21374
21813
|
name: r.repo,
|
|
@@ -21390,8 +21829,9 @@ var SqliteSecurityRepository = class {
|
|
|
21390
21829
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21391
21830
|
// capped at `limit`.
|
|
21392
21831
|
recentlyResolved(limit = 20) {
|
|
21393
|
-
const rows =
|
|
21394
|
-
|
|
21832
|
+
const rows = allRows(
|
|
21833
|
+
this.db.prepare(
|
|
21834
|
+
`SELECT f.finding_key AS finding_key,
|
|
21395
21835
|
f.rule_id AS rule_id,
|
|
21396
21836
|
f.severity AS severity,
|
|
21397
21837
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21425,7 +21865,9 @@ var SqliteSecurityRepository = class {
|
|
|
21425
21865
|
) IS NOT NULL
|
|
21426
21866
|
ORDER BY latest_resolved_at DESC
|
|
21427
21867
|
LIMIT :limit`
|
|
21428
|
-
|
|
21868
|
+
),
|
|
21869
|
+
{ limit }
|
|
21870
|
+
);
|
|
21429
21871
|
const items = rows.map((r) => ({
|
|
21430
21872
|
findingKey: r.finding_key,
|
|
21431
21873
|
ruleId: r.rule_id,
|
|
@@ -21442,12 +21884,15 @@ var SqliteSecurityRepository = class {
|
|
|
21442
21884
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21443
21885
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21444
21886
|
findingsInRange(fromMs, toMs) {
|
|
21445
|
-
const rows =
|
|
21446
|
-
|
|
21887
|
+
const rows = allRows(
|
|
21888
|
+
this.db.prepare(
|
|
21889
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21447
21890
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21448
21891
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21449
21892
|
ORDER BY e.occurred_at`
|
|
21450
|
-
|
|
21893
|
+
),
|
|
21894
|
+
{ from: fromMs, to: toMs }
|
|
21895
|
+
);
|
|
21451
21896
|
return rows.map((r) => ({
|
|
21452
21897
|
occurredAt: r.occurred_at,
|
|
21453
21898
|
severity: r.severity,
|
|
@@ -21461,12 +21906,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21461
21906
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21462
21907
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21463
21908
|
function parseNetwork(networkJson) {
|
|
21464
|
-
|
|
21465
|
-
try {
|
|
21466
|
-
return JSON.parse(networkJson);
|
|
21467
|
-
} catch {
|
|
21468
|
-
return null;
|
|
21469
|
-
}
|
|
21909
|
+
return safeJson(networkJson, null);
|
|
21470
21910
|
}
|
|
21471
21911
|
function toEndpointSummary(row) {
|
|
21472
21912
|
return {
|
|
@@ -21553,27 +21993,39 @@ var SqliteSharesRepository = class {
|
|
|
21553
21993
|
}
|
|
21554
21994
|
db;
|
|
21555
21995
|
stats() {
|
|
21556
|
-
const
|
|
21557
|
-
const
|
|
21558
|
-
const
|
|
21559
|
-
const
|
|
21560
|
-
|
|
21996
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
21997
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
21998
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
21999
|
+
const insecure = countScalar(
|
|
22000
|
+
this.db,
|
|
21561
22001
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21562
22002
|
);
|
|
21563
|
-
const needsReview =
|
|
22003
|
+
const needsReview = countScalar(
|
|
22004
|
+
this.db,
|
|
21564
22005
|
`SELECT count(DISTINCT d.id) AS n
|
|
21565
22006
|
FROM share_destination d
|
|
21566
22007
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21567
22008
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21568
22009
|
);
|
|
21569
|
-
const
|
|
21570
|
-
|
|
21571
|
-
|
|
21572
|
-
|
|
21573
|
-
const
|
|
21574
|
-
|
|
21575
|
-
|
|
21576
|
-
|
|
22010
|
+
const kindCounts = countBy(
|
|
22011
|
+
this.db,
|
|
22012
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
22013
|
+
);
|
|
22014
|
+
const byKind = {
|
|
22015
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
22016
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
22017
|
+
ip: kindCounts.get("ip") ?? 0
|
|
22018
|
+
};
|
|
22019
|
+
const trustCounts = countBy(
|
|
22020
|
+
this.db,
|
|
22021
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
22022
|
+
);
|
|
22023
|
+
const byTrust = {
|
|
22024
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
22025
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
22026
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
22027
|
+
ip: trustCounts.get("ip") ?? 0
|
|
22028
|
+
};
|
|
21577
22029
|
return Promise.resolve({
|
|
21578
22030
|
destinations,
|
|
21579
22031
|
endpoints,
|
|
@@ -21689,7 +22141,7 @@ var SqliteSharesRepository = class {
|
|
|
21689
22141
|
}
|
|
21690
22142
|
let sql;
|
|
21691
22143
|
if (q) {
|
|
21692
|
-
const pattern =
|
|
22144
|
+
const pattern = containsPattern(q);
|
|
21693
22145
|
conditions.push(
|
|
21694
22146
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21695
22147
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21709,24 +22161,31 @@ var SqliteSharesRepository = class {
|
|
|
21709
22161
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21710
22162
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21711
22163
|
}
|
|
21712
|
-
const rows =
|
|
22164
|
+
const rows = allRows(
|
|
22165
|
+
this.db.prepare(sql),
|
|
22166
|
+
params
|
|
22167
|
+
);
|
|
21713
22168
|
return rows.map((r) => this.mapDestRow(r));
|
|
21714
22169
|
}
|
|
21715
22170
|
fetchDestinationById(destinationId) {
|
|
21716
|
-
const row =
|
|
21717
|
-
|
|
22171
|
+
const row = getRow(
|
|
22172
|
+
this.db.prepare(
|
|
22173
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21718
22174
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21719
22175
|
o.decision AS overrideDecision
|
|
21720
22176
|
FROM share_destination d
|
|
21721
22177
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21722
22178
|
WHERE d.id = ?`
|
|
21723
|
-
|
|
22179
|
+
),
|
|
22180
|
+
[destinationId]
|
|
22181
|
+
);
|
|
21724
22182
|
return row ? this.mapDestRow(row) : null;
|
|
21725
22183
|
}
|
|
21726
22184
|
fetchEndpoints(destinationIds) {
|
|
21727
22185
|
if (destinationIds.length === 0) return [];
|
|
21728
|
-
const rows =
|
|
21729
|
-
|
|
22186
|
+
const rows = allRows(
|
|
22187
|
+
this.db.prepare(
|
|
22188
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21730
22189
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21731
22190
|
count(c.id) AS callSiteCount
|
|
21732
22191
|
FROM share_endpoint e
|
|
@@ -21734,7 +22193,9 @@ var SqliteSharesRepository = class {
|
|
|
21734
22193
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21735
22194
|
GROUP BY e.id
|
|
21736
22195
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21737
|
-
|
|
22196
|
+
),
|
|
22197
|
+
destinationIds
|
|
22198
|
+
);
|
|
21738
22199
|
return rows.map((r) => ({
|
|
21739
22200
|
id: r.id,
|
|
21740
22201
|
destinationId: r.destinationId,
|
|
@@ -21759,13 +22220,16 @@ var SqliteSharesRepository = class {
|
|
|
21759
22220
|
}
|
|
21760
22221
|
fetchCallSites(endpointIds) {
|
|
21761
22222
|
if (endpointIds.length === 0) return [];
|
|
21762
|
-
const rows =
|
|
21763
|
-
|
|
22223
|
+
const rows = allRows(
|
|
22224
|
+
this.db.prepare(
|
|
22225
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21764
22226
|
project_id AS projectId
|
|
21765
22227
|
FROM share_call_site
|
|
21766
22228
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21767
22229
|
ORDER BY created_at ASC, id ASC`
|
|
21768
|
-
|
|
22230
|
+
),
|
|
22231
|
+
endpointIds
|
|
22232
|
+
);
|
|
21769
22233
|
return rows.map((r) => ({
|
|
21770
22234
|
id: r.id,
|
|
21771
22235
|
endpointId: r.endpointId,
|
|
@@ -21801,27 +22265,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21801
22265
|
upsert(input, now = Date.now()) {
|
|
21802
22266
|
const id = sourceProjectId(input.url);
|
|
21803
22267
|
const row = toSourceProjectRow(input, id, now);
|
|
21804
|
-
this.upsertStmt.run(
|
|
21805
|
-
|
|
21806
|
-
|
|
21807
|
-
|
|
21808
|
-
|
|
21809
|
-
|
|
21810
|
-
|
|
21811
|
-
|
|
22268
|
+
this.upsertStmt.run(
|
|
22269
|
+
bindParams({
|
|
22270
|
+
id: row.id,
|
|
22271
|
+
url: row.url,
|
|
22272
|
+
name: row.name,
|
|
22273
|
+
attributes: row.attributes,
|
|
22274
|
+
firstSeen: row.firstSeen,
|
|
22275
|
+
lastSeen: row.lastSeen
|
|
22276
|
+
})
|
|
22277
|
+
);
|
|
21812
22278
|
return id;
|
|
21813
22279
|
}
|
|
21814
22280
|
findById(id) {
|
|
21815
|
-
return
|
|
22281
|
+
return getRow(
|
|
22282
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22283
|
+
{
|
|
22284
|
+
id
|
|
22285
|
+
}
|
|
22286
|
+
);
|
|
21816
22287
|
}
|
|
21817
22288
|
// Distinct project names — a filter facet, served from the source_project
|
|
21818
22289
|
// table, never from the audit fact table.
|
|
21819
22290
|
distinctNames() {
|
|
21820
|
-
const rows =
|
|
21821
|
-
|
|
22291
|
+
const rows = allRows(
|
|
22292
|
+
this.db.prepare(
|
|
22293
|
+
`SELECT DISTINCT name FROM source_project
|
|
21822
22294
|
WHERE name IS NOT NULL
|
|
21823
22295
|
ORDER BY name`
|
|
21824
|
-
|
|
22296
|
+
)
|
|
22297
|
+
);
|
|
21825
22298
|
return rows.map((r) => r.name);
|
|
21826
22299
|
}
|
|
21827
22300
|
};
|
|
@@ -21840,8 +22313,7 @@ function hasLegacySampleRows(db) {
|
|
|
21840
22313
|
function purgeSampleData(db) {
|
|
21841
22314
|
try {
|
|
21842
22315
|
if (!hasLegacySampleRows(db)) return;
|
|
21843
|
-
db
|
|
21844
|
-
try {
|
|
22316
|
+
withTransaction(db, () => {
|
|
21845
22317
|
db.exec(
|
|
21846
22318
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21847
22319
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21886,11 +22358,7 @@ function purgeSampleData(db) {
|
|
|
21886
22358
|
value TEXT NOT NULL
|
|
21887
22359
|
)`);
|
|
21888
22360
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21889
|
-
|
|
21890
|
-
} catch (err) {
|
|
21891
|
-
db.exec("ROLLBACK");
|
|
21892
|
-
throw err;
|
|
21893
|
-
}
|
|
22361
|
+
});
|
|
21894
22362
|
} catch {
|
|
21895
22363
|
}
|
|
21896
22364
|
}
|
|
@@ -21909,7 +22377,7 @@ function openWithPragmas(file2) {
|
|
|
21909
22377
|
function backupLegacyStore(file2) {
|
|
21910
22378
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21911
22379
|
renameSync(file2, backup);
|
|
21912
|
-
for (const sidecar of
|
|
22380
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21913
22381
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21914
22382
|
}
|
|
21915
22383
|
return backup;
|
|
@@ -21922,9 +22390,8 @@ function openLocalDatabase(dir) {
|
|
|
21922
22390
|
db.close();
|
|
21923
22391
|
const backup = backupLegacyStore(file2);
|
|
21924
22392
|
db = openWithPragmas(file2);
|
|
21925
|
-
|
|
21926
|
-
`
|
|
21927
|
-
`
|
|
22393
|
+
akaWarn(
|
|
22394
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21928
22395
|
);
|
|
21929
22396
|
}
|
|
21930
22397
|
applyMigrations(db);
|
|
@@ -21952,96 +22419,77 @@ function openLocalDatabase(dir) {
|
|
|
21952
22419
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21953
22420
|
policies.seedDefaults();
|
|
21954
22421
|
function recordCapture(event, detected) {
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21961
|
-
db.exec("COMMIT");
|
|
21962
|
-
} catch (err) {
|
|
21963
|
-
db.exec("ROLLBACK");
|
|
21964
|
-
throw err;
|
|
21965
|
-
}
|
|
21966
|
-
} catch {
|
|
21967
|
-
}
|
|
22422
|
+
failOpenTransaction(db, () => {
|
|
22423
|
+
events.insertEvent(event);
|
|
22424
|
+
const sessionId = event.metadata?.sessionId;
|
|
22425
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22426
|
+
});
|
|
21968
22427
|
}
|
|
21969
22428
|
function ensureInventory(ctx) {
|
|
21970
22429
|
const resolved = {};
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
if (ctx.harness) {
|
|
21977
|
-
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
21978
|
-
}
|
|
21979
|
-
resolved.accountId = inventory.upsert(
|
|
21980
|
-
linkHost(
|
|
21981
|
-
{
|
|
21982
|
-
objectType: "user",
|
|
21983
|
-
identityKey: "local",
|
|
21984
|
-
attributes: { source: "local" }
|
|
21985
|
-
},
|
|
21986
|
-
resolved.hostId
|
|
21987
|
-
),
|
|
21988
|
-
now
|
|
21989
|
-
);
|
|
21990
|
-
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
21991
|
-
db.exec("COMMIT");
|
|
21992
|
-
} catch (err) {
|
|
21993
|
-
db.exec("ROLLBACK");
|
|
21994
|
-
throw err;
|
|
22430
|
+
const committed = failOpenTransaction(db, () => {
|
|
22431
|
+
const now = Date.now();
|
|
22432
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22433
|
+
if (ctx.harness) {
|
|
22434
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
21995
22435
|
}
|
|
21996
|
-
|
|
21997
|
-
|
|
21998
|
-
|
|
21999
|
-
|
|
22436
|
+
resolved.accountId = inventory.upsert(
|
|
22437
|
+
linkHost(
|
|
22438
|
+
{
|
|
22439
|
+
objectType: "user",
|
|
22440
|
+
identityKey: "local",
|
|
22441
|
+
attributes: { source: "local" }
|
|
22442
|
+
},
|
|
22443
|
+
resolved.hostId
|
|
22444
|
+
),
|
|
22445
|
+
now
|
|
22446
|
+
);
|
|
22447
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22448
|
+
});
|
|
22449
|
+
return committed ? resolved : {};
|
|
22000
22450
|
}
|
|
22001
22451
|
function recordConfigScan(record2) {
|
|
22002
|
-
|
|
22003
|
-
|
|
22004
|
-
|
|
22005
|
-
|
|
22006
|
-
|
|
22007
|
-
|
|
22008
|
-
|
|
22009
|
-
|
|
22010
|
-
|
|
22011
|
-
}
|
|
22012
|
-
|
|
22013
|
-
|
|
22014
|
-
|
|
22015
|
-
|
|
22016
|
-
|
|
22017
|
-
|
|
22018
|
-
|
|
22019
|
-
|
|
22020
|
-
|
|
22021
|
-
|
|
22022
|
-
confidence: finding2.confidence
|
|
22023
|
-
});
|
|
22024
|
-
}
|
|
22025
|
-
db.exec("COMMIT");
|
|
22026
|
-
} catch (err) {
|
|
22027
|
-
db.exec("ROLLBACK");
|
|
22028
|
-
throw err;
|
|
22452
|
+
failOpenTransaction(db, () => {
|
|
22453
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22454
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22455
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22456
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22457
|
+
for (const def of record2.definitions ?? []) {
|
|
22458
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22459
|
+
}
|
|
22460
|
+
for (const finding2 of record2.findings ?? []) {
|
|
22461
|
+
const definitionId = definitionIds.get(`${finding2.ruleId}@${finding2.version}`);
|
|
22462
|
+
if (!definitionId) continue;
|
|
22463
|
+
inspectionFindings.insertFinding({
|
|
22464
|
+
id: randomUUID8(),
|
|
22465
|
+
auditEventId: record2.scanEvent.id,
|
|
22466
|
+
inspectionDefinitionId: definitionId,
|
|
22467
|
+
span: finding2.span,
|
|
22468
|
+
maskedMatch: finding2.maskedMatch,
|
|
22469
|
+
actionTaken: finding2.actionTaken,
|
|
22470
|
+
confidence: finding2.confidence
|
|
22471
|
+
});
|
|
22029
22472
|
}
|
|
22030
|
-
}
|
|
22031
|
-
}
|
|
22473
|
+
});
|
|
22032
22474
|
}
|
|
22033
22475
|
function recordProjectFiles(projectId, scan2) {
|
|
22034
22476
|
if (scan2.files.length === 0) return;
|
|
22477
|
+
failOpenTransaction(db, () => {
|
|
22478
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22479
|
+
});
|
|
22480
|
+
}
|
|
22481
|
+
async function transaction(fn) {
|
|
22482
|
+
db.exec("BEGIN");
|
|
22035
22483
|
try {
|
|
22036
|
-
|
|
22484
|
+
const result = await fn();
|
|
22485
|
+
db.exec("COMMIT");
|
|
22486
|
+
return result;
|
|
22487
|
+
} catch (err) {
|
|
22037
22488
|
try {
|
|
22038
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22039
|
-
db.exec("COMMIT");
|
|
22040
|
-
} catch (err) {
|
|
22041
22489
|
db.exec("ROLLBACK");
|
|
22042
|
-
|
|
22490
|
+
} catch {
|
|
22043
22491
|
}
|
|
22044
|
-
|
|
22492
|
+
throw err;
|
|
22045
22493
|
}
|
|
22046
22494
|
}
|
|
22047
22495
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -22060,8 +22508,7 @@ function openLocalDatabase(dir) {
|
|
|
22060
22508
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
22061
22509
|
});
|
|
22062
22510
|
if (stale.length === 0) return;
|
|
22063
|
-
db
|
|
22064
|
-
try {
|
|
22511
|
+
withTransaction(db, () => {
|
|
22065
22512
|
for (const { id } of stale) {
|
|
22066
22513
|
db.prepare(
|
|
22067
22514
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -22073,11 +22520,7 @@ function openLocalDatabase(dir) {
|
|
|
22073
22520
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
22074
22521
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
22075
22522
|
}
|
|
22076
|
-
|
|
22077
|
-
} catch (err) {
|
|
22078
|
-
db.exec("ROLLBACK");
|
|
22079
|
-
throw err;
|
|
22080
|
-
}
|
|
22523
|
+
});
|
|
22081
22524
|
} catch {
|
|
22082
22525
|
}
|
|
22083
22526
|
}
|
|
@@ -22119,6 +22562,7 @@ function openLocalDatabase(dir) {
|
|
|
22119
22562
|
purgeSampleData: () => {
|
|
22120
22563
|
purgeSampleData(db);
|
|
22121
22564
|
},
|
|
22565
|
+
transaction,
|
|
22122
22566
|
close: () => {
|
|
22123
22567
|
db.close();
|
|
22124
22568
|
}
|
|
@@ -22211,12 +22655,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22211
22655
|
}
|
|
22212
22656
|
}
|
|
22213
22657
|
function readJson(file2) {
|
|
22658
|
+
let text;
|
|
22214
22659
|
try {
|
|
22215
|
-
|
|
22216
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22660
|
+
text = readFileSync2(file2, "utf8");
|
|
22217
22661
|
} catch {
|
|
22218
22662
|
return null;
|
|
22219
22663
|
}
|
|
22664
|
+
return parseJsonObject(text) ?? null;
|
|
22665
|
+
}
|
|
22666
|
+
|
|
22667
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22668
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22669
|
+
import { join as join5 } from "path";
|
|
22670
|
+
var MARKER = "warn-era-capped";
|
|
22671
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22672
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22673
|
+
const marker = join5(dataDir2, MARKER);
|
|
22674
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22675
|
+
const capped = db.policies.capCategoryActions();
|
|
22676
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22677
|
+
`, { mode: DATA_FILE_MODE });
|
|
22678
|
+
return { capped };
|
|
22220
22679
|
}
|
|
22221
22680
|
|
|
22222
22681
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22291,7 +22750,7 @@ function resolveProviderSafe() {
|
|
|
22291
22750
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22292
22751
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22293
22752
|
import { homedir as homedir2 } from "os";
|
|
22294
|
-
import { basename as basename3, join as
|
|
22753
|
+
import { basename as basename3, join as join7 } from "path";
|
|
22295
22754
|
|
|
22296
22755
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22297
22756
|
var KeywordMatcher2 = class {
|
|
@@ -24755,8 +25214,8 @@ function maskText(text) {
|
|
|
24755
25214
|
}
|
|
24756
25215
|
|
|
24757
25216
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24758
|
-
import { existsSync as
|
|
24759
|
-
import { basename as basename2, dirname, isAbsolute, join as
|
|
25217
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25218
|
+
import { basename as basename2, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24760
25219
|
function resolveRepoIdentity(cwd) {
|
|
24761
25220
|
try {
|
|
24762
25221
|
const root = findGitRoot(cwd);
|
|
@@ -24806,7 +25265,7 @@ function resolveGitBranch(cwd) {
|
|
|
24806
25265
|
try {
|
|
24807
25266
|
const root = findGitRoot(cwd);
|
|
24808
25267
|
if (!root) return void 0;
|
|
24809
|
-
const dotGit =
|
|
25268
|
+
const dotGit = join6(root, ".git");
|
|
24810
25269
|
let gitdir;
|
|
24811
25270
|
try {
|
|
24812
25271
|
gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
|
|
@@ -24814,7 +25273,7 @@ function resolveGitBranch(cwd) {
|
|
|
24814
25273
|
return void 0;
|
|
24815
25274
|
}
|
|
24816
25275
|
if (gitdir === void 0) return void 0;
|
|
24817
|
-
const head = safeRead(
|
|
25276
|
+
const head = safeRead(join6(gitdir, "HEAD"));
|
|
24818
25277
|
if (!head) return void 0;
|
|
24819
25278
|
return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
|
|
24820
25279
|
} catch {
|
|
@@ -24824,37 +25283,37 @@ function resolveGitBranch(cwd) {
|
|
|
24824
25283
|
function resolveWorktreeGitdir(root, dotGitFile) {
|
|
24825
25284
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
|
|
24826
25285
|
if (!target) return void 0;
|
|
24827
|
-
return isAbsolute(target) ? target :
|
|
25286
|
+
return isAbsolute(target) ? target : join6(root, target);
|
|
24828
25287
|
}
|
|
24829
25288
|
function findGitRoot(start) {
|
|
24830
25289
|
let dir = start;
|
|
24831
25290
|
for (; ; ) {
|
|
24832
|
-
if (
|
|
25291
|
+
if (existsSync3(join6(dir, ".git"))) return dir;
|
|
24833
25292
|
const parent = dirname(dir);
|
|
24834
25293
|
if (parent === dir) return void 0;
|
|
24835
25294
|
dir = parent;
|
|
24836
25295
|
}
|
|
24837
25296
|
}
|
|
24838
25297
|
function resolveGitContext(root) {
|
|
24839
|
-
const dotGit =
|
|
25298
|
+
const dotGit = join6(root, ".git");
|
|
24840
25299
|
try {
|
|
24841
25300
|
if (statSync(dotGit).isDirectory()) {
|
|
24842
|
-
return { configPath:
|
|
25301
|
+
return { configPath: join6(dotGit, "config"), headRoot: root };
|
|
24843
25302
|
}
|
|
24844
25303
|
} catch {
|
|
24845
25304
|
return void 0;
|
|
24846
25305
|
}
|
|
24847
25306
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
24848
25307
|
if (!target) return void 0;
|
|
24849
|
-
const gitdir = isAbsolute(target) ? target :
|
|
24850
|
-
if (
|
|
24851
|
-
return { configPath:
|
|
25308
|
+
const gitdir = isAbsolute(target) ? target : join6(root, target);
|
|
25309
|
+
if (existsSync3(join6(gitdir, "config"))) {
|
|
25310
|
+
return { configPath: join6(gitdir, "config"), headRoot: root };
|
|
24852
25311
|
}
|
|
24853
|
-
const commonRaw = safeRead(
|
|
25312
|
+
const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
|
|
24854
25313
|
if (!commonRaw) return void 0;
|
|
24855
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
25314
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
|
|
24856
25315
|
const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
24857
|
-
return { configPath:
|
|
25316
|
+
return { configPath: join6(commonGitDir, "config"), headRoot };
|
|
24858
25317
|
}
|
|
24859
25318
|
function safeRead(path) {
|
|
24860
25319
|
try {
|
|
@@ -24920,31 +25379,31 @@ function resolveConfigInventory(input) {
|
|
|
24920
25379
|
};
|
|
24921
25380
|
try {
|
|
24922
25381
|
const home = input.homeDir ?? homedir2();
|
|
24923
|
-
const claudeDir =
|
|
25382
|
+
const claudeDir = join7(home, ".claude");
|
|
24924
25383
|
const repo = resolveRepoIdentity(input.cwd);
|
|
24925
25384
|
const repoIdentity = repo?.url ?? input.cwd;
|
|
24926
25385
|
const projectSource = `project:${repoIdentity}`;
|
|
24927
|
-
collectSettingsHooks(scan2,
|
|
24928
|
-
collectSettingsHooks(scan2,
|
|
24929
|
-
collectSettingsHooks(scan2,
|
|
25386
|
+
collectSettingsHooks(scan2, join7(claudeDir, "settings.json"), "user");
|
|
25387
|
+
collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.json"), "project");
|
|
25388
|
+
collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.local.json"), "local");
|
|
24930
25389
|
const projectOrigin = { scope: "project", project: repoIdentity };
|
|
24931
|
-
collectMcpFile(scan2,
|
|
24932
|
-
collectUserClaudeJson(scan2,
|
|
24933
|
-
collectMcpFile(scan2,
|
|
24934
|
-
collectMcpFile(scan2,
|
|
24935
|
-
collectMcpFile(scan2,
|
|
25390
|
+
collectMcpFile(scan2, join7(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
|
|
25391
|
+
collectUserClaudeJson(scan2, join7(home, ".claude.json"), input.cwd, repoIdentity);
|
|
25392
|
+
collectMcpFile(scan2, join7(claudeDir, "settings.json"), { scope: "user" });
|
|
25393
|
+
collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.json"), projectOrigin);
|
|
25394
|
+
collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.local.json"), {
|
|
24936
25395
|
scope: "local",
|
|
24937
25396
|
project: repoIdentity
|
|
24938
25397
|
});
|
|
24939
25398
|
collectConfigFiles(scan2, claudeDir, input.cwd);
|
|
24940
|
-
collectSkillsDir(scan2,
|
|
24941
|
-
collectSkillsDir(scan2,
|
|
25399
|
+
collectSkillsDir(scan2, join7(claudeDir, "skills"), { source: "local", scope: "user" });
|
|
25400
|
+
collectSkillsDir(scan2, join7(input.cwd, ".claude", "skills"), {
|
|
24942
25401
|
source: projectSource,
|
|
24943
25402
|
scope: "project"
|
|
24944
25403
|
});
|
|
24945
25404
|
collectInstalledPlugins(scan2, claudeDir);
|
|
24946
25405
|
collectMarketplaceSkills(scan2, claudeDir);
|
|
24947
|
-
collectSkillsDir(scan2,
|
|
25406
|
+
collectSkillsDir(scan2, join7(input.cwd, "skills"), { source: projectSource, scope: "project" });
|
|
24948
25407
|
scan2.skills = dedupeSkills(scan2.skills);
|
|
24949
25408
|
scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
|
|
24950
25409
|
} catch (err) {
|
|
@@ -25073,7 +25532,7 @@ function projectEntryFor(projects, cwd) {
|
|
|
25073
25532
|
return void 0;
|
|
25074
25533
|
}
|
|
25075
25534
|
function collectPluginManifestMcp(scan2, installPath, origin) {
|
|
25076
|
-
const manifestPath =
|
|
25535
|
+
const manifestPath = join7(installPath, ".claude-plugin", "plugin.json");
|
|
25077
25536
|
const raw = readOptional(manifestPath);
|
|
25078
25537
|
if (raw === void 0) return;
|
|
25079
25538
|
try {
|
|
@@ -25081,7 +25540,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
|
|
|
25081
25540
|
if (typeof parsed !== "object" || parsed === null) return;
|
|
25082
25541
|
const declared = parsed.mcpServers;
|
|
25083
25542
|
if (typeof declared === "string" && declared.length > 0) {
|
|
25084
|
-
collectMcpFile(scan2,
|
|
25543
|
+
collectMcpFile(scan2, join7(installPath, declared), origin, { recordErrors: true });
|
|
25085
25544
|
} else {
|
|
25086
25545
|
collectMcpObject(scan2, declared, manifestPath, origin);
|
|
25087
25546
|
}
|
|
@@ -25098,14 +25557,14 @@ var SETTINGS_KEY_LABELS = [
|
|
|
25098
25557
|
["statusLine", "status line"]
|
|
25099
25558
|
];
|
|
25100
25559
|
function collectConfigFiles(scan2, claudeDir, cwd) {
|
|
25101
|
-
settingsConfigFile(scan2,
|
|
25102
|
-
settingsConfigFile(scan2,
|
|
25103
|
-
settingsConfigFile(scan2,
|
|
25104
|
-
memoryConfigFile(scan2,
|
|
25105
|
-
memoryConfigFile(scan2,
|
|
25106
|
-
mcpJsonConfigFile(scan2,
|
|
25107
|
-
dirConfigFile(scan2,
|
|
25108
|
-
dirConfigFile(scan2,
|
|
25560
|
+
settingsConfigFile(scan2, join7(claudeDir, "settings.json"), "user", "User settings");
|
|
25561
|
+
settingsConfigFile(scan2, join7(cwd, ".claude", "settings.json"), "project", "Project settings");
|
|
25562
|
+
settingsConfigFile(scan2, join7(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
|
|
25563
|
+
memoryConfigFile(scan2, join7(claudeDir, "CLAUDE.md"), "user", "User memory");
|
|
25564
|
+
memoryConfigFile(scan2, join7(cwd, "CLAUDE.md"), "project", "Project memory");
|
|
25565
|
+
mcpJsonConfigFile(scan2, join7(cwd, ".mcp.json"));
|
|
25566
|
+
dirConfigFile(scan2, join7(cwd, ".claude", "commands"), "Slash commands", "command");
|
|
25567
|
+
dirConfigFile(scan2, join7(cwd, ".claude", "agents"), "Subagents", "subagent");
|
|
25109
25568
|
}
|
|
25110
25569
|
function configFileEntry(path, scope, kind) {
|
|
25111
25570
|
try {
|
|
@@ -25180,7 +25639,7 @@ function countMarkdownFiles(dir, depth) {
|
|
|
25180
25639
|
let count = 0;
|
|
25181
25640
|
for (const dirent of readdirSync(dir, { withFileTypes: true })) {
|
|
25182
25641
|
if (dirent.name.startsWith(".")) continue;
|
|
25183
|
-
if (dirent.isDirectory()) count += countMarkdownFiles(
|
|
25642
|
+
if (dirent.isDirectory()) count += countMarkdownFiles(join7(dir, dirent.name), depth + 1);
|
|
25184
25643
|
else if (dirent.name.endsWith(".md")) count += 1;
|
|
25185
25644
|
}
|
|
25186
25645
|
return count;
|
|
@@ -25193,7 +25652,7 @@ function collectSkillsDir(scan2, dir, origin) {
|
|
|
25193
25652
|
return;
|
|
25194
25653
|
}
|
|
25195
25654
|
for (const name of names) {
|
|
25196
|
-
const skillFile =
|
|
25655
|
+
const skillFile = join7(dir, name, "SKILL.md");
|
|
25197
25656
|
try {
|
|
25198
25657
|
const raw = readOptional(skillFile);
|
|
25199
25658
|
if (raw === void 0) continue;
|
|
@@ -25202,7 +25661,7 @@ function collectSkillsDir(scan2, dir, origin) {
|
|
|
25202
25661
|
name: front.name ?? name,
|
|
25203
25662
|
source: origin.source,
|
|
25204
25663
|
scope: origin.scope,
|
|
25205
|
-
location:
|
|
25664
|
+
location: join7(dir, name),
|
|
25206
25665
|
updatedAt: statSync2(skillFile).mtime.toISOString()
|
|
25207
25666
|
};
|
|
25208
25667
|
const version2 = front.version ?? origin.defaultVersion;
|
|
@@ -25233,7 +25692,7 @@ function parseFrontmatter(raw) {
|
|
|
25233
25692
|
return out;
|
|
25234
25693
|
}
|
|
25235
25694
|
function collectInstalledPlugins(scan2, claudeDir) {
|
|
25236
|
-
const manifestPath =
|
|
25695
|
+
const manifestPath = join7(claudeDir, "plugins", "installed_plugins.json");
|
|
25237
25696
|
const raw = readOptional(manifestPath);
|
|
25238
25697
|
if (raw === void 0) return;
|
|
25239
25698
|
let plugins;
|
|
@@ -25258,7 +25717,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
|
|
|
25258
25717
|
if (typeof installPath !== "string" || seen.has(installPath)) continue;
|
|
25259
25718
|
seen.add(installPath);
|
|
25260
25719
|
const version2 = install.version;
|
|
25261
|
-
const hooksPath =
|
|
25720
|
+
const hooksPath = join7(installPath, "hooks", "hooks.json");
|
|
25262
25721
|
const hooksRaw = readOptional(hooksPath);
|
|
25263
25722
|
if (hooksRaw !== void 0) {
|
|
25264
25723
|
try {
|
|
@@ -25278,22 +25737,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
|
|
|
25278
25737
|
}
|
|
25279
25738
|
const origin = { source: marketplace, scope: "plugin", pluginName };
|
|
25280
25739
|
if (typeof version2 === "string") origin.defaultVersion = version2;
|
|
25281
|
-
collectSkillsDir(scan2,
|
|
25740
|
+
collectSkillsDir(scan2, join7(installPath, "skills"), origin);
|
|
25282
25741
|
const mcpOrigin = { scope: "plugin", pluginName, marketplace };
|
|
25283
|
-
collectMcpFile(scan2,
|
|
25742
|
+
collectMcpFile(scan2, join7(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
|
|
25284
25743
|
collectPluginManifestMcp(scan2, installPath, mcpOrigin);
|
|
25285
25744
|
}
|
|
25286
25745
|
}
|
|
25287
25746
|
}
|
|
25288
25747
|
function collectMarketplaceSkills(scan2, claudeDir) {
|
|
25289
|
-
for (const mp of readMarketplaces(
|
|
25748
|
+
for (const mp of readMarketplaces(join7(claudeDir, "plugins", "known_marketplaces.json"))) {
|
|
25290
25749
|
if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
|
|
25291
|
-
collectSkillsDir(scan2,
|
|
25750
|
+
collectSkillsDir(scan2, join7(mp.installLocation, "skills"), {
|
|
25292
25751
|
source: mp.name,
|
|
25293
25752
|
scope: "plugin"
|
|
25294
25753
|
});
|
|
25295
|
-
collectPluginSkillDirs(scan2,
|
|
25296
|
-
collectPluginSkillDirs(scan2,
|
|
25754
|
+
collectPluginSkillDirs(scan2, join7(mp.installLocation, "plugins"), mp.name);
|
|
25755
|
+
collectPluginSkillDirs(scan2, join7(mp.installLocation, "external_plugins"), mp.name);
|
|
25297
25756
|
}
|
|
25298
25757
|
}
|
|
25299
25758
|
function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
|
|
@@ -25304,7 +25763,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
|
|
|
25304
25763
|
return;
|
|
25305
25764
|
}
|
|
25306
25765
|
for (const plugin of plugins) {
|
|
25307
|
-
collectSkillsDir(scan2,
|
|
25766
|
+
collectSkillsDir(scan2, join7(pluginsDir, plugin, "skills"), {
|
|
25308
25767
|
source: marketplace,
|
|
25309
25768
|
scope: "plugin",
|
|
25310
25769
|
pluginName: plugin
|
|
@@ -25413,22 +25872,22 @@ function resolveInventoryContext(input) {
|
|
|
25413
25872
|
}
|
|
25414
25873
|
|
|
25415
25874
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
25416
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
25417
|
-
import { join as
|
|
25875
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25876
|
+
import { join as join8 } from "path";
|
|
25418
25877
|
var SESSION_START_MARKER = "session-start-last";
|
|
25419
25878
|
function claimSessionStart(dataDir2, sessionId) {
|
|
25420
25879
|
return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
|
|
25421
25880
|
}
|
|
25422
25881
|
function claimOncePerSession(dataDir2, marker, sessionId) {
|
|
25423
25882
|
if (!sessionId) return true;
|
|
25424
|
-
const path =
|
|
25883
|
+
const path = join8(dataDir2, marker);
|
|
25425
25884
|
try {
|
|
25426
25885
|
if (readFileSync5(path, "utf8") === sessionId) return false;
|
|
25427
25886
|
} catch {
|
|
25428
25887
|
}
|
|
25429
25888
|
try {
|
|
25430
25889
|
mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
|
|
25431
|
-
|
|
25890
|
+
writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
|
|
25432
25891
|
} catch {
|
|
25433
25892
|
}
|
|
25434
25893
|
return true;
|
|
@@ -25436,8 +25895,8 @@ function claimOncePerSession(dataDir2, marker, sessionId) {
|
|
|
25436
25895
|
|
|
25437
25896
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25438
25897
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25439
|
-
import { existsSync as
|
|
25440
|
-
import { basename as basename4, join as
|
|
25898
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
25899
|
+
import { basename as basename4, join as join9, relative, sep as sep3 } from "path";
|
|
25441
25900
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
25442
25901
|
".git",
|
|
25443
25902
|
"node_modules",
|
|
@@ -25457,7 +25916,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
25457
25916
|
var MAX_FILES = 2e4;
|
|
25458
25917
|
function readIgnoreLayer(dir) {
|
|
25459
25918
|
try {
|
|
25460
|
-
const content = readFileSync6(
|
|
25919
|
+
const content = readFileSync6(join9(dir, ".gitignore"), "utf8");
|
|
25461
25920
|
return { base: dir, matcher: (0, import_ignore.default)().add(content) };
|
|
25462
25921
|
} catch {
|
|
25463
25922
|
return void 0;
|
|
@@ -25535,10 +25994,10 @@ function resolveProjectFiles(cwd) {
|
|
|
25535
25994
|
const layer = readIgnoreLayer(dir);
|
|
25536
25995
|
const dirLayers = layer ? [...layers, layer] : layers;
|
|
25537
25996
|
for (const entry of dirents) {
|
|
25538
|
-
const fullPath =
|
|
25997
|
+
const fullPath = join9(dir, entry.name);
|
|
25539
25998
|
if (entry.isDirectory()) {
|
|
25540
25999
|
if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
|
|
25541
|
-
if (
|
|
26000
|
+
if (existsSync4(join9(fullPath, ".git"))) continue;
|
|
25542
26001
|
if (visit2(fullPath, dirLayers)) return true;
|
|
25543
26002
|
continue;
|
|
25544
26003
|
}
|
|
@@ -25575,18 +26034,21 @@ function resolveProjectFiles(cwd) {
|
|
|
25575
26034
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25576
26035
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
25577
26036
|
|
|
26037
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
26038
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26039
|
+
|
|
25578
26040
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25579
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
25580
|
-
import { join as
|
|
26041
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
26042
|
+
import { join as join10 } from "path";
|
|
25581
26043
|
function throttled(dataDir2, markerName, windowMs) {
|
|
25582
|
-
const marker =
|
|
26044
|
+
const marker = join10(dataDir2, markerName);
|
|
25583
26045
|
try {
|
|
25584
26046
|
if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
|
|
25585
26047
|
} catch {
|
|
25586
26048
|
}
|
|
25587
26049
|
try {
|
|
25588
26050
|
mkdirSync4(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
|
|
25589
|
-
|
|
26051
|
+
writeFileSync5(marker, String(Date.now()), { mode: DATA_FILE_MODE });
|
|
25590
26052
|
} catch {
|
|
25591
26053
|
}
|
|
25592
26054
|
return false;
|
|
@@ -25786,6 +26248,14 @@ var StandaloneDataGateway = class {
|
|
|
25786
26248
|
sweepTerminalExceptions(retentionMs) {
|
|
25787
26249
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
25788
26250
|
}
|
|
26251
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
26252
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
26253
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
26254
|
+
// already-capped one).
|
|
26255
|
+
capWarnEraEnforcement(policyMode) {
|
|
26256
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
26257
|
+
return { capped };
|
|
26258
|
+
}
|
|
25789
26259
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
25790
26260
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
25791
26261
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -25908,6 +26378,15 @@ async function handleSessionStart(input, config2 = loadConfig()) {
|
|
|
25908
26378
|
await gateway.sweepTerminalExceptions(EXCEPTION_RETENTION_MS);
|
|
25909
26379
|
} catch {
|
|
25910
26380
|
}
|
|
26381
|
+
try {
|
|
26382
|
+
const { capped } = gateway.capWarnEraEnforcement(config2.settings.policy);
|
|
26383
|
+
if (capped > 0) {
|
|
26384
|
+
process.stderr.write(
|
|
26385
|
+
'AKA: the global "warn only" handling was retired; your existing block/redact categories were kept at warn. Re-run /aka:setup to adopt per-category enforcement.\n'
|
|
26386
|
+
);
|
|
26387
|
+
}
|
|
26388
|
+
} catch {
|
|
26389
|
+
}
|
|
25911
26390
|
try {
|
|
25912
26391
|
if (resolved.sourceProjectId) {
|
|
25913
26392
|
const filesScan = resolveProjectFiles(input.cwd);
|
|
@@ -26001,7 +26480,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
|
|
|
26001
26480
|
|
|
26002
26481
|
// src/history/reconcile-trigger.ts
|
|
26003
26482
|
import { spawn } from "child_process";
|
|
26004
|
-
import { dirname as dirname2, join as
|
|
26483
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
26005
26484
|
import { fileURLToPath } from "url";
|
|
26006
26485
|
|
|
26007
26486
|
// src/history/tail.ts
|
|
@@ -26013,9 +26492,9 @@ import {
|
|
|
26013
26492
|
openSync,
|
|
26014
26493
|
readFileSync as readFileSync7,
|
|
26015
26494
|
readSync,
|
|
26016
|
-
writeFileSync as
|
|
26495
|
+
writeFileSync as writeFileSync6
|
|
26017
26496
|
} from "fs";
|
|
26018
|
-
import { join as
|
|
26497
|
+
import { join as join11 } from "path";
|
|
26019
26498
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
26020
26499
|
function safeSessionId(sessionId) {
|
|
26021
26500
|
if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
|
|
@@ -26032,7 +26511,7 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
|
|
|
26032
26511
|
const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
|
|
26033
26512
|
if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
|
|
26034
26513
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
26035
|
-
const child = spawn(process.execPath, [
|
|
26514
|
+
const child = spawn(process.execPath, [join12(here, "reconcile.js"), sessionId, transcriptPath], {
|
|
26036
26515
|
detached: true,
|
|
26037
26516
|
stdio: "ignore"
|
|
26038
26517
|
});
|