@akasecurity/ai-tc-claude-code 0.8.1 → 0.9.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -4
- package/commands/setup.md +650 -70
- package/hooks/hooks.json +1 -1
- package/package.json +7 -6
- package/scripts/apply-suppressions.js +24237 -0
- package/scripts/backfill.js +1852 -937
- package/scripts/filescan.js +1636 -900
- package/scripts/firstrun.js +1796 -929
- package/scripts/intro.js +377 -119
- package/scripts/onboard.js +6162 -106
- package/scripts/package.json +1 -0
- package/scripts/post-tool-use.js +1678 -919
- package/scripts/pre-tool-use.js +1776 -931
- package/scripts/query.js +1621 -884
- package/scripts/reconcile.js +1632 -902
- package/scripts/remediate.js +26695 -0
- package/scripts/session-start.js +1680 -941
- package/scripts/start-light.js +17542 -0
- package/scripts/statusline.js +1621 -882
- package/scripts/stop.js +301 -40
- package/scripts/triage-rubric.md +92 -0
- package/scripts/user-prompt-submit.js +1654 -917
package/scripts/backfill.js
CHANGED
|
@@ -491,6 +491,9 @@ var require_ignore = __commonJS({
|
|
|
491
491
|
}
|
|
492
492
|
});
|
|
493
493
|
|
|
494
|
+
// src/backfill.ts
|
|
495
|
+
import { fileURLToPath } from "url";
|
|
496
|
+
|
|
494
497
|
// ../../packages/persistence/src/database.ts
|
|
495
498
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
496
499
|
import { existsSync, renameSync, rmSync } from "fs";
|
|
@@ -538,6 +541,10 @@ var SQLITE_MIGRATIONS = [
|
|
|
538
541
|
{
|
|
539
542
|
tag: "0009_findings_path_expression_index",
|
|
540
543
|
sql: "-- Custom migration: partial expression index for the resolver's per-path reads.\n--\n-- openAtRestKeysForPath / resolvedAtRestKeysForPath (and the scanner's tier-3\n-- open-key probe) filter at-rest events by\n-- e.kind = 'code_change' AND json_extract(e.metadata, '$.filePath') = :path\n-- once per changed/deleted file on every scan \u2014 previously a full events scan\n-- per file. json_extract is deterministic, so SQLite allows it in an index;\n-- the WHERE kind = 'code_change' keeps the index to exactly the rows those\n-- queries can match (in-flight events are never path-addressed).\nCREATE INDEX `idx_events_code_change_path` ON `events` (json_extract(`metadata`, '$.filePath')) WHERE `kind` = 'code_change';\n"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
tag: "0010_events_session_expression_index",
|
|
547
|
+
sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
|
|
541
548
|
}
|
|
542
549
|
];
|
|
543
550
|
|
|
@@ -640,6 +647,12 @@ var defaultCostModel = {
|
|
|
640
647
|
}
|
|
641
648
|
};
|
|
642
649
|
|
|
650
|
+
// ../../packages/schema/src/token/format.ts
|
|
651
|
+
var COMPACT = new Intl.NumberFormat("en-US", {
|
|
652
|
+
notation: "compact",
|
|
653
|
+
maximumFractionDigits: 1
|
|
654
|
+
});
|
|
655
|
+
|
|
643
656
|
// ../../packages/schema/src/token/token-report.ts
|
|
644
657
|
var num = (value) => value ?? 0;
|
|
645
658
|
function costUsageOf(a) {
|
|
@@ -15311,6 +15324,12 @@ var FindingInstance = external_exports.object({
|
|
|
15311
15324
|
provider: FindingProvider,
|
|
15312
15325
|
repo: external_exports.string(),
|
|
15313
15326
|
file: external_exports.string(),
|
|
15327
|
+
// Host tool that produced the scanned text (event metadata's toolName).
|
|
15328
|
+
// Present whenever the capturing hook recorded one — including
|
|
15329
|
+
// file-attributed captures (views prefer `file`); its display value is
|
|
15330
|
+
// the location fallback ("via Bash") when no filePath exists. Absent for
|
|
15331
|
+
// legacy rows and non-tool captures (prompts, worktree scans).
|
|
15332
|
+
toolName: external_exports.string().optional(),
|
|
15314
15333
|
// Effective action: override.action ?? actionTaken, translated to FindingAction.
|
|
15315
15334
|
action: FindingAction,
|
|
15316
15335
|
detectedAt: external_exports.iso.datetime(),
|
|
@@ -15367,6 +15386,9 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15367
15386
|
provider: external_exports.array(FindingProvider).optional(),
|
|
15368
15387
|
action: external_exports.array(FindingAction).optional(),
|
|
15369
15388
|
q: external_exports.string().optional(),
|
|
15389
|
+
// Scope to findings whose event carries this session id (the Activity page's
|
|
15390
|
+
// session → findings drilldown). Findings without a session never match.
|
|
15391
|
+
sessionId: external_exports.string().optional(),
|
|
15370
15392
|
groupBy: external_exports.literal("type").optional(),
|
|
15371
15393
|
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
15372
15394
|
cursor: external_exports.string().optional()
|
|
@@ -15378,7 +15400,13 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
15378
15400
|
}),
|
|
15379
15401
|
facets: FindingFacets,
|
|
15380
15402
|
items: external_exports.array(FindingGroup),
|
|
15381
|
-
nextCursor: external_exports.string().nullable()
|
|
15403
|
+
nextCursor: external_exports.string().nullable(),
|
|
15404
|
+
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
15405
|
+
// many times that rule fired in the session's persisted transcript. Findings
|
|
15406
|
+
// here are deduplicated to unique values while the transcript tally counts
|
|
15407
|
+
// every firing, so the two numbers legitimately differ — this map lets a
|
|
15408
|
+
// session-scoped view show both.
|
|
15409
|
+
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
15382
15410
|
}).meta({ id: "ListGroupedFindingsResponse" });
|
|
15383
15411
|
var ApplyFindingActionRequest = external_exports.object({
|
|
15384
15412
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
@@ -15430,6 +15458,11 @@ var AuditEventType = external_exports.enum([
|
|
|
15430
15458
|
"prompt",
|
|
15431
15459
|
"response",
|
|
15432
15460
|
"code_change",
|
|
15461
|
+
// The events.kind of a scanned tool call, widened in to keep this a
|
|
15462
|
+
// superset. Narrower than 'tool_call' above and not a duplicate of it:
|
|
15463
|
+
// 'tool_call' is the reconciler's structural row for every call, while
|
|
15464
|
+
// 'tool_use' exists only where a hook enforced against the arguments.
|
|
15465
|
+
"tool_use",
|
|
15433
15466
|
// One row per config-inventory scan, hung off the session root. It is the
|
|
15434
15467
|
// fact the posture inspection findings reference (findings require an
|
|
15435
15468
|
// audit_event_id), and its started_at is the "scanned Nm ago" the read
|
|
@@ -15784,6 +15817,10 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15784
15817
|
from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15785
15818
|
/** Upper bound on startedAt; omitted defaults to now. */
|
|
15786
15819
|
to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
|
|
15820
|
+
/** Exclude zero-activity sessions — roots whose only recorded children are
|
|
15821
|
+
* bookkeeping rows (hooks, config scans), typically background `claude`
|
|
15822
|
+
* launches. Omitted = list everything. `z.stringbool()` per the note above. */
|
|
15823
|
+
excludeEmpty: external_exports.stringbool().optional(),
|
|
15787
15824
|
/** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
|
|
15788
15825
|
limit: external_exports.coerce.number().int().min(1).max(100).default(50),
|
|
15789
15826
|
/** Opaque pagination cursor (most-recent first). */
|
|
@@ -15792,7 +15829,11 @@ var ListActivitySessionsQuery = external_exports.object({
|
|
|
15792
15829
|
var ListActivitySessionsResponse = external_exports.object({
|
|
15793
15830
|
items: external_exports.array(ActivitySessionSummary),
|
|
15794
15831
|
/** `null` once the last page is reached. */
|
|
15795
|
-
nextCursor: external_exports.string().nullable()
|
|
15832
|
+
nextCursor: external_exports.string().nullable(),
|
|
15833
|
+
/** Zero-activity sessions matching the query's filters/range (whether or
|
|
15834
|
+
* not `excludeEmpty` dropped them from `items`) — the count a UI toggle
|
|
15835
|
+
* shows when collapsing them. */
|
|
15836
|
+
emptyCount: external_exports.number().int().nonnegative()
|
|
15796
15837
|
}).meta({ id: "ListActivitySessionsResponse" });
|
|
15797
15838
|
var ListSessionEventsQuery = external_exports.object({
|
|
15798
15839
|
/** Default 100, range 1–500. */
|
|
@@ -15821,12 +15862,18 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15821
15862
|
}).meta({ id: "ActivityOverviewResponse" });
|
|
15822
15863
|
|
|
15823
15864
|
// ../../packages/schema/src/zod/event.ts
|
|
15824
|
-
var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
|
|
15865
|
+
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15825
15866
|
var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
|
|
15826
15867
|
var EventMetadata = external_exports.object({
|
|
15827
15868
|
sessionId: external_exports.string().optional(),
|
|
15828
15869
|
repo: external_exports.string().optional(),
|
|
15829
15870
|
filePath: external_exports.string().optional(),
|
|
15871
|
+
// The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
|
|
15872
|
+
// set by the tool-scanning hooks. The tool NAME only — never the tool's
|
|
15873
|
+
// arguments or output, which can carry the very value a finding masked
|
|
15874
|
+
// (metadata is stored unredacted). Gives findings on non-file captures a
|
|
15875
|
+
// display location ("via Bash") when no filePath exists.
|
|
15876
|
+
toolName: external_exports.string().optional(),
|
|
15830
15877
|
// Set (true) by the worktree scanner when the file is excluded by the
|
|
15831
15878
|
// repo's .gitignore. Gitignored files ARE still scanned — local scratch and
|
|
15832
15879
|
// generated code can leak real secrets — but the provenance is recorded so
|
|
@@ -16134,7 +16181,7 @@ var DetectionException = external_exports.object({
|
|
|
16134
16181
|
justification: external_exports.string().min(1),
|
|
16135
16182
|
conditions: ExceptionConditions.nullable(),
|
|
16136
16183
|
createdBy: external_exports.string(),
|
|
16137
|
-
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
|
|
16184
|
+
createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
|
|
16138
16185
|
createdAt: external_exports.iso.datetime(),
|
|
16139
16186
|
updatedAt: external_exports.iso.datetime(),
|
|
16140
16187
|
// Revocation is terminal and retained — consumed/expired/revoked rows are
|
|
@@ -16158,7 +16205,10 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16158
16205
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16159
16206
|
var KeywordMatcher = external_exports.object({
|
|
16160
16207
|
type: external_exports.literal("keyword"),
|
|
16161
|
-
|
|
16208
|
+
// An empty keyword matches at every position, yielding one zero-length span
|
|
16209
|
+
// per character. Rejected here because a keyword that matches everything is
|
|
16210
|
+
// never intentional.
|
|
16211
|
+
keywords: external_exports.array(external_exports.string().min(1)).min(1),
|
|
16162
16212
|
caseSensitive: external_exports.boolean().default(false)
|
|
16163
16213
|
});
|
|
16164
16214
|
function isValidRegex(pattern, flags) {
|
|
@@ -16303,20 +16353,26 @@ var PolicyBundle = external_exports.object({
|
|
|
16303
16353
|
customKeywords: external_exports.array(external_exports.string()),
|
|
16304
16354
|
fetchedAt: external_exports.iso.datetime()
|
|
16305
16355
|
}).meta({ id: "PolicyBundle" });
|
|
16306
|
-
var DEFAULT_ACTIONS = {
|
|
16307
|
-
secret: "block",
|
|
16308
|
-
pii: "redact",
|
|
16309
|
-
financial: "redact",
|
|
16310
|
-
phi: "redact",
|
|
16311
|
-
code_context: "warn",
|
|
16312
|
-
code_flaw: "warn",
|
|
16313
|
-
custom: "warn",
|
|
16314
|
-
// Config-posture findings only observe today (they land in
|
|
16315
|
-
// inspection_findings, outside the live-capture enforcement path).
|
|
16316
|
-
config: "warn"
|
|
16317
|
-
};
|
|
16318
16356
|
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
16319
16357
|
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
16358
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
16359
|
+
secret: "critical",
|
|
16360
|
+
financial: "critical",
|
|
16361
|
+
// core-financial/credit-card
|
|
16362
|
+
code_flaw: "critical",
|
|
16363
|
+
pii: "high",
|
|
16364
|
+
phi: "high",
|
|
16365
|
+
custom: "high",
|
|
16366
|
+
// user-defined; conservative
|
|
16367
|
+
code_context: "low",
|
|
16368
|
+
config: "low"
|
|
16369
|
+
// observe-only; floors to monitor regardless
|
|
16370
|
+
};
|
|
16371
|
+
function severityFloorPolicy(category) {
|
|
16372
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
16373
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
16374
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
16375
|
+
}
|
|
16320
16376
|
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
16321
16377
|
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
|
|
16322
16378
|
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
@@ -16343,6 +16399,12 @@ var BUILTIN_POLICY_SPECS = {
|
|
|
16343
16399
|
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
16344
16400
|
}
|
|
16345
16401
|
};
|
|
16402
|
+
function builtinPolicyToAction(id) {
|
|
16403
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
16404
|
+
}
|
|
16405
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
16406
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
16407
|
+
);
|
|
16346
16408
|
var BUILTIN_POLICIES = Object.fromEntries(
|
|
16347
16409
|
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
16348
16410
|
);
|
|
@@ -16395,6 +16457,10 @@ var ListEventsResponse = external_exports.object({
|
|
|
16395
16457
|
items: external_exports.array(Event),
|
|
16396
16458
|
nextCursor: external_exports.string().nullable()
|
|
16397
16459
|
}).meta({ id: "ListEventsResponse" });
|
|
16460
|
+
var IngestResponse = external_exports.object({
|
|
16461
|
+
accepted: external_exports.number().int().nonnegative(),
|
|
16462
|
+
duplicates: external_exports.number().int().nonnegative()
|
|
16463
|
+
}).meta({ id: "IngestResponse" });
|
|
16398
16464
|
var ListFindingsQuery = external_exports.object({
|
|
16399
16465
|
cursor: external_exports.string().optional(),
|
|
16400
16466
|
limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
|
|
@@ -16749,10 +16815,8 @@ function toApiProvider(sourceTool) {
|
|
|
16749
16815
|
return TOOL_TO_HARNESS[sourceTool] ?? "api";
|
|
16750
16816
|
}
|
|
16751
16817
|
var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
|
|
16752
|
-
function
|
|
16753
|
-
const statuses = new Set(
|
|
16754
|
-
instances.map((i) => i.status).filter((s) => s !== void 0)
|
|
16755
|
-
);
|
|
16818
|
+
function foldGroupStatus(instanceStatuses) {
|
|
16819
|
+
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
16756
16820
|
if (statuses.size === 0) return void 0;
|
|
16757
16821
|
for (const candidate of STATUS_PRECEDENCE) {
|
|
16758
16822
|
if (statuses.has(candidate)) return candidate;
|
|
@@ -16770,6 +16834,7 @@ function deriveFindingStatus(row) {
|
|
|
16770
16834
|
function buildFindingGroups(rows, opts = {}) {
|
|
16771
16835
|
const overrides = opts.overrides;
|
|
16772
16836
|
const packNames = opts.packNames;
|
|
16837
|
+
const aggregates = opts.aggregates;
|
|
16773
16838
|
const byRuleId = /* @__PURE__ */ new Map();
|
|
16774
16839
|
for (const row of rows) {
|
|
16775
16840
|
const existing = byRuleId.get(row.ruleId);
|
|
@@ -16785,23 +16850,27 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16785
16850
|
provider: toApiProvider(r.sourceTool),
|
|
16786
16851
|
repo: r.repo,
|
|
16787
16852
|
file: r.file,
|
|
16853
|
+
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
16788
16854
|
action: toApiAction(effectiveDbAction),
|
|
16789
16855
|
detectedAt: r.occurredAt,
|
|
16790
16856
|
confidence: r.confidence,
|
|
16791
16857
|
status: r.status
|
|
16792
16858
|
};
|
|
16793
16859
|
});
|
|
16794
|
-
const
|
|
16860
|
+
const agg = aggregates?.get(ruleId);
|
|
16861
|
+
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
16795
16862
|
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
16796
16863
|
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
16797
16864
|
);
|
|
16798
16865
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
16799
|
-
const providers = instances.map((i) => i.provider).filter((p) => {
|
|
16866
|
+
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
16800
16867
|
if (seenProviders.has(p)) return false;
|
|
16801
16868
|
seenProviders.add(p);
|
|
16802
16869
|
return true;
|
|
16803
16870
|
});
|
|
16804
|
-
const actionSet = new Set(
|
|
16871
|
+
const actionSet = new Set(
|
|
16872
|
+
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
16873
|
+
);
|
|
16805
16874
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
16806
16875
|
const severity = ruleRows[0]?.severity ?? "low";
|
|
16807
16876
|
const detection = {
|
|
@@ -16815,8 +16884,10 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16815
16884
|
contextPrefix: ""
|
|
16816
16885
|
// empty (pending privacy review)
|
|
16817
16886
|
};
|
|
16818
|
-
const status =
|
|
16819
|
-
|
|
16887
|
+
const status = foldGroupStatus(
|
|
16888
|
+
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
16889
|
+
);
|
|
16890
|
+
const group = {
|
|
16820
16891
|
id: ruleId,
|
|
16821
16892
|
category: apiCategory,
|
|
16822
16893
|
subtype: ruleId,
|
|
@@ -16825,21 +16896,26 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
16825
16896
|
match,
|
|
16826
16897
|
detection,
|
|
16827
16898
|
policy,
|
|
16828
|
-
instanceCount: instances.length,
|
|
16899
|
+
instanceCount: agg?.instanceCount ?? instances.length,
|
|
16829
16900
|
providers,
|
|
16830
16901
|
aggregateAction,
|
|
16831
16902
|
latestDetectedAt,
|
|
16832
16903
|
instances,
|
|
16833
16904
|
status
|
|
16834
|
-
}
|
|
16905
|
+
};
|
|
16906
|
+
if (agg) {
|
|
16907
|
+
actionsCache.set(group, [...actionSet]);
|
|
16908
|
+
if (agg.searchText !== void 0) {
|
|
16909
|
+
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
16910
|
+
}
|
|
16911
|
+
}
|
|
16912
|
+
groups.push(group);
|
|
16835
16913
|
}
|
|
16836
16914
|
return groups;
|
|
16837
16915
|
}
|
|
16838
16916
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
16839
|
-
function
|
|
16840
|
-
|
|
16841
|
-
if (cached2 !== void 0) return cached2;
|
|
16842
|
-
const haystack = [
|
|
16917
|
+
function buildHaystack(g, extra) {
|
|
16918
|
+
return [
|
|
16843
16919
|
g.subtype,
|
|
16844
16920
|
g.category,
|
|
16845
16921
|
g.match.maskedValue,
|
|
@@ -16847,11 +16923,26 @@ function groupHaystack(g) {
|
|
|
16847
16923
|
g.id,
|
|
16848
16924
|
...g.instances.map((i) => i.repo),
|
|
16849
16925
|
...g.instances.map((i) => i.file),
|
|
16850
|
-
...g.instances.map((i) => i.
|
|
16926
|
+
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
16927
|
+
...g.instances.map((i) => i.id),
|
|
16928
|
+
...extra === void 0 ? [] : [extra]
|
|
16851
16929
|
].join(" ").toLowerCase();
|
|
16930
|
+
}
|
|
16931
|
+
function groupHaystack(g) {
|
|
16932
|
+
const cached2 = haystackCache.get(g);
|
|
16933
|
+
if (cached2 !== void 0) return cached2;
|
|
16934
|
+
const haystack = buildHaystack(g);
|
|
16852
16935
|
haystackCache.set(g, haystack);
|
|
16853
16936
|
return haystack;
|
|
16854
16937
|
}
|
|
16938
|
+
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
16939
|
+
function groupActions(g) {
|
|
16940
|
+
const cached2 = actionsCache.get(g);
|
|
16941
|
+
if (cached2 !== void 0) return cached2;
|
|
16942
|
+
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
16943
|
+
actionsCache.set(g, actions);
|
|
16944
|
+
return actions;
|
|
16945
|
+
}
|
|
16855
16946
|
function applyFindingFilters(groups, opts) {
|
|
16856
16947
|
let filtered = groups;
|
|
16857
16948
|
if (opts.severity && opts.severity.length > 0) {
|
|
@@ -16864,7 +16955,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
16864
16955
|
}
|
|
16865
16956
|
if (opts.actions && opts.actions.length > 0) {
|
|
16866
16957
|
const actionSet = new Set(opts.actions);
|
|
16867
|
-
filtered = filtered.filter((g) => g.
|
|
16958
|
+
filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
|
|
16868
16959
|
}
|
|
16869
16960
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
16870
16961
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -16916,8 +17007,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
16916
17007
|
});
|
|
16917
17008
|
const actionMap = /* @__PURE__ */ new Map();
|
|
16918
17009
|
for (const g of forAction) {
|
|
16919
|
-
const
|
|
16920
|
-
for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
17010
|
+
for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
16921
17011
|
}
|
|
16922
17012
|
const forSubtype = applyFindingFilters(allGroups, {
|
|
16923
17013
|
providers: opts.providers,
|
|
@@ -17100,6 +17190,71 @@ var ProjectFilesScan = external_exports.object({
|
|
|
17100
17190
|
scannedAt: external_exports.string()
|
|
17101
17191
|
});
|
|
17102
17192
|
|
|
17193
|
+
// ../../packages/schema/src/zod/ranges.ts
|
|
17194
|
+
var TIME_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17195
|
+
var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
|
|
17196
|
+
var DEFAULT_TIME_RANGE = "7d";
|
|
17197
|
+
var RANGE_DAYS = {
|
|
17198
|
+
"7d": 7,
|
|
17199
|
+
"30d": 30,
|
|
17200
|
+
"3m": 90,
|
|
17201
|
+
"6m": 180
|
|
17202
|
+
};
|
|
17203
|
+
var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
|
|
17204
|
+
|
|
17205
|
+
// ../../packages/schema/src/zod/remediation.ts
|
|
17206
|
+
var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
|
|
17207
|
+
var MaskedFindingLocation = external_exports.object({
|
|
17208
|
+
filePath: external_exports.string(),
|
|
17209
|
+
span: Span.optional()
|
|
17210
|
+
}).strict();
|
|
17211
|
+
var MaskedSecretFinding = external_exports.object({
|
|
17212
|
+
provider: external_exports.string(),
|
|
17213
|
+
maskedToken: external_exports.string(),
|
|
17214
|
+
where: MaskedFindingLocation,
|
|
17215
|
+
state: SecretFindingState,
|
|
17216
|
+
observedAt: external_exports.iso.datetime().optional()
|
|
17217
|
+
}).strict();
|
|
17218
|
+
var RotationChecklistEntry = external_exports.object({
|
|
17219
|
+
provider: external_exports.string(),
|
|
17220
|
+
maskedToken: external_exports.string(),
|
|
17221
|
+
consolePath: external_exports.string(),
|
|
17222
|
+
occurrenceSpread: external_exports.number().int().positive()
|
|
17223
|
+
}).strict();
|
|
17224
|
+
var RemediationOption = external_exports.enum([
|
|
17225
|
+
"redact-rotation-checklist",
|
|
17226
|
+
"redact-only",
|
|
17227
|
+
"set-secret-redact",
|
|
17228
|
+
"leave"
|
|
17229
|
+
]);
|
|
17230
|
+
var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
|
|
17231
|
+
var RemediationEntryContext = external_exports.object({
|
|
17232
|
+
entrySource: RemediationEntrySource
|
|
17233
|
+
}).strict();
|
|
17234
|
+
var RemediationOptionChoice = external_exports.object({
|
|
17235
|
+
id: RemediationOption,
|
|
17236
|
+
label: external_exports.string()
|
|
17237
|
+
});
|
|
17238
|
+
var BatchedRemediationDecision = external_exports.object({
|
|
17239
|
+
kind: external_exports.literal("decision"),
|
|
17240
|
+
entrySource: RemediationEntrySource,
|
|
17241
|
+
secretCount: external_exports.number().int().positive(),
|
|
17242
|
+
prompt: external_exports.string(),
|
|
17243
|
+
options: external_exports.tuple([
|
|
17244
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
|
|
17245
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
|
|
17246
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
|
|
17247
|
+
RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
|
|
17248
|
+
])
|
|
17249
|
+
});
|
|
17250
|
+
var NoRemediationDecision = external_exports.object({
|
|
17251
|
+
kind: external_exports.literal("no-decision")
|
|
17252
|
+
});
|
|
17253
|
+
var BatchedRemediation = external_exports.discriminatedUnion("kind", [
|
|
17254
|
+
BatchedRemediationDecision,
|
|
17255
|
+
NoRemediationDecision
|
|
17256
|
+
]);
|
|
17257
|
+
|
|
17103
17258
|
// ../../packages/schema/src/zod/rule-test.ts
|
|
17104
17259
|
var TestRulesRequest = external_exports.object({
|
|
17105
17260
|
rules: external_exports.array(Rule).min(1).max(100),
|
|
@@ -17158,10 +17313,8 @@ var SeveritySummaryResponse = external_exports.object({
|
|
|
17158
17313
|
// All four severity levels are always present (count may be 0).
|
|
17159
17314
|
bySeverity: external_exports.array(SeveritySummaryItem)
|
|
17160
17315
|
}).meta({ id: "SeveritySummaryResponse" });
|
|
17161
|
-
var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
|
|
17162
|
-
var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
|
|
17163
17316
|
var SecurityRangeQuery = external_exports.object({
|
|
17164
|
-
range: external_exports.enum(
|
|
17317
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
|
|
17165
17318
|
});
|
|
17166
17319
|
var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
|
|
17167
17320
|
var EnforcementAction = external_exports.object({
|
|
@@ -17171,7 +17324,7 @@ var EnforcementAction = external_exports.object({
|
|
|
17171
17324
|
delta: external_exports.number().int()
|
|
17172
17325
|
}).meta({ id: "EnforcementAction" });
|
|
17173
17326
|
var EnforcementActionsResponse = external_exports.object({
|
|
17174
|
-
range:
|
|
17327
|
+
range: TimeRange,
|
|
17175
17328
|
// Sum of actions[].count in the window.
|
|
17176
17329
|
total: external_exports.number().int().nonnegative(),
|
|
17177
17330
|
// One entry per kind, always all three present (count may be 0).
|
|
@@ -17186,7 +17339,7 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
17186
17339
|
medium: external_exports.number().int().nonnegative()
|
|
17187
17340
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
17188
17341
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
17189
|
-
range:
|
|
17342
|
+
range: TimeRange,
|
|
17190
17343
|
granularity: TimeseriesGranularity,
|
|
17191
17344
|
points: external_exports.array(FindingsTimeseriesPoint)
|
|
17192
17345
|
}).meta({ id: "FindingsTimeseriesResponse" });
|
|
@@ -17201,7 +17354,7 @@ var MttrTrendPoint = external_exports.object({
|
|
|
17201
17354
|
})
|
|
17202
17355
|
}).meta({ id: "MttrTrendPoint" });
|
|
17203
17356
|
var MttrTrendResponse = external_exports.object({
|
|
17204
|
-
range:
|
|
17357
|
+
range: TimeRange,
|
|
17205
17358
|
granularity: TimeseriesGranularity,
|
|
17206
17359
|
points: external_exports.array(MttrTrendPoint)
|
|
17207
17360
|
}).meta({ id: "MttrTrendResponse" });
|
|
@@ -17228,11 +17381,11 @@ var TopSource = external_exports.object({
|
|
|
17228
17381
|
findingsCount: external_exports.number().int().nonnegative()
|
|
17229
17382
|
}).meta({ id: "TopSource" });
|
|
17230
17383
|
var TopSourcesResponse = external_exports.object({
|
|
17231
|
-
range:
|
|
17384
|
+
range: TimeRange,
|
|
17232
17385
|
items: external_exports.array(TopSource)
|
|
17233
17386
|
}).meta({ id: "TopSourcesResponse" });
|
|
17234
17387
|
var TopSourcesQuery = external_exports.object({
|
|
17235
|
-
range: external_exports.enum(
|
|
17388
|
+
range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
|
|
17236
17389
|
limit: external_exports.coerce.number().int().min(1).max(50).default(5),
|
|
17237
17390
|
// Omit for both kinds.
|
|
17238
17391
|
kind: external_exports.enum(SOURCE_KINDS).optional()
|
|
@@ -17245,7 +17398,7 @@ var ScanCoverageProvider = external_exports.object({
|
|
|
17245
17398
|
supported: external_exports.boolean()
|
|
17246
17399
|
}).meta({ id: "ScanCoverageProvider" });
|
|
17247
17400
|
var ScanCoverageResponse = external_exports.object({
|
|
17248
|
-
range:
|
|
17401
|
+
range: TimeRange,
|
|
17249
17402
|
providers: external_exports.array(ScanCoverageProvider)
|
|
17250
17403
|
}).meta({ id: "ScanCoverageResponse" });
|
|
17251
17404
|
var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
|
|
@@ -17294,6 +17447,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
|
|
|
17294
17447
|
var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
|
|
17295
17448
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
17296
17449
|
|
|
17450
|
+
// ../../packages/schema/src/zod/triage.ts
|
|
17451
|
+
var TriageHit = external_exports.object({
|
|
17452
|
+
ruleId: external_exports.string(),
|
|
17453
|
+
category: DetectionCategory,
|
|
17454
|
+
severity: Severity,
|
|
17455
|
+
maskedMatch: external_exports.string(),
|
|
17456
|
+
rawMatch: external_exports.string(),
|
|
17457
|
+
context: external_exports.string(),
|
|
17458
|
+
filePath: external_exports.string().optional(),
|
|
17459
|
+
confidence: external_exports.number().min(0).max(1),
|
|
17460
|
+
id: external_exports.string().optional(),
|
|
17461
|
+
valueFingerprint: external_exports.string().optional(),
|
|
17462
|
+
keyVersion: external_exports.number().int().nonnegative().optional()
|
|
17463
|
+
});
|
|
17464
|
+
var TriagePolicy = BuiltinPolicyId;
|
|
17465
|
+
var TriageCategoryRec = external_exports.object({
|
|
17466
|
+
category: DetectionCategory,
|
|
17467
|
+
action: TriagePolicy,
|
|
17468
|
+
reasoning: external_exports.string(),
|
|
17469
|
+
genuineCount: external_exports.number().int().nonnegative(),
|
|
17470
|
+
fpCount: external_exports.number().int().nonnegative(),
|
|
17471
|
+
// TriageHit ids judged false-positive in this category. fpCount must equal
|
|
17472
|
+
// this array's length — enforced by the consumer, not this schema.
|
|
17473
|
+
fpIds: external_exports.array(external_exports.string())
|
|
17474
|
+
});
|
|
17475
|
+
var TriageRecommendation = external_exports.object({
|
|
17476
|
+
perCategory: external_exports.array(TriageCategoryRec),
|
|
17477
|
+
notes: external_exports.string()
|
|
17478
|
+
});
|
|
17479
|
+
|
|
17480
|
+
// ../../packages/schema/src/zod/setup-frame.ts
|
|
17481
|
+
var CalibrationCounts = external_exports.object({
|
|
17482
|
+
total: external_exports.number().int().nonnegative(),
|
|
17483
|
+
important: external_exports.number().int().nonnegative(),
|
|
17484
|
+
routine: external_exports.number().int().nonnegative()
|
|
17485
|
+
}).refine((c) => c.total === c.important + c.routine, {
|
|
17486
|
+
message: "total must equal important + routine",
|
|
17487
|
+
path: ["total"]
|
|
17488
|
+
});
|
|
17489
|
+
var FalsePositivePatternValue = external_exports.object({
|
|
17490
|
+
ruleId: external_exports.string(),
|
|
17491
|
+
category: DetectionCategory,
|
|
17492
|
+
valueFingerprint: external_exports.string(),
|
|
17493
|
+
keyVersion: external_exports.number().int().nonnegative()
|
|
17494
|
+
});
|
|
17495
|
+
var FalsePositivePatternGroup = external_exports.object({
|
|
17496
|
+
pattern: external_exports.string(),
|
|
17497
|
+
count: external_exports.number().int().nonnegative(),
|
|
17498
|
+
values: external_exports.array(FalsePositivePatternValue).min(1)
|
|
17499
|
+
});
|
|
17500
|
+
var CalibrationFindingKind = external_exports.object({
|
|
17501
|
+
category: DetectionCategory,
|
|
17502
|
+
count: external_exports.number().int().nonnegative(),
|
|
17503
|
+
egress: external_exports.boolean()
|
|
17504
|
+
});
|
|
17505
|
+
var CalibrationFrame = external_exports.object({
|
|
17506
|
+
counts: CalibrationCounts,
|
|
17507
|
+
routineCategories: external_exports.array(DetectionCategory),
|
|
17508
|
+
surfacedCategories: external_exports.array(DetectionCategory),
|
|
17509
|
+
findingKinds: external_exports.array(CalibrationFindingKind),
|
|
17510
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
|
|
17511
|
+
maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
|
|
17512
|
+
falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
|
|
17513
|
+
});
|
|
17514
|
+
var CalibrationPreviewCategory = TriageCategoryRec.pick({
|
|
17515
|
+
category: true,
|
|
17516
|
+
genuineCount: true,
|
|
17517
|
+
fpCount: true
|
|
17518
|
+
}).extend({
|
|
17519
|
+
egress: external_exports.boolean()
|
|
17520
|
+
});
|
|
17521
|
+
var CalibrationPreview = external_exports.object({
|
|
17522
|
+
categories: external_exports.array(CalibrationPreviewCategory),
|
|
17523
|
+
posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
|
|
17524
|
+
});
|
|
17525
|
+
var CalibrationResult = external_exports.object({
|
|
17526
|
+
frame: CalibrationFrame,
|
|
17527
|
+
copy: external_exports.string()
|
|
17528
|
+
});
|
|
17529
|
+
var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
|
|
17530
|
+
var SetupHandoffOption = external_exports.object({
|
|
17531
|
+
id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
|
|
17532
|
+
label: external_exports.string()
|
|
17533
|
+
});
|
|
17534
|
+
var DashboardHandoffOptions = external_exports.tuple([
|
|
17535
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17536
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17537
|
+
]);
|
|
17538
|
+
var ComposedRemediationOptions = external_exports.tuple([
|
|
17539
|
+
SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
|
|
17540
|
+
SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
|
|
17541
|
+
SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
|
|
17542
|
+
]);
|
|
17543
|
+
var SetupHandoffOffer = external_exports.object({
|
|
17544
|
+
worthALook: external_exports.number().int().nonnegative(),
|
|
17545
|
+
liveKeys: external_exports.number().int().nonnegative().optional(),
|
|
17546
|
+
options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
|
|
17547
|
+
}).refine(
|
|
17548
|
+
(o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
|
|
17549
|
+
{
|
|
17550
|
+
message: "the chain-entry option is present exactly when liveKeys > 0",
|
|
17551
|
+
path: ["options"]
|
|
17552
|
+
}
|
|
17553
|
+
).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
|
|
17554
|
+
message: "liveKeys is a subset of worthALook and cannot exceed it",
|
|
17555
|
+
path: ["liveKeys"]
|
|
17556
|
+
});
|
|
17557
|
+
|
|
17297
17558
|
// ../../packages/schema/src/zod/shares.ts
|
|
17298
17559
|
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17299
17560
|
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
@@ -17479,6 +17740,102 @@ function reviewSeverityRank(reasons) {
|
|
|
17479
17740
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
17480
17741
|
}
|
|
17481
17742
|
|
|
17743
|
+
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17744
|
+
function escapeLikePattern(s) {
|
|
17745
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
17746
|
+
}
|
|
17747
|
+
function placeholders(n) {
|
|
17748
|
+
return Array.from({ length: n }, () => "?").join(", ");
|
|
17749
|
+
}
|
|
17750
|
+
function containsPattern(q) {
|
|
17751
|
+
return `%${escapeLikePattern(q)}%`;
|
|
17752
|
+
}
|
|
17753
|
+
function likeAny(exprs) {
|
|
17754
|
+
return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
|
|
17755
|
+
}
|
|
17756
|
+
|
|
17757
|
+
// ../../packages/persistence/src/internal/transactions.ts
|
|
17758
|
+
var savepointSeq = 0;
|
|
17759
|
+
function withTransaction(db, fn, mode = "DEFERRED") {
|
|
17760
|
+
if (db.isTransaction) {
|
|
17761
|
+
const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
|
|
17762
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
17763
|
+
try {
|
|
17764
|
+
fn();
|
|
17765
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17766
|
+
} catch (error51) {
|
|
17767
|
+
try {
|
|
17768
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
17769
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
17770
|
+
} catch {
|
|
17771
|
+
}
|
|
17772
|
+
throw error51;
|
|
17773
|
+
}
|
|
17774
|
+
return;
|
|
17775
|
+
}
|
|
17776
|
+
db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
17777
|
+
try {
|
|
17778
|
+
fn();
|
|
17779
|
+
db.exec("COMMIT");
|
|
17780
|
+
} catch (error51) {
|
|
17781
|
+
try {
|
|
17782
|
+
db.exec("ROLLBACK");
|
|
17783
|
+
} catch {
|
|
17784
|
+
}
|
|
17785
|
+
throw error51;
|
|
17786
|
+
}
|
|
17787
|
+
}
|
|
17788
|
+
function failOpenTransaction(db, fn, mode = "DEFERRED") {
|
|
17789
|
+
const nested = db.isTransaction;
|
|
17790
|
+
try {
|
|
17791
|
+
withTransaction(db, fn, mode);
|
|
17792
|
+
return true;
|
|
17793
|
+
} catch (error51) {
|
|
17794
|
+
if (!db.isTransaction && nested) throw error51;
|
|
17795
|
+
return false;
|
|
17796
|
+
}
|
|
17797
|
+
}
|
|
17798
|
+
|
|
17799
|
+
// ../../packages/persistence/src/internal/warn.ts
|
|
17800
|
+
function akaWarn(message) {
|
|
17801
|
+
process.stderr.write(`[aka] ${message}
|
|
17802
|
+
`);
|
|
17803
|
+
}
|
|
17804
|
+
|
|
17805
|
+
// ../../packages/persistence/src/db/migrations/introspection.ts
|
|
17806
|
+
function evidenceObjects(sql) {
|
|
17807
|
+
const objects = [];
|
|
17808
|
+
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17809
|
+
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17810
|
+
objects.push({ kind: "table", name: m[1] });
|
|
17811
|
+
}
|
|
17812
|
+
}
|
|
17813
|
+
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17814
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17815
|
+
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17816
|
+
}
|
|
17817
|
+
}
|
|
17818
|
+
return objects;
|
|
17819
|
+
}
|
|
17820
|
+
function schemaObjectExists(db, kind, name) {
|
|
17821
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
|
|
17822
|
+
return row !== void 0;
|
|
17823
|
+
}
|
|
17824
|
+
function indexExists(db, name) {
|
|
17825
|
+
return schemaObjectExists(db, "index", name);
|
|
17826
|
+
}
|
|
17827
|
+
function columnNames(db, table, opts) {
|
|
17828
|
+
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
17829
|
+
const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
|
|
17830
|
+
return columns.map((c) => c.name);
|
|
17831
|
+
}
|
|
17832
|
+
function evidenceExists(db, object2) {
|
|
17833
|
+
if (object2.kind === "column") {
|
|
17834
|
+
return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
|
|
17835
|
+
}
|
|
17836
|
+
return schemaObjectExists(db, "table", object2.name);
|
|
17837
|
+
}
|
|
17838
|
+
|
|
17482
17839
|
// ../../packages/persistence/src/ids.ts
|
|
17483
17840
|
import { createHash } from "crypto";
|
|
17484
17841
|
function sha256Hex(input) {
|
|
@@ -17515,28 +17872,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
|
|
|
17515
17872
|
}
|
|
17516
17873
|
|
|
17517
17874
|
// ../../packages/persistence/src/migrations.ts
|
|
17518
|
-
function evidenceObjects(sql) {
|
|
17519
|
-
const objects = [];
|
|
17520
|
-
for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
|
|
17521
|
-
if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
|
|
17522
|
-
objects.push({ kind: "table", name: m[1] });
|
|
17523
|
-
}
|
|
17524
|
-
}
|
|
17525
|
-
for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
|
|
17526
|
-
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
17527
|
-
objects.push({ kind: "column", table: m[1], name: m[2] });
|
|
17528
|
-
}
|
|
17529
|
-
}
|
|
17530
|
-
return objects;
|
|
17531
|
-
}
|
|
17532
|
-
function evidenceExists(db, object2) {
|
|
17533
|
-
if (object2.kind === "column") {
|
|
17534
|
-
const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
|
|
17535
|
-
return columns.some((c) => c.name === object2.name);
|
|
17536
|
-
}
|
|
17537
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
|
|
17538
|
-
return row !== void 0;
|
|
17539
|
-
}
|
|
17540
17875
|
function describeObject(object2) {
|
|
17541
17876
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
17542
17877
|
}
|
|
@@ -17547,10 +17882,6 @@ function createdIndexName(statement) {
|
|
|
17547
17882
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
17548
17883
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
17549
17884
|
}
|
|
17550
|
-
function indexExists(db, name) {
|
|
17551
|
-
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
|
|
17552
|
-
return row !== void 0;
|
|
17553
|
-
}
|
|
17554
17885
|
function applyMigrations(db) {
|
|
17555
17886
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
17556
17887
|
db.exec(
|
|
@@ -17569,44 +17900,39 @@ function applyMigrations(db) {
|
|
|
17569
17900
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
17570
17901
|
if (present.length > 0 && present.length < evidence.length) {
|
|
17571
17902
|
const missing = evidence.filter((o) => !present.includes(o));
|
|
17572
|
-
const message = `
|
|
17573
|
-
|
|
17574
|
-
`);
|
|
17575
|
-
throw new Error(message);
|
|
17903
|
+
const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
|
|
17904
|
+
akaWarn(message);
|
|
17905
|
+
throw new Error(`[aka] ${message}`);
|
|
17576
17906
|
}
|
|
17577
17907
|
const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
|
|
17578
17908
|
const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
|
|
17579
17909
|
const statements = splitStatements(migration.sql);
|
|
17580
17910
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
|
|
17581
17911
|
try {
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
const
|
|
17586
|
-
|
|
17587
|
-
if (
|
|
17588
|
-
|
|
17589
|
-
|
|
17912
|
+
withTransaction(
|
|
17913
|
+
db,
|
|
17914
|
+
() => {
|
|
17915
|
+
for (const statement of statements) {
|
|
17916
|
+
const indexName = createdIndexName(statement);
|
|
17917
|
+
if (indexName === void 0) {
|
|
17918
|
+
if (alreadyApplied) continue;
|
|
17919
|
+
} else if (indexExists(db, indexName)) {
|
|
17920
|
+
continue;
|
|
17921
|
+
}
|
|
17922
|
+
db.exec(statement);
|
|
17590
17923
|
}
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
);
|
|
17924
|
+
if (wantsFkOff && !alreadyApplied) {
|
|
17925
|
+
const violations = db.prepare("PRAGMA foreign_key_check").all();
|
|
17926
|
+
if (violations.length > 0) {
|
|
17927
|
+
throw new Error(
|
|
17928
|
+
`[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
|
|
17929
|
+
);
|
|
17930
|
+
}
|
|
17599
17931
|
}
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
try {
|
|
17605
|
-
db.exec("ROLLBACK");
|
|
17606
|
-
} catch {
|
|
17607
|
-
}
|
|
17608
|
-
throw error51;
|
|
17609
|
-
}
|
|
17932
|
+
record2.run(migration.tag, Date.now());
|
|
17933
|
+
},
|
|
17934
|
+
"IMMEDIATE"
|
|
17935
|
+
);
|
|
17610
17936
|
} finally {
|
|
17611
17937
|
if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
|
|
17612
17938
|
}
|
|
@@ -17649,8 +17975,7 @@ var TOKEN_USAGE_COLUMNS = [
|
|
|
17649
17975
|
}
|
|
17650
17976
|
];
|
|
17651
17977
|
function ensureTokenUsageColumns(db) {
|
|
17652
|
-
const
|
|
17653
|
-
const existing = new Set(columns.map((c) => c.name));
|
|
17978
|
+
const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
|
|
17654
17979
|
for (const column of TOKEN_USAGE_COLUMNS) {
|
|
17655
17980
|
if (!existing.has(column.name)) {
|
|
17656
17981
|
db.exec(column.ddl);
|
|
@@ -17688,47 +18013,39 @@ function reconcileSourceProjectIds(db) {
|
|
|
17688
18013
|
repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
|
|
17689
18014
|
}));
|
|
17690
18015
|
const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
dropCollisions
|
|
17705
|
-
|
|
18016
|
+
withTransaction(
|
|
18017
|
+
db,
|
|
18018
|
+
() => {
|
|
18019
|
+
for (const { row, canonicalId } of legacy) {
|
|
18020
|
+
foldProject.run(
|
|
18021
|
+
canonicalId,
|
|
18022
|
+
row.url,
|
|
18023
|
+
row.name,
|
|
18024
|
+
row.attributes,
|
|
18025
|
+
row.firstSeen,
|
|
18026
|
+
row.lastSeen
|
|
18027
|
+
);
|
|
18028
|
+
repointAudit.run(canonicalId, row.id);
|
|
18029
|
+
for (const { dropCollisions, repoint } of pathTables) {
|
|
18030
|
+
dropCollisions.run(row.id, canonicalId);
|
|
18031
|
+
repoint.run(canonicalId, row.id);
|
|
18032
|
+
}
|
|
18033
|
+
repointCallSite.run(canonicalId, row.id);
|
|
18034
|
+
deleteLegacy.run(row.id);
|
|
17706
18035
|
}
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
db.exec("COMMIT");
|
|
17711
|
-
} catch (error51) {
|
|
17712
|
-
try {
|
|
17713
|
-
db.exec("ROLLBACK");
|
|
17714
|
-
} catch {
|
|
17715
|
-
}
|
|
17716
|
-
throw error51;
|
|
17717
|
-
}
|
|
18036
|
+
},
|
|
18037
|
+
"IMMEDIATE"
|
|
18038
|
+
);
|
|
17718
18039
|
} catch (error51) {
|
|
17719
|
-
|
|
17720
|
-
`);
|
|
18040
|
+
akaWarn(`source_project id reconcile failed: ${String(error51)}`);
|
|
17721
18041
|
}
|
|
17722
18042
|
}
|
|
17723
18043
|
function isForeignSqliteLineage(db) {
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
|
|
17727
|
-
return eventsColumns.some((c) => c.name === "tenant_id");
|
|
18044
|
+
if (schemaObjectExists(db, "table", "tenants")) return true;
|
|
18045
|
+
return columnNames(db, "events").includes("tenant_id");
|
|
17728
18046
|
}
|
|
17729
18047
|
function ensureSyncedAtColumn(db, table) {
|
|
17730
|
-
|
|
17731
|
-
if (!columns.some((c) => c.name === "synced_at")) {
|
|
18048
|
+
if (!columnNames(db, table).includes("synced_at")) {
|
|
17732
18049
|
db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
|
|
17733
18050
|
}
|
|
17734
18051
|
}
|
|
@@ -17779,8 +18096,11 @@ function ensureDataDirSync(dir) {
|
|
|
17779
18096
|
} catch {
|
|
17780
18097
|
}
|
|
17781
18098
|
}
|
|
18099
|
+
function walSidecars(file2) {
|
|
18100
|
+
return [`${file2}-wal`, `${file2}-shm`];
|
|
18101
|
+
}
|
|
17782
18102
|
function tightenPerms(file2) {
|
|
17783
|
-
for (const path of [file2,
|
|
18103
|
+
for (const path of [file2, ...walSidecars(file2)]) {
|
|
17784
18104
|
try {
|
|
17785
18105
|
chmodSync(path, DATA_FILE_MODE);
|
|
17786
18106
|
} catch {
|
|
@@ -17788,12 +18108,68 @@ function tightenPerms(file2) {
|
|
|
17788
18108
|
}
|
|
17789
18109
|
}
|
|
17790
18110
|
|
|
17791
|
-
// ../../packages/persistence/src/
|
|
17792
|
-
function
|
|
17793
|
-
|
|
18111
|
+
// ../../packages/persistence/src/internal/json.ts
|
|
18112
|
+
function safeJson(s, fallback) {
|
|
18113
|
+
if (s == null) return fallback;
|
|
18114
|
+
try {
|
|
18115
|
+
return JSON.parse(s);
|
|
18116
|
+
} catch {
|
|
18117
|
+
return fallback;
|
|
18118
|
+
}
|
|
17794
18119
|
}
|
|
17795
|
-
function
|
|
17796
|
-
|
|
18120
|
+
function parseJsonObject(s) {
|
|
18121
|
+
if (s == null) return void 0;
|
|
18122
|
+
try {
|
|
18123
|
+
const parsed = JSON.parse(s);
|
|
18124
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18125
|
+
} catch {
|
|
18126
|
+
}
|
|
18127
|
+
return void 0;
|
|
18128
|
+
}
|
|
18129
|
+
|
|
18130
|
+
// ../../packages/persistence/src/internal/rows.ts
|
|
18131
|
+
function allRows(stmt, params) {
|
|
18132
|
+
if (params === void 0) return stmt.all();
|
|
18133
|
+
if (Array.isArray(params)) return stmt.all(...params);
|
|
18134
|
+
return stmt.all(params);
|
|
18135
|
+
}
|
|
18136
|
+
function getRow(stmt, params) {
|
|
18137
|
+
if (params === void 0) return stmt.get();
|
|
18138
|
+
if (Array.isArray(params)) return stmt.get(...params);
|
|
18139
|
+
return stmt.get(params);
|
|
18140
|
+
}
|
|
18141
|
+
function intToBool(raw) {
|
|
18142
|
+
return raw === 1 || raw === true;
|
|
18143
|
+
}
|
|
18144
|
+
function boolToInt(b) {
|
|
18145
|
+
return b ? 1 : 0;
|
|
18146
|
+
}
|
|
18147
|
+
function bindParams(row) {
|
|
18148
|
+
const out = {};
|
|
18149
|
+
for (const [key, value] of Object.entries(row)) {
|
|
18150
|
+
out[key] = value === void 0 ? null : value;
|
|
18151
|
+
}
|
|
18152
|
+
return out;
|
|
18153
|
+
}
|
|
18154
|
+
function countScalar(db, sql, params) {
|
|
18155
|
+
return getRow(db.prepare(sql), params)?.n ?? 0;
|
|
18156
|
+
}
|
|
18157
|
+
function countBy(db, sql, params) {
|
|
18158
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
18159
|
+
for (const row of allRows(db.prepare(sql), params)) {
|
|
18160
|
+
map2.set(row.k, row.n);
|
|
18161
|
+
}
|
|
18162
|
+
return map2;
|
|
18163
|
+
}
|
|
18164
|
+
function mapRowsTolerant(rows, map2) {
|
|
18165
|
+
const out = [];
|
|
18166
|
+
for (const row of rows) {
|
|
18167
|
+
try {
|
|
18168
|
+
out.push(map2(row));
|
|
18169
|
+
} catch {
|
|
18170
|
+
}
|
|
18171
|
+
}
|
|
18172
|
+
return out;
|
|
17797
18173
|
}
|
|
17798
18174
|
|
|
17799
18175
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
@@ -17845,15 +18221,11 @@ function encodeCursor(payload) {
|
|
|
17845
18221
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
17846
18222
|
}
|
|
17847
18223
|
function decodeCursor(cursor) {
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
return parsed;
|
|
17852
|
-
}
|
|
17853
|
-
return null;
|
|
17854
|
-
} catch {
|
|
17855
|
-
return null;
|
|
18224
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18225
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18226
|
+
return parsed;
|
|
17856
18227
|
}
|
|
18228
|
+
return null;
|
|
17857
18229
|
}
|
|
17858
18230
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
17859
18231
|
session: "session",
|
|
@@ -17870,15 +18242,8 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
17870
18242
|
};
|
|
17871
18243
|
function safeParseStringArray(raw) {
|
|
17872
18244
|
if (!raw) return [];
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
17876
|
-
} catch {
|
|
17877
|
-
return [];
|
|
17878
|
-
}
|
|
17879
|
-
}
|
|
17880
|
-
function toBool(raw) {
|
|
17881
|
-
return raw === 1 || raw === true;
|
|
18245
|
+
const parsed = safeJson(raw, null);
|
|
18246
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
17882
18247
|
}
|
|
17883
18248
|
function toHarness(raw) {
|
|
17884
18249
|
const parsed = Harness.safeParse(raw);
|
|
@@ -17931,8 +18296,8 @@ function buildAuditEvent(row) {
|
|
|
17931
18296
|
severity: severityParsed?.success ? severityParsed.data : null,
|
|
17932
18297
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
17933
18298
|
targetId: row.target_id,
|
|
17934
|
-
internal:
|
|
17935
|
-
flagged:
|
|
18299
|
+
internal: intToBool(row.internal),
|
|
18300
|
+
flagged: intToBool(row.flagged)
|
|
17936
18301
|
};
|
|
17937
18302
|
}
|
|
17938
18303
|
var TIMELINE_COLUMNS = `
|
|
@@ -17948,6 +18313,10 @@ var TIMELINE_COLUMNS = `
|
|
|
17948
18313
|
json_extract(attributes, '$.internal') AS internal,
|
|
17949
18314
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
17950
18315
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
18316
|
+
var HAS_ACTIVITY = `EXISTS (
|
|
18317
|
+
SELECT 1 FROM audit_events c
|
|
18318
|
+
WHERE c.root_session_id = audit_events.id
|
|
18319
|
+
AND c.event_type NOT IN ('hook', 'config_scan'))`;
|
|
17951
18320
|
var SqliteActivityRepository = class {
|
|
17952
18321
|
constructor(db, now = () => Date.now()) {
|
|
17953
18322
|
this.db = db;
|
|
@@ -17958,12 +18327,16 @@ var SqliteActivityRepository = class {
|
|
|
17958
18327
|
stats(tz) {
|
|
17959
18328
|
const window = todayWindow(tz ?? defaultTimeZone(), this.now());
|
|
17960
18329
|
const { startMs, endMs } = window;
|
|
17961
|
-
const sessionsToday =
|
|
18330
|
+
const sessionsToday = countScalar(
|
|
18331
|
+
this.db,
|
|
17962
18332
|
`SELECT count(*) AS n FROM audit_events
|
|
17963
|
-
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at <
|
|
17964
|
-
|
|
18333
|
+
WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
|
|
18334
|
+
AND ${HAS_ACTIVITY}`,
|
|
18335
|
+
[startMs, endMs]
|
|
18336
|
+
);
|
|
17965
18337
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
17966
|
-
const liveNow =
|
|
18338
|
+
const liveNow = countScalar(
|
|
18339
|
+
this.db,
|
|
17967
18340
|
`SELECT count(*) AS n FROM audit_events s
|
|
17968
18341
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
17969
18342
|
AND max(
|
|
@@ -17972,22 +18345,29 @@ var SqliteActivityRepository = class {
|
|
|
17972
18345
|
(SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
|
|
17973
18346
|
s.started_at
|
|
17974
18347
|
)
|
|
17975
|
-
) >=
|
|
17976
|
-
|
|
17977
|
-
|
|
18348
|
+
) >= ?`,
|
|
18349
|
+
[liveThreshold]
|
|
18350
|
+
);
|
|
18351
|
+
const toolCallsToday = countScalar(
|
|
18352
|
+
this.db,
|
|
17978
18353
|
`SELECT count(*) AS n FROM audit_events
|
|
17979
|
-
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at <
|
|
17980
|
-
|
|
17981
|
-
|
|
18354
|
+
WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
|
|
18355
|
+
[startMs, endMs]
|
|
18356
|
+
);
|
|
18357
|
+
const findingsToday = countScalar(
|
|
18358
|
+
this.db,
|
|
17982
18359
|
`SELECT count(*) AS n FROM inspection_findings f
|
|
17983
18360
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
17984
|
-
WHERE e.started_at >= ? AND e.started_at <
|
|
17985
|
-
|
|
17986
|
-
|
|
18361
|
+
WHERE e.started_at >= ? AND e.started_at < ?`,
|
|
18362
|
+
[startMs, endMs]
|
|
18363
|
+
);
|
|
18364
|
+
const egressToday = countScalar(
|
|
18365
|
+
this.db,
|
|
17987
18366
|
`SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
17988
18367
|
FROM audit_events
|
|
17989
|
-
WHERE event_type = 'share' AND started_at >= ? AND started_at <
|
|
17990
|
-
|
|
18368
|
+
WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
|
|
18369
|
+
[startMs, endMs]
|
|
18370
|
+
);
|
|
17991
18371
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
17992
18372
|
}
|
|
17993
18373
|
listSessions(query) {
|
|
@@ -18009,7 +18389,7 @@ var SqliteActivityRepository = class {
|
|
|
18009
18389
|
conditions.push("started_at <= ?");
|
|
18010
18390
|
params.push(toMs);
|
|
18011
18391
|
if (query.q) {
|
|
18012
|
-
const pattern =
|
|
18392
|
+
const pattern = containsPattern(query.q);
|
|
18013
18393
|
conditions.push(
|
|
18014
18394
|
`(content LIKE ? ESCAPE '\\'
|
|
18015
18395
|
OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
|
|
@@ -18023,13 +18403,21 @@ var SqliteActivityRepository = class {
|
|
|
18023
18403
|
);
|
|
18024
18404
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
18025
18405
|
}
|
|
18406
|
+
const emptyCount = countScalar(
|
|
18407
|
+
this.db,
|
|
18408
|
+
`SELECT count(*) AS n FROM audit_events
|
|
18409
|
+
WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
|
|
18410
|
+
params
|
|
18411
|
+
);
|
|
18412
|
+
if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
|
|
18026
18413
|
if (cursor) {
|
|
18027
18414
|
conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
|
|
18028
18415
|
params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
|
|
18029
18416
|
}
|
|
18030
18417
|
const limit = query.limit;
|
|
18031
|
-
const rows =
|
|
18032
|
-
|
|
18418
|
+
const rows = allRows(
|
|
18419
|
+
this.db.prepare(
|
|
18420
|
+
`SELECT id,
|
|
18033
18421
|
json_extract(attributes, '$.harness') AS harness,
|
|
18034
18422
|
content AS title,
|
|
18035
18423
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18042,7 +18430,9 @@ var SqliteActivityRepository = class {
|
|
|
18042
18430
|
WHERE ${conditions.join(" AND ")}
|
|
18043
18431
|
ORDER BY started_at DESC, id DESC
|
|
18044
18432
|
LIMIT ?`
|
|
18045
|
-
|
|
18433
|
+
),
|
|
18434
|
+
[...params, limit + 1]
|
|
18435
|
+
);
|
|
18046
18436
|
const hasMore = rows.length > limit;
|
|
18047
18437
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
18048
18438
|
const rollups = this.rollupsFor(page.map((r) => r.id));
|
|
@@ -18056,11 +18446,12 @@ var SqliteActivityRepository = class {
|
|
|
18056
18446
|
);
|
|
18057
18447
|
const last = page[page.length - 1];
|
|
18058
18448
|
const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
|
|
18059
|
-
return Promise.resolve({ items, nextCursor });
|
|
18449
|
+
return Promise.resolve({ items, nextCursor, emptyCount });
|
|
18060
18450
|
}
|
|
18061
18451
|
getSession(sessionId) {
|
|
18062
|
-
const rootRow =
|
|
18063
|
-
|
|
18452
|
+
const rootRow = getRow(
|
|
18453
|
+
this.db.prepare(
|
|
18454
|
+
`SELECT id,
|
|
18064
18455
|
json_extract(attributes, '$.harness') AS harness,
|
|
18065
18456
|
content AS title,
|
|
18066
18457
|
json_extract(attributes, '$.project') AS project,
|
|
@@ -18077,47 +18468,66 @@ var SqliteActivityRepository = class {
|
|
|
18077
18468
|
FROM audit_events
|
|
18078
18469
|
WHERE id = ? AND event_type = 'session'
|
|
18079
18470
|
LIMIT 1`
|
|
18080
|
-
|
|
18471
|
+
),
|
|
18472
|
+
[sessionId]
|
|
18473
|
+
);
|
|
18081
18474
|
if (!rootRow) return Promise.resolve(null);
|
|
18082
|
-
const timelineRows =
|
|
18083
|
-
|
|
18475
|
+
const timelineRows = allRows(
|
|
18476
|
+
this.db.prepare(
|
|
18477
|
+
`SELECT ${TIMELINE_COLUMNS}
|
|
18084
18478
|
FROM audit_events
|
|
18085
18479
|
WHERE id = ? OR root_session_id = ?
|
|
18086
18480
|
ORDER BY started_at ASC, id ASC`
|
|
18087
|
-
|
|
18481
|
+
),
|
|
18482
|
+
[sessionId, sessionId]
|
|
18483
|
+
);
|
|
18088
18484
|
const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
|
|
18089
|
-
const tokenRow =
|
|
18090
|
-
|
|
18485
|
+
const tokenRow = getRow(
|
|
18486
|
+
this.db.prepare(
|
|
18487
|
+
`SELECT
|
|
18091
18488
|
coalesce(sum(input_tokens), 0) AS input,
|
|
18092
18489
|
coalesce(sum(output_tokens), 0) AS output,
|
|
18093
18490
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
18094
18491
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
18095
18492
|
FROM audit_events
|
|
18096
18493
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18494
|
+
),
|
|
18495
|
+
[sessionId]
|
|
18496
|
+
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
18497
|
+
const primaryModel = getRow(
|
|
18498
|
+
this.db.prepare(
|
|
18499
|
+
`SELECT model, provider FROM audit_events
|
|
18500
|
+
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
18101
18501
|
ORDER BY started_at ASC, id ASC
|
|
18102
18502
|
LIMIT 1`
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18503
|
+
),
|
|
18504
|
+
[sessionId]
|
|
18505
|
+
);
|
|
18506
|
+
const toolRows = allRows(
|
|
18507
|
+
this.db.prepare(
|
|
18508
|
+
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
18106
18509
|
count(*) AS n
|
|
18107
18510
|
FROM audit_events
|
|
18108
18511
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
18109
18512
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18513
|
+
),
|
|
18514
|
+
[sessionId]
|
|
18515
|
+
);
|
|
18516
|
+
const modelRows = allRows(
|
|
18517
|
+
this.db.prepare(
|
|
18518
|
+
`SELECT DISTINCT model FROM audit_events
|
|
18113
18519
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
18114
18520
|
ORDER BY model`
|
|
18115
|
-
|
|
18521
|
+
),
|
|
18522
|
+
[sessionId]
|
|
18523
|
+
);
|
|
18116
18524
|
const derivedModels = modelRows.map((r) => r.model);
|
|
18117
|
-
const commits =
|
|
18525
|
+
const commits = countScalar(
|
|
18526
|
+
this.db,
|
|
18118
18527
|
`SELECT count(*) AS n FROM audit_events
|
|
18119
|
-
WHERE root_session_id = ? AND event_type = 'commit'
|
|
18120
|
-
|
|
18528
|
+
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
18529
|
+
[sessionId]
|
|
18530
|
+
);
|
|
18121
18531
|
const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
|
|
18122
18532
|
turns: 0,
|
|
18123
18533
|
findings: 0,
|
|
@@ -18184,7 +18594,10 @@ var SqliteActivityRepository = class {
|
|
|
18184
18594
|
`SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
|
|
18185
18595
|
FROM audit_events WHERE ${SESSION_ROOT}${where}`
|
|
18186
18596
|
);
|
|
18187
|
-
const rows =
|
|
18597
|
+
const rows = allRows(
|
|
18598
|
+
stmt,
|
|
18599
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
18600
|
+
);
|
|
18188
18601
|
const seen = /* @__PURE__ */ new Set();
|
|
18189
18602
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
18190
18603
|
return Promise.resolve([...seen]);
|
|
@@ -18207,23 +18620,23 @@ var SqliteActivityRepository = class {
|
|
|
18207
18620
|
conditions.push("started_at >= ?");
|
|
18208
18621
|
params.push(opts.fromMs);
|
|
18209
18622
|
}
|
|
18210
|
-
const rows =
|
|
18211
|
-
|
|
18623
|
+
const rows = allRows(
|
|
18624
|
+
this.db.prepare(
|
|
18625
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18212
18626
|
FROM audit_events
|
|
18213
18627
|
WHERE ${conditions.join(" AND ")}`
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
}
|
|
18225
|
-
|
|
18226
|
-
return leaves;
|
|
18628
|
+
),
|
|
18629
|
+
params
|
|
18630
|
+
);
|
|
18631
|
+
return mapRowsTolerant(
|
|
18632
|
+
rows.filter(
|
|
18633
|
+
(row) => row.sessionId !== null
|
|
18634
|
+
),
|
|
18635
|
+
(row) => ({
|
|
18636
|
+
sessionId: row.sessionId,
|
|
18637
|
+
attributes: JSON.parse(row.attributes)
|
|
18638
|
+
})
|
|
18639
|
+
);
|
|
18227
18640
|
}
|
|
18228
18641
|
/**
|
|
18229
18642
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
@@ -18237,57 +18650,72 @@ var SqliteActivityRepository = class {
|
|
|
18237
18650
|
);
|
|
18238
18651
|
if (sessionIds.length === 0) return result;
|
|
18239
18652
|
const inClause = placeholders(sessionIds.length);
|
|
18240
|
-
const lastActivityRows =
|
|
18241
|
-
|
|
18653
|
+
const lastActivityRows = allRows(
|
|
18654
|
+
this.db.prepare(
|
|
18655
|
+
`SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
|
|
18242
18656
|
WHERE root_session_id IN (${inClause})
|
|
18243
18657
|
GROUP BY root_session_id`
|
|
18244
|
-
|
|
18658
|
+
),
|
|
18659
|
+
sessionIds
|
|
18660
|
+
);
|
|
18245
18661
|
for (const row of lastActivityRows) {
|
|
18246
18662
|
if (row.id === null) continue;
|
|
18247
18663
|
const entry = result.get(row.id);
|
|
18248
18664
|
if (entry && row.m !== null) entry.lastActivityMs = row.m;
|
|
18249
18665
|
}
|
|
18250
|
-
const turnsRows =
|
|
18251
|
-
|
|
18666
|
+
const turnsRows = allRows(
|
|
18667
|
+
this.db.prepare(
|
|
18668
|
+
`SELECT root_session_id AS id, count(*) AS n FROM audit_events
|
|
18252
18669
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
18253
18670
|
GROUP BY root_session_id`
|
|
18254
|
-
|
|
18671
|
+
),
|
|
18672
|
+
sessionIds
|
|
18673
|
+
);
|
|
18255
18674
|
for (const row of turnsRows) {
|
|
18256
18675
|
if (row.id === null) continue;
|
|
18257
18676
|
const entry = result.get(row.id);
|
|
18258
18677
|
if (entry) entry.turns = row.n;
|
|
18259
18678
|
}
|
|
18260
|
-
const runKeyRows =
|
|
18261
|
-
|
|
18679
|
+
const runKeyRows = allRows(
|
|
18680
|
+
this.db.prepare(
|
|
18681
|
+
`SELECT root_session_id AS id,
|
|
18262
18682
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
18263
18683
|
FROM audit_events
|
|
18264
18684
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
18265
18685
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
18266
18686
|
GROUP BY root_session_id`
|
|
18267
|
-
|
|
18687
|
+
),
|
|
18688
|
+
sessionIds
|
|
18689
|
+
);
|
|
18268
18690
|
for (const row of runKeyRows) {
|
|
18269
18691
|
if (row.id === null) continue;
|
|
18270
18692
|
const entry = result.get(row.id);
|
|
18271
18693
|
if (entry) entry.turns = Math.max(entry.turns, row.n);
|
|
18272
18694
|
}
|
|
18273
|
-
const findingsRows =
|
|
18274
|
-
|
|
18695
|
+
const findingsRows = allRows(
|
|
18696
|
+
this.db.prepare(
|
|
18697
|
+
`SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
|
|
18275
18698
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
18276
18699
|
WHERE e.root_session_id IN (${inClause})
|
|
18277
18700
|
GROUP BY e.root_session_id`
|
|
18278
|
-
|
|
18701
|
+
),
|
|
18702
|
+
sessionIds
|
|
18703
|
+
);
|
|
18279
18704
|
for (const row of findingsRows) {
|
|
18280
18705
|
if (row.id === null) continue;
|
|
18281
18706
|
const entry = result.get(row.id);
|
|
18282
18707
|
if (entry) entry.findings = row.n;
|
|
18283
18708
|
}
|
|
18284
|
-
const sharesRows =
|
|
18285
|
-
|
|
18709
|
+
const sharesRows = allRows(
|
|
18710
|
+
this.db.prepare(
|
|
18711
|
+
`SELECT root_session_id AS id,
|
|
18286
18712
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
18287
18713
|
FROM audit_events
|
|
18288
18714
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
18289
18715
|
GROUP BY root_session_id`
|
|
18290
|
-
|
|
18716
|
+
),
|
|
18717
|
+
sessionIds
|
|
18718
|
+
);
|
|
18291
18719
|
for (const row of sharesRows) {
|
|
18292
18720
|
if (row.id === null) continue;
|
|
18293
18721
|
const entry = result.get(row.id);
|
|
@@ -18337,33 +18765,28 @@ var SqliteAuditEventsRepository = class {
|
|
|
18337
18765
|
// the caller fails open and drops the whole pass — recovered idempotently on the
|
|
18338
18766
|
// next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
|
|
18339
18767
|
runInTransaction(fn) {
|
|
18340
|
-
this.db
|
|
18341
|
-
try {
|
|
18342
|
-
fn();
|
|
18343
|
-
this.db.exec("COMMIT");
|
|
18344
|
-
} catch (err) {
|
|
18345
|
-
this.db.exec("ROLLBACK");
|
|
18346
|
-
throw err;
|
|
18347
|
-
}
|
|
18768
|
+
withTransaction(this.db, fn);
|
|
18348
18769
|
}
|
|
18349
18770
|
insertAuditEvent(input) {
|
|
18350
18771
|
const row = toAuditEventRow(input);
|
|
18351
|
-
this.insertStmt.run(
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18772
|
+
this.insertStmt.run(
|
|
18773
|
+
bindParams({
|
|
18774
|
+
id: row.id,
|
|
18775
|
+
parentId: row.parentId,
|
|
18776
|
+
rootSessionId: row.rootSessionId,
|
|
18777
|
+
eventType: row.eventType,
|
|
18778
|
+
hostId: row.hostId,
|
|
18779
|
+
harnessId: row.harnessId,
|
|
18780
|
+
sourceProjectId: row.sourceProjectId,
|
|
18781
|
+
startedAt: row.startedAt,
|
|
18782
|
+
endedAt: row.endedAt,
|
|
18783
|
+
severity: row.severity,
|
|
18784
|
+
priority: row.priority,
|
|
18785
|
+
content: row.content,
|
|
18786
|
+
contentHash: row.contentHash,
|
|
18787
|
+
attributes: row.attributes
|
|
18788
|
+
})
|
|
18789
|
+
);
|
|
18367
18790
|
}
|
|
18368
18791
|
// Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
|
|
18369
18792
|
// (which takes a caller-supplied random id), the id here is MINTED internally
|
|
@@ -18377,22 +18800,24 @@ var SqliteAuditEventsRepository = class {
|
|
|
18377
18800
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18378
18801
|
if (!Number.isFinite(startedAt)) return;
|
|
18379
18802
|
const id = llmCallId(input.sessionId, input.messageId);
|
|
18380
|
-
this.upsertLlmCallStmt.run(
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
|
|
18803
|
+
this.upsertLlmCallStmt.run(
|
|
18804
|
+
bindParams({
|
|
18805
|
+
id,
|
|
18806
|
+
parentId: input.parentId,
|
|
18807
|
+
rootSessionId: input.rootSessionId,
|
|
18808
|
+
eventType: "llm_call",
|
|
18809
|
+
hostId: null,
|
|
18810
|
+
harnessId: null,
|
|
18811
|
+
sourceProjectId: null,
|
|
18812
|
+
startedAt,
|
|
18813
|
+
endedAt: null,
|
|
18814
|
+
severity: null,
|
|
18815
|
+
priority: null,
|
|
18816
|
+
content: null,
|
|
18817
|
+
contentHash: null,
|
|
18818
|
+
attributes: JSON.stringify(input.attributes)
|
|
18819
|
+
})
|
|
18820
|
+
);
|
|
18396
18821
|
}
|
|
18397
18822
|
// Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
|
|
18398
18823
|
// MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
|
|
@@ -18416,25 +18841,29 @@ var SqliteAuditEventsRepository = class {
|
|
|
18416
18841
|
const startedAt = isoToEpochMillis(input.startedAt);
|
|
18417
18842
|
if (!Number.isFinite(startedAt)) return;
|
|
18418
18843
|
const id = toolCallId(input.sessionId, input.toolUseId);
|
|
18419
|
-
this.insertStmt.run(
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18844
|
+
this.insertStmt.run(
|
|
18845
|
+
bindParams({
|
|
18846
|
+
id,
|
|
18847
|
+
parentId: input.parentId,
|
|
18848
|
+
rootSessionId: input.rootSessionId,
|
|
18849
|
+
eventType: "tool_call",
|
|
18850
|
+
hostId: null,
|
|
18851
|
+
harnessId: null,
|
|
18852
|
+
sourceProjectId: null,
|
|
18853
|
+
startedAt,
|
|
18854
|
+
endedAt: null,
|
|
18855
|
+
severity: null,
|
|
18856
|
+
priority: null,
|
|
18857
|
+
content: null,
|
|
18858
|
+
contentHash: null,
|
|
18859
|
+
attributes: JSON.stringify(input.attributes)
|
|
18860
|
+
})
|
|
18861
|
+
);
|
|
18435
18862
|
}
|
|
18436
18863
|
findById(id) {
|
|
18437
|
-
return this.db.prepare("SELECT * FROM audit_events WHERE id = :id")
|
|
18864
|
+
return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
|
|
18865
|
+
id
|
|
18866
|
+
});
|
|
18438
18867
|
}
|
|
18439
18868
|
// Read the `provider` snapshotted onto a session root's attributes.
|
|
18440
18869
|
// The reconciler ensures the root, then reads provider back from it — SessionStart's
|
|
@@ -18444,14 +18873,8 @@ var SqliteAuditEventsRepository = class {
|
|
|
18444
18873
|
sessionProvider(sessionId) {
|
|
18445
18874
|
const row = this.findById(sessionId);
|
|
18446
18875
|
if (!row?.attributes) return void 0;
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
18450
|
-
const provider = parsed.provider;
|
|
18451
|
-
if (typeof provider === "string") return provider;
|
|
18452
|
-
}
|
|
18453
|
-
} catch {
|
|
18454
|
-
}
|
|
18876
|
+
const provider = parseJsonObject(row.attributes)?.provider;
|
|
18877
|
+
if (typeof provider === "string") return provider;
|
|
18455
18878
|
return void 0;
|
|
18456
18879
|
}
|
|
18457
18880
|
// Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
|
|
@@ -18460,11 +18883,13 @@ var SqliteAuditEventsRepository = class {
|
|
|
18460
18883
|
// is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
|
|
18461
18884
|
// rows whose attributes blob is NULL are skipped (nothing to roll up).
|
|
18462
18885
|
llmCallLeaves() {
|
|
18463
|
-
return
|
|
18464
|
-
|
|
18886
|
+
return allRows(
|
|
18887
|
+
this.db.prepare(
|
|
18888
|
+
`SELECT root_session_id AS sessionId, attributes
|
|
18465
18889
|
FROM audit_events
|
|
18466
18890
|
WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
|
|
18467
|
-
|
|
18891
|
+
)
|
|
18892
|
+
);
|
|
18468
18893
|
}
|
|
18469
18894
|
};
|
|
18470
18895
|
|
|
@@ -18483,16 +18908,29 @@ var SqliteClassifiedDataRepository = class {
|
|
|
18483
18908
|
upsert(input) {
|
|
18484
18909
|
const id = classifiedDataId(input.class);
|
|
18485
18910
|
const row = toClassifiedDataRow(input, id);
|
|
18486
|
-
this.insertStmt.run(
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18911
|
+
this.insertStmt.run(
|
|
18912
|
+
bindParams({
|
|
18913
|
+
id: row.id,
|
|
18914
|
+
class: row.class,
|
|
18915
|
+
label: row.label,
|
|
18916
|
+
attributes: row.attributes
|
|
18917
|
+
})
|
|
18918
|
+
);
|
|
18492
18919
|
return id;
|
|
18493
18920
|
}
|
|
18494
18921
|
};
|
|
18495
18922
|
|
|
18923
|
+
// ../../packages/persistence/src/repositories/config-scan.ts
|
|
18924
|
+
function latestConfigScan(db) {
|
|
18925
|
+
return getRow(
|
|
18926
|
+
db.prepare(
|
|
18927
|
+
`SELECT id, started_at, attributes FROM audit_events
|
|
18928
|
+
WHERE event_type = 'config_scan'
|
|
18929
|
+
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18930
|
+
)
|
|
18931
|
+
);
|
|
18932
|
+
}
|
|
18933
|
+
|
|
18496
18934
|
// ../../packages/persistence/src/repositories/config-inventory.ts
|
|
18497
18935
|
var SqliteConfigInventoryRepository = class {
|
|
18498
18936
|
constructor(db) {
|
|
@@ -18500,7 +18938,7 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18500
18938
|
}
|
|
18501
18939
|
db;
|
|
18502
18940
|
report() {
|
|
18503
|
-
const scan2 = this.
|
|
18941
|
+
const scan2 = latestConfigScan(this.db);
|
|
18504
18942
|
if (!scan2) {
|
|
18505
18943
|
return {
|
|
18506
18944
|
scannedAt: null,
|
|
@@ -18511,17 +18949,23 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18511
18949
|
topics: []
|
|
18512
18950
|
};
|
|
18513
18951
|
}
|
|
18514
|
-
const rows =
|
|
18515
|
-
|
|
18952
|
+
const rows = allRows(
|
|
18953
|
+
this.db.prepare(
|
|
18954
|
+
`SELECT id, object_type AS objectType, title, location, attributes FROM inventory
|
|
18516
18955
|
WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
|
|
18517
18956
|
ORDER BY object_type, title`
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18957
|
+
),
|
|
18958
|
+
{ startedAt: scan2.started_at }
|
|
18959
|
+
);
|
|
18960
|
+
const findings = allRows(
|
|
18961
|
+
this.db.prepare(
|
|
18962
|
+
`SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
|
|
18521
18963
|
FROM inspection_findings f
|
|
18522
18964
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
18523
18965
|
WHERE f.audit_event_id = :scanId`
|
|
18524
|
-
|
|
18966
|
+
),
|
|
18967
|
+
{ scanId: scan2.id }
|
|
18968
|
+
);
|
|
18525
18969
|
const skills = [];
|
|
18526
18970
|
const hooks = [];
|
|
18527
18971
|
const mcpServers = [];
|
|
@@ -18550,7 +18994,9 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18550
18994
|
// schema note); an override whose asset is gone simply never matches. A row
|
|
18551
18995
|
// with an out-of-vocabulary trust value is ignored rather than guessed at.
|
|
18552
18996
|
trustOverrides() {
|
|
18553
|
-
const rows =
|
|
18997
|
+
const rows = allRows(
|
|
18998
|
+
this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
|
|
18999
|
+
);
|
|
18554
19000
|
const map2 = /* @__PURE__ */ new Map();
|
|
18555
19001
|
for (const row of rows) {
|
|
18556
19002
|
if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
|
|
@@ -18559,13 +19005,6 @@ var SqliteConfigInventoryRepository = class {
|
|
|
18559
19005
|
}
|
|
18560
19006
|
return map2;
|
|
18561
19007
|
}
|
|
18562
|
-
latestScan() {
|
|
18563
|
-
return this.db.prepare(
|
|
18564
|
-
`SELECT id, started_at, attributes FROM audit_events
|
|
18565
|
-
WHERE event_type = 'config_scan'
|
|
18566
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
18567
|
-
).get();
|
|
18568
|
-
}
|
|
18569
19008
|
};
|
|
18570
19009
|
function toSkillItem(row, bag) {
|
|
18571
19010
|
const item = {
|
|
@@ -18676,22 +19115,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
|
|
|
18676
19115
|
return topics;
|
|
18677
19116
|
}
|
|
18678
19117
|
function countScanErrors(attributes) {
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
const parsed = JSON.parse(attributes);
|
|
18682
|
-
const errors = parsed?.errors;
|
|
18683
|
-
return typeof errors === "number" ? errors : 0;
|
|
18684
|
-
} catch {
|
|
18685
|
-
return 0;
|
|
18686
|
-
}
|
|
19118
|
+
const errors = parseJsonObject(attributes)?.errors;
|
|
19119
|
+
return typeof errors === "number" ? errors : 0;
|
|
18687
19120
|
}
|
|
18688
19121
|
function parseBag(raw) {
|
|
18689
|
-
|
|
18690
|
-
const parsed = JSON.parse(raw);
|
|
18691
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
18692
|
-
} catch {
|
|
18693
|
-
}
|
|
18694
|
-
return void 0;
|
|
19122
|
+
return parseJsonObject(raw);
|
|
18695
19123
|
}
|
|
18696
19124
|
function str(value) {
|
|
18697
19125
|
return typeof value === "string" ? value : void 0;
|
|
@@ -18700,12 +19128,7 @@ function str(value) {
|
|
|
18700
19128
|
// ../../packages/persistence/src/repositories/detections.ts
|
|
18701
19129
|
var DAY_MS2 = 864e5;
|
|
18702
19130
|
function parseRules(rulesJson) {
|
|
18703
|
-
|
|
18704
|
-
try {
|
|
18705
|
-
raw = JSON.parse(rulesJson);
|
|
18706
|
-
} catch {
|
|
18707
|
-
return [];
|
|
18708
|
-
}
|
|
19131
|
+
const raw = safeJson(rulesJson, []);
|
|
18709
19132
|
if (!Array.isArray(raw)) return [];
|
|
18710
19133
|
const rules = [];
|
|
18711
19134
|
for (const entry of raw) {
|
|
@@ -18724,11 +19147,13 @@ var SqliteDetectionsRepository = class {
|
|
|
18724
19147
|
db;
|
|
18725
19148
|
now;
|
|
18726
19149
|
listDetections(query) {
|
|
18727
|
-
const rows =
|
|
18728
|
-
|
|
19150
|
+
const rows = allRows(
|
|
19151
|
+
this.db.prepare(
|
|
19152
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18729
19153
|
rules_json AS rulesJson
|
|
18730
19154
|
FROM installed_packs`
|
|
18731
|
-
|
|
19155
|
+
)
|
|
19156
|
+
);
|
|
18732
19157
|
const available = this.availableByPack();
|
|
18733
19158
|
const summaries = rows.map((r) => {
|
|
18734
19159
|
const latest = available.get(`${r.namespace}/${r.packId}`);
|
|
@@ -18737,7 +19162,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18737
19162
|
packId: r.packId,
|
|
18738
19163
|
version: r.version,
|
|
18739
19164
|
name: r.name,
|
|
18740
|
-
enabled: r.enabled
|
|
19165
|
+
enabled: intToBool(r.enabled),
|
|
18741
19166
|
// Count rules in JS via the tolerant parse rather than SQL json_array_length,
|
|
18742
19167
|
// which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
|
|
18743
19168
|
// crash the whole list. This also keeps ruleCount identical to the detail
|
|
@@ -18754,19 +19179,23 @@ var SqliteDetectionsRepository = class {
|
|
|
18754
19179
|
// available_packs keyed by the "namespace/packId" slug (one read per list /
|
|
18755
19180
|
// detail call; the table is a handful of rows).
|
|
18756
19181
|
availableByPack() {
|
|
18757
|
-
const rows =
|
|
18758
|
-
|
|
19182
|
+
const rows = allRows(
|
|
19183
|
+
this.db.prepare(
|
|
19184
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
|
|
18759
19185
|
FROM available_packs`
|
|
18760
|
-
|
|
19186
|
+
)
|
|
19187
|
+
);
|
|
18761
19188
|
return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
|
|
18762
19189
|
}
|
|
18763
19190
|
getDetectionStats() {
|
|
18764
|
-
const rows =
|
|
19191
|
+
const rows = allRows(
|
|
19192
|
+
this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
|
|
19193
|
+
);
|
|
18765
19194
|
let rules = 0;
|
|
18766
19195
|
let active = 0;
|
|
18767
19196
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
18768
19197
|
for (const r of rows) {
|
|
18769
|
-
if (r.enabled
|
|
19198
|
+
if (intToBool(r.enabled)) active += 1;
|
|
18770
19199
|
const parsed = parseRules(r.rulesJson);
|
|
18771
19200
|
rules += parsed.length;
|
|
18772
19201
|
for (const rule of parsed) {
|
|
@@ -18784,12 +19213,15 @@ var SqliteDetectionsRepository = class {
|
|
|
18784
19213
|
const parts = splitDetectionId(id);
|
|
18785
19214
|
if (!parts) return Promise.resolve(null);
|
|
18786
19215
|
const { namespace, packId } = parts;
|
|
18787
|
-
const row =
|
|
18788
|
-
|
|
19216
|
+
const row = getRow(
|
|
19217
|
+
this.db.prepare(
|
|
19218
|
+
`SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
|
|
18789
19219
|
rules_json AS rulesJson, updated_at AS updatedAt
|
|
18790
19220
|
FROM installed_packs
|
|
18791
19221
|
WHERE namespace = ? AND pack_id = ?`
|
|
18792
|
-
|
|
19222
|
+
),
|
|
19223
|
+
[namespace, packId]
|
|
19224
|
+
);
|
|
18793
19225
|
if (!row) return Promise.resolve(null);
|
|
18794
19226
|
const rules = parseRules(row.rulesJson);
|
|
18795
19227
|
const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
|
|
@@ -18807,7 +19239,7 @@ var SqliteDetectionsRepository = class {
|
|
|
18807
19239
|
packId: row.packId,
|
|
18808
19240
|
version: row.version,
|
|
18809
19241
|
name: row.name,
|
|
18810
|
-
enabled: row.enabled
|
|
19242
|
+
enabled: intToBool(row.enabled),
|
|
18811
19243
|
rules,
|
|
18812
19244
|
updatedAt: new Date(row.updatedAt),
|
|
18813
19245
|
policyId: row.policyId
|
|
@@ -18822,13 +19254,14 @@ var SqliteDetectionsRepository = class {
|
|
|
18822
19254
|
countFindingsLast30d(ruleIds) {
|
|
18823
19255
|
if (ruleIds.length === 0) return 0;
|
|
18824
19256
|
const since = this.now() - 30 * DAY_MS2;
|
|
18825
|
-
const
|
|
18826
|
-
|
|
18827
|
-
|
|
19257
|
+
const inClause = placeholders(ruleIds.length);
|
|
19258
|
+
return countScalar(
|
|
19259
|
+
this.db,
|
|
19260
|
+
`SELECT count(*) AS n
|
|
18828
19261
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
18829
|
-
WHERE e.occurred_at >= ? AND f.rule_id IN (${
|
|
18830
|
-
|
|
18831
|
-
|
|
19262
|
+
WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
|
|
19263
|
+
[since, ...ruleIds]
|
|
19264
|
+
);
|
|
18832
19265
|
}
|
|
18833
19266
|
};
|
|
18834
19267
|
|
|
@@ -18845,16 +19278,17 @@ var SqliteEventsRepository = class {
|
|
|
18845
19278
|
insertStmt;
|
|
18846
19279
|
insertEvent(event) {
|
|
18847
19280
|
const row = toEventRow(event);
|
|
18848
|
-
this.insertStmt.run(
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
19281
|
+
this.insertStmt.run(
|
|
19282
|
+
bindParams({
|
|
19283
|
+
id: row.id,
|
|
19284
|
+
sourceTool: row.sourceTool,
|
|
19285
|
+
kind: row.kind,
|
|
19286
|
+
occurredAt: row.occurredAt,
|
|
19287
|
+
contentHash: row.contentHash,
|
|
19288
|
+
content: row.content,
|
|
19289
|
+
metadata: row.metadata
|
|
19290
|
+
})
|
|
19291
|
+
);
|
|
18858
19292
|
}
|
|
18859
19293
|
// Every recorded event's content hash — the historical backfill loads this once
|
|
18860
19294
|
// to skip transcript messages it has already stored, so re-running the scan
|
|
@@ -18862,13 +19296,23 @@ var SqliteEventsRepository = class {
|
|
|
18862
19296
|
// Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
|
|
18863
19297
|
// async EventsReadPort contract.
|
|
18864
19298
|
contentHashes() {
|
|
18865
|
-
const rows =
|
|
19299
|
+
const rows = allRows(
|
|
19300
|
+
this.db.prepare("SELECT content_hash FROM events")
|
|
19301
|
+
);
|
|
18866
19302
|
return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
|
|
18867
19303
|
}
|
|
18868
19304
|
};
|
|
18869
19305
|
|
|
18870
19306
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18871
19307
|
import { randomUUID } from "crypto";
|
|
19308
|
+
|
|
19309
|
+
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19310
|
+
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
19311
|
+
function isUniqueConstraintError(err) {
|
|
19312
|
+
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
19313
|
+
}
|
|
19314
|
+
|
|
19315
|
+
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
18872
19316
|
var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
|
|
18873
19317
|
var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18874
19318
|
var DuplicateActiveExceptionError = class extends Error {
|
|
@@ -18889,10 +19333,6 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
18889
19333
|
this.name = "AmbiguousExceptionIdError";
|
|
18890
19334
|
}
|
|
18891
19335
|
};
|
|
18892
|
-
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
18893
|
-
function isUniqueConstraintError(err) {
|
|
18894
|
-
return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
|
|
18895
|
-
}
|
|
18896
19336
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
18897
19337
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
18898
19338
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
@@ -18948,10 +19388,11 @@ var SqliteExceptionsRepository = class {
|
|
|
18948
19388
|
this.insertExceptionRow(id, input, now);
|
|
18949
19389
|
} catch (err) {
|
|
18950
19390
|
if (!isUniqueConstraintError(err)) throw err;
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
19391
|
+
withTransaction(
|
|
19392
|
+
this.db,
|
|
19393
|
+
() => {
|
|
19394
|
+
const superseded = this.db.prepare(
|
|
19395
|
+
`UPDATE exceptions
|
|
18955
19396
|
SET revoked_at = :now, revoked_by = :revokedBy,
|
|
18956
19397
|
revoke_reason = 'superseded by a new grant for the same value',
|
|
18957
19398
|
updated_at = :now
|
|
@@ -18959,24 +19400,27 @@ var SqliteExceptionsRepository = class {
|
|
|
18959
19400
|
AND key_version = :keyVersion AND revoked_at IS NULL
|
|
18960
19401
|
AND ((expires_at IS NOT NULL AND expires_at <= :now)
|
|
18961
19402
|
OR (max_uses IS NOT NULL AND use_count >= max_uses))`
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
19403
|
+
).run({
|
|
19404
|
+
now,
|
|
19405
|
+
revokedBy: input.createdBy,
|
|
19406
|
+
ruleId: input.ruleId,
|
|
19407
|
+
valueFingerprint: input.valueFingerprint,
|
|
19408
|
+
keyVersion: input.keyVersion
|
|
19409
|
+
});
|
|
19410
|
+
if (Number(superseded.changes) !== 1) {
|
|
19411
|
+
throw new DuplicateActiveExceptionError(input.ruleId);
|
|
19412
|
+
}
|
|
19413
|
+
this.insertExceptionRow(id, input, now);
|
|
19414
|
+
},
|
|
19415
|
+
"IMMEDIATE"
|
|
19416
|
+
);
|
|
19417
|
+
}
|
|
19418
|
+
const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
|
|
19419
|
+
id
|
|
19420
|
+
});
|
|
19421
|
+
if (row === void 0) {
|
|
19422
|
+
throw new Error("exception row not found immediately after insert");
|
|
18978
19423
|
}
|
|
18979
|
-
const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
|
|
18980
19424
|
return parseExceptionRow(row);
|
|
18981
19425
|
}
|
|
18982
19426
|
insertExceptionRow(id, input, now) {
|
|
@@ -19014,14 +19458,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19014
19458
|
*/
|
|
19015
19459
|
list(opts) {
|
|
19016
19460
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19017
|
-
const rows =
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
} catch {
|
|
19023
|
-
}
|
|
19024
|
-
}
|
|
19461
|
+
const rows = allRows(
|
|
19462
|
+
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19463
|
+
opts?.includeTerminal ? {} : { now: Date.now() }
|
|
19464
|
+
);
|
|
19465
|
+
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19025
19466
|
return Promise.resolve(exceptions);
|
|
19026
19467
|
}
|
|
19027
19468
|
/**
|
|
@@ -19031,7 +19472,12 @@ var SqliteExceptionsRepository = class {
|
|
|
19031
19472
|
*/
|
|
19032
19473
|
getByIdPrefix(prefix) {
|
|
19033
19474
|
if (prefix.length === 0) return Promise.resolve(void 0);
|
|
19034
|
-
const rows =
|
|
19475
|
+
const rows = allRows(
|
|
19476
|
+
this.db.prepare(
|
|
19477
|
+
String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
|
|
19478
|
+
),
|
|
19479
|
+
{ pattern: `${escapeLikePattern(prefix)}%` }
|
|
19480
|
+
);
|
|
19035
19481
|
if (rows.length > 1) {
|
|
19036
19482
|
return Promise.reject(new AmbiguousExceptionIdError(prefix));
|
|
19037
19483
|
}
|
|
@@ -19073,30 +19519,27 @@ var SqliteExceptionsRepository = class {
|
|
|
19073
19519
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19074
19520
|
*/
|
|
19075
19521
|
activeBundleEntries(keyVersion, now = Date.now()) {
|
|
19076
|
-
const rows =
|
|
19077
|
-
|
|
19522
|
+
const rows = allRows(
|
|
19523
|
+
this.db.prepare(
|
|
19524
|
+
`SELECT * FROM exceptions
|
|
19078
19525
|
WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
|
|
19079
19526
|
ORDER BY created_at DESC, rowid DESC`
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
);
|
|
19097
|
-
} catch {
|
|
19098
|
-
}
|
|
19099
|
-
}
|
|
19527
|
+
),
|
|
19528
|
+
{ keyVersion, now }
|
|
19529
|
+
);
|
|
19530
|
+
const entries = mapRowsTolerant(rows, (row) => {
|
|
19531
|
+
const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
|
|
19532
|
+
return ExceptionBundleEntry.parse({
|
|
19533
|
+
id: row.id,
|
|
19534
|
+
ruleId: row.rule_id,
|
|
19535
|
+
valueFingerprint: row.value_fingerprint,
|
|
19536
|
+
keyVersion: row.key_version,
|
|
19537
|
+
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19538
|
+
maxUses: row.max_uses,
|
|
19539
|
+
useCount: row.use_count,
|
|
19540
|
+
conditions
|
|
19541
|
+
});
|
|
19542
|
+
});
|
|
19100
19543
|
return Promise.resolve(entries);
|
|
19101
19544
|
}
|
|
19102
19545
|
/**
|
|
@@ -19123,11 +19566,14 @@ var SqliteExceptionsRepository = class {
|
|
|
19123
19566
|
}
|
|
19124
19567
|
/** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
|
|
19125
19568
|
recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
|
|
19126
|
-
const rows =
|
|
19127
|
-
|
|
19569
|
+
const rows = allRows(
|
|
19570
|
+
this.db.prepare(
|
|
19571
|
+
`SELECT * FROM blocked_detections
|
|
19128
19572
|
WHERE blocked_at > :cutoff
|
|
19129
19573
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
19130
|
-
|
|
19574
|
+
),
|
|
19575
|
+
{ cutoff: Date.now() - windowMs }
|
|
19576
|
+
);
|
|
19131
19577
|
return Promise.resolve(
|
|
19132
19578
|
rows.map((row) => ({
|
|
19133
19579
|
reference: row.reference,
|
|
@@ -19207,7 +19653,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
19207
19653
|
)`;
|
|
19208
19654
|
|
|
19209
19655
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
19210
|
-
var
|
|
19656
|
+
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
19657
|
+
var CONCAT_SEP = ",";
|
|
19658
|
+
var TUPLE_SEP = "|";
|
|
19659
|
+
function splitConcat(value) {
|
|
19660
|
+
return value === null || value === "" ? [] : value.split(CONCAT_SEP);
|
|
19661
|
+
}
|
|
19211
19662
|
function deriveInstanceStatus(row) {
|
|
19212
19663
|
return deriveFindingStatus({
|
|
19213
19664
|
kind: row.kind,
|
|
@@ -19275,13 +19726,16 @@ var SqliteFindingsRepository = class {
|
|
|
19275
19726
|
}
|
|
19276
19727
|
recentFindings(opts) {
|
|
19277
19728
|
const limit = opts?.limit ?? 50;
|
|
19278
|
-
const rows =
|
|
19279
|
-
|
|
19729
|
+
const rows = allRows(
|
|
19730
|
+
this.db.prepare(
|
|
19731
|
+
`SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
|
|
19280
19732
|
f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
|
|
19281
19733
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19282
19734
|
ORDER BY e.occurred_at DESC, f.rowid DESC
|
|
19283
19735
|
LIMIT :limit`
|
|
19284
|
-
|
|
19736
|
+
),
|
|
19737
|
+
{ limit }
|
|
19738
|
+
);
|
|
19285
19739
|
return Promise.resolve(
|
|
19286
19740
|
rows.map((r) => ({
|
|
19287
19741
|
id: r.id,
|
|
@@ -19298,26 +19752,94 @@ var SqliteFindingsRepository = class {
|
|
|
19298
19752
|
}))
|
|
19299
19753
|
);
|
|
19300
19754
|
}
|
|
19755
|
+
/** Live-enforced findings recorded for one session — a bare COUNT over the
|
|
19756
|
+
* session-stamped events (served by idx_events_session_id), so the Activity
|
|
19757
|
+
* page can label its findings link without the grouped pipeline. */
|
|
19758
|
+
sessionFindingsCount(sessionId) {
|
|
19759
|
+
if (!sessionId) return Promise.resolve(0);
|
|
19760
|
+
return Promise.resolve(
|
|
19761
|
+
countScalar(
|
|
19762
|
+
this.db,
|
|
19763
|
+
`SELECT count(*) AS n FROM findings f
|
|
19764
|
+
JOIN events e ON e.id = f.event_id
|
|
19765
|
+
WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
|
|
19766
|
+
{ sessionId }
|
|
19767
|
+
)
|
|
19768
|
+
);
|
|
19769
|
+
}
|
|
19770
|
+
/** Per-rule transcript firing tally for one session — reads the OTHER finding
|
|
19771
|
+
* store (inspection_findings, keyed to audit_events): every detection the
|
|
19772
|
+
* transcript pass recorded, counted per firing rather than per unique value.
|
|
19773
|
+
* Rides on session-scoped grouped responses so the findings view can
|
|
19774
|
+
* reconcile the Activity page's tally with the deduped groups it lists. */
|
|
19775
|
+
sessionFirings(sessionId) {
|
|
19776
|
+
return Object.fromEntries(
|
|
19777
|
+
countBy(
|
|
19778
|
+
this.db,
|
|
19779
|
+
`SELECT d.rule_id AS k, count(*) AS n
|
|
19780
|
+
FROM inspection_findings f
|
|
19781
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
19782
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
19783
|
+
WHERE e.root_session_id = :sessionId
|
|
19784
|
+
GROUP BY d.rule_id`,
|
|
19785
|
+
{ sessionId }
|
|
19786
|
+
)
|
|
19787
|
+
);
|
|
19788
|
+
}
|
|
19301
19789
|
/**
|
|
19302
|
-
* Grouped findings for the dashboard — joins findings⋈events (repo/file
|
|
19303
|
-
* from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19790
|
+
* Grouped findings for the dashboard — joins findings⋈events (repo/file/
|
|
19791
|
+
* toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
|
|
19304
19792
|
* applies the requested filters, and sorts by severity then recency. Filtering
|
|
19305
19793
|
* and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
|
|
19306
19794
|
* reflect the full filtered set; `items` is the requested
|
|
19307
|
-
* page (default
|
|
19795
|
+
* page (default 50); no cursor (nextCursor is always null).
|
|
19796
|
+
*
|
|
19797
|
+
* Two reads, neither of which materializes a row per finding:
|
|
19798
|
+
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
19799
|
+
* the group and the filters need (count, providers, actions, statuses,
|
|
19800
|
+
* latest, search text);
|
|
19801
|
+
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
19802
|
+
* populate `instances` for the table's expanded rows.
|
|
19803
|
+
* The aggregates carry raw DB values and are translated by the same
|
|
19804
|
+
* @akasecurity/schema mappers the row path uses, so no enum mapping or status
|
|
19805
|
+
* rule is ever restated in SQL.
|
|
19308
19806
|
*/
|
|
19309
19807
|
listGroupedFindings(query) {
|
|
19310
|
-
const
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
19808
|
+
const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
|
|
19809
|
+
const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
|
|
19810
|
+
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
19811
|
+
predicate: sessionPredicate,
|
|
19812
|
+
params: sessionParams
|
|
19813
|
+
});
|
|
19814
|
+
const rows = allRows(
|
|
19815
|
+
this.db.prepare(
|
|
19816
|
+
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
19817
|
+
occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
|
|
19818
|
+
FROM (
|
|
19819
|
+
SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
|
|
19820
|
+
f.severity AS severity, f.masked_match AS masked_match,
|
|
19821
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
19822
|
+
e.occurred_at AS occurred_at, e.source_tool AS source_tool,
|
|
19823
|
+
json_extract(e.metadata, '$.repo') AS repo,
|
|
19824
|
+
json_extract(e.metadata, '$.filePath') AS file,
|
|
19825
|
+
json_extract(e.metadata, '$.toolName') AS tool_name,
|
|
19826
|
+
e.kind AS kind, f.finding_key AS finding_key,
|
|
19827
|
+
latest.status AS latest_status,
|
|
19828
|
+
ROW_NUMBER() OVER (
|
|
19829
|
+
PARTITION BY f.rule_id
|
|
19830
|
+
ORDER BY e.occurred_at DESC, f.id DESC
|
|
19831
|
+
) AS rn
|
|
19832
|
+
FROM findings f
|
|
19833
|
+
JOIN events e ON e.id = f.event_id
|
|
19834
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19835
|
+
ON latest.finding_key = f.finding_key
|
|
19836
|
+
${sessionPredicate}
|
|
19837
|
+
)
|
|
19838
|
+
WHERE rn <= :cap
|
|
19839
|
+
ORDER BY occurred_at DESC, id DESC`
|
|
19840
|
+
),
|
|
19841
|
+
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
19842
|
+
);
|
|
19321
19843
|
const groupable = rows.map((r) => ({
|
|
19322
19844
|
id: r.id,
|
|
19323
19845
|
ruleId: r.rule_id,
|
|
@@ -19330,9 +19852,10 @@ var SqliteFindingsRepository = class {
|
|
|
19330
19852
|
sourceTool: r.source_tool,
|
|
19331
19853
|
repo: r.repo ?? "",
|
|
19332
19854
|
file: r.file ?? "",
|
|
19855
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
19333
19856
|
status: deriveInstanceStatus(r)
|
|
19334
19857
|
}));
|
|
19335
|
-
const allGroups = buildFindingGroups(groupable);
|
|
19858
|
+
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
19336
19859
|
const filterOpts = {
|
|
19337
19860
|
severity: query.severity,
|
|
19338
19861
|
providers: query.provider,
|
|
@@ -19348,44 +19871,134 @@ var SqliteFindingsRepository = class {
|
|
|
19348
19871
|
};
|
|
19349
19872
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
19350
19873
|
const items = sorted.slice(0, limit);
|
|
19351
|
-
return Promise.resolve({
|
|
19874
|
+
return Promise.resolve({
|
|
19875
|
+
totals,
|
|
19876
|
+
facets,
|
|
19877
|
+
items,
|
|
19878
|
+
nextCursor: null,
|
|
19879
|
+
...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
|
|
19880
|
+
});
|
|
19881
|
+
}
|
|
19882
|
+
/**
|
|
19883
|
+
* One row per rule_id, folding EVERY instance of the group into the values
|
|
19884
|
+
* buildFindingGroups cannot recover from a preview. Bounded by the number of
|
|
19885
|
+
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
19886
|
+
*
|
|
19887
|
+
* The per-instance sets ride back as group_concat lists of RAW DB values —
|
|
19888
|
+
* source_tool, action_taken, and the (kind, has-key, latest-status) triples
|
|
19889
|
+
* deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
|
|
19890
|
+
* status keeps the classifier itself in @akasecurity/schema, where
|
|
19891
|
+
* severitySummary's SQL and this query can't drift apart on what 'resolved'
|
|
19892
|
+
* means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
|
|
19893
|
+
* a group's row stays small however many findings it holds.
|
|
19894
|
+
*
|
|
19895
|
+
* `withSearchText` is the exception, and the one column here that does NOT
|
|
19896
|
+
* stay small: the group's distinct repos/filePaths, whose size tracks how many
|
|
19897
|
+
* distinct paths a rule fired across — for a rule hitting mostly-unique paths
|
|
19898
|
+
* that is a string proportional to the store (~8MB over 200k distinct paths,
|
|
19899
|
+
* and buildHaystack lowercases a second copy). It buys `q` the ability to
|
|
19900
|
+
* match an instance outside the preview, which searching the preview alone
|
|
19901
|
+
* would silently lose, so it is fetched only when the request actually
|
|
19902
|
+
* carries a `q`.
|
|
19903
|
+
*/
|
|
19904
|
+
groupAggregates(withSearchText, scope) {
|
|
19905
|
+
const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
|
|
19906
|
+
group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
|
|
19907
|
+
group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
19908
|
+
const rows = this.db.prepare(
|
|
19909
|
+
`SELECT f.rule_id AS rule_id,
|
|
19910
|
+
count(*) AS instance_count,
|
|
19911
|
+
max(e.occurred_at) AS latest_at,
|
|
19912
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
19913
|
+
group_concat(DISTINCT f.action_taken) AS actions_taken,
|
|
19914
|
+
group_concat(DISTINCT (
|
|
19915
|
+
e.kind || '${TUPLE_SEP}' ||
|
|
19916
|
+
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
19917
|
+
coalesce(latest.status, '')
|
|
19918
|
+
)) AS status_inputs
|
|
19919
|
+
${searchTextColumns}
|
|
19920
|
+
FROM findings f
|
|
19921
|
+
JOIN events e ON e.id = f.event_id
|
|
19922
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19923
|
+
ON latest.finding_key = f.finding_key
|
|
19924
|
+
${scope.predicate}
|
|
19925
|
+
GROUP BY f.rule_id`
|
|
19926
|
+
).all(scope.params);
|
|
19927
|
+
return new Map(
|
|
19928
|
+
rows.map((r) => [
|
|
19929
|
+
r.rule_id,
|
|
19930
|
+
{
|
|
19931
|
+
instanceCount: r.instance_count,
|
|
19932
|
+
sourceTools: splitConcat(r.source_tools),
|
|
19933
|
+
actionsTaken: splitConcat(r.actions_taken),
|
|
19934
|
+
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
19935
|
+
const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
|
|
19936
|
+
return {
|
|
19937
|
+
// deriveFindingStatus only distinguishes null from non-null here,
|
|
19938
|
+
// so the marker stands in for the key itself (never rendered).
|
|
19939
|
+
kind,
|
|
19940
|
+
findingKey: keyMarker === "" ? null : keyMarker,
|
|
19941
|
+
latestResolutionStatus: latestStatus === "" ? null : latestStatus
|
|
19942
|
+
};
|
|
19943
|
+
}),
|
|
19944
|
+
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
19945
|
+
// Free text only — joined and substring-matched, so group_concat's
|
|
19946
|
+
// commas need no unpicking (a repo/path containing one still matches).
|
|
19947
|
+
// Left undefined (not '') when unfetched, so buildFindingGroups can
|
|
19948
|
+
// tell "no q this request" from "a group with no repo/file at all"
|
|
19949
|
+
// and skip priming a haystack nothing will read.
|
|
19950
|
+
...withSearchText ? {
|
|
19951
|
+
searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
|
|
19952
|
+
} : {}
|
|
19953
|
+
}
|
|
19954
|
+
])
|
|
19955
|
+
);
|
|
19352
19956
|
}
|
|
19353
19957
|
healthSummary() {
|
|
19354
|
-
const total = this.db
|
|
19958
|
+
const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
|
|
19355
19959
|
const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
|
|
19356
|
-
const grouped =
|
|
19960
|
+
const grouped = allRows(
|
|
19961
|
+
this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
|
|
19962
|
+
);
|
|
19357
19963
|
for (const row of grouped) {
|
|
19358
19964
|
if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
|
|
19359
19965
|
}
|
|
19360
19966
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
19361
|
-
const sevRows =
|
|
19362
|
-
|
|
19967
|
+
const sevRows = allRows(
|
|
19968
|
+
this.db.prepare(
|
|
19969
|
+
`SELECT f.severity AS severity, count(*) AS c
|
|
19363
19970
|
FROM findings f
|
|
19364
19971
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
19365
19972
|
ON latest.finding_key = f.finding_key
|
|
19366
19973
|
WHERE latest.status IS NULL OR latest.status != 'resolved'
|
|
19367
19974
|
GROUP BY f.severity`
|
|
19368
|
-
|
|
19975
|
+
)
|
|
19976
|
+
);
|
|
19369
19977
|
for (const row of sevRows) {
|
|
19370
19978
|
if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
|
|
19371
19979
|
}
|
|
19372
19980
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
19373
|
-
const enabledRows =
|
|
19374
|
-
|
|
19981
|
+
const enabledRows = allRows(
|
|
19982
|
+
this.db.prepare(
|
|
19983
|
+
`SELECT DISTINCT json_extract(target, '$.category') AS category
|
|
19375
19984
|
FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
|
|
19376
|
-
|
|
19985
|
+
)
|
|
19986
|
+
);
|
|
19377
19987
|
const enabled = new Set(enabledRows.map((r) => r.category));
|
|
19378
19988
|
const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
|
|
19379
19989
|
return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
|
|
19380
19990
|
}
|
|
19381
19991
|
activityByDay(days = 7) {
|
|
19382
19992
|
const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
|
|
19383
|
-
const rows =
|
|
19384
|
-
|
|
19993
|
+
const rows = allRows(
|
|
19994
|
+
this.db.prepare(
|
|
19995
|
+
`SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
|
|
19385
19996
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
19386
19997
|
WHERE e.occurred_at >= :since
|
|
19387
19998
|
GROUP BY day, f.action_taken`
|
|
19388
|
-
|
|
19999
|
+
),
|
|
20000
|
+
{ since }
|
|
20001
|
+
);
|
|
19389
20002
|
const buckets = /* @__PURE__ */ new Map();
|
|
19390
20003
|
for (let i = 0; i < days; i++) {
|
|
19391
20004
|
const day = isoDay(since + i * DAY_MS3);
|
|
@@ -19457,17 +20070,19 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
19457
20070
|
insertStmt;
|
|
19458
20071
|
insertFinding(input) {
|
|
19459
20072
|
const row = toInspectionFindingRow(input);
|
|
19460
|
-
this.insertStmt.run(
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
|
|
20073
|
+
this.insertStmt.run(
|
|
20074
|
+
bindParams({
|
|
20075
|
+
id: row.id,
|
|
20076
|
+
auditEventId: row.auditEventId,
|
|
20077
|
+
inspectionDefinitionId: row.inspectionDefinitionId,
|
|
20078
|
+
classifiedDataId: row.classifiedDataId,
|
|
20079
|
+
spanStart: row.spanStart,
|
|
20080
|
+
spanEnd: row.spanEnd,
|
|
20081
|
+
maskedMatch: row.maskedMatch,
|
|
20082
|
+
actionTaken: row.actionTaken,
|
|
20083
|
+
confidence: row.confidence
|
|
20084
|
+
})
|
|
20085
|
+
);
|
|
19471
20086
|
}
|
|
19472
20087
|
};
|
|
19473
20088
|
|
|
@@ -19543,12 +20158,7 @@ function isMirrorDowngrade(incoming, stored) {
|
|
|
19543
20158
|
}
|
|
19544
20159
|
function ruleIdsOf(rulesJson) {
|
|
19545
20160
|
const ids = /* @__PURE__ */ new Set();
|
|
19546
|
-
|
|
19547
|
-
try {
|
|
19548
|
-
raw = JSON.parse(rulesJson);
|
|
19549
|
-
} catch {
|
|
19550
|
-
return ids;
|
|
19551
|
-
}
|
|
20161
|
+
const raw = safeJson(rulesJson, []);
|
|
19552
20162
|
if (!Array.isArray(raw)) return ids;
|
|
19553
20163
|
for (const entry of raw) {
|
|
19554
20164
|
if (entry && typeof entry === "object") {
|
|
@@ -19621,47 +20231,48 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19621
20231
|
}));
|
|
19622
20232
|
if (this.storedSignature() === inventorySignature(rows)) return;
|
|
19623
20233
|
const now = Date.now();
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
const
|
|
19630
|
-
|
|
19631
|
-
namespace: row.namespace,
|
|
19632
|
-
packId: row.packId,
|
|
19633
|
-
version: row.version,
|
|
19634
|
-
name: row.name,
|
|
19635
|
-
rulesJson: row.rulesJson,
|
|
19636
|
-
now
|
|
19637
|
-
};
|
|
19638
|
-
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
19639
|
-
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
19640
|
-
this.upsertAvailableStmt.run({
|
|
19641
|
-
...params,
|
|
20234
|
+
withTransaction(
|
|
20235
|
+
this.db,
|
|
20236
|
+
() => {
|
|
20237
|
+
const mirror = this.mirrorState();
|
|
20238
|
+
let behind = false;
|
|
20239
|
+
for (const row of rows) {
|
|
20240
|
+
const params = {
|
|
19642
20241
|
id: randomUUID2(),
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
20242
|
+
namespace: row.namespace,
|
|
20243
|
+
packId: row.packId,
|
|
20244
|
+
version: row.version,
|
|
20245
|
+
name: row.name,
|
|
20246
|
+
rulesJson: row.rulesJson,
|
|
20247
|
+
now
|
|
20248
|
+
};
|
|
20249
|
+
const stored = mirror.get(`${row.namespace}/${row.packId}`);
|
|
20250
|
+
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20251
|
+
this.upsertAvailableStmt.run({
|
|
20252
|
+
...params,
|
|
20253
|
+
id: randomUUID2(),
|
|
20254
|
+
recordedBy: meta3?.recordedBy ?? null
|
|
20255
|
+
});
|
|
20256
|
+
} else {
|
|
20257
|
+
behind = true;
|
|
20258
|
+
}
|
|
20259
|
+
this.insertMissingStmt.run(params);
|
|
19647
20260
|
}
|
|
19648
|
-
this.
|
|
19649
|
-
}
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
} catch (err) {
|
|
19653
|
-
this.db.exec("ROLLBACK");
|
|
19654
|
-
throw err;
|
|
19655
|
-
}
|
|
20261
|
+
if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
|
|
20262
|
+
},
|
|
20263
|
+
"IMMEDIATE"
|
|
20264
|
+
);
|
|
19656
20265
|
} catch {
|
|
19657
20266
|
}
|
|
19658
20267
|
}
|
|
19659
20268
|
// The mirror's current (namespace/packId → {version, ruleIds}) map — the
|
|
19660
20269
|
// input to the downgrade guard. Read INSIDE the write transaction.
|
|
19661
20270
|
mirrorState() {
|
|
19662
|
-
const rows =
|
|
19663
|
-
|
|
19664
|
-
|
|
20271
|
+
const rows = allRows(
|
|
20272
|
+
this.db.prepare(
|
|
20273
|
+
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
20274
|
+
)
|
|
20275
|
+
);
|
|
19665
20276
|
return new Map(
|
|
19666
20277
|
rows.map((r) => [
|
|
19667
20278
|
`${r.namespace}/${r.packId}`,
|
|
@@ -19673,7 +20284,9 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19673
20284
|
// (keys joined with '/', matching the detection id slug encoding — packId may
|
|
19674
20285
|
// itself contain '/', but namespace may not, so the join is unambiguous).
|
|
19675
20286
|
pruneAvailable(keep) {
|
|
19676
|
-
const rows =
|
|
20287
|
+
const rows = allRows(
|
|
20288
|
+
this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
|
|
20289
|
+
);
|
|
19677
20290
|
const keepSet = new Set(keep);
|
|
19678
20291
|
const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
|
|
19679
20292
|
for (const r of rows) {
|
|
@@ -19700,11 +20313,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19700
20313
|
if (this.db.isTransaction) {
|
|
19701
20314
|
throw new Error("applyUpdate must not be called inside an open transaction");
|
|
19702
20315
|
}
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
this.db
|
|
19706
|
-
|
|
19707
|
-
|
|
20316
|
+
let changed = false;
|
|
20317
|
+
withTransaction(
|
|
20318
|
+
this.db,
|
|
20319
|
+
() => {
|
|
20320
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
|
|
20321
|
+
const res = this.db.prepare(
|
|
20322
|
+
`UPDATE installed_packs SET
|
|
19708
20323
|
version = (SELECT a.version FROM available_packs a
|
|
19709
20324
|
WHERE a.namespace = :namespace AND a.pack_id = :packId),
|
|
19710
20325
|
name = (SELECT a.name FROM available_packs a
|
|
@@ -19715,17 +20330,13 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19715
20330
|
WHERE namespace = :namespace AND pack_id = :packId
|
|
19716
20331
|
AND EXISTS (SELECT 1 FROM available_packs a
|
|
19717
20332
|
WHERE a.namespace = :namespace AND a.pack_id = :packId)`
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
} catch {
|
|
19726
|
-
}
|
|
19727
|
-
throw err;
|
|
19728
|
-
}
|
|
20333
|
+
).run({ namespace, packId, now: Date.now() });
|
|
20334
|
+
this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
|
|
20335
|
+
changed = Number(res.changes) > 0;
|
|
20336
|
+
},
|
|
20337
|
+
"IMMEDIATE"
|
|
20338
|
+
);
|
|
20339
|
+
return changed;
|
|
19729
20340
|
}
|
|
19730
20341
|
/**
|
|
19731
20342
|
* The scan-time ruleset: every rule under an ENABLED installed pack that
|
|
@@ -19739,9 +20350,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19739
20350
|
* JSON-level failure therefore counts as invalid.
|
|
19740
20351
|
*/
|
|
19741
20352
|
installedRuleset() {
|
|
19742
|
-
const rows =
|
|
19743
|
-
|
|
19744
|
-
|
|
20353
|
+
const rows = allRows(
|
|
20354
|
+
this.db.prepare(
|
|
20355
|
+
`SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
|
|
20356
|
+
)
|
|
20357
|
+
);
|
|
19745
20358
|
const out = {
|
|
19746
20359
|
installedPacks: rows.length,
|
|
19747
20360
|
enabledPacks: 0,
|
|
@@ -19750,7 +20363,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19750
20363
|
ruleActions: /* @__PURE__ */ new Map()
|
|
19751
20364
|
};
|
|
19752
20365
|
for (const row of rows) {
|
|
19753
|
-
if (row.enabled
|
|
20366
|
+
if (!intToBool(row.enabled)) continue;
|
|
19754
20367
|
out.enabledPacks += 1;
|
|
19755
20368
|
const action = policyIdToAction(row.policyId);
|
|
19756
20369
|
let raw;
|
|
@@ -19785,9 +20398,11 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19785
20398
|
* running max would mask a genuinely-newer parseable stamp.
|
|
19786
20399
|
*/
|
|
19787
20400
|
newestRecordedBinary() {
|
|
19788
|
-
const rows =
|
|
19789
|
-
|
|
19790
|
-
|
|
20401
|
+
const rows = allRows(
|
|
20402
|
+
this.db.prepare(
|
|
20403
|
+
`SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
|
|
20404
|
+
)
|
|
20405
|
+
);
|
|
19791
20406
|
let newest = null;
|
|
19792
20407
|
for (const row of rows) {
|
|
19793
20408
|
const at = row.recordedBy.lastIndexOf("@");
|
|
@@ -19802,13 +20417,15 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19802
20417
|
return newest;
|
|
19803
20418
|
}
|
|
19804
20419
|
counts() {
|
|
19805
|
-
const row =
|
|
19806
|
-
|
|
20420
|
+
const row = getRow(
|
|
20421
|
+
this.db.prepare(
|
|
20422
|
+
`SELECT count(*) AS packs,
|
|
19807
20423
|
coalesce(sum(json_array_length(rules_json)), 0) AS rules,
|
|
19808
20424
|
coalesce(sum(enabled), 0) AS enabled
|
|
19809
20425
|
FROM installed_packs`
|
|
19810
|
-
|
|
19811
|
-
|
|
20426
|
+
)
|
|
20427
|
+
);
|
|
20428
|
+
return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
|
|
19812
20429
|
}
|
|
19813
20430
|
// ─── Policy-catalog reads ────────────────────────────────────────────────────
|
|
19814
20431
|
// Back the Policies page's built-in catalog: how many
|
|
@@ -19821,26 +20438,29 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19821
20438
|
* attributed to Monitor, matching the Detections views.
|
|
19822
20439
|
*/
|
|
19823
20440
|
countsByPolicyId() {
|
|
19824
|
-
|
|
19825
|
-
|
|
20441
|
+
return countBy(
|
|
20442
|
+
this.db,
|
|
20443
|
+
`SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
|
|
19826
20444
|
FROM installed_packs
|
|
19827
|
-
GROUP BY
|
|
19828
|
-
)
|
|
19829
|
-
return new Map(rows.map((r) => [r.pid, r.n]));
|
|
20445
|
+
GROUP BY k`
|
|
20446
|
+
);
|
|
19830
20447
|
}
|
|
19831
20448
|
/** The detections governed by a built-in policy — one UsedByItem per pack. */
|
|
19832
20449
|
listByPolicyId(policyId) {
|
|
19833
|
-
const rows =
|
|
19834
|
-
|
|
20450
|
+
const rows = allRows(
|
|
20451
|
+
this.db.prepare(
|
|
20452
|
+
`SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
|
|
19835
20453
|
FROM installed_packs
|
|
19836
20454
|
WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
|
|
19837
20455
|
ORDER BY name ASC`
|
|
19838
|
-
|
|
20456
|
+
),
|
|
20457
|
+
[policyId]
|
|
20458
|
+
);
|
|
19839
20459
|
return rows.map((r) => ({
|
|
19840
20460
|
id: `${r.namespace}/${r.packId}`,
|
|
19841
20461
|
name: r.name,
|
|
19842
20462
|
ruleCount: parseRules(r.rulesJson).length,
|
|
19843
|
-
enabled: r.enabled
|
|
20463
|
+
enabled: intToBool(r.enabled)
|
|
19844
20464
|
}));
|
|
19845
20465
|
}
|
|
19846
20466
|
// ─── Writes ────────────────────────────────────────────────────────────────
|
|
@@ -19869,14 +20489,14 @@ var SqliteInstalledPacksRepository = class {
|
|
|
19869
20489
|
const res = this.db.prepare(
|
|
19870
20490
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
19871
20491
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
19872
|
-
).run({ enabled: enabled
|
|
20492
|
+
).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
|
|
19873
20493
|
return Number(res.changes) > 0;
|
|
19874
20494
|
}
|
|
19875
20495
|
// Fingerprint of the recorded available mirror — compared against the
|
|
19876
20496
|
// incoming inventory's signature to skip the write entirely when the running
|
|
19877
20497
|
// binary's inventory hasn't changed since the last record.
|
|
19878
20498
|
storedSignature() {
|
|
19879
|
-
const rows = this.signatureStmt
|
|
20499
|
+
const rows = allRows(this.signatureStmt);
|
|
19880
20500
|
return inventorySignature(rows);
|
|
19881
20501
|
}
|
|
19882
20502
|
};
|
|
@@ -19904,42 +20524,48 @@ var SqliteInventoryRepository = class {
|
|
|
19904
20524
|
upsert(input, now = Date.now()) {
|
|
19905
20525
|
const id = inventoryId(input.objectType, input.identityKey);
|
|
19906
20526
|
const row = toInventoryRow(input, id, now);
|
|
19907
|
-
this.upsertStmt.run(
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
20527
|
+
this.upsertStmt.run(
|
|
20528
|
+
bindParams({
|
|
20529
|
+
id: row.id,
|
|
20530
|
+
objectType: row.objectType,
|
|
20531
|
+
location: row.location,
|
|
20532
|
+
title: row.title,
|
|
20533
|
+
hostId: row.hostId,
|
|
20534
|
+
attributes: row.attributes,
|
|
20535
|
+
firstSeen: row.firstSeen,
|
|
20536
|
+
lastSeen: row.lastSeen
|
|
20537
|
+
})
|
|
20538
|
+
);
|
|
19917
20539
|
return id;
|
|
19918
20540
|
}
|
|
19919
20541
|
// The full row, for round-trip assertions.
|
|
19920
20542
|
findById(id) {
|
|
19921
|
-
|
|
19922
|
-
return row;
|
|
20543
|
+
return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
|
|
19923
20544
|
}
|
|
19924
20545
|
// Distinct titles for an object_type — a filter facet (e.g. hostnames),
|
|
19925
20546
|
// served from the object_type index, never from audit_events.
|
|
19926
20547
|
distinctTitles(objectType) {
|
|
19927
|
-
const rows =
|
|
19928
|
-
|
|
20548
|
+
const rows = allRows(
|
|
20549
|
+
this.db.prepare(
|
|
20550
|
+
`SELECT DISTINCT title FROM inventory
|
|
19929
20551
|
WHERE object_type = :objectType AND title IS NOT NULL
|
|
19930
20552
|
ORDER BY title`
|
|
19931
|
-
|
|
20553
|
+
),
|
|
20554
|
+
{ objectType }
|
|
20555
|
+
);
|
|
19932
20556
|
return rows.map((r) => r.title);
|
|
19933
20557
|
}
|
|
19934
20558
|
// Distinct host os_version values — a facet served from an inventory index
|
|
19935
20559
|
// over the generated column, never from the audit fact (confirm via EXPLAIN
|
|
19936
20560
|
// QUERY PLAN).
|
|
19937
20561
|
osVersions() {
|
|
19938
|
-
const rows =
|
|
19939
|
-
|
|
20562
|
+
const rows = allRows(
|
|
20563
|
+
this.db.prepare(
|
|
20564
|
+
`SELECT DISTINCT os_version AS value FROM inventory
|
|
19940
20565
|
WHERE object_type = 'host' AND os_version IS NOT NULL
|
|
19941
20566
|
ORDER BY value`
|
|
19942
|
-
|
|
20567
|
+
)
|
|
20568
|
+
);
|
|
19943
20569
|
return rows.map((r) => r.value);
|
|
19944
20570
|
}
|
|
19945
20571
|
};
|
|
@@ -19959,14 +20585,6 @@ var EMPTY_PROJECT_AGG = {
|
|
|
19959
20585
|
accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
|
|
19960
20586
|
findingsCount: 0
|
|
19961
20587
|
};
|
|
19962
|
-
function safeJson(s, fallback) {
|
|
19963
|
-
if (s == null) return fallback;
|
|
19964
|
-
try {
|
|
19965
|
-
return JSON.parse(s);
|
|
19966
|
-
} catch {
|
|
19967
|
-
return fallback;
|
|
19968
|
-
}
|
|
19969
|
-
}
|
|
19970
20588
|
function resolveHarnessId(attrs, row) {
|
|
19971
20589
|
if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
|
|
19972
20590
|
return attrs.provider;
|
|
@@ -20162,30 +20780,49 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20162
20780
|
configRowsCache;
|
|
20163
20781
|
// ─── stats ─────────────────────────────────────────────────────────────────
|
|
20164
20782
|
getInventoryStats() {
|
|
20165
|
-
const
|
|
20166
|
-
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
byType
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
|
|
20783
|
+
const typeCounts = countBy(
|
|
20784
|
+
this.db,
|
|
20785
|
+
"SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
|
|
20786
|
+
);
|
|
20787
|
+
const byType = {
|
|
20788
|
+
project: 0,
|
|
20789
|
+
skill: typeCounts.get("skill") ?? 0,
|
|
20790
|
+
mcp: typeCounts.get("mcp") ?? 0,
|
|
20791
|
+
hook: typeCounts.get("hook") ?? 0,
|
|
20792
|
+
config: typeCounts.get("config") ?? 0
|
|
20793
|
+
};
|
|
20794
|
+
byType.project = countScalar(
|
|
20795
|
+
this.db,
|
|
20796
|
+
`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
|
|
20797
|
+
);
|
|
20798
|
+
const mcpTrustCounts = countBy(
|
|
20799
|
+
this.db,
|
|
20800
|
+
`SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
|
|
20173
20801
|
FROM inventory_asset a
|
|
20174
20802
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20175
20803
|
WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
|
|
20176
20804
|
GROUP BY coalesce(o.trust, a.trust)`
|
|
20177
|
-
)
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20805
|
+
);
|
|
20806
|
+
const mcpTrust = {
|
|
20807
|
+
"known-good": mcpTrustCounts.get("known-good") ?? 0,
|
|
20808
|
+
risky: mcpTrustCounts.get("risky") ?? 0,
|
|
20809
|
+
unapproved: mcpTrustCounts.get("unapproved") ?? 0
|
|
20810
|
+
};
|
|
20811
|
+
const harnesses = countScalar(
|
|
20812
|
+
this.db,
|
|
20181
20813
|
`SELECT count(*) AS n FROM inventory
|
|
20182
20814
|
WHERE object_type = 'harness'
|
|
20183
|
-
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')
|
|
20184
|
-
|
|
20185
|
-
|
|
20186
|
-
const
|
|
20815
|
+
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
|
|
20816
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
20817
|
+
);
|
|
20818
|
+
const flaggedAssets = countScalar(
|
|
20819
|
+
this.db,
|
|
20820
|
+
"SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
|
|
20821
|
+
);
|
|
20822
|
+
const flaggedProjects = countScalar(
|
|
20823
|
+
this.db,
|
|
20187
20824
|
`SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
|
|
20188
|
-
)
|
|
20825
|
+
);
|
|
20189
20826
|
const configRows = this.configAssetRows();
|
|
20190
20827
|
for (const r of configRows) {
|
|
20191
20828
|
byType[r.assetType] += 1;
|
|
@@ -20442,12 +21079,15 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20442
21079
|
}
|
|
20443
21080
|
// ─── raw fetchers ────────────────────────────────────────────────────────────
|
|
20444
21081
|
fetchHarnessRows() {
|
|
20445
|
-
return
|
|
20446
|
-
|
|
21082
|
+
return allRows(
|
|
21083
|
+
this.db.prepare(
|
|
21084
|
+
`SELECT id, title, attributes, harness_version AS harnessVersion
|
|
20447
21085
|
FROM inventory
|
|
20448
21086
|
WHERE object_type = 'harness'
|
|
20449
21087
|
AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
|
|
20450
|
-
|
|
21088
|
+
),
|
|
21089
|
+
{ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
|
|
21090
|
+
);
|
|
20451
21091
|
}
|
|
20452
21092
|
// Every harness's assets in ONE grouped query, keyed by harness inventory id —
|
|
20453
21093
|
// replaces the per-harness-row query the listHarnesses loop used to make.
|
|
@@ -20457,12 +21097,13 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20457
21097
|
const params = [...harnessInvIds];
|
|
20458
21098
|
let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
|
|
20459
21099
|
if (q) {
|
|
20460
|
-
const pat =
|
|
20461
|
-
where +=
|
|
21100
|
+
const pat = containsPattern(q);
|
|
21101
|
+
where += ` AND ${likeAny(["a.name", "a.sub"])}`;
|
|
20462
21102
|
params.push(pat, pat);
|
|
20463
21103
|
}
|
|
20464
|
-
const rows =
|
|
20465
|
-
|
|
21104
|
+
const rows = allRows(
|
|
21105
|
+
this.db.prepare(
|
|
21106
|
+
`SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
|
|
20466
21107
|
a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20467
21108
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20468
21109
|
FROM harness_asset ha
|
|
@@ -20470,7 +21111,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20470
21111
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20471
21112
|
WHERE ${where}
|
|
20472
21113
|
ORDER BY a.name ASC`
|
|
20473
|
-
|
|
21114
|
+
),
|
|
21115
|
+
params
|
|
21116
|
+
);
|
|
20474
21117
|
for (const raw of rows) {
|
|
20475
21118
|
const harnessInvId = raw.harnessInvId;
|
|
20476
21119
|
const [asset] = this.mapAssetRows([raw]);
|
|
@@ -20489,21 +21132,24 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20489
21132
|
params.push(...types);
|
|
20490
21133
|
}
|
|
20491
21134
|
if (q) {
|
|
20492
|
-
const pat =
|
|
20493
|
-
conditions.push("
|
|
21135
|
+
const pat = containsPattern(q);
|
|
21136
|
+
conditions.push(likeAny(["a.name", "a.sub"]));
|
|
20494
21137
|
params.push(pat, pat);
|
|
20495
21138
|
}
|
|
20496
21139
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
20497
21140
|
const sampleRows = this.mapAssetRows(
|
|
20498
|
-
|
|
20499
|
-
|
|
21141
|
+
allRows(
|
|
21142
|
+
this.db.prepare(
|
|
21143
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20500
21144
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20501
21145
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20502
21146
|
FROM inventory_asset a
|
|
20503
21147
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20504
21148
|
${where}
|
|
20505
21149
|
ORDER BY a.name ASC`
|
|
20506
|
-
|
|
21150
|
+
),
|
|
21151
|
+
params
|
|
21152
|
+
)
|
|
20507
21153
|
);
|
|
20508
21154
|
const configRows = this.configAssetRows(q).filter(
|
|
20509
21155
|
(r) => !types || types.length === 0 || types.includes(r.assetType)
|
|
@@ -20512,14 +21158,17 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20512
21158
|
}
|
|
20513
21159
|
fetchAssetById(assetId) {
|
|
20514
21160
|
const rows = this.mapAssetRows(
|
|
20515
|
-
|
|
20516
|
-
|
|
21161
|
+
allRows(
|
|
21162
|
+
this.db.prepare(
|
|
21163
|
+
`SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
|
|
20517
21164
|
a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
|
|
20518
21165
|
a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
|
|
20519
21166
|
FROM inventory_asset a
|
|
20520
21167
|
LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
|
|
20521
21168
|
WHERE a.id = ?`
|
|
20522
|
-
|
|
21169
|
+
),
|
|
21170
|
+
[assetId]
|
|
21171
|
+
)
|
|
20523
21172
|
);
|
|
20524
21173
|
return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
|
|
20525
21174
|
}
|
|
@@ -20575,37 +21224,39 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20575
21224
|
return rows;
|
|
20576
21225
|
}
|
|
20577
21226
|
latestConfigScanId() {
|
|
20578
|
-
|
|
20579
|
-
`SELECT id FROM audit_events WHERE event_type = 'config_scan'
|
|
20580
|
-
ORDER BY started_at DESC, id DESC LIMIT 1`
|
|
20581
|
-
).get();
|
|
20582
|
-
return row?.id ?? null;
|
|
21227
|
+
return latestConfigScan(this.db)?.id ?? null;
|
|
20583
21228
|
}
|
|
20584
21229
|
fetchProjects(q) {
|
|
20585
21230
|
let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
|
|
20586
21231
|
WHERE ${WORKTREE_CHECKOUT_FILTER}`;
|
|
20587
21232
|
const params = [];
|
|
20588
21233
|
if (q) {
|
|
20589
|
-
const pat =
|
|
20590
|
-
sql +=
|
|
21234
|
+
const pat = containsPattern(q);
|
|
21235
|
+
sql += ` AND ${likeAny(["name", "url"])}`;
|
|
20591
21236
|
params.push(pat, pat);
|
|
20592
21237
|
}
|
|
20593
21238
|
sql += " ORDER BY name ASC";
|
|
20594
|
-
return this.db.prepare(sql)
|
|
21239
|
+
return allRows(this.db.prepare(sql), params);
|
|
20595
21240
|
}
|
|
20596
21241
|
fetchProjectById(projectId) {
|
|
20597
|
-
return
|
|
20598
|
-
|
|
20599
|
-
|
|
21242
|
+
return getRow(
|
|
21243
|
+
this.db.prepare(
|
|
21244
|
+
"SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
|
|
21245
|
+
),
|
|
21246
|
+
[projectId]
|
|
21247
|
+
) ?? null;
|
|
20600
21248
|
}
|
|
20601
21249
|
// The referenced projects in ONE `id IN (…)` fetch, keyed by id.
|
|
20602
21250
|
fetchProjectsByIds(projectIds) {
|
|
20603
21251
|
const map2 = /* @__PURE__ */ new Map();
|
|
20604
21252
|
if (projectIds.length === 0) return map2;
|
|
20605
|
-
const rows =
|
|
20606
|
-
|
|
21253
|
+
const rows = allRows(
|
|
21254
|
+
this.db.prepare(
|
|
21255
|
+
`SELECT id, url, name, attributes, last_seen AS lastSeen
|
|
20607
21256
|
FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
|
|
20608
|
-
|
|
21257
|
+
),
|
|
21258
|
+
projectIds
|
|
21259
|
+
);
|
|
20609
21260
|
for (const r of rows) map2.set(r.id, r);
|
|
20610
21261
|
return map2;
|
|
20611
21262
|
}
|
|
@@ -20616,8 +21267,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20616
21267
|
projectAggregates(projectIds) {
|
|
20617
21268
|
const map2 = /* @__PURE__ */ new Map();
|
|
20618
21269
|
if (projectIds.length === 0) return map2;
|
|
20619
|
-
const rows =
|
|
20620
|
-
|
|
21270
|
+
const rows = allRows(
|
|
21271
|
+
this.db.prepare(
|
|
21272
|
+
`SELECT f.project_id AS projectId,
|
|
20621
21273
|
coalesce(o.access, f.default_access) AS eff,
|
|
20622
21274
|
count(*) AS n,
|
|
20623
21275
|
coalesce(sum(f.findings_count), 0) AS findings
|
|
@@ -20625,7 +21277,9 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20625
21277
|
LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
|
|
20626
21278
|
WHERE f.project_id IN (${placeholders(projectIds.length)})
|
|
20627
21279
|
GROUP BY f.project_id, eff`
|
|
20628
|
-
|
|
21280
|
+
),
|
|
21281
|
+
projectIds
|
|
21282
|
+
);
|
|
20629
21283
|
for (const r of rows) {
|
|
20630
21284
|
let agg = map2.get(r.projectId);
|
|
20631
21285
|
if (!agg) {
|
|
@@ -20663,37 +21317,52 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
20663
21317
|
fetchProjectFilesUnder(projectId, prefix) {
|
|
20664
21318
|
if (prefix === "") {
|
|
20665
21319
|
return this.mapFileRows(
|
|
20666
|
-
|
|
21320
|
+
allRows(
|
|
21321
|
+
this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
|
|
21322
|
+
[projectId]
|
|
21323
|
+
)
|
|
20667
21324
|
);
|
|
20668
21325
|
}
|
|
20669
21326
|
return this.mapFileRows(
|
|
20670
|
-
|
|
20671
|
-
this.
|
|
20672
|
-
|
|
21327
|
+
allRows(
|
|
21328
|
+
this.db.prepare(
|
|
21329
|
+
this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
|
|
21330
|
+
),
|
|
21331
|
+
[projectId, `${escapeLikePattern(prefix)}/%`]
|
|
21332
|
+
)
|
|
20673
21333
|
);
|
|
20674
21334
|
}
|
|
20675
21335
|
fetchProjectFilesSearch(projectId, q) {
|
|
20676
|
-
const pat =
|
|
21336
|
+
const pat = containsPattern(q);
|
|
20677
21337
|
return this.mapFileRows(
|
|
20678
|
-
|
|
20679
|
-
this.
|
|
20680
|
-
|
|
20681
|
-
|
|
20682
|
-
|
|
21338
|
+
allRows(
|
|
21339
|
+
this.db.prepare(
|
|
21340
|
+
this.fileSelect(
|
|
21341
|
+
"f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
|
|
21342
|
+
)
|
|
21343
|
+
),
|
|
21344
|
+
[projectId, pat, pat]
|
|
21345
|
+
)
|
|
20683
21346
|
);
|
|
20684
21347
|
}
|
|
20685
21348
|
fetchProjectFilesBlocked(projectId) {
|
|
20686
21349
|
return this.mapFileRows(
|
|
20687
|
-
|
|
20688
|
-
this.
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
21350
|
+
allRows(
|
|
21351
|
+
this.db.prepare(
|
|
21352
|
+
this.fileSelect(
|
|
21353
|
+
"f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
|
|
21354
|
+
)
|
|
21355
|
+
),
|
|
21356
|
+
[projectId]
|
|
21357
|
+
)
|
|
20692
21358
|
);
|
|
20693
21359
|
}
|
|
20694
21360
|
fetchProjectFile(projectId, path) {
|
|
20695
21361
|
const rows = this.mapFileRows(
|
|
20696
|
-
|
|
21362
|
+
allRows(
|
|
21363
|
+
this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
|
|
21364
|
+
[projectId, path]
|
|
21365
|
+
)
|
|
20697
21366
|
);
|
|
20698
21367
|
return rows[0] ?? null;
|
|
20699
21368
|
}
|
|
@@ -20707,39 +21376,32 @@ var SqlitePoliciesRepository = class {
|
|
|
20707
21376
|
}
|
|
20708
21377
|
db;
|
|
20709
21378
|
readPolicies() {
|
|
20710
|
-
const rows = this.db.prepare("SELECT * FROM policies")
|
|
20711
|
-
const policies =
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
customKeywords
|
|
20724
|
-
})
|
|
20725
|
-
);
|
|
20726
|
-
} catch {
|
|
20727
|
-
}
|
|
20728
|
-
}
|
|
21379
|
+
const rows = allRows(this.db.prepare("SELECT * FROM policies"));
|
|
21380
|
+
const policies = mapRowsTolerant(rows, (row) => {
|
|
21381
|
+
const target = JSON.parse(row.target);
|
|
21382
|
+
const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
|
|
21383
|
+
return Policy.parse({
|
|
21384
|
+
id: row.id,
|
|
21385
|
+
scope: row.scope,
|
|
21386
|
+
target,
|
|
21387
|
+
action: row.action,
|
|
21388
|
+
enabled: intToBool(row.enabled),
|
|
21389
|
+
customKeywords
|
|
21390
|
+
});
|
|
21391
|
+
});
|
|
20729
21392
|
return Promise.resolve(policies);
|
|
20730
21393
|
}
|
|
20731
21394
|
// Seed one policy per bundled category from DEFAULT_ACTIONS so the
|
|
20732
21395
|
// detection-type config exists from first run. Only when the table is empty,
|
|
20733
21396
|
// so a user's edits are never clobbered.
|
|
20734
21397
|
seedDefaults() {
|
|
20735
|
-
const count = this.db
|
|
21398
|
+
const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
|
|
20736
21399
|
if (count > 0) return;
|
|
20737
21400
|
const stmt = this.db.prepare(
|
|
20738
21401
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
20739
21402
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)`
|
|
20740
21403
|
);
|
|
20741
|
-
this.db
|
|
20742
|
-
try {
|
|
21404
|
+
failOpenTransaction(this.db, () => {
|
|
20743
21405
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
20744
21406
|
stmt.run({
|
|
20745
21407
|
id: randomUUID4(),
|
|
@@ -20748,10 +21410,41 @@ var SqlitePoliciesRepository = class {
|
|
|
20748
21410
|
now: Date.now()
|
|
20749
21411
|
});
|
|
20750
21412
|
}
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
|
|
20754
|
-
|
|
21413
|
+
});
|
|
21414
|
+
}
|
|
21415
|
+
// Insert-or-update the single global per-category policy row, keyed on the
|
|
21416
|
+
// existing uq_policies_scope_target unique index (scope, target). `action`
|
|
21417
|
+
// uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
|
|
21418
|
+
// values), so the runtime's resolveAction reads rows written by either path
|
|
21419
|
+
// identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
|
|
21420
|
+
// `id` and `created_at` are left exactly as they were.
|
|
21421
|
+
upsertCategoryAction(category, action) {
|
|
21422
|
+
const now = Date.now();
|
|
21423
|
+
this.db.prepare(
|
|
21424
|
+
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21425
|
+
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21426
|
+
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21427
|
+
).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
|
|
21428
|
+
}
|
|
21429
|
+
// Caps every global per-category policy currently set to block/redact down
|
|
21430
|
+
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
21431
|
+
// Returns the number of rows changed.
|
|
21432
|
+
capCategoryActions() {
|
|
21433
|
+
const info = this.db.prepare(
|
|
21434
|
+
`UPDATE policies SET action='warn', updated_at=:now
|
|
21435
|
+
WHERE scope='global' AND action IN ('block','redact')
|
|
21436
|
+
AND json_extract(target,'$.category') IS NOT NULL`
|
|
21437
|
+
).run({ now: Date.now() });
|
|
21438
|
+
return Number(info.changes);
|
|
21439
|
+
}
|
|
21440
|
+
// Read the current action for a single global per-category policy row, mirroring
|
|
21441
|
+
// upsertCategoryAction's category-lookup predicate. Returns undefined when no
|
|
21442
|
+
// row exists yet, so callers can distinguish an unset category from a set one.
|
|
21443
|
+
getCategoryAction(category) {
|
|
21444
|
+
const row = this.db.prepare(
|
|
21445
|
+
`SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
|
|
21446
|
+
).get({ category });
|
|
21447
|
+
return row?.action;
|
|
20755
21448
|
}
|
|
20756
21449
|
};
|
|
20757
21450
|
|
|
@@ -20827,7 +21520,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
20827
21520
|
maxStampStmt;
|
|
20828
21521
|
/** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
|
|
20829
21522
|
replaceForProject(projectId, scan2, now) {
|
|
20830
|
-
const
|
|
21523
|
+
const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
|
|
20831
21524
|
const stamp = Math.max(now, maxStamp + 1);
|
|
20832
21525
|
for (const file2 of scan2.files) {
|
|
20833
21526
|
this.upsertStmt.run({
|
|
@@ -20910,7 +21603,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20910
21603
|
}
|
|
20911
21604
|
/** The newest disposition recorded for a finding key, or undefined if none. */
|
|
20912
21605
|
latestByKey(key) {
|
|
20913
|
-
const row = this.latestStmt
|
|
21606
|
+
const row = getRow(this.latestStmt, { findingKey: key });
|
|
20914
21607
|
if (!row) return void 0;
|
|
20915
21608
|
return {
|
|
20916
21609
|
// Safe narrows: insertResolution enum-parses both columns on every write,
|
|
@@ -20927,7 +21620,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20927
21620
|
* the CLI) surfaces for that file.
|
|
20928
21621
|
*/
|
|
20929
21622
|
openAtRestKeysForPath(path) {
|
|
20930
|
-
const rows = this.openAtRestStmt
|
|
21623
|
+
const rows = allRows(this.openAtRestStmt, { path });
|
|
20931
21624
|
return rows.map((r) => r.finding_key);
|
|
20932
21625
|
}
|
|
20933
21626
|
/**
|
|
@@ -20938,7 +21631,7 @@ var SqliteResolutionsRepository = class {
|
|
|
20938
21631
|
* resolution row (see scan.ts).
|
|
20939
21632
|
*/
|
|
20940
21633
|
resolvedAtRestKeysForPath(path) {
|
|
20941
|
-
const rows = this.resolvedAtRestStmt
|
|
21634
|
+
const rows = allRows(this.resolvedAtRestStmt, { path });
|
|
20942
21635
|
return rows.map((r) => r.finding_key);
|
|
20943
21636
|
}
|
|
20944
21637
|
};
|
|
@@ -20967,38 +21660,31 @@ var SqliteScanLedgerRepository = class {
|
|
|
20967
21660
|
// Previously scanned files under THIS ruleset, keyed by path. Rows from an
|
|
20968
21661
|
// older ruleset are simply absent, which reads as "never scanned".
|
|
20969
21662
|
entriesForRuleset(rulesetHash) {
|
|
20970
|
-
const rows = this.readStmt
|
|
21663
|
+
const rows = allRows(this.readStmt, {
|
|
21664
|
+
rulesetHash
|
|
21665
|
+
});
|
|
20971
21666
|
return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
|
|
20972
21667
|
}
|
|
20973
21668
|
upsertEntries(entries) {
|
|
20974
21669
|
if (entries.length === 0) return;
|
|
20975
21670
|
const scannedAt = Date.now();
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
20983
|
-
|
|
20984
|
-
|
|
20985
|
-
scannedAt
|
|
20986
|
-
});
|
|
20987
|
-
}
|
|
20988
|
-
this.db.exec("COMMIT");
|
|
20989
|
-
} catch (err) {
|
|
20990
|
-
this.db.exec("ROLLBACK");
|
|
20991
|
-
throw err;
|
|
21671
|
+
failOpenTransaction(this.db, () => {
|
|
21672
|
+
for (const entry of entries) {
|
|
21673
|
+
this.upsertStmt.run({
|
|
21674
|
+
path: entry.path,
|
|
21675
|
+
mtime: entry.mtime,
|
|
21676
|
+
contentHash: entry.contentHash,
|
|
21677
|
+
rulesetHash: entry.rulesetHash,
|
|
21678
|
+
scannedAt
|
|
21679
|
+
});
|
|
20992
21680
|
}
|
|
20993
|
-
}
|
|
20994
|
-
}
|
|
21681
|
+
});
|
|
20995
21682
|
}
|
|
20996
21683
|
};
|
|
20997
21684
|
|
|
20998
21685
|
// ../../packages/persistence/src/repositories/security.ts
|
|
20999
21686
|
var DAY_MS4 = 864e5;
|
|
21000
21687
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
21001
|
-
var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
|
|
21002
21688
|
var ACTION_TO_KIND = {
|
|
21003
21689
|
block: "blocked",
|
|
21004
21690
|
redact: "redacted",
|
|
@@ -21013,8 +21699,14 @@ var SCAN_COVERAGE = [
|
|
|
21013
21699
|
{ provider: "copilot", coverage: 0, supported: false },
|
|
21014
21700
|
{ provider: "api", coverage: 0, supported: false }
|
|
21015
21701
|
];
|
|
21702
|
+
var GRANULARITY = {
|
|
21703
|
+
"7d": "day",
|
|
21704
|
+
"30d": "day",
|
|
21705
|
+
"3m": "week",
|
|
21706
|
+
"6m": "week"
|
|
21707
|
+
};
|
|
21016
21708
|
function granularityFor(range) {
|
|
21017
|
-
return range
|
|
21709
|
+
return GRANULARITY[range];
|
|
21018
21710
|
}
|
|
21019
21711
|
function startOfUtcDay2(ms) {
|
|
21020
21712
|
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
@@ -21068,8 +21760,9 @@ var SqliteSecurityRepository = class {
|
|
|
21068
21760
|
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
21069
21761
|
// double-counting a key that accumulated several append-only rows.
|
|
21070
21762
|
severitySummary() {
|
|
21071
|
-
const rows =
|
|
21072
|
-
|
|
21763
|
+
const rows = allRows(
|
|
21764
|
+
this.db.prepare(
|
|
21765
|
+
`SELECT f.severity AS severity,
|
|
21073
21766
|
COUNT(*) AS count,
|
|
21074
21767
|
SUM(CASE
|
|
21075
21768
|
WHEN e.kind != 'code_change' THEN 1
|
|
@@ -21088,7 +21781,8 @@ var SqliteSecurityRepository = class {
|
|
|
21088
21781
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
21089
21782
|
ON latest.finding_key = f.finding_key
|
|
21090
21783
|
GROUP BY f.severity`
|
|
21091
|
-
|
|
21784
|
+
)
|
|
21785
|
+
);
|
|
21092
21786
|
const byRow = new Map(rows.map((r) => [r.severity, r]));
|
|
21093
21787
|
const bySeverity = SEVERITIES.map((severity) => ({
|
|
21094
21788
|
severity,
|
|
@@ -21172,14 +21866,15 @@ var SqliteSecurityRepository = class {
|
|
|
21172
21866
|
const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
|
|
21173
21867
|
const now = this.now();
|
|
21174
21868
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
21175
|
-
const rows =
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21180
|
-
|
|
21181
|
-
|
|
21182
|
-
|
|
21869
|
+
const rows = allRows(
|
|
21870
|
+
this.db.prepare(
|
|
21871
|
+
// first_detected_at is the PRESERVED first-detection time (set once on a
|
|
21872
|
+
// finding's INSERT, never overwritten on the re-detection upsert), so MTTR
|
|
21873
|
+
// measures from first sighting — not the latest re-scan's event, whose
|
|
21874
|
+
// occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
|
|
21875
|
+
// the parent event's occurred_at defends against any legacy/edge row the
|
|
21876
|
+
// backfill left null.
|
|
21877
|
+
`SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
|
|
21183
21878
|
(
|
|
21184
21879
|
SELECT fr.status FROM finding_resolution fr
|
|
21185
21880
|
WHERE fr.finding_key = f.finding_key
|
|
@@ -21205,14 +21900,16 @@ var SqliteSecurityRepository = class {
|
|
|
21205
21900
|
WHERE fr.finding_key = f.finding_key
|
|
21206
21901
|
AND fr.resolved_at >= :windowStart
|
|
21207
21902
|
)`
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
|
|
21903
|
+
// The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
|
|
21904
|
+
// any resolution activity at/after the window start — a row this method
|
|
21905
|
+
// ultimately counts has its LATEST resolution inside the window, which
|
|
21906
|
+
// implies such a row exists, so nothing wanted is dropped. The exact
|
|
21907
|
+
// latest-wins + status/method + window gate stays in JS below,
|
|
21908
|
+
// dialect-agnostic. Without this, a
|
|
21909
|
+
// 7d request evaluated the store's entire trackable-findings history.
|
|
21910
|
+
),
|
|
21911
|
+
{ windowStart }
|
|
21912
|
+
);
|
|
21216
21913
|
const sums = /* @__PURE__ */ new Map();
|
|
21217
21914
|
const counts = /* @__PURE__ */ new Map();
|
|
21218
21915
|
for (const r of rows) {
|
|
@@ -21242,8 +21939,9 @@ var SqliteSecurityRepository = class {
|
|
|
21242
21939
|
if (opts.kind === "user") return Promise.resolve({ range, items: [] });
|
|
21243
21940
|
const now = this.now();
|
|
21244
21941
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
21245
|
-
const rows =
|
|
21246
|
-
|
|
21942
|
+
const rows = allRows(
|
|
21943
|
+
this.db.prepare(
|
|
21944
|
+
`SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
|
|
21247
21945
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21248
21946
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21249
21947
|
AND json_extract(e.metadata, '$.repo') IS NOT NULL
|
|
@@ -21251,7 +21949,9 @@ var SqliteSecurityRepository = class {
|
|
|
21251
21949
|
GROUP BY repo
|
|
21252
21950
|
ORDER BY c DESC, repo
|
|
21253
21951
|
LIMIT :limit`
|
|
21254
|
-
|
|
21952
|
+
),
|
|
21953
|
+
{ from, to: now, limit }
|
|
21954
|
+
);
|
|
21255
21955
|
const items = rows.map((r) => ({
|
|
21256
21956
|
id: `repo_${r.repo}`,
|
|
21257
21957
|
name: r.repo,
|
|
@@ -21273,8 +21973,9 @@ var SqliteSecurityRepository = class {
|
|
|
21273
21973
|
// resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
|
|
21274
21974
|
// capped at `limit`.
|
|
21275
21975
|
recentlyResolved(limit = 20) {
|
|
21276
|
-
const rows =
|
|
21277
|
-
|
|
21976
|
+
const rows = allRows(
|
|
21977
|
+
this.db.prepare(
|
|
21978
|
+
`SELECT f.finding_key AS finding_key,
|
|
21278
21979
|
f.rule_id AS rule_id,
|
|
21279
21980
|
f.severity AS severity,
|
|
21280
21981
|
json_extract(e.metadata, '$.filePath') AS path,
|
|
@@ -21308,7 +22009,9 @@ var SqliteSecurityRepository = class {
|
|
|
21308
22009
|
) IS NOT NULL
|
|
21309
22010
|
ORDER BY latest_resolved_at DESC
|
|
21310
22011
|
LIMIT :limit`
|
|
21311
|
-
|
|
22012
|
+
),
|
|
22013
|
+
{ limit }
|
|
22014
|
+
);
|
|
21312
22015
|
const items = rows.map((r) => ({
|
|
21313
22016
|
findingKey: r.finding_key,
|
|
21314
22017
|
ruleId: r.rule_id,
|
|
@@ -21325,12 +22028,15 @@ var SqliteSecurityRepository = class {
|
|
|
21325
22028
|
// epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
|
|
21326
22029
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
21327
22030
|
findingsInRange(fromMs, toMs) {
|
|
21328
|
-
const rows =
|
|
21329
|
-
|
|
22031
|
+
const rows = allRows(
|
|
22032
|
+
this.db.prepare(
|
|
22033
|
+
`SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
|
|
21330
22034
|
FROM findings f JOIN events e ON e.id = f.event_id
|
|
21331
22035
|
WHERE e.occurred_at >= :from AND e.occurred_at < :to
|
|
21332
22036
|
ORDER BY e.occurred_at`
|
|
21333
|
-
|
|
22037
|
+
),
|
|
22038
|
+
{ from: fromMs, to: toMs }
|
|
22039
|
+
);
|
|
21334
22040
|
return rows.map((r) => ({
|
|
21335
22041
|
occurredAt: r.occurred_at,
|
|
21336
22042
|
severity: r.severity,
|
|
@@ -21344,12 +22050,7 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
21344
22050
|
var KIND_ORDER = ["provider", "internal", "ip"];
|
|
21345
22051
|
var CALL_SITE_EMBED_CAP = 200;
|
|
21346
22052
|
function parseNetwork(networkJson) {
|
|
21347
|
-
|
|
21348
|
-
try {
|
|
21349
|
-
return JSON.parse(networkJson);
|
|
21350
|
-
} catch {
|
|
21351
|
-
return null;
|
|
21352
|
-
}
|
|
22053
|
+
return safeJson(networkJson, null);
|
|
21353
22054
|
}
|
|
21354
22055
|
function toEndpointSummary(row) {
|
|
21355
22056
|
return {
|
|
@@ -21436,27 +22137,39 @@ var SqliteSharesRepository = class {
|
|
|
21436
22137
|
}
|
|
21437
22138
|
db;
|
|
21438
22139
|
stats() {
|
|
21439
|
-
const
|
|
21440
|
-
const
|
|
21441
|
-
const
|
|
21442
|
-
const
|
|
21443
|
-
|
|
22140
|
+
const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
|
|
22141
|
+
const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
|
|
22142
|
+
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22143
|
+
const insecure = countScalar(
|
|
22144
|
+
this.db,
|
|
21444
22145
|
"SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
|
|
21445
22146
|
);
|
|
21446
|
-
const needsReview =
|
|
22147
|
+
const needsReview = countScalar(
|
|
22148
|
+
this.db,
|
|
21447
22149
|
`SELECT count(DISTINCT d.id) AS n
|
|
21448
22150
|
FROM share_destination d
|
|
21449
22151
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
|
|
21450
22152
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
21451
22153
|
);
|
|
21452
|
-
const
|
|
21453
|
-
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
const
|
|
21457
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
22154
|
+
const kindCounts = countBy(
|
|
22155
|
+
this.db,
|
|
22156
|
+
"SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
|
|
22157
|
+
);
|
|
22158
|
+
const byKind = {
|
|
22159
|
+
provider: kindCounts.get("provider") ?? 0,
|
|
22160
|
+
internal: kindCounts.get("internal") ?? 0,
|
|
22161
|
+
ip: kindCounts.get("ip") ?? 0
|
|
22162
|
+
};
|
|
22163
|
+
const trustCounts = countBy(
|
|
22164
|
+
this.db,
|
|
22165
|
+
"SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
|
|
22166
|
+
);
|
|
22167
|
+
const byTrust = {
|
|
22168
|
+
recognized: trustCounts.get("recognized") ?? 0,
|
|
22169
|
+
internal: trustCounts.get("internal") ?? 0,
|
|
22170
|
+
unverified: trustCounts.get("unverified") ?? 0,
|
|
22171
|
+
ip: trustCounts.get("ip") ?? 0
|
|
22172
|
+
};
|
|
21460
22173
|
return Promise.resolve({
|
|
21461
22174
|
destinations,
|
|
21462
22175
|
endpoints,
|
|
@@ -21572,7 +22285,7 @@ var SqliteSharesRepository = class {
|
|
|
21572
22285
|
}
|
|
21573
22286
|
let sql;
|
|
21574
22287
|
if (q) {
|
|
21575
|
-
const pattern =
|
|
22288
|
+
const pattern = containsPattern(q);
|
|
21576
22289
|
conditions.push(
|
|
21577
22290
|
`(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
|
|
21578
22291
|
OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
|
|
@@ -21592,24 +22305,31 @@ var SqliteSharesRepository = class {
|
|
|
21592
22305
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
21593
22306
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
21594
22307
|
}
|
|
21595
|
-
const rows =
|
|
22308
|
+
const rows = allRows(
|
|
22309
|
+
this.db.prepare(sql),
|
|
22310
|
+
params
|
|
22311
|
+
);
|
|
21596
22312
|
return rows.map((r) => this.mapDestRow(r));
|
|
21597
22313
|
}
|
|
21598
22314
|
fetchDestinationById(destinationId) {
|
|
21599
|
-
const row =
|
|
21600
|
-
|
|
22315
|
+
const row = getRow(
|
|
22316
|
+
this.db.prepare(
|
|
22317
|
+
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
21601
22318
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
21602
22319
|
o.decision AS overrideDecision
|
|
21603
22320
|
FROM share_destination d
|
|
21604
22321
|
LEFT JOIN egress_decision_override o ON o.destination_id = d.id
|
|
21605
22322
|
WHERE d.id = ?`
|
|
21606
|
-
|
|
22323
|
+
),
|
|
22324
|
+
[destinationId]
|
|
22325
|
+
);
|
|
21607
22326
|
return row ? this.mapDestRow(row) : null;
|
|
21608
22327
|
}
|
|
21609
22328
|
fetchEndpoints(destinationIds) {
|
|
21610
22329
|
if (destinationIds.length === 0) return [];
|
|
21611
|
-
const rows =
|
|
21612
|
-
|
|
22330
|
+
const rows = allRows(
|
|
22331
|
+
this.db.prepare(
|
|
22332
|
+
`SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
|
|
21613
22333
|
e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
|
|
21614
22334
|
count(c.id) AS callSiteCount
|
|
21615
22335
|
FROM share_endpoint e
|
|
@@ -21617,7 +22337,9 @@ var SqliteSharesRepository = class {
|
|
|
21617
22337
|
WHERE e.destination_id IN (${placeholders(destinationIds.length)})
|
|
21618
22338
|
GROUP BY e.id
|
|
21619
22339
|
ORDER BY e.created_at ASC, e.id ASC`
|
|
21620
|
-
|
|
22340
|
+
),
|
|
22341
|
+
destinationIds
|
|
22342
|
+
);
|
|
21621
22343
|
return rows.map((r) => ({
|
|
21622
22344
|
id: r.id,
|
|
21623
22345
|
destinationId: r.destinationId,
|
|
@@ -21642,13 +22364,16 @@ var SqliteSharesRepository = class {
|
|
|
21642
22364
|
}
|
|
21643
22365
|
fetchCallSites(endpointIds) {
|
|
21644
22366
|
if (endpointIds.length === 0) return [];
|
|
21645
|
-
const rows =
|
|
21646
|
-
|
|
22367
|
+
const rows = allRows(
|
|
22368
|
+
this.db.prepare(
|
|
22369
|
+
`SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
|
|
21647
22370
|
project_id AS projectId
|
|
21648
22371
|
FROM share_call_site
|
|
21649
22372
|
WHERE endpoint_id IN (${placeholders(endpointIds.length)})
|
|
21650
22373
|
ORDER BY created_at ASC, id ASC`
|
|
21651
|
-
|
|
22374
|
+
),
|
|
22375
|
+
endpointIds
|
|
22376
|
+
);
|
|
21652
22377
|
return rows.map((r) => ({
|
|
21653
22378
|
id: r.id,
|
|
21654
22379
|
endpointId: r.endpointId,
|
|
@@ -21684,27 +22409,36 @@ var SqliteSourceProjectRepository = class {
|
|
|
21684
22409
|
upsert(input, now = Date.now()) {
|
|
21685
22410
|
const id = sourceProjectId(input.url);
|
|
21686
22411
|
const row = toSourceProjectRow(input, id, now);
|
|
21687
|
-
this.upsertStmt.run(
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
21692
|
-
|
|
21693
|
-
|
|
21694
|
-
|
|
22412
|
+
this.upsertStmt.run(
|
|
22413
|
+
bindParams({
|
|
22414
|
+
id: row.id,
|
|
22415
|
+
url: row.url,
|
|
22416
|
+
name: row.name,
|
|
22417
|
+
attributes: row.attributes,
|
|
22418
|
+
firstSeen: row.firstSeen,
|
|
22419
|
+
lastSeen: row.lastSeen
|
|
22420
|
+
})
|
|
22421
|
+
);
|
|
21695
22422
|
return id;
|
|
21696
22423
|
}
|
|
21697
22424
|
findById(id) {
|
|
21698
|
-
return
|
|
22425
|
+
return getRow(
|
|
22426
|
+
this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
|
|
22427
|
+
{
|
|
22428
|
+
id
|
|
22429
|
+
}
|
|
22430
|
+
);
|
|
21699
22431
|
}
|
|
21700
22432
|
// Distinct project names — a filter facet, served from the source_project
|
|
21701
22433
|
// table, never from the audit fact table.
|
|
21702
22434
|
distinctNames() {
|
|
21703
|
-
const rows =
|
|
21704
|
-
|
|
22435
|
+
const rows = allRows(
|
|
22436
|
+
this.db.prepare(
|
|
22437
|
+
`SELECT DISTINCT name FROM source_project
|
|
21705
22438
|
WHERE name IS NOT NULL
|
|
21706
22439
|
ORDER BY name`
|
|
21707
|
-
|
|
22440
|
+
)
|
|
22441
|
+
);
|
|
21708
22442
|
return rows.map((r) => r.name);
|
|
21709
22443
|
}
|
|
21710
22444
|
};
|
|
@@ -21723,8 +22457,7 @@ function hasLegacySampleRows(db) {
|
|
|
21723
22457
|
function purgeSampleData(db) {
|
|
21724
22458
|
try {
|
|
21725
22459
|
if (!hasLegacySampleRows(db)) return;
|
|
21726
|
-
db
|
|
21727
|
-
try {
|
|
22460
|
+
withTransaction(db, () => {
|
|
21728
22461
|
db.exec(
|
|
21729
22462
|
`DELETE FROM share_call_site WHERE endpoint_id IN (
|
|
21730
22463
|
SELECT e.id FROM share_endpoint e
|
|
@@ -21769,11 +22502,7 @@ function purgeSampleData(db) {
|
|
|
21769
22502
|
value TEXT NOT NULL
|
|
21770
22503
|
)`);
|
|
21771
22504
|
db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
|
|
21772
|
-
|
|
21773
|
-
} catch (err) {
|
|
21774
|
-
db.exec("ROLLBACK");
|
|
21775
|
-
throw err;
|
|
21776
|
-
}
|
|
22505
|
+
});
|
|
21777
22506
|
} catch {
|
|
21778
22507
|
}
|
|
21779
22508
|
}
|
|
@@ -21792,7 +22521,7 @@ function openWithPragmas(file2) {
|
|
|
21792
22521
|
function backupLegacyStore(file2) {
|
|
21793
22522
|
const backup = `${file2}.legacy.${String(Date.now())}.bak`;
|
|
21794
22523
|
renameSync(file2, backup);
|
|
21795
|
-
for (const sidecar of
|
|
22524
|
+
for (const sidecar of walSidecars(file2)) {
|
|
21796
22525
|
if (existsSync(sidecar)) rmSync(sidecar);
|
|
21797
22526
|
}
|
|
21798
22527
|
return backup;
|
|
@@ -21805,9 +22534,8 @@ function openLocalDatabase(dir) {
|
|
|
21805
22534
|
db.close();
|
|
21806
22535
|
const backup = backupLegacyStore(file2);
|
|
21807
22536
|
db = openWithPragmas(file2);
|
|
21808
|
-
|
|
21809
|
-
`
|
|
21810
|
-
`
|
|
22537
|
+
akaWarn(
|
|
22538
|
+
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
21811
22539
|
);
|
|
21812
22540
|
}
|
|
21813
22541
|
applyMigrations(db);
|
|
@@ -21835,96 +22563,77 @@ function openLocalDatabase(dir) {
|
|
|
21835
22563
|
const configInventory = new SqliteConfigInventoryRepository(db);
|
|
21836
22564
|
policies.seedDefaults();
|
|
21837
22565
|
function recordCapture(event, detected) {
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21843
|
-
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
21844
|
-
db.exec("COMMIT");
|
|
21845
|
-
} catch (err) {
|
|
21846
|
-
db.exec("ROLLBACK");
|
|
21847
|
-
throw err;
|
|
21848
|
-
}
|
|
21849
|
-
} catch {
|
|
21850
|
-
}
|
|
22566
|
+
failOpenTransaction(db, () => {
|
|
22567
|
+
events.insertEvent(event);
|
|
22568
|
+
const sessionId = event.metadata?.sessionId;
|
|
22569
|
+
findings.insertFindings(detected, sessionId ? { sessionId } : {});
|
|
22570
|
+
});
|
|
21851
22571
|
}
|
|
21852
22572
|
function ensureInventory(ctx) {
|
|
21853
22573
|
const resolved = {};
|
|
21854
|
-
|
|
21855
|
-
|
|
21856
|
-
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
db.exec("COMMIT");
|
|
21875
|
-
} catch (err) {
|
|
21876
|
-
db.exec("ROLLBACK");
|
|
21877
|
-
throw err;
|
|
21878
|
-
}
|
|
21879
|
-
} catch {
|
|
21880
|
-
return {};
|
|
21881
|
-
}
|
|
21882
|
-
return resolved;
|
|
22574
|
+
const committed = failOpenTransaction(db, () => {
|
|
22575
|
+
const now = Date.now();
|
|
22576
|
+
if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
|
|
22577
|
+
if (ctx.harness) {
|
|
22578
|
+
resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
|
|
22579
|
+
}
|
|
22580
|
+
resolved.accountId = inventory.upsert(
|
|
22581
|
+
linkHost(
|
|
22582
|
+
{
|
|
22583
|
+
objectType: "user",
|
|
22584
|
+
identityKey: "local",
|
|
22585
|
+
attributes: { source: "local" }
|
|
22586
|
+
},
|
|
22587
|
+
resolved.hostId
|
|
22588
|
+
),
|
|
22589
|
+
now
|
|
22590
|
+
);
|
|
22591
|
+
if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
|
|
22592
|
+
});
|
|
22593
|
+
return committed ? resolved : {};
|
|
21883
22594
|
}
|
|
21884
22595
|
function recordConfigScan(record2) {
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
}
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
|
|
21903
|
-
|
|
21904
|
-
|
|
21905
|
-
confidence: finding.confidence
|
|
21906
|
-
});
|
|
21907
|
-
}
|
|
21908
|
-
db.exec("COMMIT");
|
|
21909
|
-
} catch (err) {
|
|
21910
|
-
db.exec("ROLLBACK");
|
|
21911
|
-
throw err;
|
|
22596
|
+
failOpenTransaction(db, () => {
|
|
22597
|
+
const now = isoToEpochMillis(record2.scanEvent.startedAt);
|
|
22598
|
+
for (const item of record2.items) inventory.upsert(item, now);
|
|
22599
|
+
auditEvents.insertAuditEvent(record2.scanEvent);
|
|
22600
|
+
const definitionIds = /* @__PURE__ */ new Map();
|
|
22601
|
+
for (const def of record2.definitions ?? []) {
|
|
22602
|
+
definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
|
|
22603
|
+
}
|
|
22604
|
+
for (const finding of record2.findings ?? []) {
|
|
22605
|
+
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
22606
|
+
if (!definitionId) continue;
|
|
22607
|
+
inspectionFindings.insertFinding({
|
|
22608
|
+
id: randomUUID8(),
|
|
22609
|
+
auditEventId: record2.scanEvent.id,
|
|
22610
|
+
inspectionDefinitionId: definitionId,
|
|
22611
|
+
span: finding.span,
|
|
22612
|
+
maskedMatch: finding.maskedMatch,
|
|
22613
|
+
actionTaken: finding.actionTaken,
|
|
22614
|
+
confidence: finding.confidence
|
|
22615
|
+
});
|
|
21912
22616
|
}
|
|
21913
|
-
}
|
|
21914
|
-
}
|
|
22617
|
+
});
|
|
21915
22618
|
}
|
|
21916
22619
|
function recordProjectFiles(projectId, scan2) {
|
|
21917
22620
|
if (scan2.files.length === 0) return;
|
|
22621
|
+
failOpenTransaction(db, () => {
|
|
22622
|
+
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
22623
|
+
});
|
|
22624
|
+
}
|
|
22625
|
+
async function transaction(fn) {
|
|
22626
|
+
db.exec("BEGIN");
|
|
21918
22627
|
try {
|
|
21919
|
-
|
|
22628
|
+
const result = await fn();
|
|
22629
|
+
db.exec("COMMIT");
|
|
22630
|
+
return result;
|
|
22631
|
+
} catch (err) {
|
|
21920
22632
|
try {
|
|
21921
|
-
projectFiles.replaceForProject(projectId, scan2, Date.now());
|
|
21922
|
-
db.exec("COMMIT");
|
|
21923
|
-
} catch (err) {
|
|
21924
22633
|
db.exec("ROLLBACK");
|
|
21925
|
-
|
|
22634
|
+
} catch {
|
|
21926
22635
|
}
|
|
21927
|
-
|
|
22636
|
+
throw err;
|
|
21928
22637
|
}
|
|
21929
22638
|
}
|
|
21930
22639
|
function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
|
|
@@ -21943,8 +22652,7 @@ function openLocalDatabase(dir) {
|
|
|
21943
22652
|
patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
|
|
21944
22653
|
});
|
|
21945
22654
|
if (stale.length === 0) return;
|
|
21946
|
-
db
|
|
21947
|
-
try {
|
|
22655
|
+
withTransaction(db, () => {
|
|
21948
22656
|
for (const { id } of stale) {
|
|
21949
22657
|
db.prepare(
|
|
21950
22658
|
"UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
|
|
@@ -21956,11 +22664,7 @@ function openLocalDatabase(dir) {
|
|
|
21956
22664
|
db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
|
|
21957
22665
|
db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
|
|
21958
22666
|
}
|
|
21959
|
-
|
|
21960
|
-
} catch (err) {
|
|
21961
|
-
db.exec("ROLLBACK");
|
|
21962
|
-
throw err;
|
|
21963
|
-
}
|
|
22667
|
+
});
|
|
21964
22668
|
} catch {
|
|
21965
22669
|
}
|
|
21966
22670
|
}
|
|
@@ -22002,6 +22706,7 @@ function openLocalDatabase(dir) {
|
|
|
22002
22706
|
purgeSampleData: () => {
|
|
22003
22707
|
purgeSampleData(db);
|
|
22004
22708
|
},
|
|
22709
|
+
transaction,
|
|
22005
22710
|
close: () => {
|
|
22006
22711
|
db.close();
|
|
22007
22712
|
}
|
|
@@ -22122,12 +22827,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
22122
22827
|
}
|
|
22123
22828
|
}
|
|
22124
22829
|
function readJson(file2) {
|
|
22830
|
+
let text;
|
|
22125
22831
|
try {
|
|
22126
|
-
|
|
22127
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
22832
|
+
text = readFileSync2(file2, "utf8");
|
|
22128
22833
|
} catch {
|
|
22129
22834
|
return null;
|
|
22130
22835
|
}
|
|
22836
|
+
return parseJsonObject(text) ?? null;
|
|
22837
|
+
}
|
|
22838
|
+
|
|
22839
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
22840
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
22841
|
+
import { join as join5 } from "path";
|
|
22842
|
+
var MARKER = "warn-era-capped";
|
|
22843
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
22844
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
22845
|
+
const marker = join5(dataDir2, MARKER);
|
|
22846
|
+
if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
|
|
22847
|
+
const capped = db.policies.capCategoryActions();
|
|
22848
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
22849
|
+
`, { mode: DATA_FILE_MODE });
|
|
22850
|
+
return { capped };
|
|
22131
22851
|
}
|
|
22132
22852
|
|
|
22133
22853
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
@@ -22212,23 +22932,30 @@ function resolveProviderSafe() {
|
|
|
22212
22932
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
22213
22933
|
import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
22214
22934
|
import { homedir as homedir2 } from "os";
|
|
22215
|
-
import { basename as basename2, join as
|
|
22935
|
+
import { basename as basename2, join as join7 } from "path";
|
|
22936
|
+
|
|
22937
|
+
// ../../packages/detections/src/escape-regexp.ts
|
|
22938
|
+
function escapeRegExp(value) {
|
|
22939
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22940
|
+
}
|
|
22941
|
+
|
|
22942
|
+
// ../../packages/detections/src/matchers/limits.ts
|
|
22943
|
+
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22216
22944
|
|
|
22217
22945
|
// ../../packages/detections/src/matchers/keyword.ts
|
|
22218
22946
|
var KeywordMatcher2 = class {
|
|
22219
22947
|
match(text, rule) {
|
|
22220
22948
|
if (rule.matcher.type !== "keyword") return [];
|
|
22221
22949
|
const { keywords, caseSensitive } = rule.matcher;
|
|
22222
|
-
const haystack = caseSensitive ? text : text.toLowerCase();
|
|
22223
22950
|
const spans = [];
|
|
22224
22951
|
for (const kw of keywords) {
|
|
22225
|
-
|
|
22226
|
-
|
|
22227
|
-
|
|
22228
|
-
|
|
22229
|
-
|
|
22230
|
-
spans.push({ start:
|
|
22231
|
-
|
|
22952
|
+
if (kw.length === 0) continue;
|
|
22953
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22954
|
+
const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
|
|
22955
|
+
let m;
|
|
22956
|
+
while ((m = re.exec(text)) !== null) {
|
|
22957
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
22958
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22232
22959
|
}
|
|
22233
22960
|
}
|
|
22234
22961
|
return spans;
|
|
@@ -22236,7 +22963,6 @@ var KeywordMatcher2 = class {
|
|
|
22236
22963
|
};
|
|
22237
22964
|
|
|
22238
22965
|
// ../../packages/detections/src/matchers/regex.ts
|
|
22239
|
-
var MAX_MATCHES_PER_RULE = 1e4;
|
|
22240
22966
|
var RegexMatcher2 = class {
|
|
22241
22967
|
match(text, rule) {
|
|
22242
22968
|
if (rule.matcher.type !== "regex") return [];
|
|
@@ -22322,9 +23048,6 @@ function registerPack(pack) {
|
|
|
22322
23048
|
function getLoadedRules() {
|
|
22323
23049
|
return [...packs.values()].flatMap((p) => p.rules);
|
|
22324
23050
|
}
|
|
22325
|
-
function escapeRegExp(value) {
|
|
22326
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22327
|
-
}
|
|
22328
23051
|
function isCorroborated(candidate, candidates, text) {
|
|
22329
23052
|
const req = candidate.rule.requiresNearby;
|
|
22330
23053
|
if (!req) return true;
|
|
@@ -23106,7 +23829,6 @@ var db_table_name_default = {
|
|
|
23106
23829
|
"SELECT * FROM ",
|
|
23107
23830
|
"SELECT COUNT(*) FROM ",
|
|
23108
23831
|
"INSERT INTO ",
|
|
23109
|
-
"UPDATE ",
|
|
23110
23832
|
"DELETE FROM ",
|
|
23111
23833
|
"CREATE TABLE ",
|
|
23112
23834
|
"ALTER TABLE ",
|
|
@@ -24591,8 +25313,8 @@ function scanText(text) {
|
|
|
24591
25313
|
}
|
|
24592
25314
|
|
|
24593
25315
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
24594
|
-
import { existsSync as
|
|
24595
|
-
import { basename, dirname, isAbsolute, join as
|
|
25316
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25317
|
+
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
24596
25318
|
function resolveRepoIdentity(cwd) {
|
|
24597
25319
|
try {
|
|
24598
25320
|
const root = findGitRoot(cwd);
|
|
@@ -24625,32 +25347,32 @@ function resolveRepoNwo(cwd) {
|
|
|
24625
25347
|
function findGitRoot(start) {
|
|
24626
25348
|
let dir = start;
|
|
24627
25349
|
for (; ; ) {
|
|
24628
|
-
if (
|
|
25350
|
+
if (existsSync3(join6(dir, ".git"))) return dir;
|
|
24629
25351
|
const parent = dirname(dir);
|
|
24630
25352
|
if (parent === dir) return void 0;
|
|
24631
25353
|
dir = parent;
|
|
24632
25354
|
}
|
|
24633
25355
|
}
|
|
24634
25356
|
function resolveGitContext(root) {
|
|
24635
|
-
const dotGit =
|
|
25357
|
+
const dotGit = join6(root, ".git");
|
|
24636
25358
|
try {
|
|
24637
25359
|
if (statSync(dotGit).isDirectory()) {
|
|
24638
|
-
return { configPath:
|
|
25360
|
+
return { configPath: join6(dotGit, "config"), headRoot: root };
|
|
24639
25361
|
}
|
|
24640
25362
|
} catch {
|
|
24641
25363
|
return void 0;
|
|
24642
25364
|
}
|
|
24643
25365
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
24644
25366
|
if (!target) return void 0;
|
|
24645
|
-
const gitdir = isAbsolute(target) ? target :
|
|
24646
|
-
if (
|
|
24647
|
-
return { configPath:
|
|
25367
|
+
const gitdir = isAbsolute(target) ? target : join6(root, target);
|
|
25368
|
+
if (existsSync3(join6(gitdir, "config"))) {
|
|
25369
|
+
return { configPath: join6(gitdir, "config"), headRoot: root };
|
|
24648
25370
|
}
|
|
24649
|
-
const commonRaw = safeRead(
|
|
25371
|
+
const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
|
|
24650
25372
|
if (!commonRaw) return void 0;
|
|
24651
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
25373
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
|
|
24652
25374
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
24653
|
-
return { configPath:
|
|
25375
|
+
return { configPath: join6(commonGitDir, "config"), headRoot };
|
|
24654
25376
|
}
|
|
24655
25377
|
function safeRead(path) {
|
|
24656
25378
|
try {
|
|
@@ -24767,16 +25489,57 @@ function resolveInventoryContext(input) {
|
|
|
24767
25489
|
}
|
|
24768
25490
|
|
|
24769
25491
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
24770
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as
|
|
24771
|
-
import { join as
|
|
25492
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25493
|
+
import { join as join8 } from "path";
|
|
24772
25494
|
|
|
24773
25495
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
24774
25496
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
24775
|
-
import { existsSync as
|
|
24776
|
-
import { basename as basename3, join as
|
|
25497
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
25498
|
+
import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
|
|
25499
|
+
|
|
25500
|
+
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
25501
|
+
var RawEgressError = class extends Error {
|
|
25502
|
+
constructor(message) {
|
|
25503
|
+
super(message);
|
|
25504
|
+
this.name = "RawEgressError";
|
|
25505
|
+
}
|
|
25506
|
+
};
|
|
25507
|
+
var MIN_RAW_LEN = 4;
|
|
25508
|
+
function maskContextSlice(slice, sliceStart, hits) {
|
|
25509
|
+
const findings = [];
|
|
25510
|
+
for (const h of hits) {
|
|
25511
|
+
const start = Math.max(0, h.span.start - sliceStart);
|
|
25512
|
+
const end = Math.min(slice.length, h.span.end - sliceStart);
|
|
25513
|
+
if (end > start) {
|
|
25514
|
+
findings.push({
|
|
25515
|
+
ruleId: "raw-egress",
|
|
25516
|
+
category: "secret",
|
|
25517
|
+
severity: "critical",
|
|
25518
|
+
span: { start, end },
|
|
25519
|
+
rawMatch: "",
|
|
25520
|
+
confidence: 1
|
|
25521
|
+
});
|
|
25522
|
+
}
|
|
25523
|
+
}
|
|
25524
|
+
const masked = findings.length > 0 ? redact(slice, findings) : slice;
|
|
25525
|
+
for (const h of hits) {
|
|
25526
|
+
if (h.rawMatch.length >= MIN_RAW_LEN && masked.includes(h.rawMatch)) {
|
|
25527
|
+
throw new RawEgressError("raw match survived context masking");
|
|
25528
|
+
}
|
|
25529
|
+
}
|
|
25530
|
+
return masked;
|
|
25531
|
+
}
|
|
25532
|
+
function safeMaskedMatch(rawMatch) {
|
|
25533
|
+
const masked = maskMatch(rawMatch);
|
|
25534
|
+
if (masked === rawMatch || rawMatch.length >= MIN_RAW_LEN && masked.includes(rawMatch)) {
|
|
25535
|
+
return "***";
|
|
25536
|
+
}
|
|
25537
|
+
return masked;
|
|
25538
|
+
}
|
|
24777
25539
|
|
|
24778
25540
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
24779
25541
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
25542
|
+
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
24780
25543
|
var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
|
|
24781
25544
|
function entryIsActive(entry, now) {
|
|
24782
25545
|
if (entry.expiresAt !== null && Date.parse(entry.expiresAt) <= now) return false;
|
|
@@ -24849,17 +25612,22 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24849
25612
|
const fallback = DEFAULT_ACTIONS[category];
|
|
24850
25613
|
return fallback ?? "log";
|
|
24851
25614
|
}
|
|
25615
|
+
function actionForFinding(finding, excepted) {
|
|
25616
|
+
if (excepted?.has(finding)) return "allow";
|
|
25617
|
+
const action = resolveAction(finding.ruleId, finding.category);
|
|
25618
|
+
if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
|
|
25619
|
+
return "warn";
|
|
25620
|
+
}
|
|
25621
|
+
return action;
|
|
25622
|
+
}
|
|
24852
25623
|
function decide(findings, text, excepted) {
|
|
24853
25624
|
if (findings.length === 0) return { action: "log", text, findings: [] };
|
|
24854
|
-
const actionFor = (finding) =>
|
|
25625
|
+
const actionFor = (finding) => actionForFinding(finding, excepted);
|
|
24855
25626
|
let worst = "log";
|
|
24856
25627
|
for (const finding of findings) {
|
|
24857
25628
|
const action = actionFor(finding);
|
|
24858
25629
|
if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
|
|
24859
25630
|
}
|
|
24860
|
-
if (policyMode === "warn" && (worst === "block" || worst === "redact")) {
|
|
24861
|
-
return { action: "warn", text, findings };
|
|
24862
|
-
}
|
|
24863
25631
|
if (worst === "block") return { action: "block", text: null, findings };
|
|
24864
25632
|
if (worst === "redact") {
|
|
24865
25633
|
const redactFindings = findings.filter((f) => actionFor(f) === "redact");
|
|
@@ -24879,7 +25647,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24879
25647
|
const excepted = /* @__PURE__ */ new Set();
|
|
24880
25648
|
const exceptionIds = [];
|
|
24881
25649
|
try {
|
|
24882
|
-
if (policyMode === "warn") return { excepted, exceptionIds };
|
|
25650
|
+
if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn") return { excepted, exceptionIds };
|
|
24883
25651
|
const enforced = findings.filter((f) => {
|
|
24884
25652
|
const action = resolveAction(f.ruleId, f.category);
|
|
24885
25653
|
return action === "block" || action === "redact";
|
|
@@ -24932,8 +25700,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
24932
25700
|
if (!key) return references;
|
|
24933
25701
|
const seen = /* @__PURE__ */ new Set();
|
|
24934
25702
|
for (const finding of decision.findings) {
|
|
24935
|
-
const action =
|
|
24936
|
-
if (action !== "block" && action !== "redact"
|
|
25703
|
+
const action = actionForFinding(finding, excepted);
|
|
25704
|
+
if (action !== "block" && action !== "redact") continue;
|
|
24937
25705
|
const fp = fingerprintOf(key, finding, fpCache);
|
|
24938
25706
|
const pair = `${finding.ruleId}:${fp}`;
|
|
24939
25707
|
if (seen.has(pair)) continue;
|
|
@@ -25022,7 +25790,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25022
25790
|
severity: match.severity,
|
|
25023
25791
|
span: match.span,
|
|
25024
25792
|
maskedMatch,
|
|
25025
|
-
actionTaken:
|
|
25793
|
+
actionTaken: actionForFinding(match, excepted),
|
|
25026
25794
|
confidence: match.confidence,
|
|
25027
25795
|
...findingKey ? { findingKey } : {}
|
|
25028
25796
|
};
|
|
@@ -25050,9 +25818,12 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25050
25818
|
return { processText, capture, rulesetFingerprint, close };
|
|
25051
25819
|
}
|
|
25052
25820
|
|
|
25821
|
+
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
25822
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25823
|
+
|
|
25053
25824
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
25054
|
-
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as
|
|
25055
|
-
import { join as
|
|
25825
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
25826
|
+
import { join as join10 } from "path";
|
|
25056
25827
|
|
|
25057
25828
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25058
25829
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -25245,6 +26016,14 @@ var StandaloneDataGateway = class {
|
|
|
25245
26016
|
sweepTerminalExceptions(retentionMs) {
|
|
25246
26017
|
return this.db.exceptions.sweepTerminal(retentionMs);
|
|
25247
26018
|
}
|
|
26019
|
+
// The warn-era enforcement cap, standalone-only store maintenance invoked
|
|
26020
|
+
// from SessionStart, not part of the DataGateway port. Returns the number
|
|
26021
|
+
// of block/redact rows capped to warn (0 for a redact-policy store or an
|
|
26022
|
+
// already-capped one).
|
|
26023
|
+
capWarnEraEnforcement(policyMode) {
|
|
26024
|
+
const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
|
|
26025
|
+
return { capped };
|
|
26026
|
+
}
|
|
25248
26027
|
// One project-file scan → the local project_file tree (one transaction inside
|
|
25249
26028
|
// the LocalDatabase, fail-open there). Like the sweep above, this is
|
|
25250
26029
|
// NOT part of the DataGateway port: the file tree is a local-store read model.
|
|
@@ -25344,9 +26123,9 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
|
25344
26123
|
// src/history/transcripts.ts
|
|
25345
26124
|
import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
25346
26125
|
import { homedir as homedir3 } from "os";
|
|
25347
|
-
import { join as
|
|
25348
|
-
function transcriptsDir() {
|
|
25349
|
-
return
|
|
26126
|
+
import { join as join11 } from "path";
|
|
26127
|
+
function transcriptsDir(home) {
|
|
26128
|
+
return join11(home ?? homedir3(), ".claude", "projects");
|
|
25350
26129
|
}
|
|
25351
26130
|
function isRecord(value) {
|
|
25352
26131
|
return typeof value === "object" && value !== null;
|
|
@@ -25362,7 +26141,7 @@ function extractText(content) {
|
|
|
25362
26141
|
}
|
|
25363
26142
|
return parts.join("\n");
|
|
25364
26143
|
}
|
|
25365
|
-
function parseTranscript(jsonl, sinceMs = 0) {
|
|
26144
|
+
function parseTranscript(jsonl, sinceMs = 0, beforeMs = Infinity, filePath = "") {
|
|
25366
26145
|
const out = [];
|
|
25367
26146
|
for (const line of jsonl.split("\n")) {
|
|
25368
26147
|
const trimmed = line.trim();
|
|
@@ -25380,11 +26159,12 @@ function parseTranscript(jsonl, sinceMs = 0) {
|
|
|
25380
26159
|
const occurredMs = Date.parse(occurredAt);
|
|
25381
26160
|
if (Number.isNaN(occurredMs)) continue;
|
|
25382
26161
|
if (sinceMs > 0 && occurredMs < sinceMs) continue;
|
|
26162
|
+
if (occurredMs >= beforeMs) continue;
|
|
25383
26163
|
const message = rec.message;
|
|
25384
26164
|
if (!isRecord(message)) continue;
|
|
25385
26165
|
const text = extractText(message.content);
|
|
25386
26166
|
if (text.trim() === "") continue;
|
|
25387
|
-
out.push({ kind: rec.type === "user" ? "prompt" : "response", text, occurredAt });
|
|
26167
|
+
out.push({ kind: rec.type === "user" ? "prompt" : "response", text, occurredAt, filePath });
|
|
25388
26168
|
}
|
|
25389
26169
|
return out;
|
|
25390
26170
|
}
|
|
@@ -25585,7 +26365,7 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
|
|
|
25585
26365
|
return out;
|
|
25586
26366
|
}
|
|
25587
26367
|
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
25588
|
-
function* iterateFileContents(dir) {
|
|
26368
|
+
function* iterateFileContents(dir, excludeSessionId) {
|
|
25589
26369
|
let projects;
|
|
25590
26370
|
try {
|
|
25591
26371
|
projects = readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
@@ -25593,7 +26373,7 @@ function* iterateFileContents(dir) {
|
|
|
25593
26373
|
return;
|
|
25594
26374
|
}
|
|
25595
26375
|
for (const project of projects) {
|
|
25596
|
-
const projectDir =
|
|
26376
|
+
const projectDir = join11(dir, project);
|
|
25597
26377
|
let files;
|
|
25598
26378
|
try {
|
|
25599
26379
|
files = readdirSync3(projectDir).filter((name) => name.endsWith(".jsonl"));
|
|
@@ -25601,13 +26381,16 @@ function* iterateFileContents(dir) {
|
|
|
25601
26381
|
continue;
|
|
25602
26382
|
}
|
|
25603
26383
|
for (const file2 of files) {
|
|
26384
|
+
if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
|
|
26385
|
+
continue;
|
|
26386
|
+
const filePath = join11(projectDir, file2);
|
|
25604
26387
|
let content;
|
|
25605
26388
|
try {
|
|
25606
|
-
content = readFileSync7(
|
|
26389
|
+
content = readFileSync7(filePath, "utf8");
|
|
25607
26390
|
} catch {
|
|
25608
26391
|
continue;
|
|
25609
26392
|
}
|
|
25610
|
-
yield content;
|
|
26393
|
+
yield { content, filePath };
|
|
25611
26394
|
}
|
|
25612
26395
|
}
|
|
25613
26396
|
}
|
|
@@ -25617,12 +26400,16 @@ function windowStartMs(opts) {
|
|
|
25617
26400
|
}
|
|
25618
26401
|
function* iterateHistory(opts = {}) {
|
|
25619
26402
|
const sinceMs = windowStartMs(opts);
|
|
25620
|
-
|
|
25621
|
-
|
|
26403
|
+
const beforeMs = opts.beforeMs ?? Infinity;
|
|
26404
|
+
for (const { content, filePath } of iterateFileContents(
|
|
26405
|
+
opts.dir ?? transcriptsDir(),
|
|
26406
|
+
opts.excludeSessionId
|
|
26407
|
+
))
|
|
26408
|
+
yield* parseTranscript(content, sinceMs, beforeMs, filePath);
|
|
25622
26409
|
}
|
|
25623
26410
|
function* iterateUsageAndToolCalls(opts = {}) {
|
|
25624
26411
|
const sinceMs = windowStartMs(opts);
|
|
25625
|
-
for (const content of iterateFileContents(opts.dir ?? transcriptsDir())) {
|
|
26412
|
+
for (const { content } of iterateFileContents(opts.dir ?? transcriptsDir())) {
|
|
25626
26413
|
yield {
|
|
25627
26414
|
usage: parseTranscriptUsage(content, sinceMs),
|
|
25628
26415
|
toolCalls: parseTranscriptToolCalls(content, sinceMs)
|
|
@@ -25631,7 +26418,36 @@ function* iterateUsageAndToolCalls(opts = {}) {
|
|
|
25631
26418
|
}
|
|
25632
26419
|
|
|
25633
26420
|
// src/history/scan.ts
|
|
25634
|
-
|
|
26421
|
+
var CONTEXT_RADIUS = 120;
|
|
26422
|
+
function redactOverlapping(rawContext, contextStart, others) {
|
|
26423
|
+
if (others.length === 0) return rawContext;
|
|
26424
|
+
try {
|
|
26425
|
+
return maskContextSlice(rawContext, contextStart, others);
|
|
26426
|
+
} catch {
|
|
26427
|
+
let safe = rawContext;
|
|
26428
|
+
for (const other of others) {
|
|
26429
|
+
if (other.rawMatch.length > 0) safe = safe.split(other.rawMatch).join("[REDACTED]");
|
|
26430
|
+
}
|
|
26431
|
+
return safe;
|
|
26432
|
+
}
|
|
26433
|
+
}
|
|
26434
|
+
function buildTriageHit(text, f, otherFindings = [], filePath = "") {
|
|
26435
|
+
const start = Math.max(0, f.span.start - CONTEXT_RADIUS);
|
|
26436
|
+
const end = Math.min(text.length, f.span.end + CONTEXT_RADIUS);
|
|
26437
|
+
const rawContext = text.slice(start, end);
|
|
26438
|
+
const overlapping = otherFindings.filter((o) => o.span.start < end && o.span.end > start);
|
|
26439
|
+
return {
|
|
26440
|
+
ruleId: f.ruleId,
|
|
26441
|
+
category: f.category,
|
|
26442
|
+
severity: f.severity,
|
|
26443
|
+
maskedMatch: safeMaskedMatch(f.rawMatch),
|
|
26444
|
+
rawMatch: f.rawMatch,
|
|
26445
|
+
context: redactOverlapping(rawContext, start, overlapping),
|
|
26446
|
+
confidence: f.confidence,
|
|
26447
|
+
...filePath !== "" ? { filePath } : {}
|
|
26448
|
+
};
|
|
26449
|
+
}
|
|
26450
|
+
async function scanHistory(config2, opts = {}, onHit) {
|
|
25635
26451
|
const windowDays = opts.windowDays ?? 30;
|
|
25636
26452
|
if (config2.settings.historicalAccess !== "full") {
|
|
25637
26453
|
return { consented: false, scanned: 0, skipped: 0, findings: 0, bySeverity: {}, windowDays };
|
|
@@ -25667,6 +26483,14 @@ async function scanHistory(config2, opts = {}) {
|
|
|
25667
26483
|
for (const finding of result.findings) {
|
|
25668
26484
|
findings++;
|
|
25669
26485
|
bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1;
|
|
26486
|
+
if (onHit) {
|
|
26487
|
+
const otherFindings = result.findings.filter((other) => other !== finding);
|
|
26488
|
+
const hit = buildTriageHit(message.text, finding, otherFindings, message.filePath);
|
|
26489
|
+
try {
|
|
26490
|
+
onHit(hit);
|
|
26491
|
+
} catch {
|
|
26492
|
+
}
|
|
26493
|
+
}
|
|
25670
26494
|
}
|
|
25671
26495
|
}
|
|
25672
26496
|
} finally {
|
|
@@ -25684,9 +26508,9 @@ import {
|
|
|
25684
26508
|
openSync,
|
|
25685
26509
|
readFileSync as readFileSync8,
|
|
25686
26510
|
readSync,
|
|
25687
|
-
writeFileSync as
|
|
26511
|
+
writeFileSync as writeFileSync6
|
|
25688
26512
|
} from "fs";
|
|
25689
|
-
import { join as
|
|
26513
|
+
import { join as join12 } from "path";
|
|
25690
26514
|
|
|
25691
26515
|
// src/history/usage.ts
|
|
25692
26516
|
var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
|
|
@@ -25929,25 +26753,116 @@ function fenced(body) {
|
|
|
25929
26753
|
}
|
|
25930
26754
|
|
|
25931
26755
|
// src/backfill.ts
|
|
25932
|
-
|
|
25933
|
-
|
|
25934
|
-
|
|
25935
|
-
|
|
25936
|
-
|
|
25937
|
-
}
|
|
25938
|
-
const summary = await scanHistory(cfg);
|
|
26756
|
+
var TRIAGE_STATUSES = ["complete", "complete:no-history", "skipped:no-consent"];
|
|
26757
|
+
function triageSentinel(count, status) {
|
|
26758
|
+
return JSON.stringify({ done: true, count, status }) + "\n";
|
|
26759
|
+
}
|
|
26760
|
+
async function runBackfill(deps) {
|
|
26761
|
+
const { triage, io } = deps;
|
|
25939
26762
|
try {
|
|
25940
|
-
|
|
25941
|
-
|
|
25942
|
-
|
|
25943
|
-
|
|
25944
|
-
|
|
25945
|
-
|
|
25946
|
-
|
|
26763
|
+
const cfg = deps.loadConfig();
|
|
26764
|
+
if (cfg.settings.historicalAccess !== "full") {
|
|
26765
|
+
if (triage) {
|
|
26766
|
+
io.stdout(triageSentinel(0, "skipped:no-consent"));
|
|
26767
|
+
} else {
|
|
26768
|
+
io.stdout("Historical scan skipped \u2014 full review was not granted.\n");
|
|
26769
|
+
}
|
|
26770
|
+
return;
|
|
26771
|
+
}
|
|
26772
|
+
let fpKey = null;
|
|
26773
|
+
if (triage) {
|
|
26774
|
+
try {
|
|
26775
|
+
fpKey = loadOrCreateFingerprintKey(cfg.dataDir);
|
|
26776
|
+
} catch {
|
|
26777
|
+
fpKey = null;
|
|
26778
|
+
}
|
|
26779
|
+
}
|
|
26780
|
+
let count = 0;
|
|
26781
|
+
let onHitError;
|
|
26782
|
+
const summary = await deps.scanHistory(
|
|
26783
|
+
cfg,
|
|
26784
|
+
deps.guard ?? {},
|
|
26785
|
+
triage ? (hit) => {
|
|
26786
|
+
if (onHitError !== void 0) return;
|
|
26787
|
+
try {
|
|
26788
|
+
const enriched = {
|
|
26789
|
+
...hit,
|
|
26790
|
+
id: String(count),
|
|
26791
|
+
valueFingerprint: fpKey ? fingerprintValue(fpKey, hit.rawMatch) : void 0,
|
|
26792
|
+
keyVersion: fpKey?.version
|
|
26793
|
+
};
|
|
26794
|
+
const validated = TriageHit.safeParse(enriched);
|
|
26795
|
+
if (!validated.success) {
|
|
26796
|
+
throw new Error("enriched triage hit failed TriageHit validation");
|
|
26797
|
+
}
|
|
26798
|
+
io.stdout(JSON.stringify(validated.data) + "\n");
|
|
26799
|
+
count += 1;
|
|
26800
|
+
} catch (err) {
|
|
26801
|
+
onHitError = err;
|
|
26802
|
+
}
|
|
26803
|
+
} : void 0
|
|
26804
|
+
);
|
|
26805
|
+
if (onHitError !== void 0) {
|
|
26806
|
+
throw onHitError instanceof Error ? onHitError : new Error(typeof onHitError === "string" ? onHitError : "triage stream write failed");
|
|
26807
|
+
}
|
|
26808
|
+
try {
|
|
26809
|
+
await deps.reconcileHistory(cfg);
|
|
26810
|
+
} catch {
|
|
26811
|
+
}
|
|
26812
|
+
if (triage) {
|
|
26813
|
+
const status = summary.scanned === 0 && summary.skipped === 0 ? "complete:no-history" : "complete";
|
|
26814
|
+
io.stdout(triageSentinel(count, status));
|
|
26815
|
+
} else {
|
|
26816
|
+
const heading = "\u2713 Historical scan complete";
|
|
26817
|
+
const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
|
|
26818
|
+
const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
|
|
26819
|
+
io.stdout(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
|
|
25947
26820
|
`);
|
|
25948
|
-
}
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
26821
|
+
}
|
|
26822
|
+
} catch (err) {
|
|
26823
|
+
if (triage) {
|
|
26824
|
+
io.stderr(`aka backfill --triage: history scan failed: ${String(err)}
|
|
26825
|
+
`);
|
|
26826
|
+
io.fail();
|
|
26827
|
+
} else {
|
|
26828
|
+
io.stdout(
|
|
26829
|
+
"AKA could not scan your history right now. It will still protect everything from here on.\n"
|
|
26830
|
+
);
|
|
26831
|
+
}
|
|
26832
|
+
}
|
|
26833
|
+
}
|
|
26834
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
26835
|
+
const triage = process.argv.includes("--triage");
|
|
26836
|
+
const startedAt = Date.now();
|
|
26837
|
+
const sessionId = process.env.CLAUDE_CODE_BRIDGE_SESSION_ID;
|
|
26838
|
+
await runBackfill({
|
|
26839
|
+
triage,
|
|
26840
|
+
io: {
|
|
26841
|
+
stdout: (chunk) => process.stdout.write(chunk),
|
|
26842
|
+
stderr: (chunk) => process.stderr.write(chunk),
|
|
26843
|
+
fail: () => {
|
|
26844
|
+
process.exitCode = 1;
|
|
26845
|
+
}
|
|
26846
|
+
},
|
|
26847
|
+
loadConfig: () => loadConfig(),
|
|
26848
|
+
scanHistory,
|
|
26849
|
+
reconcileHistory: (cfg) => reconcileHistory(cfg),
|
|
26850
|
+
guard: {
|
|
26851
|
+
beforeMs: startedAt,
|
|
26852
|
+
...sessionId ? { excludeSessionId: sessionId } : {}
|
|
26853
|
+
}
|
|
26854
|
+
});
|
|
26855
|
+
if (process.stdout.writableLength > 0) {
|
|
26856
|
+
await new Promise((resolve) => {
|
|
26857
|
+
process.stdout.write("", () => {
|
|
26858
|
+
resolve();
|
|
26859
|
+
});
|
|
26860
|
+
});
|
|
26861
|
+
}
|
|
26862
|
+
process.exit(process.exitCode ?? 0);
|
|
25952
26863
|
}
|
|
25953
|
-
|
|
26864
|
+
export {
|
|
26865
|
+
TRIAGE_STATUSES,
|
|
26866
|
+
runBackfill,
|
|
26867
|
+
triageSentinel
|
|
26868
|
+
};
|