@spool-lab/core 0.4.12 → 0.5.0
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/README.md +2 -2
- package/dist/db/db.d.ts +10 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +119 -10
- package/dist/db/db.js.map +1 -1
- package/dist/db/queries.d.ts +5 -5
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +32 -14
- package/dist/db/queries.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/observability/exporter-file.d.ts +30 -0
- package/dist/observability/exporter-file.d.ts.map +1 -0
- package/dist/observability/exporter-file.js +125 -0
- package/dist/observability/exporter-file.js.map +1 -0
- package/dist/observability/exporter-pretty.d.ts +27 -0
- package/dist/observability/exporter-pretty.d.ts.map +1 -0
- package/dist/observability/exporter-pretty.js +84 -0
- package/dist/observability/exporter-pretty.js.map +1 -0
- package/dist/observability/index.d.ts +2 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +2 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/layer.d.ts +29 -0
- package/dist/observability/layer.d.ts.map +1 -0
- package/dist/observability/layer.js +45 -0
- package/dist/observability/layer.js.map +1 -0
- package/dist/parsers/opencode.d.ts +23 -0
- package/dist/parsers/opencode.d.ts.map +1 -0
- package/dist/parsers/opencode.js +358 -0
- package/dist/parsers/opencode.js.map +1 -0
- package/dist/projects/sessions.d.ts +29 -1
- package/dist/projects/sessions.d.ts.map +1 -1
- package/dist/projects/sessions.js +115 -11
- package/dist/projects/sessions.js.map +1 -1
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +7 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/maintenance.d.ts +15 -0
- package/dist/security/maintenance.d.ts.map +1 -0
- package/dist/security/maintenance.js +68 -0
- package/dist/security/maintenance.js.map +1 -0
- package/dist/security/profile.d.ts +47 -0
- package/dist/security/profile.d.ts.map +1 -0
- package/dist/security/profile.js +131 -0
- package/dist/security/profile.js.map +1 -0
- package/dist/security/purge.d.ts +68 -0
- package/dist/security/purge.d.ts.map +1 -0
- package/dist/security/purge.js +193 -0
- package/dist/security/purge.js.map +1 -0
- package/dist/security/repo.d.ts +190 -0
- package/dist/security/repo.d.ts.map +1 -0
- package/dist/security/repo.js +594 -0
- package/dist/security/repo.js.map +1 -0
- package/dist/security/scan.d.ts +50 -0
- package/dist/security/scan.d.ts.map +1 -0
- package/dist/security/scan.js +122 -0
- package/dist/security/scan.js.map +1 -0
- package/dist/security/types.d.ts +146 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +10 -0
- package/dist/security/types.js.map +1 -0
- package/dist/security/worker.d.ts +69 -0
- package/dist/security/worker.d.ts.map +1 -0
- package/dist/security/worker.js +228 -0
- package/dist/security/worker.js.map +1 -0
- package/dist/sync/source-paths.d.ts +1 -0
- package/dist/sync/source-paths.d.ts.map +1 -1
- package/dist/sync/source-paths.js +48 -6
- package/dist/sync/source-paths.js.map +1 -1
- package/dist/sync/syncer.d.ts +9 -1
- package/dist/sync/syncer.d.ts.map +1 -1
- package/dist/sync/syncer.js +128 -16
- package/dist/sync/syncer.js.map +1 -1
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +8 -2
- package/dist/sync/watcher.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/util/resolve-bin.d.ts +34 -0
- package/dist/util/resolve-bin.d.ts.map +1 -1
- package/dist/util/resolve-bin.js +175 -20
- package/dist/util/resolve-bin.js.map +1 -1
- package/package.json +11 -2
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// Findings + allowlist repository.
|
|
2
|
+
//
|
|
3
|
+
// Plain better-sqlite3 prepared statements. No Effect here — this
|
|
4
|
+
// module is called from both the scan worker (Effect, wraps these in
|
|
5
|
+
// Effect.sync) and the IPC handlers (Promise-shaped). Keeping it
|
|
6
|
+
// effect-free preserves the worker / boundary split documented in
|
|
7
|
+
// the spec's "Implementation: Effect TS scope" section.
|
|
8
|
+
//
|
|
9
|
+
// Invariant: `findings.value_hash` is FNV-1a, computed via the
|
|
10
|
+
// share-kit-aligned `hashValueForRedactExclude` so identity matches
|
|
11
|
+
// across surfaces (share editor's exclude list, security allowlists).
|
|
12
|
+
import { HIGH_SEVERITY_KINDS, INFO_SEVERITY_KINDS, severityOf, } from '@spool-lab/redact';
|
|
13
|
+
function rowToFinding(r) {
|
|
14
|
+
return {
|
|
15
|
+
id: r.id,
|
|
16
|
+
sessionId: r.session_id,
|
|
17
|
+
messageId: r.message_id,
|
|
18
|
+
kind: r.kind,
|
|
19
|
+
valueHash: r.value_hash,
|
|
20
|
+
confidence: r.confidence,
|
|
21
|
+
provider: r.provider,
|
|
22
|
+
startOffset: r.start_offset,
|
|
23
|
+
endOffset: r.end_offset,
|
|
24
|
+
state: r.state,
|
|
25
|
+
detectedAt: r.detected_at,
|
|
26
|
+
stateChangedAt: r.state_changed_at,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Insert a batch of findings. Caller wraps in a transaction. */
|
|
30
|
+
export function insertFindings(db, rows) {
|
|
31
|
+
if (rows.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
const stmt = db.prepare(`INSERT INTO findings
|
|
34
|
+
(session_id, message_id, kind, value_hash, confidence, provider,
|
|
35
|
+
start_offset, end_offset, state, state_changed_at)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
37
|
+
for (const r of rows) {
|
|
38
|
+
stmt.run(r.sessionId, r.messageId, r.kind, r.valueHash, r.confidence, r.provider, r.startOffset, r.endOffset, r.state, r.state !== 'active' ? new Date().toISOString() : null);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Delete all non-purged rows for the providers being rescanned —
|
|
42
|
+
* i.e. wipe the producer's previous output so the upcoming
|
|
43
|
+
* `insertFindings` is the canonical truth.
|
|
44
|
+
*
|
|
45
|
+
* Includes `state='dismissed'` deliberately: those rows are derived
|
|
46
|
+
* state. The user's "ignore" decisions live in `allowlist_session`
|
|
47
|
+
* and `allowlist_global` (preserved here), and the scan path
|
|
48
|
+
* re-emits findings as `state='dismissed'` whenever it encounters a
|
|
49
|
+
* hit that matches an allowlist row. Re-inserting also keeps
|
|
50
|
+
* `findings.value_hash` aligned with the per-kind allowlist
|
|
51
|
+
* preference (security.json `kindAllowlist`) — without this delete,
|
|
52
|
+
* a mute → unmute cycle would accumulate phantom dismissed rows
|
|
53
|
+
* (one set per cycle), inflating audit counts and breaking
|
|
54
|
+
* `riskByCategory` totals over time.
|
|
55
|
+
*
|
|
56
|
+
* Purged rows are the only state that's NOT producer-derived —
|
|
57
|
+
* they correspond to destructive `messages.content_text` rewrites
|
|
58
|
+
* the user explicitly approved. Preserving them is the audit
|
|
59
|
+
* contract documented in the design doc. */
|
|
60
|
+
export function deleteRefreshableFindings(db, sessionId, providers) {
|
|
61
|
+
if (providers.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
const placeholders = providers.map(() => '?').join(',');
|
|
64
|
+
db.prepare(`DELETE FROM findings
|
|
65
|
+
WHERE session_id = ?
|
|
66
|
+
AND state IN ('active','dismissed')
|
|
67
|
+
AND provider IN (${placeholders})`).run(sessionId, ...providers);
|
|
68
|
+
}
|
|
69
|
+
/** @deprecated Renamed to {@link deleteRefreshableFindings} after the
|
|
70
|
+
* active-only filter was found to leak phantom dismissed rows
|
|
71
|
+
* across mute→unmute cycles. Kept as a thin alias so callers
|
|
72
|
+
* outside the repo don't break mid-stack. */
|
|
73
|
+
export const deleteActiveFindings = deleteRefreshableFindings;
|
|
74
|
+
// ─── Counts (denormalised on sessions) ────────────────────────────
|
|
75
|
+
/** Recompute sessions.scan_finding_count + scan_high_count from the
|
|
76
|
+
* findings table.
|
|
77
|
+
*
|
|
78
|
+
* Counts EXCLUDE info-tier kinds (absolute-path, ip, internal-host).
|
|
79
|
+
* Those are stored — useful for an opt-in "Show informational
|
|
80
|
+
* signals" toggle — but they don't drive the Library row badge
|
|
81
|
+
* because their pattern-only signal has too high a false-positive
|
|
82
|
+
* rate to be meaningful at first glance. */
|
|
83
|
+
export function updateSessionCounts(db, sessionId) {
|
|
84
|
+
const active = db.prepare(`SELECT
|
|
85
|
+
SUM(CASE WHEN kind NOT IN (${infoKindsPlaceholders()}) THEN 1 ELSE 0 END) AS total,
|
|
86
|
+
SUM(CASE WHEN kind IN (${highKindsPlaceholders()}) THEN 1 ELSE 0 END) AS high
|
|
87
|
+
FROM findings
|
|
88
|
+
WHERE session_id = ? AND state = 'active'`).get(...INFO_SEVERITY_KINDS_ARRAY, ...HIGH_SEVERITY_KINDS_ARRAY, sessionId);
|
|
89
|
+
const purged = db.prepare(`SELECT COUNT(*) AS c
|
|
90
|
+
FROM findings
|
|
91
|
+
WHERE session_id = ? AND state = 'purged'`).get(sessionId);
|
|
92
|
+
db.prepare(`UPDATE sessions
|
|
93
|
+
SET scan_finding_count = ?,
|
|
94
|
+
scan_high_count = ?,
|
|
95
|
+
scan_purged_count = ?
|
|
96
|
+
WHERE id = ?`).run(active.total ?? 0, active.high ?? 0, purged.c, sessionId);
|
|
97
|
+
}
|
|
98
|
+
const HIGH_SEVERITY_KINDS_ARRAY = Array.from(HIGH_SEVERITY_KINDS);
|
|
99
|
+
const INFO_SEVERITY_KINDS_ARRAY = Array.from(INFO_SEVERITY_KINDS);
|
|
100
|
+
function highKindsPlaceholders() {
|
|
101
|
+
return HIGH_SEVERITY_KINDS_ARRAY.map(() => '?').join(',');
|
|
102
|
+
}
|
|
103
|
+
function infoKindsPlaceholders() {
|
|
104
|
+
return INFO_SEVERITY_KINDS_ARRAY.map(() => '?').join(',');
|
|
105
|
+
}
|
|
106
|
+
export function setSessionScanProfile(db, sessionId, profile, completedAt) {
|
|
107
|
+
db.prepare(`UPDATE sessions
|
|
108
|
+
SET scan_profile = ?, scan_completed_at = ?
|
|
109
|
+
WHERE id = ?`).run(profile, completedAt, sessionId);
|
|
110
|
+
}
|
|
111
|
+
/** Set every session's scan_profile to NULL — used by "Rescan all" so
|
|
112
|
+
* every session re-enqueues. */
|
|
113
|
+
export function invalidateAllScanProfiles(db) {
|
|
114
|
+
const r = db.prepare('UPDATE sessions SET scan_profile = NULL').run();
|
|
115
|
+
return r.changes;
|
|
116
|
+
}
|
|
117
|
+
/** Drop the scan profile for one session — used by the sync cascade
|
|
118
|
+
* when a session's messages change after first scan. */
|
|
119
|
+
export function invalidateSessionScanProfile(db, sessionId) {
|
|
120
|
+
db.prepare(`UPDATE sessions
|
|
121
|
+
SET scan_profile = NULL,
|
|
122
|
+
scan_completed_at = NULL
|
|
123
|
+
WHERE id = ?`).run(sessionId);
|
|
124
|
+
}
|
|
125
|
+
/** Sessions whose stored profile doesn't structurally match
|
|
126
|
+
* `currentProfile`. Used at boot for backfill enqueue. */
|
|
127
|
+
export function listSessionsNeedingScan(db, currentProfile) {
|
|
128
|
+
const rows = db.prepare(`SELECT id, scan_profile
|
|
129
|
+
FROM sessions
|
|
130
|
+
ORDER BY started_at DESC`).all();
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const r of rows) {
|
|
133
|
+
if (r.scan_profile === null || r.scan_profile !== currentProfile) {
|
|
134
|
+
out.push(r.id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
// ─── Reads (UI path) ──────────────────────────────────────────────
|
|
140
|
+
export function listFindings(db, filter) {
|
|
141
|
+
const where = [];
|
|
142
|
+
const params = [];
|
|
143
|
+
if (filter.sessionId !== undefined) {
|
|
144
|
+
where.push('session_id = ?');
|
|
145
|
+
params.push(filter.sessionId);
|
|
146
|
+
}
|
|
147
|
+
if (filter.kinds && filter.kinds.length > 0) {
|
|
148
|
+
const placeholders = filter.kinds.map(() => '?').join(',');
|
|
149
|
+
where.push(`kind IN (${placeholders})`);
|
|
150
|
+
params.push(...filter.kinds);
|
|
151
|
+
}
|
|
152
|
+
else if (filter.kind !== undefined) {
|
|
153
|
+
where.push('kind = ?');
|
|
154
|
+
params.push(filter.kind);
|
|
155
|
+
}
|
|
156
|
+
if (filter.state && filter.state !== 'any') {
|
|
157
|
+
where.push('state = ?');
|
|
158
|
+
params.push(filter.state);
|
|
159
|
+
}
|
|
160
|
+
else if (!filter.state) {
|
|
161
|
+
// Default: active only.
|
|
162
|
+
where.push("state = 'active'");
|
|
163
|
+
}
|
|
164
|
+
if (filter.severity === 'high') {
|
|
165
|
+
where.push(`kind IN (${highKindsPlaceholders()})`);
|
|
166
|
+
params.push(...HIGH_SEVERITY_KINDS_ARRAY);
|
|
167
|
+
}
|
|
168
|
+
else if (filter.severity === 'low') {
|
|
169
|
+
where.push(`kind NOT IN (${highKindsPlaceholders()})`);
|
|
170
|
+
params.push(...HIGH_SEVERITY_KINDS_ARRAY);
|
|
171
|
+
}
|
|
172
|
+
// Skip info-tier kinds unless the caller is explicitly asking for
|
|
173
|
+
// one. Saves the renderer from paging through 800+ absolute-path
|
|
174
|
+
// findings before reaching the api-key that actually matters.
|
|
175
|
+
const explicitKindsInclude = (kinds, kind) => {
|
|
176
|
+
if (kinds && kinds.length > 0)
|
|
177
|
+
return kinds.some(k => INFO_SEVERITY_KINDS.has(k));
|
|
178
|
+
if (kind !== undefined)
|
|
179
|
+
return INFO_SEVERITY_KINDS.has(kind);
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
if (filter.excludeInfo && !explicitKindsInclude(filter.kinds, filter.kind)) {
|
|
183
|
+
where.push(`kind NOT IN (${infoKindsPlaceholders()})`);
|
|
184
|
+
params.push(...INFO_SEVERITY_KINDS_ARRAY);
|
|
185
|
+
}
|
|
186
|
+
let pagination = '';
|
|
187
|
+
if (filter.limit !== undefined) {
|
|
188
|
+
pagination = ' LIMIT ? OFFSET ?';
|
|
189
|
+
params.push(filter.limit, filter.offset ?? 0);
|
|
190
|
+
}
|
|
191
|
+
const sql = `SELECT * FROM findings
|
|
192
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
193
|
+
ORDER BY detected_at DESC, id DESC${pagination}`;
|
|
194
|
+
const rows = db.prepare(sql).all(...params);
|
|
195
|
+
return rows.map(rowToFinding);
|
|
196
|
+
}
|
|
197
|
+
/** limit+1 peek to set `hasMore` without an extra COUNT round-trip. */
|
|
198
|
+
function paginate(filter, fetch) {
|
|
199
|
+
if (filter.limit === undefined)
|
|
200
|
+
return { rows: fetch(filter), hasMore: false };
|
|
201
|
+
const peeked = fetch({ ...filter, limit: filter.limit + 1 });
|
|
202
|
+
const hasMore = peeked.length > filter.limit;
|
|
203
|
+
return { rows: hasMore ? peeked.slice(0, filter.limit) : peeked, hasMore };
|
|
204
|
+
}
|
|
205
|
+
export function listFindingsPage(db, filter) {
|
|
206
|
+
return paginate(filter, f => listFindings(db, f));
|
|
207
|
+
}
|
|
208
|
+
/** Shared WHERE-clause builder for the session-findings join. Used by
|
|
209
|
+
* both `listSessionsWithFindings` (paginated rows) and
|
|
210
|
+
* `countSessionsWithFindings` (total) so the two stay consistent. */
|
|
211
|
+
function buildSessionFindingWhereSql(filter) {
|
|
212
|
+
const params = [];
|
|
213
|
+
const stateCondition = filter.state && filter.state !== 'any'
|
|
214
|
+
? 'f.state = ?'
|
|
215
|
+
: "f.state = 'active'";
|
|
216
|
+
if (filter.state && filter.state !== 'any')
|
|
217
|
+
params.push(filter.state);
|
|
218
|
+
let kindCondition = '';
|
|
219
|
+
const explicitKinds = filter.kinds && filter.kinds.length > 0
|
|
220
|
+
? filter.kinds
|
|
221
|
+
: filter.kind !== undefined ? [filter.kind] : undefined;
|
|
222
|
+
if (explicitKinds) {
|
|
223
|
+
const placeholders = explicitKinds.map(() => '?').join(',');
|
|
224
|
+
kindCondition = `AND f.kind IN (${placeholders})`;
|
|
225
|
+
params.push(...explicitKinds);
|
|
226
|
+
}
|
|
227
|
+
let severityCondition = '';
|
|
228
|
+
if (filter.severity === 'high') {
|
|
229
|
+
severityCondition = `AND f.kind IN (${highKindsPlaceholders()})`;
|
|
230
|
+
params.push(...HIGH_SEVERITY_KINDS_ARRAY);
|
|
231
|
+
}
|
|
232
|
+
else if (filter.severity === 'low') {
|
|
233
|
+
// Exclude both HIGH (not low) and INFO (noisy infra signals) so
|
|
234
|
+
// `severity:low` returns only the identity-tier kinds (email,
|
|
235
|
+
// phone, person-name, etc.).
|
|
236
|
+
severityCondition = `AND f.kind NOT IN (${highKindsPlaceholders()}) AND f.kind NOT IN (${infoKindsPlaceholders()})`;
|
|
237
|
+
params.push(...HIGH_SEVERITY_KINDS_ARRAY, ...INFO_SEVERITY_KINDS_ARRAY);
|
|
238
|
+
}
|
|
239
|
+
// Default exclusion: info-tier findings don't surface a session on
|
|
240
|
+
// their own — they're stored as an audit record (see Info drawer at
|
|
241
|
+
// the bottom of the page). Only when the user has explicitly pinned
|
|
242
|
+
// an info kind via filter.kind/filter.kinds do we let them through.
|
|
243
|
+
let infoExclusion = '';
|
|
244
|
+
if (!explicitKinds && filter.severity !== 'high' && filter.severity !== 'low') {
|
|
245
|
+
infoExclusion = `AND f.kind NOT IN (${infoKindsPlaceholders()})`;
|
|
246
|
+
params.push(...INFO_SEVERITY_KINDS_ARRAY);
|
|
247
|
+
}
|
|
248
|
+
let textCondition = '';
|
|
249
|
+
if (filter.text && filter.text.trim().length > 0) {
|
|
250
|
+
textCondition = `AND s.title LIKE ?`;
|
|
251
|
+
params.push(`%${filter.text.trim()}%`);
|
|
252
|
+
}
|
|
253
|
+
const whereClause = `${stateCondition} ${kindCondition} ${severityCondition} ${infoExclusion} ${textCondition}
|
|
254
|
+
AND COALESCE(s.message_count, 0) > 0`;
|
|
255
|
+
return { whereClause, params };
|
|
256
|
+
}
|
|
257
|
+
export function listSessionsWithFindings(db, filter) {
|
|
258
|
+
// For category/severity-aware filtering we need to compute counts
|
|
259
|
+
// from the findings table directly (denormalised counters can't
|
|
260
|
+
// express "only api-key findings"). We always recompute here.
|
|
261
|
+
const { whereClause, params } = buildSessionFindingWhereSql(filter);
|
|
262
|
+
const sql = `
|
|
263
|
+
SELECT
|
|
264
|
+
s.id AS id,
|
|
265
|
+
s.session_uuid AS session_uuid,
|
|
266
|
+
s.title AS title,
|
|
267
|
+
s.started_at AS started_at,
|
|
268
|
+
s.scan_completed_at AS scan_completed_at,
|
|
269
|
+
s.scan_purged_count AS purged_count,
|
|
270
|
+
s.message_count AS message_count,
|
|
271
|
+
s.model AS model,
|
|
272
|
+
s.cwd AS cwd,
|
|
273
|
+
src.name AS source,
|
|
274
|
+
p.display_name AS project_display_name,
|
|
275
|
+
SUM(1) AS finding_count,
|
|
276
|
+
SUM(CASE WHEN f.kind IN (${highKindsPlaceholders()}) THEN 1 ELSE 0 END) AS high_count
|
|
277
|
+
FROM sessions s
|
|
278
|
+
JOIN findings f ON f.session_id = s.id
|
|
279
|
+
JOIN sources src ON src.id = s.source_id
|
|
280
|
+
JOIN projects p ON p.id = s.project_id
|
|
281
|
+
WHERE ${whereClause}
|
|
282
|
+
GROUP BY s.id
|
|
283
|
+
ORDER BY high_count DESC, finding_count DESC, s.started_at DESC, s.id DESC
|
|
284
|
+
${filter.limit !== undefined ? 'LIMIT ? OFFSET ?' : ''}
|
|
285
|
+
`;
|
|
286
|
+
// Placeholder order matches SQL: SELECT's CASE WHEN IN (?) binds first,
|
|
287
|
+
// then the WHERE clause's params.
|
|
288
|
+
const allParams = [...HIGH_SEVERITY_KINDS_ARRAY, ...params];
|
|
289
|
+
if (filter.limit !== undefined) {
|
|
290
|
+
allParams.push(filter.limit, filter.offset ?? 0);
|
|
291
|
+
}
|
|
292
|
+
const rows = db.prepare(sql).all(...allParams);
|
|
293
|
+
return rows.map(r => ({
|
|
294
|
+
id: r.id,
|
|
295
|
+
sessionUuid: r.session_uuid,
|
|
296
|
+
title: r.title,
|
|
297
|
+
startedAt: r.started_at,
|
|
298
|
+
scanCompletedAt: r.scan_completed_at,
|
|
299
|
+
findingCount: r.finding_count,
|
|
300
|
+
highCount: r.high_count ?? 0,
|
|
301
|
+
purgedCount: r.purged_count ?? 0,
|
|
302
|
+
source: r.source,
|
|
303
|
+
messageCount: r.message_count ?? 0,
|
|
304
|
+
model: r.model,
|
|
305
|
+
cwd: r.cwd,
|
|
306
|
+
projectDisplayName: r.project_display_name,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
export function listSessionsWithFindingsPage(db, filter) {
|
|
310
|
+
return paginate(filter, f => listSessionsWithFindings(db, f));
|
|
311
|
+
}
|
|
312
|
+
/** Total distinct sessions matching the same filter as
|
|
313
|
+
* `listSessionsWithFindings` — used by the renderer to show
|
|
314
|
+
* "涉及 N 个会话" without loading every page. Cheaper than the list
|
|
315
|
+
* query: no SELECT projection, no GROUP BY, no ORDER BY. */
|
|
316
|
+
export function countSessionsWithFindings(db, filter) {
|
|
317
|
+
const { whereClause, params } = buildSessionFindingWhereSql(filter);
|
|
318
|
+
const sql = `
|
|
319
|
+
SELECT COUNT(DISTINCT s.id) AS total
|
|
320
|
+
FROM sessions s
|
|
321
|
+
JOIN findings f ON f.session_id = s.id
|
|
322
|
+
JOIN sources src ON src.id = s.source_id
|
|
323
|
+
JOIN projects p ON p.id = s.project_id
|
|
324
|
+
WHERE ${whereClause}
|
|
325
|
+
`;
|
|
326
|
+
const row = db.prepare(sql).get(...params);
|
|
327
|
+
return row.total;
|
|
328
|
+
}
|
|
329
|
+
/** Risk by category — one row per kind that has ≥ 1 active finding.
|
|
330
|
+
* Drives the Watchtower-style panel on the Security page. */
|
|
331
|
+
export function riskByCategory(db) {
|
|
332
|
+
const rows = db.prepare(`SELECT kind,
|
|
333
|
+
COUNT(*) AS count,
|
|
334
|
+
COUNT(DISTINCT session_id) AS sessions
|
|
335
|
+
FROM findings
|
|
336
|
+
WHERE state = 'active'
|
|
337
|
+
GROUP BY kind
|
|
338
|
+
ORDER BY count DESC`).all();
|
|
339
|
+
return rows.map(r => ({
|
|
340
|
+
kind: r.kind,
|
|
341
|
+
severity: severityOf(r.kind),
|
|
342
|
+
count: r.count,
|
|
343
|
+
sessions: r.sessions,
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
/** Cross-session blast radius for one leaked value. Returns every
|
|
347
|
+
* session that has ≥ 1 ACTIVE finding sharing the given
|
|
348
|
+
* `(kind, valueHash)`, with the per-session occurrence count, project,
|
|
349
|
+
* and most-recent detection time. Ordered most-recent-first.
|
|
350
|
+
*
|
|
351
|
+
* Spool already collapses repeated occurrences of a value WITHIN a
|
|
352
|
+
* session into one row (×N); this extends that view across the whole
|
|
353
|
+
* archive so the user can see "this same key also leaked in 4 other
|
|
354
|
+
* sessions". Dismissed/purged occurrences are excluded — they no
|
|
355
|
+
* longer expose the value through search / AI / browse. */
|
|
356
|
+
export function occurrencesByValueHash(db, kind, valueHash) {
|
|
357
|
+
const rows = db.prepare(`SELECT
|
|
358
|
+
s.id AS session_id,
|
|
359
|
+
s.session_uuid AS session_uuid,
|
|
360
|
+
s.title AS session_title,
|
|
361
|
+
src.name AS source,
|
|
362
|
+
p.display_name AS project,
|
|
363
|
+
COUNT(*) AS count,
|
|
364
|
+
MAX(f.detected_at) AS last_seen
|
|
365
|
+
FROM findings f
|
|
366
|
+
JOIN sessions s ON s.id = f.session_id
|
|
367
|
+
JOIN sources src ON src.id = s.source_id
|
|
368
|
+
JOIN projects p ON p.id = s.project_id
|
|
369
|
+
WHERE f.kind = ? AND f.value_hash = ? AND f.state = 'active'
|
|
370
|
+
GROUP BY s.id
|
|
371
|
+
ORDER BY last_seen DESC, s.id DESC`).all(kind, valueHash);
|
|
372
|
+
return rows.map(r => ({
|
|
373
|
+
sessionId: r.session_id,
|
|
374
|
+
sessionUuid: r.session_uuid,
|
|
375
|
+
sessionTitle: r.session_title,
|
|
376
|
+
source: r.source,
|
|
377
|
+
project: r.project,
|
|
378
|
+
count: r.count,
|
|
379
|
+
lastSeen: r.last_seen,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
/** The most recent `scan_completed_at` across ALL sessions, regardless
|
|
383
|
+
* of whether they currently have active findings. Drives the Security
|
|
384
|
+
* page's "scanned X ago" label. Deriving it from the filtered,
|
|
385
|
+
* paginated findings list undercounts: a session scanned + cleaned
|
|
386
|
+
* (no active findings) drops off that list and stops contributing, so
|
|
387
|
+
* the label could read older than the true last scan. Returns null
|
|
388
|
+
* when nothing has been scanned yet. */
|
|
389
|
+
export function lastScanCompletedAt(db) {
|
|
390
|
+
const row = db.prepare(`SELECT MAX(scan_completed_at) AS last
|
|
391
|
+
FROM sessions
|
|
392
|
+
WHERE scan_completed_at IS NOT NULL`).get();
|
|
393
|
+
return row.last;
|
|
394
|
+
}
|
|
395
|
+
/** Read the live raw value for one finding. Used by the UI's review
|
|
396
|
+
* panel and the Purge confirm dialog. Returns null when the finding
|
|
397
|
+
* no longer points at a valid message (race against session
|
|
398
|
+
* deletion) or has been purged (offsets now point at the mask). */
|
|
399
|
+
export function getFindingValue(db, findingId) {
|
|
400
|
+
const row = db.prepare(`SELECT f.start_offset, f.end_offset, f.state, m.content_text
|
|
401
|
+
FROM findings f
|
|
402
|
+
LEFT JOIN messages m ON m.id = f.message_id
|
|
403
|
+
WHERE f.id = ?`).get(findingId);
|
|
404
|
+
if (!row || row.content_text === null)
|
|
405
|
+
return null;
|
|
406
|
+
if (row.state === 'purged')
|
|
407
|
+
return null;
|
|
408
|
+
return row.content_text.slice(row.start_offset, row.end_offset);
|
|
409
|
+
}
|
|
410
|
+
/** Bulk version of `getFindingValue` — one SQL query for N finding
|
|
411
|
+
* ids instead of N round-trips. Caller fans the result map back out
|
|
412
|
+
* by id; missing keys are treated as `null` (purged / vanished). */
|
|
413
|
+
export function getFindingValues(db, findingIds) {
|
|
414
|
+
const out = {};
|
|
415
|
+
if (findingIds.length === 0)
|
|
416
|
+
return out;
|
|
417
|
+
const placeholders = findingIds.map(() => '?').join(',');
|
|
418
|
+
const rows = db.prepare(`SELECT f.id, f.start_offset, f.end_offset, f.state, m.content_text
|
|
419
|
+
FROM findings f
|
|
420
|
+
LEFT JOIN messages m ON m.id = f.message_id
|
|
421
|
+
WHERE f.id IN (${placeholders})`).all(...findingIds);
|
|
422
|
+
for (const r of rows) {
|
|
423
|
+
if (r.content_text === null || r.state === 'purged') {
|
|
424
|
+
out[r.id] = null;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
out[r.id] = r.content_text.slice(r.start_offset, r.end_offset);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Fill in nulls for ids that didn't come back (deleted / cascade).
|
|
431
|
+
for (const id of findingIds) {
|
|
432
|
+
if (!(id in out))
|
|
433
|
+
out[id] = null;
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
const allowKey = (kind, hash) => `${kind}|${hash}`;
|
|
438
|
+
export function getAllowlists(db, sessionId) {
|
|
439
|
+
const sessionRows = db.prepare('SELECT kind, value_hash FROM allowlist_session WHERE session_id = ?').all(sessionId);
|
|
440
|
+
const globalRows = db.prepare('SELECT kind, value_hash FROM allowlist_global').all();
|
|
441
|
+
return {
|
|
442
|
+
session: new Set(sessionRows.map(r => allowKey(r.kind, r.value_hash))),
|
|
443
|
+
global: new Set(globalRows.map(r => allowKey(r.kind, r.value_hash))),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
export function isAllowlisted(allow, kind, valueHash) {
|
|
447
|
+
const key = allowKey(kind, valueHash);
|
|
448
|
+
return allow.session.has(key) || allow.global.has(key);
|
|
449
|
+
}
|
|
450
|
+
export function addAllowlistSession(db, sessionId, kind, valueHash) {
|
|
451
|
+
db.prepare(`INSERT OR IGNORE INTO allowlist_session (session_id, kind, value_hash)
|
|
452
|
+
VALUES (?, ?, ?)`).run(sessionId, kind, valueHash);
|
|
453
|
+
}
|
|
454
|
+
export function addAllowlistGlobal(db, kind, valueHash) {
|
|
455
|
+
db.prepare(`INSERT OR IGNORE INTO allowlist_global (kind, value_hash)
|
|
456
|
+
VALUES (?, ?)`).run(kind, valueHash);
|
|
457
|
+
}
|
|
458
|
+
export function removeAllowlistSession(db, sessionId, kind, valueHash) {
|
|
459
|
+
db.prepare(`DELETE FROM allowlist_session
|
|
460
|
+
WHERE session_id = ? AND kind = ? AND value_hash = ?`).run(sessionId, kind, valueHash);
|
|
461
|
+
}
|
|
462
|
+
export function removeAllowlistGlobal(db, kind, valueHash) {
|
|
463
|
+
db.prepare(`DELETE FROM allowlist_global WHERE kind = ? AND value_hash = ?`).run(kind, valueHash);
|
|
464
|
+
}
|
|
465
|
+
/** All allowlist rows from both tables, joined with the originating
|
|
466
|
+
* session metadata. Drives the Settings → Security pane's "Manage…"
|
|
467
|
+
* modal so the user can review and revoke past decisions.
|
|
468
|
+
*
|
|
469
|
+
* Each entry's display value is reconstructed live from a matching
|
|
470
|
+
* finding's message text — the same plaintext the findings view
|
|
471
|
+
* shows (blurred). Nothing is persisted on the allowlist row. */
|
|
472
|
+
export function listAllowlistEntries(db) {
|
|
473
|
+
const sessionRows = db.prepare(`SELECT a.session_id AS session_id,
|
|
474
|
+
a.kind AS kind,
|
|
475
|
+
a.value_hash AS value_hash,
|
|
476
|
+
a.created_at AS created_at,
|
|
477
|
+
s.session_uuid AS session_uuid,
|
|
478
|
+
s.title AS session_title
|
|
479
|
+
FROM allowlist_session a
|
|
480
|
+
JOIN sessions s ON s.id = a.session_id
|
|
481
|
+
ORDER BY a.created_at DESC`).all();
|
|
482
|
+
const globalRows = db.prepare(`SELECT kind, value_hash, created_at
|
|
483
|
+
FROM allowlist_global
|
|
484
|
+
ORDER BY created_at DESC`).all();
|
|
485
|
+
// Reconstruct the live value from any surviving non-purged finding
|
|
486
|
+
// whose message text we can still read. N is small (the allowlist is
|
|
487
|
+
// a hand-curated list), so a prepared statement per row is fine.
|
|
488
|
+
const sessionValueStmt = db.prepare(`SELECT substr(m.content_text, f.start_offset + 1, f.end_offset - f.start_offset) AS value
|
|
489
|
+
FROM findings f
|
|
490
|
+
LEFT JOIN messages m ON m.id = f.message_id
|
|
491
|
+
WHERE f.session_id = ? AND f.kind = ? AND f.value_hash = ?
|
|
492
|
+
AND f.state != 'purged' AND m.content_text IS NOT NULL
|
|
493
|
+
LIMIT 1`);
|
|
494
|
+
const globalValueStmt = db.prepare(`SELECT substr(m.content_text, f.start_offset + 1, f.end_offset - f.start_offset) AS value
|
|
495
|
+
FROM findings f
|
|
496
|
+
LEFT JOIN messages m ON m.id = f.message_id
|
|
497
|
+
WHERE f.kind = ? AND f.value_hash = ?
|
|
498
|
+
AND f.state != 'purged' AND m.content_text IS NOT NULL
|
|
499
|
+
LIMIT 1`);
|
|
500
|
+
return [
|
|
501
|
+
...globalRows.map((r) => ({
|
|
502
|
+
scope: 'global',
|
|
503
|
+
kind: r.kind,
|
|
504
|
+
valueHash: r.value_hash,
|
|
505
|
+
createdAt: r.created_at,
|
|
506
|
+
sessionUuid: null,
|
|
507
|
+
sessionTitle: null,
|
|
508
|
+
value: globalValueStmt.get(r.kind, r.value_hash)?.value ?? null,
|
|
509
|
+
})),
|
|
510
|
+
...sessionRows.map((r) => ({
|
|
511
|
+
scope: 'session',
|
|
512
|
+
kind: r.kind,
|
|
513
|
+
valueHash: r.value_hash,
|
|
514
|
+
createdAt: r.created_at,
|
|
515
|
+
sessionUuid: r.session_uuid,
|
|
516
|
+
sessionTitle: r.session_title,
|
|
517
|
+
value: sessionValueStmt.get(r.session_id, r.kind, r.value_hash)?.value ?? null,
|
|
518
|
+
})),
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
/** Total allowlist rows across both scopes. A cheap header badge —
|
|
522
|
+
* two `SELECT COUNT(*)` summed, no per-row value reconstruction.
|
|
523
|
+
* Use this instead of `listAllowlistEntries().length` for counts. */
|
|
524
|
+
export function countAllowlistEntries(db) {
|
|
525
|
+
const session = db.prepare('SELECT COUNT(*) AS c FROM allowlist_session').get();
|
|
526
|
+
const global = db.prepare('SELECT COUNT(*) AS c FROM allowlist_global').get();
|
|
527
|
+
return session.c + global.c;
|
|
528
|
+
}
|
|
529
|
+
// ─── Mutations called from IPC dismiss handlers ───────────────────
|
|
530
|
+
/** Flip a finding to 'dismissed' and, depending on scope, write the
|
|
531
|
+
* allowlist entry so future rescans honor the decision. Caller
|
|
532
|
+
* wraps in a transaction. */
|
|
533
|
+
export function dismissFinding(db, findingId, scope, recomputeCounts = true) {
|
|
534
|
+
const f = db.prepare(`SELECT session_id, kind, value_hash FROM findings WHERE id = ?`).get(findingId);
|
|
535
|
+
if (!f)
|
|
536
|
+
return null;
|
|
537
|
+
db.prepare(`UPDATE findings
|
|
538
|
+
SET state = 'dismissed',
|
|
539
|
+
state_changed_at = datetime('now')
|
|
540
|
+
WHERE id = ?`).run(findingId);
|
|
541
|
+
if (scope === 'session') {
|
|
542
|
+
addAllowlistSession(db, f.session_id, f.kind, f.value_hash);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
addAllowlistGlobal(db, f.kind, f.value_hash);
|
|
546
|
+
}
|
|
547
|
+
if (recomputeCounts)
|
|
548
|
+
updateSessionCounts(db, f.session_id);
|
|
549
|
+
return f.session_id;
|
|
550
|
+
}
|
|
551
|
+
/** Batch variant of {@link dismissFinding}. Dismisses many findings in a
|
|
552
|
+
* single transaction — one allowlist write + one `updateSessionCounts`
|
|
553
|
+
* per affected session instead of N parallel IPC round-trips, each of
|
|
554
|
+
* which recomputes counts. Mirrors the bulk-purge pattern
|
|
555
|
+
* (`purgeFindings`). Returns the distinct session ids touched, in
|
|
556
|
+
* first-seen order, so the IPC layer can emit one change event per
|
|
557
|
+
* session. Unknown ids are skipped. */
|
|
558
|
+
export function dismissFindings(db, findingIds, scope) {
|
|
559
|
+
if (findingIds.length === 0)
|
|
560
|
+
return [];
|
|
561
|
+
const touched = [];
|
|
562
|
+
const seen = new Set();
|
|
563
|
+
const txn = db.transaction((ids) => {
|
|
564
|
+
for (const id of ids) {
|
|
565
|
+
// Defer count recompute — many ids usually share one session, so
|
|
566
|
+
// recomputing per-id would redo the same aggregate N times.
|
|
567
|
+
const sessionId = dismissFinding(db, id, scope, false);
|
|
568
|
+
if (sessionId != null && !seen.has(sessionId)) {
|
|
569
|
+
seen.add(sessionId);
|
|
570
|
+
touched.push(sessionId);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const sessionId of touched)
|
|
574
|
+
updateSessionCounts(db, sessionId);
|
|
575
|
+
});
|
|
576
|
+
txn(findingIds);
|
|
577
|
+
return touched;
|
|
578
|
+
}
|
|
579
|
+
/** Re-activate a dismissed finding and remove the allowlist entry
|
|
580
|
+
* that pinned it (both scopes, since UI doesn't always know which). */
|
|
581
|
+
export function undismissFinding(db, findingId) {
|
|
582
|
+
const f = db.prepare('SELECT session_id, kind, value_hash FROM findings WHERE id = ?').get(findingId);
|
|
583
|
+
if (!f)
|
|
584
|
+
return null;
|
|
585
|
+
db.prepare(`UPDATE findings
|
|
586
|
+
SET state = 'active',
|
|
587
|
+
state_changed_at = datetime('now')
|
|
588
|
+
WHERE id = ?`).run(findingId);
|
|
589
|
+
removeAllowlistSession(db, f.session_id, f.kind, f.value_hash);
|
|
590
|
+
removeAllowlistGlobal(db, f.kind, f.value_hash);
|
|
591
|
+
updateSessionCounts(db, f.session_id);
|
|
592
|
+
return f.session_id;
|
|
593
|
+
}
|
|
594
|
+
//# sourceMappingURL=repo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/security/repo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
import type { RedactProvider } from '@spool-lab/redact';
|
|
4
|
+
import type { FindingsChange } from './types.js';
|
|
5
|
+
declare const ScanError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
6
|
+
readonly _tag: "ScanError";
|
|
7
|
+
} & Readonly<A>;
|
|
8
|
+
export declare class ScanError extends ScanError_base<{
|
|
9
|
+
readonly sessionId: number;
|
|
10
|
+
readonly cause: unknown;
|
|
11
|
+
readonly reason: 'session-not-found' | 'provider-failed' | 'db-failed';
|
|
12
|
+
}> {
|
|
13
|
+
}
|
|
14
|
+
/** Result of one session scan — useful for tests and progress UIs. */
|
|
15
|
+
export interface ScanResult {
|
|
16
|
+
sessionId: number;
|
|
17
|
+
inserted: number;
|
|
18
|
+
/** Profile string written to sessions.scan_profile. */
|
|
19
|
+
profile: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ScanSessionDeps {
|
|
22
|
+
/** SQLite database handle (same instance the Syncer uses). */
|
|
23
|
+
db: Database.Database;
|
|
24
|
+
/** Active providers, evaluated in priority order. First entry wins
|
|
25
|
+
* on overlap (regex before pf — pattern matches are authoritative
|
|
26
|
+
* for known credential prefixes). */
|
|
27
|
+
providers: readonly RedactProvider[];
|
|
28
|
+
/** Profile string for the active provider set; persisted on
|
|
29
|
+
* successful scan. */
|
|
30
|
+
currentProfile: string;
|
|
31
|
+
/** Names of providers whose prior `active` findings should be
|
|
32
|
+
* deleted before re-insert. Almost always equals the names in
|
|
33
|
+
* `providers`; passed explicitly so the worker can choose to
|
|
34
|
+
* preserve historical pf findings even when pf is disabled. */
|
|
35
|
+
providerNames: readonly string[];
|
|
36
|
+
/** Sink for change notifications. The worker layer wraps PubSub
|
|
37
|
+
* here; tests pass a simple collector. */
|
|
38
|
+
publish: (change: FindingsChange) => Effect.Effect<void>;
|
|
39
|
+
/** Kind-level allowlist. Findings whose kind is in this set get
|
|
40
|
+
* inserted with state='dismissed' (instead of 'active') without
|
|
41
|
+
* needing a per-value allowlist row. Driven by the Settings →
|
|
42
|
+
* Security pane's multi-select. */
|
|
43
|
+
kindAllowlist?: ReadonlySet<string>;
|
|
44
|
+
}
|
|
45
|
+
/** Scan one session end to end. Idempotent: running twice produces
|
|
46
|
+
* the same findings (same value_hash + same offsets → same rows
|
|
47
|
+
* after the delete-then-insert reset). */
|
|
48
|
+
export declare function scanSession(sessionId: number, deps: ScanSessionDeps): Effect.Effect<ScanResult, ScanError>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=scan.d.ts.map
|