0sec-cli 0.15.0 → 0.17.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/0sec.js +322 -0
- package/README.md +63 -52
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-ECQG7454.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
- package/chunks/assumption-mining-BVMMUZHB.js +79 -0
- package/chunks/chunk-2CJ776PV.js +2430 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-2SANI5RH.js +4728 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-3QFDYBZQ.js +5108 -0
- package/chunks/chunk-47TQJWDH.js +3729 -0
- package/chunks/chunk-4Y4KQJXE.js +600 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-A6CLR72I.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-CLHCDHP4.js +471 -0
- package/chunks/chunk-D6S3IIPG.js +518 -0
- package/chunks/chunk-DN25OQQA.js +1070 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-H44E2CRN.js +193 -0
- package/chunks/chunk-HDWV7PGI.js +596 -0
- package/chunks/chunk-HE7LCA7G.js +381 -0
- package/chunks/chunk-HT6P7RY3.js +2597 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IORC6MQX.js +72828 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-K26SZ37E.js +5735 -0
- package/chunks/chunk-KKGE5RQE.js +1094 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-KTQDLNSR.js +1732 -0
- package/chunks/chunk-LOHTE223.js +2572 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MMLDQR4H.js +852 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-O462Y7P2.js +33019 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-QJQKHO7G.js +917 -0
- package/chunks/chunk-QKICO43A.js +808 -0
- package/chunks/chunk-QOKTUOU2.js +102 -0
- package/chunks/chunk-RDZYQQLW.js +596 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SNKTC4BP.js +1422 -0
- package/chunks/chunk-SOG2U7B3.js +662 -0
- package/chunks/chunk-SZJCPG2I.js +110 -0
- package/chunks/chunk-UJ4IK5PO.js +2010 -0
- package/chunks/chunk-UM3ZNQIM.js +9329 -0
- package/chunks/chunk-VGRDNSHA.js +3249 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WKNNZVJS.js +2798 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/commands-U3AMV5ZU.js +16795 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-26NGQFKO.js +16 -0
- package/chunks/disclose-A6AEIEDA.js +132 -0
- package/chunks/dist-FDKALV4R.js +2772 -0
- package/chunks/dist-GL66KMUX.js +74 -0
- package/chunks/dist-JR67XKYO.js +159 -0
- package/chunks/eval-runner-DDLE5RQ3.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-XP23ISN7.js +98 -0
- package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
- package/chunks/exploit-climb-2FAUP6XU.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-HWOYFKFY.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-XPELSPQQ.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-TENE3R7T.js +44 -0
- package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-NTONUDP7.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-ZUWAUWBC.js +62 -0
- package/chunks/pipeline-FCARFZU3.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-3Q7QJIOZ.js +13 -0
- package/chunks/replay-runner-RRR4E2A7.js +39 -0
- package/chunks/run-ESNN4V5W.js +32632 -0
- package/chunks/runtime-62ZQAH7H.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-BCFQYDCE.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-6KM6F6MD.js +13 -0
- package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,2597 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
homeStateDir,
|
|
6
|
+
runStateDir
|
|
7
|
+
} from "./chunk-KKGE5RQE.js";
|
|
8
|
+
import {
|
|
9
|
+
__export
|
|
10
|
+
} from "./chunk-IR537GON.js";
|
|
11
|
+
|
|
12
|
+
// packages/db/dist/wasm-shim.js
|
|
13
|
+
import { BetterSQLiteSession } from "drizzle-orm/better-sqlite3/session";
|
|
14
|
+
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db";
|
|
15
|
+
import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect";
|
|
16
|
+
import { createTableRelationsHelpers, extractTablesRelationalConfig } from "drizzle-orm/relations";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
function isBunRuntime() {
|
|
19
|
+
const g = globalThis;
|
|
20
|
+
if (typeof g.Bun !== "undefined")
|
|
21
|
+
return true;
|
|
22
|
+
const versions = typeof process !== "undefined" && process.versions;
|
|
23
|
+
return !!(versions && versions.bun);
|
|
24
|
+
}
|
|
25
|
+
function normalizeBindArgs(args) {
|
|
26
|
+
if (args.length === 0)
|
|
27
|
+
return void 0;
|
|
28
|
+
if (args.length === 1) {
|
|
29
|
+
const a = args[0];
|
|
30
|
+
if (a !== null && typeof a === "object" && !Array.isArray(a) && !(a instanceof Uint8Array)) {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, v] of Object.entries(a)) {
|
|
33
|
+
out[/^[:@$]/.test(k) ? k : `@${k}`] = v;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
return [a];
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
var StatementShim = class _StatementShim {
|
|
42
|
+
stmt;
|
|
43
|
+
rawMode;
|
|
44
|
+
constructor(stmt, rawMode = false) {
|
|
45
|
+
this.stmt = stmt;
|
|
46
|
+
this.rawMode = rawMode;
|
|
47
|
+
}
|
|
48
|
+
run(...args) {
|
|
49
|
+
const bind = normalizeBindArgs(args);
|
|
50
|
+
return this.stmt.run(bind);
|
|
51
|
+
}
|
|
52
|
+
get(...args) {
|
|
53
|
+
const bind = normalizeBindArgs(args);
|
|
54
|
+
if (this.rawMode) {
|
|
55
|
+
const rows = this.stmt.values(bind);
|
|
56
|
+
return rows[0];
|
|
57
|
+
}
|
|
58
|
+
const row = this.stmt.get(bind);
|
|
59
|
+
if (row == null)
|
|
60
|
+
return void 0;
|
|
61
|
+
return row;
|
|
62
|
+
}
|
|
63
|
+
all(...args) {
|
|
64
|
+
const bind = normalizeBindArgs(args);
|
|
65
|
+
if (this.rawMode)
|
|
66
|
+
return this.stmt.values(bind);
|
|
67
|
+
return this.stmt.all(bind);
|
|
68
|
+
}
|
|
69
|
+
/** better-sqlite3 compat: return a statement that yields rows as arrays. */
|
|
70
|
+
raw() {
|
|
71
|
+
return new _StatementShim(this.stmt, true);
|
|
72
|
+
}
|
|
73
|
+
// Stubs for methods drizzle never calls but better-sqlite3 exposes.
|
|
74
|
+
// Keeps TypeScript structural typing happy if something reaches for them.
|
|
75
|
+
pluck() {
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
expand() {
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
bind() {
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var ShimmedDatabase = class {
|
|
86
|
+
engine;
|
|
87
|
+
constructor(engine) {
|
|
88
|
+
this.engine = engine;
|
|
89
|
+
}
|
|
90
|
+
prepare(sql) {
|
|
91
|
+
return new StatementShim(this.engine.prepare(sql));
|
|
92
|
+
}
|
|
93
|
+
exec(sql) {
|
|
94
|
+
this.engine.exec(sql);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* better-sqlite3 uses `.pragma()` both as a getter and a setter. Our code
|
|
99
|
+
* only uses it as a setter ("journal_mode = WAL", "foreign_keys = ON"), so
|
|
100
|
+
* we just `exec()` the pragma and swallow errors — some PRAGMAs (notably
|
|
101
|
+
* WAL journal mode) are not supported by node-sqlite3-wasm's VFS and error
|
|
102
|
+
* here, but losing them is acceptable for 0sec's single-process workload.
|
|
103
|
+
*/
|
|
104
|
+
pragma(query, _opts) {
|
|
105
|
+
try {
|
|
106
|
+
this.engine.exec(`PRAGMA ${query}`);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* better-sqlite3's `db.transaction(fn)` returns a callable object with
|
|
113
|
+
* `.deferred`, `.immediate`, `.exclusive`, and `.default` methods, each
|
|
114
|
+
* running `fn` inside a BEGIN/COMMIT of the corresponding isolation level.
|
|
115
|
+
* All three variants are functionally equivalent for 0sec's single-writer
|
|
116
|
+
* workload, so we wire them to the same underlying implementation.
|
|
117
|
+
*/
|
|
118
|
+
transaction(fn) {
|
|
119
|
+
const engine = this.engine;
|
|
120
|
+
const runInTx = (...args) => {
|
|
121
|
+
engine.exec("BEGIN");
|
|
122
|
+
try {
|
|
123
|
+
const result = fn(...args);
|
|
124
|
+
engine.exec("COMMIT");
|
|
125
|
+
return result;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
try {
|
|
128
|
+
engine.exec("ROLLBACK");
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return Object.assign(runInTx, {
|
|
135
|
+
default: runInTx,
|
|
136
|
+
deferred: runInTx,
|
|
137
|
+
immediate: runInTx,
|
|
138
|
+
exclusive: runInTx
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
close() {
|
|
142
|
+
this.engine.close();
|
|
143
|
+
}
|
|
144
|
+
get inTransaction() {
|
|
145
|
+
return this.engine.inTransaction;
|
|
146
|
+
}
|
|
147
|
+
get open() {
|
|
148
|
+
return this.engine.isOpen;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
function createWasmEngine(path) {
|
|
152
|
+
const sqliteWasm = createRequire(import.meta.url)("node-sqlite3-wasm");
|
|
153
|
+
const { Database: WasmDatabaseCtor } = sqliteWasm;
|
|
154
|
+
const wasm = new WasmDatabaseCtor(path);
|
|
155
|
+
const wrapStatement = (stmt) => ({
|
|
156
|
+
run: (bind) => stmt.run(bind),
|
|
157
|
+
get: (bind) => {
|
|
158
|
+
const row = stmt.get(bind);
|
|
159
|
+
return row == null ? void 0 : row;
|
|
160
|
+
},
|
|
161
|
+
all: (bind) => stmt.all(bind),
|
|
162
|
+
values: (bind) => {
|
|
163
|
+
const rows = stmt.all(bind);
|
|
164
|
+
return rows.map((r) => Object.values(r));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
prepare: (sql) => wrapStatement(wasm.prepare(sql)),
|
|
169
|
+
exec: (sql) => {
|
|
170
|
+
wasm.exec(sql);
|
|
171
|
+
},
|
|
172
|
+
close: () => wasm.close(),
|
|
173
|
+
get inTransaction() {
|
|
174
|
+
return wasm.inTransaction;
|
|
175
|
+
},
|
|
176
|
+
get isOpen() {
|
|
177
|
+
return wasm.isOpen;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function createBunEngine(path) {
|
|
182
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
183
|
+
const { Database: BunDatabase } = requireFromHere("bun:sqlite");
|
|
184
|
+
const db = new BunDatabase(path);
|
|
185
|
+
const wrapStatement = (stmt) => ({
|
|
186
|
+
run: (bind) => {
|
|
187
|
+
const r = bind === void 0 ? stmt.run() : stmt.run(bind);
|
|
188
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
189
|
+
},
|
|
190
|
+
get: (bind) => {
|
|
191
|
+
const row = bind === void 0 ? stmt.get() : stmt.get(bind);
|
|
192
|
+
return row == null ? void 0 : row;
|
|
193
|
+
},
|
|
194
|
+
all: (bind) => bind === void 0 ? stmt.all() : stmt.all(bind),
|
|
195
|
+
values: (bind) => bind === void 0 ? stmt.values() : stmt.values(bind)
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
prepare: (sql) => wrapStatement(db.prepare(sql)),
|
|
199
|
+
exec: (sql) => {
|
|
200
|
+
db.exec(sql);
|
|
201
|
+
},
|
|
202
|
+
close: () => db.close(),
|
|
203
|
+
get inTransaction() {
|
|
204
|
+
return Boolean(db.inTransaction);
|
|
205
|
+
},
|
|
206
|
+
get isOpen() {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function createShimmedDatabase(path) {
|
|
212
|
+
const engine = isBunRuntime() ? createBunEngine(path) : createWasmEngine(path);
|
|
213
|
+
return new ShimmedDatabase(engine);
|
|
214
|
+
}
|
|
215
|
+
function createDrizzleFromShim(client, config = {}) {
|
|
216
|
+
const dialect = new SQLiteSyncDialect({});
|
|
217
|
+
let schema;
|
|
218
|
+
if (config.schema) {
|
|
219
|
+
const tablesConfig = extractTablesRelationalConfig(config.schema, createTableRelationsHelpers);
|
|
220
|
+
schema = {
|
|
221
|
+
fullSchema: config.schema,
|
|
222
|
+
schema: tablesConfig.tables,
|
|
223
|
+
tableNamesMap: tablesConfig.tableNamesMap
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const session = new BetterSQLiteSession(client, dialect, schema, {});
|
|
227
|
+
const db = new BaseSQLiteDatabase("sync", dialect, session, schema);
|
|
228
|
+
db.$client = client;
|
|
229
|
+
return db;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// packages/db/dist/database.js
|
|
233
|
+
import { asc, eq, desc, and, gt, inArray, or } from "drizzle-orm";
|
|
234
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
235
|
+
import { dirname, join } from "node:path";
|
|
236
|
+
import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, rmSync, statSync, writeSync } from "node:fs";
|
|
237
|
+
|
|
238
|
+
// packages/db/dist/schema.js
|
|
239
|
+
var schema_exports = {};
|
|
240
|
+
__export(schema_exports, {
|
|
241
|
+
agentSessions: () => agentSessions,
|
|
242
|
+
artifactKinds: () => artifactKinds,
|
|
243
|
+
artifacts: () => artifacts,
|
|
244
|
+
attackResults: () => attackResults,
|
|
245
|
+
caseStatuses: () => caseStatuses,
|
|
246
|
+
cases: () => cases,
|
|
247
|
+
credentialKinds: () => credentialKinds,
|
|
248
|
+
findingStatuses: () => findingStatuses,
|
|
249
|
+
findingTriageStatuses: () => findingTriageStatuses,
|
|
250
|
+
findingWorkflowStatuses: () => findingWorkflowStatuses,
|
|
251
|
+
findings: () => findings,
|
|
252
|
+
persistentCredentials: () => persistentCredentials,
|
|
253
|
+
pipelineEvents: () => pipelineEvents,
|
|
254
|
+
scans: () => scans,
|
|
255
|
+
targets: () => targets,
|
|
256
|
+
triageMemories: () => triageMemories,
|
|
257
|
+
trustGraphEdges: () => trustGraphEdges,
|
|
258
|
+
verdicts: () => verdicts,
|
|
259
|
+
workItemKinds: () => workItemKinds,
|
|
260
|
+
workItemStatuses: () => workItemStatuses,
|
|
261
|
+
workItems: () => workItems,
|
|
262
|
+
workerStatuses: () => workerStatuses,
|
|
263
|
+
workers: () => workers
|
|
264
|
+
});
|
|
265
|
+
import { sqliteTable, text, integer, real, index } from "drizzle-orm/sqlite-core";
|
|
266
|
+
var findingStatuses = [
|
|
267
|
+
"discovered",
|
|
268
|
+
"verified",
|
|
269
|
+
"confirmed",
|
|
270
|
+
"scored",
|
|
271
|
+
"reported",
|
|
272
|
+
"fixed",
|
|
273
|
+
"false-positive"
|
|
274
|
+
];
|
|
275
|
+
var findingTriageStatuses = ["new", "accepted", "suppressed"];
|
|
276
|
+
var findingWorkflowStatuses = [
|
|
277
|
+
"backlog",
|
|
278
|
+
"todo",
|
|
279
|
+
"agent_review",
|
|
280
|
+
"in_progress",
|
|
281
|
+
"human_review",
|
|
282
|
+
"blocked",
|
|
283
|
+
"done",
|
|
284
|
+
"cancelled"
|
|
285
|
+
];
|
|
286
|
+
var caseStatuses = ["open", "in_progress", "human_review", "done", "cancelled"];
|
|
287
|
+
var workItemKinds = [
|
|
288
|
+
"surface_map",
|
|
289
|
+
"hypothesis",
|
|
290
|
+
"poc_build",
|
|
291
|
+
"blind_verify",
|
|
292
|
+
"consensus",
|
|
293
|
+
"human_review"
|
|
294
|
+
];
|
|
295
|
+
var workItemStatuses = ["backlog", "todo", "in_progress", "blocked", "done", "cancelled"];
|
|
296
|
+
var artifactKinds = ["request", "response", "analysis", "verdicts", "sessions", "events"];
|
|
297
|
+
var workerStatuses = ["idle", "claiming", "running", "sleeping", "stopped", "error"];
|
|
298
|
+
var credentialKinds = [
|
|
299
|
+
"credential",
|
|
300
|
+
"token",
|
|
301
|
+
"path",
|
|
302
|
+
"endpoint",
|
|
303
|
+
"hash",
|
|
304
|
+
"cookie"
|
|
305
|
+
];
|
|
306
|
+
var scans = sqliteTable("scans", {
|
|
307
|
+
id: text("id").primaryKey(),
|
|
308
|
+
target: text("target").notNull(),
|
|
309
|
+
depth: text("depth").notNull(),
|
|
310
|
+
runtime: text("runtime").notNull().default("api"),
|
|
311
|
+
mode: text("mode").notNull().default("probe"),
|
|
312
|
+
status: text("status").notNull().default("running"),
|
|
313
|
+
startedAt: text("startedAt").notNull(),
|
|
314
|
+
completedAt: text("completedAt"),
|
|
315
|
+
durationMs: integer("durationMs"),
|
|
316
|
+
summary: text("summary")
|
|
317
|
+
// JSON-encoded ReportSummary
|
|
318
|
+
});
|
|
319
|
+
var targets = sqliteTable("targets", {
|
|
320
|
+
id: text("id").primaryKey(),
|
|
321
|
+
url: text("url").notNull().unique(),
|
|
322
|
+
type: text("type").notNull().default("unknown"),
|
|
323
|
+
model: text("model"),
|
|
324
|
+
systemPrompt: text("systemPrompt"),
|
|
325
|
+
detectedFeatures: text("detectedFeatures"),
|
|
326
|
+
// JSON array
|
|
327
|
+
endpoints: text("endpoints"),
|
|
328
|
+
// JSON array
|
|
329
|
+
firstSeenAt: text("firstSeenAt").notNull(),
|
|
330
|
+
lastSeenAt: text("lastSeenAt").notNull()
|
|
331
|
+
}, (table) => [index("idx_targets_url").on(table.url)]);
|
|
332
|
+
var findings = sqliteTable("findings", {
|
|
333
|
+
id: text("id").primaryKey(),
|
|
334
|
+
scanId: text("scanId").notNull().references(() => scans.id),
|
|
335
|
+
templateId: text("templateId").notNull(),
|
|
336
|
+
title: text("title").notNull(),
|
|
337
|
+
description: text("description").notNull(),
|
|
338
|
+
severity: text("severity").notNull(),
|
|
339
|
+
category: text("category").notNull(),
|
|
340
|
+
status: text("status").notNull().default("discovered"),
|
|
341
|
+
fingerprint: text("fingerprint"),
|
|
342
|
+
triageStatus: text("triageStatus").notNull().default("new"),
|
|
343
|
+
triageNote: text("triageNote"),
|
|
344
|
+
triagedAt: text("triagedAt"),
|
|
345
|
+
workflowStatus: text("workflowStatus").notNull().default("backlog"),
|
|
346
|
+
workflowAssignee: text("workflowAssignee"),
|
|
347
|
+
workflowUpdatedAt: text("workflowUpdatedAt"),
|
|
348
|
+
score: integer("score"),
|
|
349
|
+
// CVSS-like 0-100 score, set during "scored" stage
|
|
350
|
+
confidence: real("confidence"),
|
|
351
|
+
// 0.0-1.0 agent-assessed confidence
|
|
352
|
+
cvssVector: text("cvssVector"),
|
|
353
|
+
// CVSS vector string
|
|
354
|
+
cvssScore: real("cvssScore"),
|
|
355
|
+
// CVSS numeric score (0-10)
|
|
356
|
+
evidenceRequest: text("evidenceRequest").notNull(),
|
|
357
|
+
evidenceResponse: text("evidenceResponse").notNull(),
|
|
358
|
+
evidenceAnalysis: text("evidenceAnalysis"),
|
|
359
|
+
/**
|
|
360
|
+
* JSON-stringified LayerVerdict[] (see @0sec/shared types). NULL until
|
|
361
|
+
* the triage stage runs. Stored as text rather than a join table because
|
|
362
|
+
* the array is read-and-write together at finding-save time and we never
|
|
363
|
+
* query individual verdict rows. See 0sec#112.
|
|
364
|
+
*/
|
|
365
|
+
layerVerdicts: text("layerVerdicts"),
|
|
366
|
+
impactAssessment: text("impactAssessment"),
|
|
367
|
+
// JSON: ImpactAssessment (reachability/blast/weaponizability/business)
|
|
368
|
+
/**
|
|
369
|
+
* JSON-stringified PocStep[] (see @0sec/shared types). NULL when the
|
|
370
|
+
* agent only produced prose evidence (the legacy default). Stored as
|
|
371
|
+
* text rather than a join table because the array is read-and-write
|
|
372
|
+
* together at finding-save time and we never query individual steps.
|
|
373
|
+
* See 0sec#170.
|
|
374
|
+
*/
|
|
375
|
+
pocSteps: text("pocSteps"),
|
|
376
|
+
/**
|
|
377
|
+
* JSON-stringified VerificationSpec (see @0sec/shared types). NULL
|
|
378
|
+
* when the agent produced a finding without a deterministic re-check
|
|
379
|
+
* contract — every reader must continue to work in that case. Stored
|
|
380
|
+
* as text because the spec is an opaque blob that the verifier reads
|
|
381
|
+
* whole; we never query individual predicates. See 0sec#193 /
|
|
382
|
+
* 0sec-cloud#111.
|
|
383
|
+
*/
|
|
384
|
+
verificationSpec: text("verificationSpec"),
|
|
385
|
+
/**
|
|
386
|
+
* JSON-stringified semantic-dedupe mapping assigned by the post-process
|
|
387
|
+
* pass. NULL until that opt-in pass runs. Keeping this with the finding
|
|
388
|
+
* preserves canonical anchors across later scans.
|
|
389
|
+
*/
|
|
390
|
+
semanticDedupe: text("semanticDedupe"),
|
|
391
|
+
/**
|
|
392
|
+
* Per-scan post-process rank. NULL when incremental ranking did not run.
|
|
393
|
+
*/
|
|
394
|
+
findingRank: integer("findingRank"),
|
|
395
|
+
/**
|
|
396
|
+
* JSON-stringified PocExecutionReport written when `disclose --target-url`
|
|
397
|
+
* runs the step graph against a live target. NULL until that runs. See
|
|
398
|
+
* 0sec#171.
|
|
399
|
+
*/
|
|
400
|
+
pocExecution: text("pocExecution"),
|
|
401
|
+
/**
|
|
402
|
+
* JSON-stringified VerificationResult (see @0sec/shared
|
|
403
|
+
* `VerificationResultSchema`) — the last deterministic-replay result
|
|
404
|
+
* attached to the finding. NULL when the finding was never verified;
|
|
405
|
+
* readers MUST treat NULL / malformed / statusless payloads as
|
|
406
|
+
* *absent* rather than as an empty result, because downstream gates
|
|
407
|
+
* (the TUI `f` source-fix action, disclosure promotion) key off
|
|
408
|
+
* `status === "reproduced"`. Stored as text for the same reason as
|
|
409
|
+
* `verificationSpec`: the blob is read and written whole.
|
|
410
|
+
*/
|
|
411
|
+
verificationResult: text("verificationResult"),
|
|
412
|
+
/**
|
|
413
|
+
* JSON-stringified Finding["reviewAnnotation"] — the scoped source
|
|
414
|
+
* reference (`path` + line range, optional suggestion / knownMarker)
|
|
415
|
+
* the agent cited at save_finding time. NULL when the finding carries
|
|
416
|
+
* no workspace-contained source location. Additive: every reader must
|
|
417
|
+
* keep working when it is absent.
|
|
418
|
+
*/
|
|
419
|
+
reviewAnnotation: text("reviewAnnotation"),
|
|
420
|
+
timestamp: integer("timestamp").notNull()
|
|
421
|
+
}, (table) => [
|
|
422
|
+
index("idx_findings_scanId").on(table.scanId),
|
|
423
|
+
index("idx_findings_severity").on(table.severity),
|
|
424
|
+
index("idx_findings_category").on(table.category),
|
|
425
|
+
index("idx_findings_status").on(table.status),
|
|
426
|
+
index("idx_findings_fingerprint").on(table.fingerprint),
|
|
427
|
+
index("idx_findings_triageStatus").on(table.triageStatus),
|
|
428
|
+
index("idx_findings_workflowStatus").on(table.workflowStatus)
|
|
429
|
+
]);
|
|
430
|
+
var attackResults = sqliteTable("attack_results", {
|
|
431
|
+
id: text("id").primaryKey(),
|
|
432
|
+
scanId: text("scanId").notNull().references(() => scans.id),
|
|
433
|
+
templateId: text("templateId").notNull(),
|
|
434
|
+
payloadId: text("payloadId").notNull(),
|
|
435
|
+
outcome: text("outcome").notNull(),
|
|
436
|
+
request: text("request").notNull(),
|
|
437
|
+
response: text("response").notNull(),
|
|
438
|
+
latencyMs: integer("latencyMs").notNull(),
|
|
439
|
+
timestamp: integer("timestamp").notNull(),
|
|
440
|
+
error: text("error")
|
|
441
|
+
}, (table) => [index("idx_attack_results_scanId").on(table.scanId)]);
|
|
442
|
+
var verdicts = sqliteTable("verdicts", {
|
|
443
|
+
id: text("id").primaryKey(),
|
|
444
|
+
findingId: text("findingId").notNull().references(() => findings.id),
|
|
445
|
+
agentRole: text("agentRole").notNull(),
|
|
446
|
+
model: text("model").notNull().default(""),
|
|
447
|
+
verdict: text("verdict").notNull(),
|
|
448
|
+
// TRUE_POSITIVE | FALSE_POSITIVE | UNSURE
|
|
449
|
+
confidence: real("confidence").notNull().default(0),
|
|
450
|
+
reasoning: text("reasoning").notNull().default(""),
|
|
451
|
+
timestamp: integer("timestamp").notNull()
|
|
452
|
+
}, (table) => [index("idx_verdicts_findingId").on(table.findingId)]);
|
|
453
|
+
var pipelineEvents = sqliteTable("pipeline_events", {
|
|
454
|
+
id: text("id").primaryKey(),
|
|
455
|
+
scanId: text("scanId").notNull().references(() => scans.id),
|
|
456
|
+
stage: text("stage").notNull(),
|
|
457
|
+
eventType: text("eventType").notNull(),
|
|
458
|
+
findingId: text("findingId"),
|
|
459
|
+
agentRole: text("agentRole"),
|
|
460
|
+
source: text("source").notNull().default("core"),
|
|
461
|
+
payload: text("payload").notNull().default("{}"),
|
|
462
|
+
// JSON
|
|
463
|
+
timestamp: integer("timestamp").notNull()
|
|
464
|
+
}, (table) => [
|
|
465
|
+
index("idx_events_scanId").on(table.scanId),
|
|
466
|
+
index("idx_events_stage").on(table.stage),
|
|
467
|
+
index("idx_events_findingId").on(table.findingId)
|
|
468
|
+
]);
|
|
469
|
+
var agentSessions = sqliteTable("agent_sessions", {
|
|
470
|
+
id: text("id").primaryKey(),
|
|
471
|
+
scanId: text("scanId").notNull().references(() => scans.id),
|
|
472
|
+
agentRole: text("agentRole").notNull(),
|
|
473
|
+
turnCount: integer("turnCount").notNull().default(0),
|
|
474
|
+
messages: text("messages").notNull().default("[]"),
|
|
475
|
+
// JSON serialized conversation
|
|
476
|
+
toolContext: text("toolContext").notNull().default("{}"),
|
|
477
|
+
// JSON serialized context
|
|
478
|
+
status: text("status").notNull().default("running"),
|
|
479
|
+
// running | paused | completed | failed
|
|
480
|
+
createdAt: text("createdAt").notNull(),
|
|
481
|
+
updatedAt: text("updatedAt").notNull()
|
|
482
|
+
}, (table) => [
|
|
483
|
+
index("idx_sessions_scanId").on(table.scanId),
|
|
484
|
+
index("idx_sessions_role").on(table.agentRole)
|
|
485
|
+
]);
|
|
486
|
+
var cases = sqliteTable("cases", {
|
|
487
|
+
id: text("id").primaryKey(),
|
|
488
|
+
target: text("target").notNull().unique(),
|
|
489
|
+
targetType: text("targetType").notNull().default("unknown"),
|
|
490
|
+
latestScanId: text("latestScanId"),
|
|
491
|
+
status: text("status").notNull().default("open"),
|
|
492
|
+
createdAt: text("createdAt").notNull(),
|
|
493
|
+
updatedAt: text("updatedAt").notNull()
|
|
494
|
+
}, (table) => [
|
|
495
|
+
index("idx_cases_target").on(table.target),
|
|
496
|
+
index("idx_cases_status").on(table.status)
|
|
497
|
+
]);
|
|
498
|
+
var workItems = sqliteTable("work_items", {
|
|
499
|
+
id: text("id").primaryKey(),
|
|
500
|
+
caseId: text("caseId").notNull().references(() => cases.id),
|
|
501
|
+
findingFingerprint: text("findingFingerprint"),
|
|
502
|
+
kind: text("kind").notNull(),
|
|
503
|
+
title: text("title").notNull(),
|
|
504
|
+
owner: text("owner"),
|
|
505
|
+
status: text("status").notNull().default("backlog"),
|
|
506
|
+
summary: text("summary"),
|
|
507
|
+
dependsOn: text("dependsOn"),
|
|
508
|
+
createdAt: text("createdAt").notNull(),
|
|
509
|
+
updatedAt: text("updatedAt").notNull()
|
|
510
|
+
}, (table) => [
|
|
511
|
+
index("idx_work_items_caseId").on(table.caseId),
|
|
512
|
+
index("idx_work_items_fingerprint").on(table.findingFingerprint),
|
|
513
|
+
index("idx_work_items_status").on(table.status)
|
|
514
|
+
]);
|
|
515
|
+
var artifacts = sqliteTable("artifacts", {
|
|
516
|
+
id: text("id").primaryKey(),
|
|
517
|
+
caseId: text("caseId").notNull().references(() => cases.id),
|
|
518
|
+
findingFingerprint: text("findingFingerprint"),
|
|
519
|
+
workItemId: text("workItemId"),
|
|
520
|
+
kind: text("kind").notNull(),
|
|
521
|
+
label: text("label").notNull(),
|
|
522
|
+
content: text("content"),
|
|
523
|
+
metadata: text("metadata"),
|
|
524
|
+
createdAt: text("createdAt").notNull(),
|
|
525
|
+
updatedAt: text("updatedAt").notNull()
|
|
526
|
+
}, (table) => [
|
|
527
|
+
index("idx_artifacts_caseId").on(table.caseId),
|
|
528
|
+
index("idx_artifacts_fingerprint").on(table.findingFingerprint),
|
|
529
|
+
index("idx_artifacts_workItemId").on(table.workItemId)
|
|
530
|
+
]);
|
|
531
|
+
var triageMemories = sqliteTable("triage_memories", {
|
|
532
|
+
id: text("id").primaryKey(),
|
|
533
|
+
scope: text("scope").notNull(),
|
|
534
|
+
// global | target | package
|
|
535
|
+
scopeValue: text("scope_value"),
|
|
536
|
+
category: text("category").notNull(),
|
|
537
|
+
pattern: text("pattern").notNull(),
|
|
538
|
+
reasoning: text("reasoning").notNull(),
|
|
539
|
+
createdAt: integer("created_at").notNull(),
|
|
540
|
+
appliedCount: integer("applied_count").notNull().default(0)
|
|
541
|
+
}, (table) => [
|
|
542
|
+
index("idx_memories_category").on(table.category, table.scope),
|
|
543
|
+
index("idx_memories_scope").on(table.scope, table.scopeValue)
|
|
544
|
+
]);
|
|
545
|
+
var workers = sqliteTable("workers", {
|
|
546
|
+
id: text("id").primaryKey(),
|
|
547
|
+
role: text("role").notNull().default("orchestrator"),
|
|
548
|
+
status: text("status").notNull().default("idle"),
|
|
549
|
+
label: text("label").notNull(),
|
|
550
|
+
currentCaseId: text("currentCaseId"),
|
|
551
|
+
currentWorkItemId: text("currentWorkItemId"),
|
|
552
|
+
currentScanId: text("currentScanId"),
|
|
553
|
+
pid: integer("pid"),
|
|
554
|
+
host: text("host"),
|
|
555
|
+
lastError: text("lastError"),
|
|
556
|
+
heartbeatAt: text("heartbeatAt").notNull(),
|
|
557
|
+
startedAt: text("startedAt").notNull(),
|
|
558
|
+
updatedAt: text("updatedAt").notNull()
|
|
559
|
+
}, (table) => [
|
|
560
|
+
index("idx_workers_status").on(table.status),
|
|
561
|
+
index("idx_workers_heartbeat").on(table.heartbeatAt)
|
|
562
|
+
]);
|
|
563
|
+
var persistentCredentials = sqliteTable("persistent_credentials", {
|
|
564
|
+
id: text("id").primaryKey(),
|
|
565
|
+
credentialKind: text("credentialKind").notNull(),
|
|
566
|
+
/** SHA-256 hex of the normalized value. Half of the natural key. */
|
|
567
|
+
valueHash: text("valueHash").notNull(),
|
|
568
|
+
/** Short, redacted preview for recognition (e.g. `admin:hun…`). Not a secret. */
|
|
569
|
+
valuePreview: text("valuePreview").notNull(),
|
|
570
|
+
/** Optional short label the value sat behind (e.g. `password`, `jwt`). */
|
|
571
|
+
context: text("context"),
|
|
572
|
+
/** Target the value was first discovered against. */
|
|
573
|
+
target: text("target"),
|
|
574
|
+
/** Discovery attribution — first sighting. */
|
|
575
|
+
firstScanId: text("firstScanId"),
|
|
576
|
+
firstFindingId: text("firstFindingId"),
|
|
577
|
+
firstSource: text("firstSource"),
|
|
578
|
+
firstTurn: integer("firstTurn"),
|
|
579
|
+
/** Most-recent sighting attribution. */
|
|
580
|
+
lastScanId: text("lastScanId"),
|
|
581
|
+
timesSeen: integer("timesSeen").notNull().default(1),
|
|
582
|
+
firstSeenAt: text("firstSeenAt").notNull(),
|
|
583
|
+
lastSeenAt: text("lastSeenAt").notNull()
|
|
584
|
+
}, (table) => [
|
|
585
|
+
index("idx_pcred_kind_hash").on(table.credentialKind, table.valueHash),
|
|
586
|
+
index("idx_pcred_target").on(table.target),
|
|
587
|
+
index("idx_pcred_kind").on(table.credentialKind)
|
|
588
|
+
]);
|
|
589
|
+
var trustGraphEdges = sqliteTable("trust_graph_edges", {
|
|
590
|
+
id: text("id").primaryKey(),
|
|
591
|
+
srcKind: text("srcKind").notNull(),
|
|
592
|
+
srcId: text("srcId").notNull(),
|
|
593
|
+
dstKind: text("dstKind").notNull(),
|
|
594
|
+
dstId: text("dstId").notNull(),
|
|
595
|
+
relation: text("relation").notNull(),
|
|
596
|
+
/** Optional attribution for when/where this edge was observed. */
|
|
597
|
+
scanId: text("scanId"),
|
|
598
|
+
findingId: text("findingId"),
|
|
599
|
+
/** 0.0-1.0 confidence the edge is real. */
|
|
600
|
+
confidence: real("confidence"),
|
|
601
|
+
note: text("note"),
|
|
602
|
+
createdAt: text("createdAt").notNull(),
|
|
603
|
+
updatedAt: text("updatedAt").notNull()
|
|
604
|
+
}, (table) => [
|
|
605
|
+
index("idx_tge_src").on(table.srcKind, table.srcId),
|
|
606
|
+
index("idx_tge_dst").on(table.dstKind, table.dstId),
|
|
607
|
+
index("idx_tge_relation").on(table.relation)
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
// packages/db/dist/database.js
|
|
611
|
+
var DEFAULT_DB_DIR = homeStateDir();
|
|
612
|
+
var DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, "0sec.db");
|
|
613
|
+
function resolveOsecDbPath(dbPath) {
|
|
614
|
+
const configuredPath = process.env["0SEC_DB_PATH"]?.trim();
|
|
615
|
+
return dbPath ?? (configuredPath ? configuredPath : DEFAULT_DB_PATH);
|
|
616
|
+
}
|
|
617
|
+
function resetOsecDatabase(dbPath) {
|
|
618
|
+
const path = resolveOsecDbPath(dbPath);
|
|
619
|
+
if (path !== ":memory:") {
|
|
620
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
621
|
+
}
|
|
622
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
623
|
+
const candidate = `${path}${suffix}`;
|
|
624
|
+
if (existsSync(candidate)) {
|
|
625
|
+
rmSync(candidate, { force: true });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return path;
|
|
629
|
+
}
|
|
630
|
+
function repairOsecDatabase(dbPath) {
|
|
631
|
+
const path = resolveOsecDbPath(dbPath);
|
|
632
|
+
if (path !== ":memory:") {
|
|
633
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
634
|
+
}
|
|
635
|
+
clearStaleLockIfAny(path);
|
|
636
|
+
const backupPath = backupCorruptDatabase(path) ?? void 0;
|
|
637
|
+
resetOsecDatabase(path);
|
|
638
|
+
const db = new osecDB(path);
|
|
639
|
+
db.close();
|
|
640
|
+
return { path, backupPath };
|
|
641
|
+
}
|
|
642
|
+
function isRecoverableDatabaseError(error) {
|
|
643
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
644
|
+
return /database disk image is malformed|file is not a database|malformed|invalid page number|database main|btree|b-tree|database corrupt/i.test(message);
|
|
645
|
+
}
|
|
646
|
+
function timestampTag() {
|
|
647
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
648
|
+
}
|
|
649
|
+
function backupCorruptDatabase(path) {
|
|
650
|
+
if (!existsSync(path))
|
|
651
|
+
return null;
|
|
652
|
+
const backupPath = `${path}.corrupt-${timestampTag()}`;
|
|
653
|
+
renameSync(path, backupPath);
|
|
654
|
+
for (const suffix of ["-wal", "-shm", ".lock"]) {
|
|
655
|
+
const candidate = `${path}${suffix}`;
|
|
656
|
+
if (!existsSync(candidate))
|
|
657
|
+
continue;
|
|
658
|
+
try {
|
|
659
|
+
renameSync(candidate, `${backupPath}${suffix}`);
|
|
660
|
+
} catch {
|
|
661
|
+
if (suffix === ".lock") {
|
|
662
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return backupPath;
|
|
667
|
+
}
|
|
668
|
+
function ensureDatabaseHealthy(sqlite) {
|
|
669
|
+
const rows = sqlite.prepare("PRAGMA quick_check(1)").all();
|
|
670
|
+
const first = rows[0] ? Object.values(rows[0])[0] : "ok";
|
|
671
|
+
if (typeof first === "string" && first.toLowerCase() !== "ok") {
|
|
672
|
+
throw new Error(first);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function migrateWalHeaderIfNeeded(path) {
|
|
676
|
+
if (!existsSync(path))
|
|
677
|
+
return;
|
|
678
|
+
let size;
|
|
679
|
+
try {
|
|
680
|
+
size = statSync(path).size;
|
|
681
|
+
} catch {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (size < 20)
|
|
685
|
+
return;
|
|
686
|
+
const header = Buffer.alloc(100);
|
|
687
|
+
try {
|
|
688
|
+
const fd = openSync(path, "r");
|
|
689
|
+
try {
|
|
690
|
+
readSync(fd, header, 0, 100, 0);
|
|
691
|
+
} finally {
|
|
692
|
+
closeSync(fd);
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (header.subarray(0, 15).toString("ascii") !== "SQLite format 3")
|
|
698
|
+
return;
|
|
699
|
+
if (header[18] !== 2 && header[19] !== 2)
|
|
700
|
+
return;
|
|
701
|
+
const walPath = `${path}-wal`;
|
|
702
|
+
const shmPath = `${path}-shm`;
|
|
703
|
+
let hadWalData = false;
|
|
704
|
+
try {
|
|
705
|
+
if (existsSync(walPath) && statSync(walPath).size > 0)
|
|
706
|
+
hadWalData = true;
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const fd = openSync(path, "r+");
|
|
711
|
+
try {
|
|
712
|
+
writeSync(fd, Buffer.from([1, 1]), 0, 2, 18);
|
|
713
|
+
} finally {
|
|
714
|
+
closeSync(fd);
|
|
715
|
+
}
|
|
716
|
+
} catch (err) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
for (const sidecar of [walPath, shmPath]) {
|
|
720
|
+
if (existsSync(sidecar)) {
|
|
721
|
+
try {
|
|
722
|
+
rmSync(sidecar, { force: true });
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (hadWalData) {
|
|
728
|
+
console.warn(`[0sec] Migrated ${path} from WAL mode to rollback mode for WASM engine compatibility. A non-empty WAL sidecar was present and has been removed; any uncommitted transactions from a prior crashed run were discarded.`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function clearStaleLockIfAny(path) {
|
|
732
|
+
const lockPath = `${path}.lock`;
|
|
733
|
+
let stat;
|
|
734
|
+
try {
|
|
735
|
+
stat = statSync(lockPath);
|
|
736
|
+
} catch {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (!stat.isDirectory())
|
|
740
|
+
return;
|
|
741
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
742
|
+
const STALE_THRESHOLD_MS = 1e4;
|
|
743
|
+
if (ageMs < STALE_THRESHOLD_MS)
|
|
744
|
+
return;
|
|
745
|
+
try {
|
|
746
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
747
|
+
console.warn(`[0sec] Removed stale database lock at ${lockPath} (age ${Math.floor(ageMs / 1e3)}s \u2014 previous holder likely crashed).`);
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
var osecDB = class {
|
|
752
|
+
sqlite;
|
|
753
|
+
db;
|
|
754
|
+
constructor(dbPath, options = {}) {
|
|
755
|
+
const path = resolveOsecDbPath(dbPath);
|
|
756
|
+
if (options.readOnly) {
|
|
757
|
+
if (path !== ":memory:" && !existsSync(path)) {
|
|
758
|
+
throw new Error(`Database does not exist: ${path}`);
|
|
759
|
+
}
|
|
760
|
+
this.initializeReadOnlyDatabase(path);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (path !== ":memory:") {
|
|
764
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
765
|
+
}
|
|
766
|
+
this.openWithRecovery(path);
|
|
767
|
+
}
|
|
768
|
+
openWithRecovery(path) {
|
|
769
|
+
migrateWalHeaderIfNeeded(path);
|
|
770
|
+
clearStaleLockIfAny(path);
|
|
771
|
+
try {
|
|
772
|
+
this.initializeDatabase(path);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
try {
|
|
775
|
+
this.sqlite?.close();
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
if (!isRecoverableDatabaseError(error))
|
|
779
|
+
throw error;
|
|
780
|
+
const backupPath = backupCorruptDatabase(path);
|
|
781
|
+
this.initializeDatabase(path);
|
|
782
|
+
console.warn(backupPath ? `[0sec] Recovered malformed database at ${path}. Backup saved to ${backupPath}.` : `[0sec] Recovered malformed database state at ${path} by recreating a fresh database.`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
initializeDatabase(path) {
|
|
786
|
+
this.sqlite = createShimmedDatabase(path);
|
|
787
|
+
this.sqlite.pragma("foreign_keys = ON");
|
|
788
|
+
ensureDatabaseHealthy(this.sqlite);
|
|
789
|
+
this.db = createDrizzleFromShim(this.sqlite, { schema: schema_exports });
|
|
790
|
+
this.sqlite.exec(SCHEMA_TABLES_SQL);
|
|
791
|
+
this.migrate();
|
|
792
|
+
this.sqlite.exec(SCHEMA_INDEXES_SQL);
|
|
793
|
+
}
|
|
794
|
+
initializeReadOnlyDatabase(path) {
|
|
795
|
+
this.sqlite = createShimmedDatabase(path);
|
|
796
|
+
this.sqlite.pragma("foreign_keys = ON");
|
|
797
|
+
this.db = createDrizzleFromShim(this.sqlite, { schema: schema_exports });
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Upgrade databases created by older 0sec versions.
|
|
801
|
+
*
|
|
802
|
+
* SCHEMA_TABLES_SQL (above) now contains every table and every column from
|
|
803
|
+
* the drizzle schema, so fresh installs need no patching. This method only
|
|
804
|
+
* runs idempotent ALTER TABLEs for databases that were created before the
|
|
805
|
+
* corresponding columns/tables existed. Each guard is cheap (PRAGMA
|
|
806
|
+
* table_info is cached by SQLite) and the ALTERs are no-ops when the
|
|
807
|
+
* column is already present, so running on a fresh DB is harmless.
|
|
808
|
+
*
|
|
809
|
+
* See: https://github.com/0sec-labs/0sec/issues/420
|
|
810
|
+
*/
|
|
811
|
+
migrate() {
|
|
812
|
+
const cols = this.sqlite.prepare("PRAGMA table_info(findings)").all();
|
|
813
|
+
const colNames = new Set(cols.map((c) => c.name));
|
|
814
|
+
if (!colNames.has("score")) {
|
|
815
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN score INTEGER");
|
|
816
|
+
}
|
|
817
|
+
if (!colNames.has("confidence")) {
|
|
818
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN confidence REAL");
|
|
819
|
+
}
|
|
820
|
+
if (!colNames.has("cvssVector")) {
|
|
821
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN cvssVector TEXT");
|
|
822
|
+
}
|
|
823
|
+
if (!colNames.has("cvssScore")) {
|
|
824
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN cvssScore REAL");
|
|
825
|
+
}
|
|
826
|
+
if (!colNames.has("fingerprint")) {
|
|
827
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN fingerprint TEXT");
|
|
828
|
+
}
|
|
829
|
+
if (!colNames.has("triageStatus")) {
|
|
830
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN triageStatus TEXT NOT NULL DEFAULT 'new'");
|
|
831
|
+
}
|
|
832
|
+
if (!colNames.has("triageNote")) {
|
|
833
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN triageNote TEXT");
|
|
834
|
+
}
|
|
835
|
+
if (!colNames.has("triagedAt")) {
|
|
836
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN triagedAt TEXT");
|
|
837
|
+
}
|
|
838
|
+
if (!colNames.has("workflowStatus")) {
|
|
839
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN workflowStatus TEXT NOT NULL DEFAULT 'backlog'");
|
|
840
|
+
}
|
|
841
|
+
if (!colNames.has("workflowAssignee")) {
|
|
842
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN workflowAssignee TEXT");
|
|
843
|
+
}
|
|
844
|
+
if (!colNames.has("workflowUpdatedAt")) {
|
|
845
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN workflowUpdatedAt TEXT");
|
|
846
|
+
}
|
|
847
|
+
if (!colNames.has("layerVerdicts")) {
|
|
848
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN layerVerdicts TEXT");
|
|
849
|
+
}
|
|
850
|
+
if (!colNames.has("impactAssessment")) {
|
|
851
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN impactAssessment TEXT");
|
|
852
|
+
}
|
|
853
|
+
if (!colNames.has("pocSteps")) {
|
|
854
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN pocSteps TEXT");
|
|
855
|
+
}
|
|
856
|
+
if (!colNames.has("verificationSpec")) {
|
|
857
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN verificationSpec TEXT");
|
|
858
|
+
}
|
|
859
|
+
if (!colNames.has("pocExecution")) {
|
|
860
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN pocExecution TEXT");
|
|
861
|
+
}
|
|
862
|
+
if (!colNames.has("semanticDedupe")) {
|
|
863
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN semanticDedupe TEXT");
|
|
864
|
+
}
|
|
865
|
+
if (!colNames.has("findingRank")) {
|
|
866
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN findingRank INTEGER");
|
|
867
|
+
}
|
|
868
|
+
if (!colNames.has("verificationResult")) {
|
|
869
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN verificationResult TEXT");
|
|
870
|
+
}
|
|
871
|
+
if (!colNames.has("reviewAnnotation")) {
|
|
872
|
+
this.sqlite.exec("ALTER TABLE findings ADD COLUMN reviewAnnotation TEXT");
|
|
873
|
+
}
|
|
874
|
+
this.sqlite.exec("UPDATE findings SET fingerprint = id WHERE fingerprint IS NULL OR fingerprint = ''");
|
|
875
|
+
this.sqlite.exec("UPDATE findings SET triageStatus = 'new' WHERE triageStatus IS NULL OR triageStatus = ''");
|
|
876
|
+
this.sqlite.exec(`
|
|
877
|
+
UPDATE findings
|
|
878
|
+
SET workflowStatus = CASE
|
|
879
|
+
WHEN workflowStatus IS NOT NULL AND workflowStatus != '' THEN workflowStatus
|
|
880
|
+
WHEN triageStatus = 'accepted' OR status = 'reported' THEN 'done'
|
|
881
|
+
WHEN triageStatus = 'suppressed' OR status = 'false-positive' THEN 'cancelled'
|
|
882
|
+
WHEN status IN ('verified', 'confirmed', 'scored') THEN 'human_review'
|
|
883
|
+
ELSE 'backlog'
|
|
884
|
+
END
|
|
885
|
+
`);
|
|
886
|
+
const eventCols = this.sqlite.prepare("PRAGMA table_info(pipeline_events)").all();
|
|
887
|
+
if (!new Set(eventCols.map((column) => column.name)).has("source")) {
|
|
888
|
+
this.sqlite.exec("ALTER TABLE pipeline_events ADD COLUMN source TEXT NOT NULL DEFAULT 'core'");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// ── Triage Memories (Semgrep-style per-target FP learning) ──
|
|
892
|
+
insertTriageMemory(row) {
|
|
893
|
+
this.sqlite.prepare(`INSERT INTO triage_memories (id, scope, scope_value, category, pattern, reasoning, created_at, applied_count)
|
|
894
|
+
VALUES (@id, @scope, @scopeValue, @category, @pattern, @reasoning, @createdAt, @appliedCount)`).run({
|
|
895
|
+
id: row.id,
|
|
896
|
+
scope: row.scope,
|
|
897
|
+
scopeValue: row.scopeValue ?? null,
|
|
898
|
+
category: row.category,
|
|
899
|
+
pattern: row.pattern,
|
|
900
|
+
reasoning: row.reasoning,
|
|
901
|
+
createdAt: row.createdAt,
|
|
902
|
+
appliedCount: row.appliedCount ?? 0
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
listTriageMemories(opts) {
|
|
906
|
+
const where = [];
|
|
907
|
+
const params = {};
|
|
908
|
+
if (opts?.scope) {
|
|
909
|
+
where.push("scope = @scope");
|
|
910
|
+
params.scope = opts.scope;
|
|
911
|
+
}
|
|
912
|
+
if (opts?.scopeValue) {
|
|
913
|
+
where.push("scope_value = @scopeValue");
|
|
914
|
+
params.scopeValue = opts.scopeValue;
|
|
915
|
+
}
|
|
916
|
+
if (opts?.category) {
|
|
917
|
+
where.push("category = @category");
|
|
918
|
+
params.category = opts.category;
|
|
919
|
+
}
|
|
920
|
+
const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
921
|
+
const limit = opts?.limit ?? 500;
|
|
922
|
+
const rows = this.sqlite.prepare(`SELECT id, scope, scope_value as scopeValue, category, pattern, reasoning,
|
|
923
|
+
created_at as createdAt, applied_count as appliedCount
|
|
924
|
+
FROM triage_memories
|
|
925
|
+
${whereSql}
|
|
926
|
+
ORDER BY applied_count DESC, created_at DESC
|
|
927
|
+
LIMIT ${limit}`).all(params);
|
|
928
|
+
return rows;
|
|
929
|
+
}
|
|
930
|
+
deleteTriageMemory(id) {
|
|
931
|
+
const result = this.sqlite.prepare("DELETE FROM triage_memories WHERE id = ?").run(id);
|
|
932
|
+
return result.changes > 0;
|
|
933
|
+
}
|
|
934
|
+
incrementMemoryAppliedCount(id) {
|
|
935
|
+
this.sqlite.prepare("UPDATE triage_memories SET applied_count = applied_count + 1 WHERE id = ?").run(id);
|
|
936
|
+
}
|
|
937
|
+
buildCaseId(target) {
|
|
938
|
+
return `case:${encodeURIComponent(target.trim().toLowerCase())}`;
|
|
939
|
+
}
|
|
940
|
+
inferCaseTargetType(scan) {
|
|
941
|
+
if (scan.mode === "web")
|
|
942
|
+
return "web-app";
|
|
943
|
+
if (scan.mode === "probe" || scan.mode === "mcp")
|
|
944
|
+
return "ai-app";
|
|
945
|
+
if (scan.target.startsWith("http://") || scan.target.startsWith("https://"))
|
|
946
|
+
return "ai-app";
|
|
947
|
+
if (scan.target.startsWith("/") || scan.target.startsWith(".") || scan.target.includes("/"))
|
|
948
|
+
return "repository";
|
|
949
|
+
if (!scan.target.includes(" "))
|
|
950
|
+
return "package";
|
|
951
|
+
return "unknown";
|
|
952
|
+
}
|
|
953
|
+
syncCaseForScan(scanId) {
|
|
954
|
+
const scan = this.getScan(scanId);
|
|
955
|
+
if (!scan)
|
|
956
|
+
return null;
|
|
957
|
+
const caseId = this.buildCaseId(scan.target);
|
|
958
|
+
const status = scan.status === "running" ? "in_progress" : scan.status === "failed" ? "open" : "open";
|
|
959
|
+
this.upsertCase({
|
|
960
|
+
id: caseId,
|
|
961
|
+
target: scan.target,
|
|
962
|
+
targetType: this.inferCaseTargetType(scan),
|
|
963
|
+
latestScanId: scan.id,
|
|
964
|
+
status
|
|
965
|
+
});
|
|
966
|
+
return caseId;
|
|
967
|
+
}
|
|
968
|
+
syncFindingGraph(scanId, fingerprint) {
|
|
969
|
+
const scan = this.getScan(scanId);
|
|
970
|
+
if (!scan)
|
|
971
|
+
return;
|
|
972
|
+
const caseId = this.syncCaseForScan(scanId);
|
|
973
|
+
if (!caseId)
|
|
974
|
+
return;
|
|
975
|
+
const rows = this.getRelatedFindings(fingerprint);
|
|
976
|
+
if (rows.length === 0)
|
|
977
|
+
return;
|
|
978
|
+
const latest = rows[0];
|
|
979
|
+
const scanIds = [...new Set(rows.map((row) => row.scanId))];
|
|
980
|
+
const verdicts2 = this.listVerdicts(rows.map((row) => row.id));
|
|
981
|
+
const sessions = this.listSessions({ scanIds });
|
|
982
|
+
const runningRoles = new Set(sessions.filter((session) => session.status === "running").map((session) => session.agentRole));
|
|
983
|
+
const completedRoles = new Set(sessions.filter((session) => session.status === "completed").map((session) => session.agentRole));
|
|
984
|
+
const failedRoles = new Set(sessions.filter((session) => session.status === "failed").map((session) => session.agentRole));
|
|
985
|
+
const hasAnalysis = Boolean(latest.evidenceAnalysis?.trim());
|
|
986
|
+
const hasExploitArtifacts = Boolean(latest.evidenceRequest?.trim() || latest.evidenceResponse?.trim());
|
|
987
|
+
const hasVerifierVotes = verdicts2.length > 0;
|
|
988
|
+
const workflowStatus = normalizeWorkflowStatus(latest.workflowStatus, latest);
|
|
989
|
+
const phase = workflowStatus === "done" || workflowStatus === "cancelled" || workflowStatus === "blocked" ? workflowStatus : workflowStatus === "in_progress" ? "in_progress" : workflowStatus === "todo" ? "todo" : "backlog";
|
|
990
|
+
const reviewGate = workflowStatus === "agent_review" || workflowStatus === "human_review" ? workflowStatus : latest.status === "verified" || latest.status === "confirmed" || latest.status === "scored" || latest.status === "reported" || latest.status === "fixed" || latest.status === "false-positive" ? "human_review" : "none";
|
|
991
|
+
const surfaceMapStatus = failedRoles.has("discovery") ? "blocked" : runningRoles.has("discovery") ? "in_progress" : completedRoles.has("discovery") || scan.status !== "running" || rows.length > 0 ? "done" : "todo";
|
|
992
|
+
const hypothesisStatus = failedRoles.has("review") || failedRoles.has("audit") ? "blocked" : runningRoles.has("review") || runningRoles.has("audit") ? "in_progress" : hasAnalysis || completedRoles.has("review") || completedRoles.has("audit") ? "done" : surfaceMapStatus === "done" ? "todo" : "backlog";
|
|
993
|
+
const pocBuildStatus = failedRoles.has("attack") ? "blocked" : runningRoles.has("attack") ? "in_progress" : hasExploitArtifacts || completedRoles.has("attack") ? "done" : hypothesisStatus === "done" ? "todo" : "backlog";
|
|
994
|
+
const blindVerifyStatus = failedRoles.has("verify") ? "blocked" : runningRoles.has("verify") ? "in_progress" : hasVerifierVotes || ["verified", "confirmed", "scored", "reported", "fixed", "false-positive"].includes(latest.status) ? "done" : pocBuildStatus === "done" ? "todo" : "backlog";
|
|
995
|
+
const consensusStatus = workflowStatus === "done" || workflowStatus === "cancelled" || ["verified", "confirmed", "scored", "reported", "fixed", "false-positive"].includes(latest.status) ? "done" : reviewGate === "agent_review" ? hasVerifierVotes ? "todo" : blindVerifyStatus === "in_progress" ? "backlog" : "todo" : blindVerifyStatus === "done" ? "todo" : "backlog";
|
|
996
|
+
const humanReviewStatus = phase === "done" || phase === "cancelled" ? "done" : reviewGate === "human_review" ? "todo" : reviewGate === "agent_review" ? "blocked" : consensusStatus === "done" ? "todo" : "backlog";
|
|
997
|
+
const items = [
|
|
998
|
+
{
|
|
999
|
+
kind: "surface_map",
|
|
1000
|
+
title: "Attack surface mapping",
|
|
1001
|
+
owner: "attack-surface-agent",
|
|
1002
|
+
status: surfaceMapStatus,
|
|
1003
|
+
summary: "Initial target surface and candidate family context captured.",
|
|
1004
|
+
dependsOn: null
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
kind: "hypothesis",
|
|
1008
|
+
title: "Exploit hypothesis",
|
|
1009
|
+
owner: "research-agent",
|
|
1010
|
+
status: hypothesisStatus,
|
|
1011
|
+
summary: "Research context and exploit framing for this family.",
|
|
1012
|
+
dependsOn: `${fingerprint}:surface_map`
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
kind: "poc_build",
|
|
1016
|
+
title: "PoC build",
|
|
1017
|
+
owner: latest.workflowAssignee ?? null,
|
|
1018
|
+
status: pocBuildStatus,
|
|
1019
|
+
summary: "Exploit request/response artifacts and reproduction chain.",
|
|
1020
|
+
dependsOn: `${fingerprint}:hypothesis`
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
kind: "blind_verify",
|
|
1024
|
+
title: "Blind verify",
|
|
1025
|
+
owner: null,
|
|
1026
|
+
status: blindVerifyStatus,
|
|
1027
|
+
summary: "Independent verification pass without research reasoning.",
|
|
1028
|
+
dependsOn: `${fingerprint}:poc_build`
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
kind: "consensus",
|
|
1032
|
+
title: "Consensus",
|
|
1033
|
+
owner: "consensus-agent",
|
|
1034
|
+
status: consensusStatus,
|
|
1035
|
+
summary: "Resolve verifier evidence into a concrete next step.",
|
|
1036
|
+
dependsOn: `${fingerprint}:blind_verify`
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
kind: "human_review",
|
|
1040
|
+
title: "Human review",
|
|
1041
|
+
owner: "operator",
|
|
1042
|
+
status: humanReviewStatus,
|
|
1043
|
+
summary: "Final operator sign-off before closure, suppression, or reporting.",
|
|
1044
|
+
dependsOn: `${fingerprint}:consensus`
|
|
1045
|
+
}
|
|
1046
|
+
];
|
|
1047
|
+
for (const item of items) {
|
|
1048
|
+
this.upsertWorkItem({
|
|
1049
|
+
id: `${fingerprint}:${item.kind}`,
|
|
1050
|
+
caseId,
|
|
1051
|
+
findingFingerprint: fingerprint,
|
|
1052
|
+
kind: item.kind,
|
|
1053
|
+
title: item.title,
|
|
1054
|
+
owner: item.owner,
|
|
1055
|
+
status: item.status,
|
|
1056
|
+
summary: item.summary,
|
|
1057
|
+
dependsOn: item.dependsOn ?? null
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
this.upsertArtifact({
|
|
1061
|
+
id: `${fingerprint}:request`,
|
|
1062
|
+
caseId,
|
|
1063
|
+
findingFingerprint: fingerprint,
|
|
1064
|
+
kind: "request",
|
|
1065
|
+
label: "Exploit request",
|
|
1066
|
+
content: latest.evidenceRequest,
|
|
1067
|
+
metadata: { findingId: latest.id, scanId: latest.scanId }
|
|
1068
|
+
});
|
|
1069
|
+
this.upsertArtifact({
|
|
1070
|
+
id: `${fingerprint}:response`,
|
|
1071
|
+
caseId,
|
|
1072
|
+
findingFingerprint: fingerprint,
|
|
1073
|
+
kind: "response",
|
|
1074
|
+
label: "Exploit response",
|
|
1075
|
+
content: latest.evidenceResponse,
|
|
1076
|
+
metadata: { findingId: latest.id, scanId: latest.scanId }
|
|
1077
|
+
});
|
|
1078
|
+
this.upsertArtifact({
|
|
1079
|
+
id: `${fingerprint}:analysis`,
|
|
1080
|
+
caseId,
|
|
1081
|
+
findingFingerprint: fingerprint,
|
|
1082
|
+
kind: "analysis",
|
|
1083
|
+
label: "Research analysis",
|
|
1084
|
+
content: latest.evidenceAnalysis ?? null,
|
|
1085
|
+
metadata: { findingId: latest.id, scanId: latest.scanId }
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
roleToWorkItemKind(role) {
|
|
1089
|
+
if (role === "discovery")
|
|
1090
|
+
return "surface_map";
|
|
1091
|
+
if (role === "attack")
|
|
1092
|
+
return "poc_build";
|
|
1093
|
+
if (role === "verify")
|
|
1094
|
+
return "blind_verify";
|
|
1095
|
+
if (role === "review" || role === "audit")
|
|
1096
|
+
return "hypothesis";
|
|
1097
|
+
if (role === "report")
|
|
1098
|
+
return "human_review";
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
workItemTemplate(kind) {
|
|
1102
|
+
switch (kind) {
|
|
1103
|
+
case "surface_map":
|
|
1104
|
+
return {
|
|
1105
|
+
title: "Attack surface mapping",
|
|
1106
|
+
owner: "attack-surface-agent",
|
|
1107
|
+
summary: "Map the target and establish the initial attack surface context.",
|
|
1108
|
+
dependsOn: null
|
|
1109
|
+
};
|
|
1110
|
+
case "hypothesis":
|
|
1111
|
+
return {
|
|
1112
|
+
title: "Exploit hypothesis",
|
|
1113
|
+
owner: "research-agent",
|
|
1114
|
+
summary: "Turn the surface signal into a concrete exploit theory.",
|
|
1115
|
+
dependsOn: "surface_map"
|
|
1116
|
+
};
|
|
1117
|
+
case "poc_build":
|
|
1118
|
+
return {
|
|
1119
|
+
title: "PoC build",
|
|
1120
|
+
owner: "research-agent",
|
|
1121
|
+
summary: "Create the exploit artifact chain and reproduction path.",
|
|
1122
|
+
dependsOn: "hypothesis"
|
|
1123
|
+
};
|
|
1124
|
+
case "blind_verify":
|
|
1125
|
+
return {
|
|
1126
|
+
title: "Blind verify",
|
|
1127
|
+
owner: "verify-agent",
|
|
1128
|
+
summary: "Reproduce the issue independently without research reasoning.",
|
|
1129
|
+
dependsOn: "poc_build"
|
|
1130
|
+
};
|
|
1131
|
+
case "consensus":
|
|
1132
|
+
return {
|
|
1133
|
+
title: "Consensus",
|
|
1134
|
+
owner: "consensus-agent",
|
|
1135
|
+
summary: "Resolve verifier evidence into the next decision.",
|
|
1136
|
+
dependsOn: "blind_verify"
|
|
1137
|
+
};
|
|
1138
|
+
case "human_review":
|
|
1139
|
+
return {
|
|
1140
|
+
title: "Human review",
|
|
1141
|
+
owner: "operator",
|
|
1142
|
+
summary: "Final sign-off before report, suppression, or closure.",
|
|
1143
|
+
dependsOn: "consensus"
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
ensureCaseWorkPlan(scanId) {
|
|
1148
|
+
const caseId = this.syncCaseForScan(scanId);
|
|
1149
|
+
if (!caseId)
|
|
1150
|
+
return null;
|
|
1151
|
+
for (const kind of ["surface_map", "hypothesis", "poc_build", "blind_verify", "consensus", "human_review"]) {
|
|
1152
|
+
const template = this.workItemTemplate(kind);
|
|
1153
|
+
this.upsertWorkItem({
|
|
1154
|
+
id: `${caseId}:${kind}`,
|
|
1155
|
+
caseId,
|
|
1156
|
+
kind,
|
|
1157
|
+
title: template.title,
|
|
1158
|
+
owner: template.owner,
|
|
1159
|
+
status: "backlog",
|
|
1160
|
+
summary: template.summary,
|
|
1161
|
+
dependsOn: template.dependsOn ? `${caseId}:${template.dependsOn}` : null
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
return caseId;
|
|
1165
|
+
}
|
|
1166
|
+
transitionCaseWorkItem(scanId, kind, status, opts) {
|
|
1167
|
+
this.transaction(() => {
|
|
1168
|
+
const caseId = this.ensureCaseWorkPlan(scanId);
|
|
1169
|
+
if (!caseId)
|
|
1170
|
+
return;
|
|
1171
|
+
const template = this.workItemTemplate(kind);
|
|
1172
|
+
const finding = this.getLatestFindingForScan(scanId);
|
|
1173
|
+
this.upsertWorkItem({
|
|
1174
|
+
id: `${caseId}:${kind}`,
|
|
1175
|
+
caseId,
|
|
1176
|
+
kind,
|
|
1177
|
+
title: template.title,
|
|
1178
|
+
owner: opts?.owner ?? template.owner,
|
|
1179
|
+
status,
|
|
1180
|
+
summary: opts?.summary ?? template.summary,
|
|
1181
|
+
dependsOn: template.dependsOn ? `${caseId}:${template.dependsOn}` : null
|
|
1182
|
+
});
|
|
1183
|
+
this.logEvent({
|
|
1184
|
+
scanId,
|
|
1185
|
+
stage: kind,
|
|
1186
|
+
eventType: "work_item_transition",
|
|
1187
|
+
findingId: finding?.id ?? void 0,
|
|
1188
|
+
agentRole: opts?.owner ?? template.owner ?? void 0,
|
|
1189
|
+
payload: {
|
|
1190
|
+
kind,
|
|
1191
|
+
status,
|
|
1192
|
+
owner: opts?.owner ?? template.owner ?? null,
|
|
1193
|
+
summary: opts?.summary ?? template.summary,
|
|
1194
|
+
caseId
|
|
1195
|
+
},
|
|
1196
|
+
timestamp: Date.now()
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
syncSessionGraph(session) {
|
|
1201
|
+
const caseId = this.syncCaseForScan(session.scanId);
|
|
1202
|
+
if (!caseId)
|
|
1203
|
+
return;
|
|
1204
|
+
this.upsertArtifact({
|
|
1205
|
+
id: `session:${session.id}`,
|
|
1206
|
+
caseId,
|
|
1207
|
+
kind: "sessions",
|
|
1208
|
+
label: `${session.agentRole} session`,
|
|
1209
|
+
content: JSON.stringify(session.toolContext),
|
|
1210
|
+
metadata: { scanId: session.scanId, agentRole: session.agentRole, status: session.status }
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
syncEventGraph(event, eventId) {
|
|
1214
|
+
const caseId = this.syncCaseForScan(event.scanId);
|
|
1215
|
+
if (!caseId)
|
|
1216
|
+
return;
|
|
1217
|
+
const finding = event.findingId ? this.getFinding(event.findingId) : null;
|
|
1218
|
+
this.upsertArtifact({
|
|
1219
|
+
id: `event:${eventId}`,
|
|
1220
|
+
caseId,
|
|
1221
|
+
findingFingerprint: finding?.fingerprint ?? null,
|
|
1222
|
+
kind: "events",
|
|
1223
|
+
label: `${event.stage}:${event.eventType}`,
|
|
1224
|
+
content: JSON.stringify(event.payload),
|
|
1225
|
+
metadata: {
|
|
1226
|
+
scanId: event.scanId,
|
|
1227
|
+
agentRole: event.agentRole ?? null,
|
|
1228
|
+
findingId: event.findingId ?? null,
|
|
1229
|
+
timestamp: event.timestamp
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
// ── Scans ──
|
|
1234
|
+
createScan(config, id = randomUUID()) {
|
|
1235
|
+
const sqliteId = id;
|
|
1236
|
+
this.db.insert(scans).values({
|
|
1237
|
+
id: sqliteId,
|
|
1238
|
+
target: config.target,
|
|
1239
|
+
depth: config.depth,
|
|
1240
|
+
runtime: config.runtime ?? "api",
|
|
1241
|
+
mode: config.mode ?? "probe",
|
|
1242
|
+
status: "running",
|
|
1243
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1244
|
+
}).run();
|
|
1245
|
+
this.syncCaseForScan(id);
|
|
1246
|
+
return id;
|
|
1247
|
+
}
|
|
1248
|
+
deleteScan(scanId) {
|
|
1249
|
+
const scan = this.getScan(scanId);
|
|
1250
|
+
if (!scan)
|
|
1251
|
+
return false;
|
|
1252
|
+
const findingIds = this.db.select({ id: findings.id, fingerprint: findings.fingerprint }).from(findings).where(eq(findings.scanId, scanId)).all();
|
|
1253
|
+
this.transaction(() => {
|
|
1254
|
+
if (findingIds.length > 0) {
|
|
1255
|
+
const ids = findingIds.map((finding) => finding.id);
|
|
1256
|
+
this.db.delete(verdicts).where(inArray(verdicts.findingId, ids)).run();
|
|
1257
|
+
}
|
|
1258
|
+
this.db.delete(agentSessions).where(eq(agentSessions.scanId, scanId)).run();
|
|
1259
|
+
this.db.delete(pipelineEvents).where(eq(pipelineEvents.scanId, scanId)).run();
|
|
1260
|
+
this.db.delete(attackResults).where(eq(attackResults.scanId, scanId)).run();
|
|
1261
|
+
this.db.delete(findings).where(eq(findings.scanId, scanId)).run();
|
|
1262
|
+
this.db.delete(scans).where(eq(scans.id, scanId)).run();
|
|
1263
|
+
const remainingTargetScans = this.db.select({ id: scans.id, startedAt: scans.startedAt }).from(scans).where(eq(scans.target, scan.target)).orderBy(desc(scans.startedAt)).all();
|
|
1264
|
+
const caseId = this.buildCaseId(scan.target);
|
|
1265
|
+
if (remainingTargetScans.length === 0) {
|
|
1266
|
+
this.db.delete(cases).where(eq(cases.id, caseId)).run();
|
|
1267
|
+
} else {
|
|
1268
|
+
this.db.update(cases).set({
|
|
1269
|
+
latestScanId: remainingTargetScans[0].id,
|
|
1270
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1271
|
+
}).where(eq(cases.id, caseId)).run();
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
completeScan(scanId, summary) {
|
|
1277
|
+
const scan = this.db.select({ startedAt: scans.startedAt }).from(scans).where(eq(scans.id, scanId)).get();
|
|
1278
|
+
const durationMs = scan ? Date.now() - new Date(scan.startedAt).getTime() : 0;
|
|
1279
|
+
this.db.update(scans).set({
|
|
1280
|
+
status: "completed",
|
|
1281
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1282
|
+
durationMs,
|
|
1283
|
+
summary: JSON.stringify(summary)
|
|
1284
|
+
}).where(eq(scans.id, scanId)).run();
|
|
1285
|
+
this.syncCaseForScan(scanId);
|
|
1286
|
+
}
|
|
1287
|
+
reopenScan(scanId) {
|
|
1288
|
+
this.db.update(scans).set({
|
|
1289
|
+
status: "running",
|
|
1290
|
+
completedAt: null,
|
|
1291
|
+
durationMs: null
|
|
1292
|
+
}).where(eq(scans.id, scanId)).run();
|
|
1293
|
+
this.syncCaseForScan(scanId);
|
|
1294
|
+
}
|
|
1295
|
+
failScan(scanId, error) {
|
|
1296
|
+
this.db.update(scans).set({
|
|
1297
|
+
status: "failed",
|
|
1298
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1299
|
+
summary: JSON.stringify({ error })
|
|
1300
|
+
}).where(eq(scans.id, scanId)).run();
|
|
1301
|
+
this.syncCaseForScan(scanId);
|
|
1302
|
+
}
|
|
1303
|
+
getScan(scanId) {
|
|
1304
|
+
return this.db.select().from(scans).where(eq(scans.id, scanId)).get();
|
|
1305
|
+
}
|
|
1306
|
+
listScans(limit = 20) {
|
|
1307
|
+
return this.db.select().from(scans).orderBy(desc(scans.startedAt)).limit(limit).all();
|
|
1308
|
+
}
|
|
1309
|
+
listScansByTarget(target, opts) {
|
|
1310
|
+
const query = this.db.select().from(scans).where(eq(scans.target, target)).orderBy(desc(scans.startedAt));
|
|
1311
|
+
if (opts?.limit !== void 0)
|
|
1312
|
+
query.limit(opts.limit);
|
|
1313
|
+
return query.all();
|
|
1314
|
+
}
|
|
1315
|
+
// ── Targets ──
|
|
1316
|
+
upsertTarget(info) {
|
|
1317
|
+
const existing = this.db.select({ id: targets.id }).from(targets).where(eq(targets.url, info.url)).get();
|
|
1318
|
+
if (existing) {
|
|
1319
|
+
this.db.update(targets).set({
|
|
1320
|
+
type: info.type,
|
|
1321
|
+
model: info.model ?? null,
|
|
1322
|
+
systemPrompt: info.systemPrompt ?? null,
|
|
1323
|
+
detectedFeatures: info.detectedFeatures ? JSON.stringify(info.detectedFeatures) : null,
|
|
1324
|
+
endpoints: info.endpoints ? JSON.stringify(info.endpoints) : null,
|
|
1325
|
+
lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1326
|
+
}).where(eq(targets.id, existing.id)).run();
|
|
1327
|
+
return existing.id;
|
|
1328
|
+
}
|
|
1329
|
+
const id = randomUUID();
|
|
1330
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1331
|
+
this.db.insert(targets).values({
|
|
1332
|
+
id,
|
|
1333
|
+
url: info.url,
|
|
1334
|
+
type: info.type,
|
|
1335
|
+
model: info.model ?? null,
|
|
1336
|
+
systemPrompt: info.systemPrompt ?? null,
|
|
1337
|
+
detectedFeatures: info.detectedFeatures ? JSON.stringify(info.detectedFeatures) : null,
|
|
1338
|
+
endpoints: info.endpoints ? JSON.stringify(info.endpoints) : null,
|
|
1339
|
+
firstSeenAt: now,
|
|
1340
|
+
lastSeenAt: now
|
|
1341
|
+
}).run();
|
|
1342
|
+
return id;
|
|
1343
|
+
}
|
|
1344
|
+
getTarget(url) {
|
|
1345
|
+
return this.db.select().from(targets).where(eq(targets.url, url)).get();
|
|
1346
|
+
}
|
|
1347
|
+
// ── Findings ──
|
|
1348
|
+
saveFinding(scanId, finding) {
|
|
1349
|
+
const workflowFinding = finding;
|
|
1350
|
+
const scan = this.getScan(scanId);
|
|
1351
|
+
const fingerprint = finding.fingerprint ?? buildFindingFingerprint(scan?.target ?? scanId, finding);
|
|
1352
|
+
const inheritedTriage = this.getLatestFindingByFingerprint(fingerprint);
|
|
1353
|
+
const inheritedWorkflowStatus = normalizeWorkflowStatus(workflowFinding.workflowStatus ?? inheritedTriage?.workflowStatus, {
|
|
1354
|
+
status: finding.status,
|
|
1355
|
+
triageStatus: finding.triageStatus ?? inheritedTriage?.triageStatus ?? "new"
|
|
1356
|
+
});
|
|
1357
|
+
const inheritedWorkflowAssignee = workflowFinding.workflowAssignee ?? inheritedTriage?.workflowAssignee ?? null;
|
|
1358
|
+
const impactAssessmentJson = finding.impactAssessment ? JSON.stringify(finding.impactAssessment) : null;
|
|
1359
|
+
const layerVerdictsJson = finding.layerVerdicts && finding.layerVerdicts.length > 0 ? JSON.stringify(finding.layerVerdicts) : null;
|
|
1360
|
+
const pocStepsJson = finding.pocSteps && finding.pocSteps.length > 0 ? JSON.stringify(finding.pocSteps) : null;
|
|
1361
|
+
const verificationSpecJson = finding.verificationSpec ? JSON.stringify(finding.verificationSpec) : null;
|
|
1362
|
+
const semanticDedupeJson = finding.semanticDedupe ? JSON.stringify(finding.semanticDedupe) : null;
|
|
1363
|
+
const verificationResultJson = serializeFindingVerificationResult(finding.verification_result);
|
|
1364
|
+
const reviewAnnotationJson = serializeFindingReviewAnnotation(finding.reviewAnnotation);
|
|
1365
|
+
const candidateFindingRank = finding.findingRank;
|
|
1366
|
+
const findingRank = typeof candidateFindingRank === "number" && Number.isSafeInteger(candidateFindingRank) && candidateFindingRank > 0 ? candidateFindingRank : null;
|
|
1367
|
+
const derivedEvidence = deriveEvidenceFromPocSteps(finding);
|
|
1368
|
+
const evidenceRequest = finding.evidence.request || derivedEvidence.request;
|
|
1369
|
+
const evidenceResponse = finding.evidence.response || derivedEvidence.response;
|
|
1370
|
+
const evidenceAnalysis = finding.evidence.analysis ?? derivedEvidence.analysis ?? null;
|
|
1371
|
+
this.db.insert(findings).values({
|
|
1372
|
+
id: finding.id,
|
|
1373
|
+
scanId,
|
|
1374
|
+
templateId: finding.templateId,
|
|
1375
|
+
title: finding.title,
|
|
1376
|
+
description: finding.description,
|
|
1377
|
+
severity: finding.severity,
|
|
1378
|
+
category: finding.category,
|
|
1379
|
+
status: finding.status,
|
|
1380
|
+
fingerprint,
|
|
1381
|
+
triageStatus: finding.triageStatus ?? inheritedTriage?.triageStatus ?? "new",
|
|
1382
|
+
triageNote: finding.triageNote ?? inheritedTriage?.triageNote ?? null,
|
|
1383
|
+
triagedAt: finding.triageStatus || inheritedTriage?.triageStatus ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
1384
|
+
workflowStatus: inheritedWorkflowStatus,
|
|
1385
|
+
workflowAssignee: inheritedWorkflowAssignee,
|
|
1386
|
+
workflowUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1387
|
+
confidence: finding.confidence ?? null,
|
|
1388
|
+
cvssVector: finding.cvssVector ?? null,
|
|
1389
|
+
cvssScore: finding.cvssScore ?? null,
|
|
1390
|
+
evidenceRequest,
|
|
1391
|
+
evidenceResponse,
|
|
1392
|
+
evidenceAnalysis,
|
|
1393
|
+
layerVerdicts: layerVerdictsJson,
|
|
1394
|
+
impactAssessment: impactAssessmentJson,
|
|
1395
|
+
pocSteps: pocStepsJson,
|
|
1396
|
+
verificationSpec: verificationSpecJson,
|
|
1397
|
+
semanticDedupe: semanticDedupeJson,
|
|
1398
|
+
findingRank,
|
|
1399
|
+
verificationResult: verificationResultJson,
|
|
1400
|
+
reviewAnnotation: reviewAnnotationJson,
|
|
1401
|
+
timestamp: finding.timestamp
|
|
1402
|
+
}).onConflictDoUpdate({
|
|
1403
|
+
target: findings.id,
|
|
1404
|
+
set: {
|
|
1405
|
+
templateId: finding.templateId,
|
|
1406
|
+
title: finding.title,
|
|
1407
|
+
description: finding.description,
|
|
1408
|
+
severity: finding.severity,
|
|
1409
|
+
category: finding.category,
|
|
1410
|
+
status: finding.status,
|
|
1411
|
+
fingerprint,
|
|
1412
|
+
triageStatus: finding.triageStatus ?? inheritedTriage?.triageStatus ?? "new",
|
|
1413
|
+
triageNote: finding.triageNote ?? inheritedTriage?.triageNote ?? null,
|
|
1414
|
+
triagedAt: finding.triageStatus || inheritedTriage?.triageStatus ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
1415
|
+
workflowStatus: inheritedWorkflowStatus,
|
|
1416
|
+
workflowAssignee: inheritedWorkflowAssignee,
|
|
1417
|
+
workflowUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1418
|
+
confidence: finding.confidence ?? null,
|
|
1419
|
+
cvssVector: finding.cvssVector ?? null,
|
|
1420
|
+
cvssScore: finding.cvssScore ?? null,
|
|
1421
|
+
evidenceRequest,
|
|
1422
|
+
evidenceResponse,
|
|
1423
|
+
evidenceAnalysis,
|
|
1424
|
+
layerVerdicts: layerVerdictsJson,
|
|
1425
|
+
impactAssessment: impactAssessmentJson,
|
|
1426
|
+
pocSteps: pocStepsJson,
|
|
1427
|
+
verificationSpec: verificationSpecJson,
|
|
1428
|
+
semanticDedupe: semanticDedupeJson,
|
|
1429
|
+
findingRank,
|
|
1430
|
+
verificationResult: verificationResultJson,
|
|
1431
|
+
reviewAnnotation: reviewAnnotationJson,
|
|
1432
|
+
timestamp: finding.timestamp
|
|
1433
|
+
}
|
|
1434
|
+
}).run();
|
|
1435
|
+
this.syncFindingGraph(scanId, fingerprint);
|
|
1436
|
+
}
|
|
1437
|
+
getFinding(findingId) {
|
|
1438
|
+
return this.db.select().from(findings).where(eq(findings.id, findingId)).get();
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Read path for the two source-fix gate fields. Returns the hydrated
|
|
1442
|
+
* `verification_result` / `reviewAnnotation` for a finding, with each key
|
|
1443
|
+
* omitted when the finding has none (including rows written before the
|
|
1444
|
+
* columns existed). Callers building a `Finding` spread this over the row:
|
|
1445
|
+
* `{ ...mapped, ...db.getFindingReviewFields(id) }`.
|
|
1446
|
+
*/
|
|
1447
|
+
getFindingReviewFields(findingId) {
|
|
1448
|
+
return restoreFindingReviewFields(this.getFinding(findingId));
|
|
1449
|
+
}
|
|
1450
|
+
getFindings(scanId) {
|
|
1451
|
+
return this.db.select().from(findings).where(eq(findings.scanId, scanId)).orderBy(findings.severity, findings.timestamp).all();
|
|
1452
|
+
}
|
|
1453
|
+
getScanFindings(scanId) {
|
|
1454
|
+
return this.getFindings(scanId);
|
|
1455
|
+
}
|
|
1456
|
+
getLatestFindingForScan(scanId) {
|
|
1457
|
+
return this.db.select().from(findings).where(eq(findings.scanId, scanId)).orderBy(desc(findings.timestamp)).get();
|
|
1458
|
+
}
|
|
1459
|
+
listFindings(opts) {
|
|
1460
|
+
const conditions = [];
|
|
1461
|
+
if (opts?.scanId)
|
|
1462
|
+
conditions.push(eq(findings.scanId, opts.scanId));
|
|
1463
|
+
if (opts?.severity)
|
|
1464
|
+
conditions.push(eq(findings.severity, opts.severity));
|
|
1465
|
+
if (opts?.category)
|
|
1466
|
+
conditions.push(eq(findings.category, opts.category));
|
|
1467
|
+
if (opts?.status)
|
|
1468
|
+
conditions.push(eq(findings.status, opts.status));
|
|
1469
|
+
if (opts?.triageStatus)
|
|
1470
|
+
conditions.push(eq(findings.triageStatus, opts.triageStatus));
|
|
1471
|
+
const query = this.db.select().from(findings).orderBy(desc(findings.timestamp)).limit(opts?.limit ?? 100);
|
|
1472
|
+
if (conditions.length > 0) {
|
|
1473
|
+
return query.where(and(...conditions)).all();
|
|
1474
|
+
}
|
|
1475
|
+
return query.all();
|
|
1476
|
+
}
|
|
1477
|
+
/** Alias for listFindings — backward compat with core agent tools */
|
|
1478
|
+
queryFindings(opts) {
|
|
1479
|
+
return this.listFindings(opts);
|
|
1480
|
+
}
|
|
1481
|
+
updateFindingStatus(findingId, status) {
|
|
1482
|
+
const finding = this.getFinding(findingId);
|
|
1483
|
+
this.db.update(findings).set({ status }).where(eq(findings.id, findingId)).run();
|
|
1484
|
+
if (finding?.fingerprint)
|
|
1485
|
+
this.syncFindingGraph(finding.scanId, finding.fingerprint);
|
|
1486
|
+
}
|
|
1487
|
+
saveFindingPocExecution(findingId, execution) {
|
|
1488
|
+
const serialized = JSON.stringify(execution);
|
|
1489
|
+
this.db.update(findings).set({ pocExecution: serialized }).where(eq(findings.id, findingId)).run();
|
|
1490
|
+
}
|
|
1491
|
+
updateFindingTriageByFingerprint(fingerprint, triageStatus, triageNote) {
|
|
1492
|
+
const latestFinding = this.getLatestFindingByFingerprint(fingerprint);
|
|
1493
|
+
const workflowStatus = triageStatus === "accepted" ? "done" : triageStatus === "suppressed" ? "cancelled" : null;
|
|
1494
|
+
this.db.update(findings).set({
|
|
1495
|
+
triageStatus,
|
|
1496
|
+
triageNote: triageNote ?? null,
|
|
1497
|
+
triagedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1498
|
+
...workflowStatus ? {
|
|
1499
|
+
workflowStatus,
|
|
1500
|
+
workflowUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1501
|
+
} : {}
|
|
1502
|
+
}).where(eq(findings.fingerprint, fingerprint)).run();
|
|
1503
|
+
const refreshedFinding = this.getLatestFindingByFingerprint(fingerprint) ?? latestFinding;
|
|
1504
|
+
if (refreshedFinding) {
|
|
1505
|
+
this.logEvent({
|
|
1506
|
+
scanId: refreshedFinding.scanId,
|
|
1507
|
+
stage: "triage",
|
|
1508
|
+
eventType: "triage_updated",
|
|
1509
|
+
findingId: refreshedFinding.id,
|
|
1510
|
+
payload: {
|
|
1511
|
+
fingerprint,
|
|
1512
|
+
triageStatus,
|
|
1513
|
+
workflowStatus: workflowStatus ?? "backlog",
|
|
1514
|
+
triageNote: triageNote ?? null
|
|
1515
|
+
},
|
|
1516
|
+
timestamp: Date.now()
|
|
1517
|
+
});
|
|
1518
|
+
this.syncFindingGraph(refreshedFinding.scanId, fingerprint);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
this.syncFindingGraph("", fingerprint);
|
|
1522
|
+
}
|
|
1523
|
+
updateFindingWorkflowByFingerprint(fingerprint, workflowStatus, workflowAssignee) {
|
|
1524
|
+
const latestFinding = this.getLatestFindingByFingerprint(fingerprint);
|
|
1525
|
+
const normalizedWorkflowStatus = normalizeWorkflowStatus(workflowStatus);
|
|
1526
|
+
const triageStatus = normalizedWorkflowStatus === "done" ? "accepted" : normalizedWorkflowStatus === "cancelled" ? "suppressed" : "new";
|
|
1527
|
+
this.db.update(findings).set({
|
|
1528
|
+
workflowStatus: normalizedWorkflowStatus,
|
|
1529
|
+
workflowAssignee: workflowAssignee ?? null,
|
|
1530
|
+
workflowUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1531
|
+
triageStatus,
|
|
1532
|
+
triagedAt: triageStatus === "new" ? null : (/* @__PURE__ */ new Date()).toISOString()
|
|
1533
|
+
}).where(eq(findings.fingerprint, fingerprint)).run();
|
|
1534
|
+
const refreshedFinding = this.getLatestFindingByFingerprint(fingerprint) ?? latestFinding;
|
|
1535
|
+
if (refreshedFinding) {
|
|
1536
|
+
this.logEvent({
|
|
1537
|
+
scanId: refreshedFinding.scanId,
|
|
1538
|
+
stage: "workflow",
|
|
1539
|
+
eventType: "workflow_status_changed",
|
|
1540
|
+
findingId: refreshedFinding.id,
|
|
1541
|
+
payload: {
|
|
1542
|
+
fingerprint,
|
|
1543
|
+
workflowStatus: normalizedWorkflowStatus,
|
|
1544
|
+
workflowAssignee: workflowAssignee ?? null,
|
|
1545
|
+
triageStatus
|
|
1546
|
+
},
|
|
1547
|
+
timestamp: Date.now()
|
|
1548
|
+
});
|
|
1549
|
+
this.syncFindingGraph(refreshedFinding.scanId, fingerprint);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
this.syncFindingGraph("", fingerprint);
|
|
1553
|
+
}
|
|
1554
|
+
updateFindingTriage(findingId, triageStatus, triageNote) {
|
|
1555
|
+
const finding = this.getFinding(findingId);
|
|
1556
|
+
if (!finding)
|
|
1557
|
+
throw new Error(`Finding ${findingId} not found`);
|
|
1558
|
+
if (!finding.fingerprint)
|
|
1559
|
+
throw new Error(`Finding ${findingId} has no fingerprint`);
|
|
1560
|
+
this.updateFindingTriageByFingerprint(finding.fingerprint, triageStatus, triageNote);
|
|
1561
|
+
}
|
|
1562
|
+
getLatestFindingByFingerprint(fingerprint) {
|
|
1563
|
+
return this.db.select().from(findings).where(eq(findings.fingerprint, fingerprint)).orderBy(desc(findings.timestamp)).get();
|
|
1564
|
+
}
|
|
1565
|
+
getRelatedFindings(fingerprint) {
|
|
1566
|
+
return this.db.select().from(findings).where(eq(findings.fingerprint, fingerprint)).orderBy(desc(findings.timestamp)).all();
|
|
1567
|
+
}
|
|
1568
|
+
// ── Status Pipeline: discovered → verified → confirmed → scored → reported → fixed ──
|
|
1569
|
+
transitionFindingStatus(findingId, newStatus) {
|
|
1570
|
+
const finding = this.getFinding(findingId);
|
|
1571
|
+
if (!finding)
|
|
1572
|
+
throw new Error(`Finding ${findingId} not found`);
|
|
1573
|
+
const currentIdx = findingStatuses.indexOf(finding.status);
|
|
1574
|
+
const newIdx = findingStatuses.indexOf(newStatus);
|
|
1575
|
+
if (newStatus !== "false-positive" && newIdx <= currentIdx) {
|
|
1576
|
+
throw new Error(`Cannot transition from '${finding.status}' to '${newStatus}'. Pipeline: ${findingStatuses.join(" \u2192 ")}`);
|
|
1577
|
+
}
|
|
1578
|
+
this.db.update(findings).set({ status: newStatus }).where(eq(findings.id, findingId)).run();
|
|
1579
|
+
if (finding.fingerprint)
|
|
1580
|
+
this.syncFindingGraph(finding.scanId, finding.fingerprint);
|
|
1581
|
+
}
|
|
1582
|
+
scoreFinding(findingId, score) {
|
|
1583
|
+
const finding = this.getFinding(findingId);
|
|
1584
|
+
if (score < 0 || score > 100)
|
|
1585
|
+
throw new Error("Score must be 0-100");
|
|
1586
|
+
this.db.update(findings).set({ score, status: "scored" }).where(eq(findings.id, findingId)).run();
|
|
1587
|
+
if (finding?.fingerprint)
|
|
1588
|
+
this.syncFindingGraph(finding.scanId, finding.fingerprint);
|
|
1589
|
+
}
|
|
1590
|
+
// ── Attack Results ──
|
|
1591
|
+
saveAttackResult(scanId, result) {
|
|
1592
|
+
const id = randomUUID();
|
|
1593
|
+
this.db.insert(attackResults).values({
|
|
1594
|
+
id,
|
|
1595
|
+
scanId,
|
|
1596
|
+
templateId: result.templateId,
|
|
1597
|
+
payloadId: result.payloadId,
|
|
1598
|
+
outcome: result.outcome,
|
|
1599
|
+
request: result.request,
|
|
1600
|
+
response: result.response,
|
|
1601
|
+
latencyMs: result.latencyMs,
|
|
1602
|
+
timestamp: result.timestamp,
|
|
1603
|
+
error: result.error ?? null
|
|
1604
|
+
}).run();
|
|
1605
|
+
}
|
|
1606
|
+
getAttackResults(scanId) {
|
|
1607
|
+
return this.db.select().from(attackResults).where(eq(attackResults.scanId, scanId)).orderBy(attackResults.timestamp).all();
|
|
1608
|
+
}
|
|
1609
|
+
// ── Verdicts (multi-agent consensus) ──
|
|
1610
|
+
addVerdict(verdict) {
|
|
1611
|
+
this.db.insert(verdicts).values({
|
|
1612
|
+
id: verdict.id,
|
|
1613
|
+
findingId: verdict.findingId,
|
|
1614
|
+
agentRole: verdict.agentRole,
|
|
1615
|
+
model: verdict.model,
|
|
1616
|
+
verdict: verdict.verdict,
|
|
1617
|
+
confidence: verdict.confidence,
|
|
1618
|
+
reasoning: verdict.reasoning,
|
|
1619
|
+
timestamp: verdict.timestamp
|
|
1620
|
+
}).run();
|
|
1621
|
+
const finding = this.getFinding(verdict.findingId);
|
|
1622
|
+
if (finding?.fingerprint)
|
|
1623
|
+
this.syncFindingGraph(finding.scanId, finding.fingerprint);
|
|
1624
|
+
}
|
|
1625
|
+
getVerdicts(findingId) {
|
|
1626
|
+
return this.db.select().from(verdicts).where(eq(verdicts.findingId, findingId)).orderBy(desc(verdicts.timestamp)).all();
|
|
1627
|
+
}
|
|
1628
|
+
listVerdicts(findingIds) {
|
|
1629
|
+
if (!findingIds || findingIds.length === 0)
|
|
1630
|
+
return [];
|
|
1631
|
+
const placeholders = findingIds.map(() => "?").join(",");
|
|
1632
|
+
return this.sqlite.prepare(`SELECT * FROM verdicts WHERE findingId IN (${placeholders}) ORDER BY timestamp DESC`).all(...findingIds);
|
|
1633
|
+
}
|
|
1634
|
+
/** Compute consensus: all agree TRUE_POSITIVE → verified, all FALSE_POSITIVE → false-positive */
|
|
1635
|
+
computeConsensus(findingId) {
|
|
1636
|
+
const vds = this.getVerdicts(findingId);
|
|
1637
|
+
if (vds.length === 0)
|
|
1638
|
+
return "pending";
|
|
1639
|
+
const types = new Set(vds.map((v) => v.verdict));
|
|
1640
|
+
if (types.size === 1 && types.has("TRUE_POSITIVE"))
|
|
1641
|
+
return "verified";
|
|
1642
|
+
if (types.size === 1 && types.has("FALSE_POSITIVE"))
|
|
1643
|
+
return "false-positive";
|
|
1644
|
+
return "disputed";
|
|
1645
|
+
}
|
|
1646
|
+
// ── Pipeline Events (audit trail) ──
|
|
1647
|
+
logEvent(event) {
|
|
1648
|
+
const id = randomUUID();
|
|
1649
|
+
this.db.insert(pipelineEvents).values({
|
|
1650
|
+
id,
|
|
1651
|
+
scanId: event.scanId,
|
|
1652
|
+
stage: event.stage,
|
|
1653
|
+
eventType: event.eventType,
|
|
1654
|
+
findingId: event.findingId ?? null,
|
|
1655
|
+
agentRole: event.agentRole ?? null,
|
|
1656
|
+
source: event.source ?? "core",
|
|
1657
|
+
payload: JSON.stringify(event.payload),
|
|
1658
|
+
timestamp: event.timestamp
|
|
1659
|
+
}).run();
|
|
1660
|
+
this.syncEventGraph(event, id);
|
|
1661
|
+
return id;
|
|
1662
|
+
}
|
|
1663
|
+
getEvents(scanId, opts) {
|
|
1664
|
+
const conditions = [eq(pipelineEvents.scanId, scanId)];
|
|
1665
|
+
if (opts?.stage)
|
|
1666
|
+
conditions.push(eq(pipelineEvents.stage, opts.stage));
|
|
1667
|
+
if (opts?.eventType)
|
|
1668
|
+
conditions.push(eq(pipelineEvents.eventType, opts.eventType));
|
|
1669
|
+
return this.db.select().from(pipelineEvents).where(and(...conditions)).orderBy(pipelineEvents.timestamp).all();
|
|
1670
|
+
}
|
|
1671
|
+
listRecentEvents(limit = 50) {
|
|
1672
|
+
return this.db.select({
|
|
1673
|
+
id: pipelineEvents.id,
|
|
1674
|
+
scanId: pipelineEvents.scanId,
|
|
1675
|
+
scanTarget: scans.target,
|
|
1676
|
+
stage: pipelineEvents.stage,
|
|
1677
|
+
eventType: pipelineEvents.eventType,
|
|
1678
|
+
findingId: pipelineEvents.findingId,
|
|
1679
|
+
findingFingerprint: findings.fingerprint,
|
|
1680
|
+
agentRole: pipelineEvents.agentRole,
|
|
1681
|
+
source: pipelineEvents.source,
|
|
1682
|
+
payload: pipelineEvents.payload,
|
|
1683
|
+
timestamp: pipelineEvents.timestamp
|
|
1684
|
+
}).from(pipelineEvents).innerJoin(scans, eq(pipelineEvents.scanId, scans.id)).leftJoin(findings, eq(pipelineEvents.findingId, findings.id)).orderBy(desc(pipelineEvents.timestamp)).limit(limit).all();
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Read persisted events strictly after a durable `(timestamp, id)` cursor.
|
|
1688
|
+
* The UUID tie-breaker makes same-millisecond multi-process writes stable.
|
|
1689
|
+
*/
|
|
1690
|
+
listEventsAfter(cursor, limit = 250) {
|
|
1691
|
+
const selectEvents = () => this.db.select({
|
|
1692
|
+
id: pipelineEvents.id,
|
|
1693
|
+
scanId: pipelineEvents.scanId,
|
|
1694
|
+
scanTarget: scans.target,
|
|
1695
|
+
stage: pipelineEvents.stage,
|
|
1696
|
+
eventType: pipelineEvents.eventType,
|
|
1697
|
+
findingId: pipelineEvents.findingId,
|
|
1698
|
+
findingFingerprint: findings.fingerprint,
|
|
1699
|
+
agentRole: pipelineEvents.agentRole,
|
|
1700
|
+
source: pipelineEvents.source,
|
|
1701
|
+
payload: pipelineEvents.payload,
|
|
1702
|
+
timestamp: pipelineEvents.timestamp
|
|
1703
|
+
}).from(pipelineEvents).innerJoin(scans, eq(pipelineEvents.scanId, scans.id)).leftJoin(findings, eq(pipelineEvents.findingId, findings.id));
|
|
1704
|
+
if (!cursor) {
|
|
1705
|
+
return selectEvents().orderBy(asc(pipelineEvents.timestamp), asc(pipelineEvents.id)).limit(limit).all();
|
|
1706
|
+
}
|
|
1707
|
+
return selectEvents().where(or(gt(pipelineEvents.timestamp, cursor.timestamp), and(eq(pipelineEvents.timestamp, cursor.timestamp), gt(pipelineEvents.id, cursor.id)))).orderBy(asc(pipelineEvents.timestamp), asc(pipelineEvents.id)).limit(limit).all();
|
|
1708
|
+
}
|
|
1709
|
+
// ── Agent Sessions (resumable state) ──
|
|
1710
|
+
saveSession(session) {
|
|
1711
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1712
|
+
this.db.insert(agentSessions).values({
|
|
1713
|
+
id: session.id,
|
|
1714
|
+
scanId: session.scanId,
|
|
1715
|
+
agentRole: session.agentRole,
|
|
1716
|
+
turnCount: session.turnCount,
|
|
1717
|
+
messages: JSON.stringify(session.messages),
|
|
1718
|
+
toolContext: JSON.stringify(session.toolContext),
|
|
1719
|
+
status: session.status,
|
|
1720
|
+
createdAt: now,
|
|
1721
|
+
updatedAt: now
|
|
1722
|
+
}).onConflictDoUpdate({
|
|
1723
|
+
target: agentSessions.id,
|
|
1724
|
+
set: {
|
|
1725
|
+
turnCount: session.turnCount,
|
|
1726
|
+
messages: JSON.stringify(session.messages),
|
|
1727
|
+
toolContext: JSON.stringify(session.toolContext),
|
|
1728
|
+
status: session.status,
|
|
1729
|
+
updatedAt: now
|
|
1730
|
+
}
|
|
1731
|
+
}).run();
|
|
1732
|
+
this.syncSessionGraph(session);
|
|
1733
|
+
}
|
|
1734
|
+
getSession(scanId, agentRole) {
|
|
1735
|
+
return this.db.select().from(agentSessions).where(and(eq(agentSessions.scanId, scanId), eq(agentSessions.agentRole, agentRole))).get();
|
|
1736
|
+
}
|
|
1737
|
+
getSessionById(sessionId) {
|
|
1738
|
+
return this.db.select().from(agentSessions).where(eq(agentSessions.id, sessionId)).get();
|
|
1739
|
+
}
|
|
1740
|
+
listSessions(opts) {
|
|
1741
|
+
const conditions = [];
|
|
1742
|
+
const params = [];
|
|
1743
|
+
if (opts?.scanIds?.length) {
|
|
1744
|
+
conditions.push(`scanId IN (${opts.scanIds.map(() => "?").join(",")})`);
|
|
1745
|
+
params.push(...opts.scanIds);
|
|
1746
|
+
}
|
|
1747
|
+
if (opts?.status) {
|
|
1748
|
+
conditions.push("status = ?");
|
|
1749
|
+
params.push(opts.status);
|
|
1750
|
+
}
|
|
1751
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1752
|
+
return this.sqlite.prepare(`SELECT * FROM agent_sessions ${where} ORDER BY updatedAt DESC`).all(...params);
|
|
1753
|
+
}
|
|
1754
|
+
// ── Cases / Work Items / Artifacts ──
|
|
1755
|
+
upsertCase(record) {
|
|
1756
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1757
|
+
this.db.insert(cases).values({
|
|
1758
|
+
id: record.id,
|
|
1759
|
+
target: record.target,
|
|
1760
|
+
targetType: record.targetType,
|
|
1761
|
+
latestScanId: record.latestScanId ?? null,
|
|
1762
|
+
status: record.status,
|
|
1763
|
+
createdAt: record.createdAt ?? now,
|
|
1764
|
+
updatedAt: record.updatedAt ?? now
|
|
1765
|
+
}).onConflictDoUpdate({
|
|
1766
|
+
target: cases.id,
|
|
1767
|
+
set: {
|
|
1768
|
+
target: record.target,
|
|
1769
|
+
targetType: record.targetType,
|
|
1770
|
+
latestScanId: record.latestScanId ?? null,
|
|
1771
|
+
status: record.status,
|
|
1772
|
+
updatedAt: record.updatedAt ?? now
|
|
1773
|
+
}
|
|
1774
|
+
}).run();
|
|
1775
|
+
}
|
|
1776
|
+
getCase(caseId) {
|
|
1777
|
+
return this.db.select().from(cases).where(eq(cases.id, caseId)).get();
|
|
1778
|
+
}
|
|
1779
|
+
listCases(limit = 100) {
|
|
1780
|
+
return this.db.select().from(cases).orderBy(desc(cases.updatedAt)).limit(limit).all();
|
|
1781
|
+
}
|
|
1782
|
+
upsertWorkItem(record) {
|
|
1783
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1784
|
+
this.db.insert(workItems).values({
|
|
1785
|
+
id: record.id,
|
|
1786
|
+
caseId: record.caseId,
|
|
1787
|
+
findingFingerprint: record.findingFingerprint ?? null,
|
|
1788
|
+
kind: record.kind,
|
|
1789
|
+
title: record.title,
|
|
1790
|
+
owner: record.owner ?? null,
|
|
1791
|
+
status: record.status,
|
|
1792
|
+
summary: record.summary ?? null,
|
|
1793
|
+
dependsOn: record.dependsOn ?? null,
|
|
1794
|
+
createdAt: record.createdAt ?? now,
|
|
1795
|
+
updatedAt: record.updatedAt ?? now
|
|
1796
|
+
}).onConflictDoUpdate({
|
|
1797
|
+
target: workItems.id,
|
|
1798
|
+
set: {
|
|
1799
|
+
caseId: record.caseId,
|
|
1800
|
+
findingFingerprint: record.findingFingerprint ?? null,
|
|
1801
|
+
kind: record.kind,
|
|
1802
|
+
title: record.title,
|
|
1803
|
+
owner: record.owner ?? null,
|
|
1804
|
+
status: record.status,
|
|
1805
|
+
summary: record.summary ?? null,
|
|
1806
|
+
dependsOn: record.dependsOn ?? null,
|
|
1807
|
+
updatedAt: record.updatedAt ?? now
|
|
1808
|
+
}
|
|
1809
|
+
}).run();
|
|
1810
|
+
}
|
|
1811
|
+
listWorkItems(opts) {
|
|
1812
|
+
const conditions = [];
|
|
1813
|
+
if (opts?.caseId)
|
|
1814
|
+
conditions.push(eq(workItems.caseId, opts.caseId));
|
|
1815
|
+
if (opts?.findingFingerprint)
|
|
1816
|
+
conditions.push(eq(workItems.findingFingerprint, opts.findingFingerprint));
|
|
1817
|
+
if (opts?.status)
|
|
1818
|
+
conditions.push(eq(workItems.status, opts.status));
|
|
1819
|
+
const query = this.db.select().from(workItems).orderBy(desc(workItems.updatedAt)).limit(opts?.limit ?? 200);
|
|
1820
|
+
if (conditions.length > 0)
|
|
1821
|
+
return query.where(and(...conditions)).all();
|
|
1822
|
+
return query.all();
|
|
1823
|
+
}
|
|
1824
|
+
claimWorkItem(workItemId, workerId, opts) {
|
|
1825
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1826
|
+
const result = this.sqlite.prepare(`
|
|
1827
|
+
UPDATE work_items
|
|
1828
|
+
SET status = 'in_progress',
|
|
1829
|
+
owner = @owner,
|
|
1830
|
+
summary = COALESCE(@summary, summary),
|
|
1831
|
+
updatedAt = @updatedAt
|
|
1832
|
+
WHERE id = @id
|
|
1833
|
+
AND status = @expectedStatus
|
|
1834
|
+
`).run({
|
|
1835
|
+
id: workItemId,
|
|
1836
|
+
owner: opts?.owner ?? workerId,
|
|
1837
|
+
summary: opts?.summary ?? null,
|
|
1838
|
+
updatedAt: now,
|
|
1839
|
+
expectedStatus: opts?.expectedStatus ?? "todo"
|
|
1840
|
+
});
|
|
1841
|
+
return result.changes > 0;
|
|
1842
|
+
}
|
|
1843
|
+
upsertArtifact(record) {
|
|
1844
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1845
|
+
this.db.insert(artifacts).values({
|
|
1846
|
+
id: record.id,
|
|
1847
|
+
caseId: record.caseId,
|
|
1848
|
+
findingFingerprint: record.findingFingerprint ?? null,
|
|
1849
|
+
workItemId: record.workItemId ?? null,
|
|
1850
|
+
kind: record.kind,
|
|
1851
|
+
label: record.label,
|
|
1852
|
+
content: record.content ?? null,
|
|
1853
|
+
metadata: record.metadata ? JSON.stringify(record.metadata) : null,
|
|
1854
|
+
createdAt: record.createdAt ?? now,
|
|
1855
|
+
updatedAt: record.updatedAt ?? now
|
|
1856
|
+
}).onConflictDoUpdate({
|
|
1857
|
+
target: artifacts.id,
|
|
1858
|
+
set: {
|
|
1859
|
+
caseId: record.caseId,
|
|
1860
|
+
findingFingerprint: record.findingFingerprint ?? null,
|
|
1861
|
+
workItemId: record.workItemId ?? null,
|
|
1862
|
+
kind: record.kind,
|
|
1863
|
+
label: record.label,
|
|
1864
|
+
content: record.content ?? null,
|
|
1865
|
+
metadata: record.metadata ? JSON.stringify(record.metadata) : null,
|
|
1866
|
+
updatedAt: record.updatedAt ?? now
|
|
1867
|
+
}
|
|
1868
|
+
}).run();
|
|
1869
|
+
}
|
|
1870
|
+
listArtifacts(opts) {
|
|
1871
|
+
const conditions = [];
|
|
1872
|
+
if (opts?.caseId)
|
|
1873
|
+
conditions.push(eq(artifacts.caseId, opts.caseId));
|
|
1874
|
+
if (opts?.findingFingerprint)
|
|
1875
|
+
conditions.push(eq(artifacts.findingFingerprint, opts.findingFingerprint));
|
|
1876
|
+
if (opts?.workItemId)
|
|
1877
|
+
conditions.push(eq(artifacts.workItemId, opts.workItemId));
|
|
1878
|
+
const query = this.db.select().from(artifacts).orderBy(desc(artifacts.updatedAt)).limit(opts?.limit ?? 200);
|
|
1879
|
+
if (conditions.length > 0)
|
|
1880
|
+
return query.where(and(...conditions)).all();
|
|
1881
|
+
return query.all();
|
|
1882
|
+
}
|
|
1883
|
+
upsertWorker(record) {
|
|
1884
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1885
|
+
this.db.insert(workers).values({
|
|
1886
|
+
id: record.id,
|
|
1887
|
+
role: record.role,
|
|
1888
|
+
status: record.status,
|
|
1889
|
+
label: record.label,
|
|
1890
|
+
currentCaseId: record.currentCaseId ?? null,
|
|
1891
|
+
currentWorkItemId: record.currentWorkItemId ?? null,
|
|
1892
|
+
currentScanId: record.currentScanId ?? null,
|
|
1893
|
+
pid: record.pid ?? null,
|
|
1894
|
+
host: record.host ?? null,
|
|
1895
|
+
lastError: record.lastError ?? null,
|
|
1896
|
+
heartbeatAt: record.heartbeatAt ?? now,
|
|
1897
|
+
startedAt: record.startedAt ?? now,
|
|
1898
|
+
updatedAt: record.updatedAt ?? now
|
|
1899
|
+
}).onConflictDoUpdate({
|
|
1900
|
+
target: workers.id,
|
|
1901
|
+
set: {
|
|
1902
|
+
role: record.role,
|
|
1903
|
+
status: record.status,
|
|
1904
|
+
label: record.label,
|
|
1905
|
+
currentCaseId: record.currentCaseId ?? null,
|
|
1906
|
+
currentWorkItemId: record.currentWorkItemId ?? null,
|
|
1907
|
+
currentScanId: record.currentScanId ?? null,
|
|
1908
|
+
pid: record.pid ?? null,
|
|
1909
|
+
host: record.host ?? null,
|
|
1910
|
+
lastError: record.lastError ?? null,
|
|
1911
|
+
heartbeatAt: record.heartbeatAt ?? now,
|
|
1912
|
+
updatedAt: record.updatedAt ?? now
|
|
1913
|
+
}
|
|
1914
|
+
}).run();
|
|
1915
|
+
}
|
|
1916
|
+
listWorkers(limit = 50) {
|
|
1917
|
+
return this.db.select().from(workers).orderBy(desc(workers.heartbeatAt)).limit(limit).all();
|
|
1918
|
+
}
|
|
1919
|
+
stopWorkersByLabel(label, exceptId) {
|
|
1920
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1921
|
+
const result = this.sqlite.prepare(`
|
|
1922
|
+
UPDATE workers
|
|
1923
|
+
SET status = 'stopped',
|
|
1924
|
+
currentCaseId = NULL,
|
|
1925
|
+
currentWorkItemId = NULL,
|
|
1926
|
+
currentScanId = NULL,
|
|
1927
|
+
updatedAt = @updatedAt,
|
|
1928
|
+
heartbeatAt = @heartbeatAt,
|
|
1929
|
+
lastError = CASE
|
|
1930
|
+
WHEN lastError IS NULL OR lastError = '' THEN 'Superseded by a newer worker with the same label.'
|
|
1931
|
+
ELSE lastError
|
|
1932
|
+
END
|
|
1933
|
+
WHERE label = @label
|
|
1934
|
+
AND (@exceptId IS NULL OR id != @exceptId)
|
|
1935
|
+
AND status != 'stopped'
|
|
1936
|
+
`).run({
|
|
1937
|
+
label,
|
|
1938
|
+
exceptId: exceptId ?? null,
|
|
1939
|
+
updatedAt: now,
|
|
1940
|
+
heartbeatAt: now
|
|
1941
|
+
});
|
|
1942
|
+
return result.changes;
|
|
1943
|
+
}
|
|
1944
|
+
deleteWorkersByStatus(statuses) {
|
|
1945
|
+
const values = Array.isArray(statuses) ? statuses : [statuses];
|
|
1946
|
+
if (values.length === 0)
|
|
1947
|
+
return 0;
|
|
1948
|
+
const placeholders = values.map(() => "?").join(",");
|
|
1949
|
+
const result = this.sqlite.prepare(`DELETE FROM workers WHERE status IN (${placeholders})`).run(...values);
|
|
1950
|
+
return result.changes;
|
|
1951
|
+
}
|
|
1952
|
+
// ── Persistent credential store (0sec#771) ──
|
|
1953
|
+
/**
|
|
1954
|
+
* Upsert a discovered foothold keyed by (credentialKind, valueHash). First
|
|
1955
|
+
* write records the discovery attribution (target/finding/scan/turn);
|
|
1956
|
+
* subsequent sightings bump `timesSeen` and refresh `lastScanId`/`lastSeenAt`
|
|
1957
|
+
* without overwriting the original discovery columns. Returns the stored row.
|
|
1958
|
+
*
|
|
1959
|
+
* Plaintext is never accepted here — the caller passes a precomputed
|
|
1960
|
+
* `valueHash` and a redacted `valuePreview`.
|
|
1961
|
+
*/
|
|
1962
|
+
upsertPersistentCredential(input) {
|
|
1963
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1964
|
+
const existing = this.getPersistentCredential(input.credentialKind, input.valueHash);
|
|
1965
|
+
if (existing) {
|
|
1966
|
+
this.sqlite.prepare(`UPDATE persistent_credentials
|
|
1967
|
+
SET timesSeen = timesSeen + 1,
|
|
1968
|
+
lastScanId = @lastScanId,
|
|
1969
|
+
lastSeenAt = @lastSeenAt,
|
|
1970
|
+
context = COALESCE(context, @context)
|
|
1971
|
+
WHERE id = @id`).run({
|
|
1972
|
+
id: existing.id,
|
|
1973
|
+
lastScanId: input.scanId ?? existing.lastScanId ?? null,
|
|
1974
|
+
lastSeenAt: now,
|
|
1975
|
+
context: input.context ?? null
|
|
1976
|
+
});
|
|
1977
|
+
return this.getPersistentCredential(input.credentialKind, input.valueHash);
|
|
1978
|
+
}
|
|
1979
|
+
const id = randomUUID();
|
|
1980
|
+
this.sqlite.prepare(`INSERT INTO persistent_credentials
|
|
1981
|
+
(id, credentialKind, valueHash, valuePreview, context, target,
|
|
1982
|
+
firstScanId, firstFindingId, firstSource, firstTurn, lastScanId,
|
|
1983
|
+
timesSeen, firstSeenAt, lastSeenAt)
|
|
1984
|
+
VALUES
|
|
1985
|
+
(@id, @credentialKind, @valueHash, @valuePreview, @context, @target,
|
|
1986
|
+
@firstScanId, @firstFindingId, @firstSource, @firstTurn, @lastScanId,
|
|
1987
|
+
1, @firstSeenAt, @lastSeenAt)`).run({
|
|
1988
|
+
id,
|
|
1989
|
+
credentialKind: input.credentialKind,
|
|
1990
|
+
valueHash: input.valueHash,
|
|
1991
|
+
valuePreview: input.valuePreview,
|
|
1992
|
+
context: input.context ?? null,
|
|
1993
|
+
target: input.target ?? null,
|
|
1994
|
+
firstScanId: input.scanId ?? null,
|
|
1995
|
+
firstFindingId: input.findingId ?? null,
|
|
1996
|
+
firstSource: input.source ?? null,
|
|
1997
|
+
firstTurn: input.turn ?? null,
|
|
1998
|
+
lastScanId: input.scanId ?? null,
|
|
1999
|
+
firstSeenAt: now,
|
|
2000
|
+
lastSeenAt: now
|
|
2001
|
+
});
|
|
2002
|
+
return this.getPersistentCredential(input.credentialKind, input.valueHash);
|
|
2003
|
+
}
|
|
2004
|
+
getPersistentCredential(credentialKind, valueHash) {
|
|
2005
|
+
return this.sqlite.prepare(`SELECT * FROM persistent_credentials WHERE credentialKind = ? AND valueHash = ?`).get(credentialKind, valueHash);
|
|
2006
|
+
}
|
|
2007
|
+
listPersistentCredentials(query = {}) {
|
|
2008
|
+
const where = [];
|
|
2009
|
+
const params = [];
|
|
2010
|
+
if (query.credentialKind) {
|
|
2011
|
+
where.push("credentialKind = ?");
|
|
2012
|
+
params.push(query.credentialKind);
|
|
2013
|
+
}
|
|
2014
|
+
if (query.target) {
|
|
2015
|
+
where.push("target = ?");
|
|
2016
|
+
params.push(query.target);
|
|
2017
|
+
}
|
|
2018
|
+
const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
2019
|
+
const limit = query.limit ?? 500;
|
|
2020
|
+
return this.sqlite.prepare(`SELECT * FROM persistent_credentials ${whereSql} ORDER BY lastSeenAt DESC LIMIT ${limit}`).all(...params);
|
|
2021
|
+
}
|
|
2022
|
+
// ── Trust graph edges (0sec#771) ──
|
|
2023
|
+
/**
|
|
2024
|
+
* Upsert a directed trust edge keyed by
|
|
2025
|
+
* (srcKind, srcId, dstKind, dstId, relation). Re-observing an edge refreshes
|
|
2026
|
+
* `updatedAt` (and `confidence`/`note`/attribution when provided) rather than
|
|
2027
|
+
* inserting a duplicate. Returns the stored row.
|
|
2028
|
+
*/
|
|
2029
|
+
upsertTrustGraphEdge(input) {
|
|
2030
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2031
|
+
const existing = this.sqlite.prepare(`SELECT * FROM trust_graph_edges
|
|
2032
|
+
WHERE srcKind = ? AND srcId = ? AND dstKind = ? AND dstId = ? AND relation = ?`).get(input.srcKind, input.srcId, input.dstKind, input.dstId, input.relation);
|
|
2033
|
+
if (existing) {
|
|
2034
|
+
this.sqlite.prepare(`UPDATE trust_graph_edges
|
|
2035
|
+
SET scanId = COALESCE(@scanId, scanId),
|
|
2036
|
+
findingId = COALESCE(@findingId, findingId),
|
|
2037
|
+
confidence = COALESCE(@confidence, confidence),
|
|
2038
|
+
note = COALESCE(@note, note),
|
|
2039
|
+
updatedAt = @updatedAt
|
|
2040
|
+
WHERE id = @id`).run({
|
|
2041
|
+
id: existing.id,
|
|
2042
|
+
scanId: input.scanId ?? null,
|
|
2043
|
+
findingId: input.findingId ?? null,
|
|
2044
|
+
confidence: input.confidence ?? null,
|
|
2045
|
+
note: input.note ?? null,
|
|
2046
|
+
updatedAt: now
|
|
2047
|
+
});
|
|
2048
|
+
return { ...existing, updatedAt: now };
|
|
2049
|
+
}
|
|
2050
|
+
const id = randomUUID();
|
|
2051
|
+
this.sqlite.prepare(`INSERT INTO trust_graph_edges
|
|
2052
|
+
(id, srcKind, srcId, dstKind, dstId, relation, scanId, findingId,
|
|
2053
|
+
confidence, note, createdAt, updatedAt)
|
|
2054
|
+
VALUES
|
|
2055
|
+
(@id, @srcKind, @srcId, @dstKind, @dstId, @relation, @scanId,
|
|
2056
|
+
@findingId, @confidence, @note, @createdAt, @updatedAt)`).run({
|
|
2057
|
+
id,
|
|
2058
|
+
srcKind: input.srcKind,
|
|
2059
|
+
srcId: input.srcId,
|
|
2060
|
+
dstKind: input.dstKind,
|
|
2061
|
+
dstId: input.dstId,
|
|
2062
|
+
relation: input.relation,
|
|
2063
|
+
scanId: input.scanId ?? null,
|
|
2064
|
+
findingId: input.findingId ?? null,
|
|
2065
|
+
confidence: input.confidence ?? null,
|
|
2066
|
+
note: input.note ?? null,
|
|
2067
|
+
createdAt: now,
|
|
2068
|
+
updatedAt: now
|
|
2069
|
+
});
|
|
2070
|
+
return {
|
|
2071
|
+
id,
|
|
2072
|
+
srcKind: input.srcKind,
|
|
2073
|
+
srcId: input.srcId,
|
|
2074
|
+
dstKind: input.dstKind,
|
|
2075
|
+
dstId: input.dstId,
|
|
2076
|
+
relation: input.relation,
|
|
2077
|
+
scanId: input.scanId ?? null,
|
|
2078
|
+
findingId: input.findingId ?? null,
|
|
2079
|
+
confidence: input.confidence ?? null,
|
|
2080
|
+
note: input.note ?? null,
|
|
2081
|
+
createdAt: now,
|
|
2082
|
+
updatedAt: now
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
listTrustGraphEdges(query = {}) {
|
|
2086
|
+
const where = [];
|
|
2087
|
+
const params = [];
|
|
2088
|
+
if (query.srcKind) {
|
|
2089
|
+
where.push("srcKind = ?");
|
|
2090
|
+
params.push(query.srcKind);
|
|
2091
|
+
}
|
|
2092
|
+
if (query.srcId) {
|
|
2093
|
+
where.push("srcId = ?");
|
|
2094
|
+
params.push(query.srcId);
|
|
2095
|
+
}
|
|
2096
|
+
if (query.relation) {
|
|
2097
|
+
where.push("relation = ?");
|
|
2098
|
+
params.push(query.relation);
|
|
2099
|
+
}
|
|
2100
|
+
const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
2101
|
+
const limit = query.limit ?? 500;
|
|
2102
|
+
return this.sqlite.prepare(`SELECT * FROM trust_graph_edges ${whereSql} ORDER BY updatedAt DESC LIMIT ${limit}`).all(...params);
|
|
2103
|
+
}
|
|
2104
|
+
// ── Utilities ──
|
|
2105
|
+
close() {
|
|
2106
|
+
this.sqlite.close();
|
|
2107
|
+
}
|
|
2108
|
+
transaction(fn) {
|
|
2109
|
+
return this.sqlite.transaction(fn)();
|
|
2110
|
+
}
|
|
2111
|
+
/** Get summary stats across all findings */
|
|
2112
|
+
getStats() {
|
|
2113
|
+
const rows = this.sqlite.prepare(`SELECT severity, COUNT(*) as count FROM findings GROUP BY severity`).all();
|
|
2114
|
+
const stats = {};
|
|
2115
|
+
for (const row of rows)
|
|
2116
|
+
stats[row.severity] = row.count;
|
|
2117
|
+
return {
|
|
2118
|
+
total: rows.reduce((sum, r) => sum + r.count, 0),
|
|
2119
|
+
critical: stats["critical"] ?? 0,
|
|
2120
|
+
high: stats["high"] ?? 0,
|
|
2121
|
+
medium: stats["medium"] ?? 0,
|
|
2122
|
+
low: stats["low"] ?? 0,
|
|
2123
|
+
info: stats["info"] ?? 0
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
function normalizeFingerprintPart(value) {
|
|
2128
|
+
return value.toLowerCase().replace(/\r/g, "").replace(/:\d+(?::\d+)?/g, "").replace(/\s+/g, " ").trim();
|
|
2129
|
+
}
|
|
2130
|
+
function buildFindingFingerprint(target, finding) {
|
|
2131
|
+
const key = [
|
|
2132
|
+
normalizeFingerprintPart(target),
|
|
2133
|
+
normalizeFingerprintPart(finding.category),
|
|
2134
|
+
normalizeFingerprintPart(finding.title),
|
|
2135
|
+
normalizeFingerprintPart(finding.evidence.request.split("\n")[0] ?? "")
|
|
2136
|
+
].join("::");
|
|
2137
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 24);
|
|
2138
|
+
}
|
|
2139
|
+
function asPlainObject(value) {
|
|
2140
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2141
|
+
return void 0;
|
|
2142
|
+
return value;
|
|
2143
|
+
}
|
|
2144
|
+
function parseJsonObjectColumn(value) {
|
|
2145
|
+
if (value == null)
|
|
2146
|
+
return void 0;
|
|
2147
|
+
if (typeof value === "string") {
|
|
2148
|
+
if (value.length === 0)
|
|
2149
|
+
return void 0;
|
|
2150
|
+
try {
|
|
2151
|
+
return asPlainObject(JSON.parse(value));
|
|
2152
|
+
} catch {
|
|
2153
|
+
return void 0;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return asPlainObject(value);
|
|
2157
|
+
}
|
|
2158
|
+
function parseFindingVerificationResult(value) {
|
|
2159
|
+
const parsed = parseJsonObjectColumn(value);
|
|
2160
|
+
if (!parsed || typeof parsed.status !== "string" || parsed.status.length === 0)
|
|
2161
|
+
return void 0;
|
|
2162
|
+
return parsed;
|
|
2163
|
+
}
|
|
2164
|
+
function parseFindingReviewAnnotation(value) {
|
|
2165
|
+
const parsed = parseJsonObjectColumn(value);
|
|
2166
|
+
if (!parsed || typeof parsed.path !== "string" || parsed.path.length === 0)
|
|
2167
|
+
return void 0;
|
|
2168
|
+
return parsed;
|
|
2169
|
+
}
|
|
2170
|
+
function serializeFindingVerificationResult(value) {
|
|
2171
|
+
const usable = parseFindingVerificationResult(value);
|
|
2172
|
+
return usable ? JSON.stringify(usable) : null;
|
|
2173
|
+
}
|
|
2174
|
+
function serializeFindingReviewAnnotation(value) {
|
|
2175
|
+
const usable = parseFindingReviewAnnotation(value);
|
|
2176
|
+
return usable ? JSON.stringify(usable) : null;
|
|
2177
|
+
}
|
|
2178
|
+
function restoreFindingReviewFields(row) {
|
|
2179
|
+
if (!row)
|
|
2180
|
+
return {};
|
|
2181
|
+
const verificationResult = parseFindingVerificationResult(row.verificationResult);
|
|
2182
|
+
const reviewAnnotation = parseFindingReviewAnnotation(row.reviewAnnotation);
|
|
2183
|
+
return {
|
|
2184
|
+
...verificationResult ? { verification_result: verificationResult } : {},
|
|
2185
|
+
...reviewAnnotation ? { reviewAnnotation } : {}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
function deriveEvidenceFromPocSteps(finding) {
|
|
2189
|
+
if (!finding.pocSteps || finding.pocSteps.length === 0) {
|
|
2190
|
+
return { request: "", response: "", analysis: void 0 };
|
|
2191
|
+
}
|
|
2192
|
+
const requestParts = [];
|
|
2193
|
+
const responseParts = [];
|
|
2194
|
+
const analysisParts = [];
|
|
2195
|
+
for (const step of finding.pocSteps) {
|
|
2196
|
+
const prefix = `[${step.kind}] ${step.summary}`;
|
|
2197
|
+
if (step.action.type === "shell") {
|
|
2198
|
+
requestParts.push(`${prefix}
|
|
2199
|
+
$ ${step.action.cmd}`);
|
|
2200
|
+
} else if (step.action.type === "http") {
|
|
2201
|
+
requestParts.push(`${prefix}
|
|
2202
|
+
${step.action.method.toUpperCase()} ${step.action.url}`);
|
|
2203
|
+
} else if (step.action.type === "docker") {
|
|
2204
|
+
requestParts.push(`${prefix}
|
|
2205
|
+
docker run ${step.action.image} ${step.action.args.join(" ")}`.trim());
|
|
2206
|
+
} else {
|
|
2207
|
+
responseParts.push(`${prefix}
|
|
2208
|
+
${step.action.text}`);
|
|
2209
|
+
}
|
|
2210
|
+
if (step.expect) {
|
|
2211
|
+
analysisParts.push(`- ${step.id}: expect ${step.expect.type}`);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return {
|
|
2215
|
+
request: requestParts.join("\n\n"),
|
|
2216
|
+
response: responseParts.join("\n\n"),
|
|
2217
|
+
analysis: analysisParts.length > 0 ? `PoC step expectations:
|
|
2218
|
+
${analysisParts.join("\n")}` : void 0
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
var SCHEMA_TABLES_SQL = `
|
|
2222
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
2223
|
+
id TEXT PRIMARY KEY,
|
|
2224
|
+
target TEXT NOT NULL,
|
|
2225
|
+
depth TEXT NOT NULL,
|
|
2226
|
+
runtime TEXT NOT NULL DEFAULT 'api',
|
|
2227
|
+
mode TEXT NOT NULL DEFAULT 'probe',
|
|
2228
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
2229
|
+
startedAt TEXT NOT NULL,
|
|
2230
|
+
completedAt TEXT,
|
|
2231
|
+
durationMs INTEGER,
|
|
2232
|
+
summary TEXT
|
|
2233
|
+
);
|
|
2234
|
+
|
|
2235
|
+
CREATE TABLE IF NOT EXISTS targets (
|
|
2236
|
+
id TEXT PRIMARY KEY,
|
|
2237
|
+
url TEXT NOT NULL UNIQUE,
|
|
2238
|
+
type TEXT NOT NULL DEFAULT 'unknown',
|
|
2239
|
+
model TEXT,
|
|
2240
|
+
systemPrompt TEXT,
|
|
2241
|
+
detectedFeatures TEXT,
|
|
2242
|
+
endpoints TEXT,
|
|
2243
|
+
firstSeenAt TEXT NOT NULL,
|
|
2244
|
+
lastSeenAt TEXT NOT NULL
|
|
2245
|
+
);
|
|
2246
|
+
|
|
2247
|
+
CREATE TABLE IF NOT EXISTS findings (
|
|
2248
|
+
id TEXT PRIMARY KEY,
|
|
2249
|
+
scanId TEXT NOT NULL REFERENCES scans(id),
|
|
2250
|
+
templateId TEXT NOT NULL,
|
|
2251
|
+
title TEXT NOT NULL,
|
|
2252
|
+
description TEXT NOT NULL,
|
|
2253
|
+
severity TEXT NOT NULL,
|
|
2254
|
+
category TEXT NOT NULL,
|
|
2255
|
+
status TEXT NOT NULL DEFAULT 'discovered',
|
|
2256
|
+
fingerprint TEXT,
|
|
2257
|
+
triageStatus TEXT NOT NULL DEFAULT 'new',
|
|
2258
|
+
triageNote TEXT,
|
|
2259
|
+
triagedAt TEXT,
|
|
2260
|
+
workflowStatus TEXT NOT NULL DEFAULT 'backlog',
|
|
2261
|
+
workflowAssignee TEXT,
|
|
2262
|
+
workflowUpdatedAt TEXT,
|
|
2263
|
+
score INTEGER,
|
|
2264
|
+
confidence REAL,
|
|
2265
|
+
cvssVector TEXT,
|
|
2266
|
+
cvssScore REAL,
|
|
2267
|
+
evidenceRequest TEXT NOT NULL,
|
|
2268
|
+
evidenceResponse TEXT NOT NULL,
|
|
2269
|
+
evidenceAnalysis TEXT,
|
|
2270
|
+
layerVerdicts TEXT,
|
|
2271
|
+
impactAssessment TEXT,
|
|
2272
|
+
pocSteps TEXT,
|
|
2273
|
+
verificationSpec TEXT,
|
|
2274
|
+
pocExecution TEXT,
|
|
2275
|
+
semanticDedupe TEXT,
|
|
2276
|
+
findingRank INTEGER,
|
|
2277
|
+
verificationResult TEXT,
|
|
2278
|
+
reviewAnnotation TEXT,
|
|
2279
|
+
timestamp INTEGER NOT NULL
|
|
2280
|
+
);
|
|
2281
|
+
|
|
2282
|
+
CREATE TABLE IF NOT EXISTS attack_results (
|
|
2283
|
+
id TEXT PRIMARY KEY,
|
|
2284
|
+
scanId TEXT NOT NULL REFERENCES scans(id),
|
|
2285
|
+
templateId TEXT NOT NULL,
|
|
2286
|
+
payloadId TEXT NOT NULL,
|
|
2287
|
+
outcome TEXT NOT NULL,
|
|
2288
|
+
request TEXT NOT NULL,
|
|
2289
|
+
response TEXT NOT NULL,
|
|
2290
|
+
latencyMs INTEGER NOT NULL,
|
|
2291
|
+
timestamp INTEGER NOT NULL,
|
|
2292
|
+
error TEXT
|
|
2293
|
+
);
|
|
2294
|
+
|
|
2295
|
+
CREATE TABLE IF NOT EXISTS verdicts (
|
|
2296
|
+
id TEXT PRIMARY KEY,
|
|
2297
|
+
findingId TEXT NOT NULL REFERENCES findings(id),
|
|
2298
|
+
agentRole TEXT NOT NULL,
|
|
2299
|
+
model TEXT NOT NULL DEFAULT '',
|
|
2300
|
+
verdict TEXT NOT NULL,
|
|
2301
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
2302
|
+
reasoning TEXT NOT NULL DEFAULT '',
|
|
2303
|
+
timestamp INTEGER NOT NULL
|
|
2304
|
+
);
|
|
2305
|
+
|
|
2306
|
+
CREATE TABLE IF NOT EXISTS pipeline_events (
|
|
2307
|
+
id TEXT PRIMARY KEY,
|
|
2308
|
+
scanId TEXT NOT NULL REFERENCES scans(id),
|
|
2309
|
+
source TEXT NOT NULL DEFAULT 'core',
|
|
2310
|
+
stage TEXT NOT NULL,
|
|
2311
|
+
eventType TEXT NOT NULL,
|
|
2312
|
+
findingId TEXT,
|
|
2313
|
+
agentRole TEXT,
|
|
2314
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
2315
|
+
timestamp INTEGER NOT NULL
|
|
2316
|
+
);
|
|
2317
|
+
|
|
2318
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
2319
|
+
id TEXT PRIMARY KEY,
|
|
2320
|
+
scanId TEXT NOT NULL REFERENCES scans(id),
|
|
2321
|
+
agentRole TEXT NOT NULL,
|
|
2322
|
+
turnCount INTEGER NOT NULL DEFAULT 0,
|
|
2323
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
2324
|
+
toolContext TEXT NOT NULL DEFAULT '{}',
|
|
2325
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
2326
|
+
createdAt TEXT NOT NULL,
|
|
2327
|
+
updatedAt TEXT NOT NULL
|
|
2328
|
+
);
|
|
2329
|
+
|
|
2330
|
+
CREATE TABLE IF NOT EXISTS cases (
|
|
2331
|
+
id TEXT PRIMARY KEY,
|
|
2332
|
+
target TEXT NOT NULL UNIQUE,
|
|
2333
|
+
targetType TEXT NOT NULL DEFAULT 'unknown',
|
|
2334
|
+
latestScanId TEXT,
|
|
2335
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
2336
|
+
createdAt TEXT NOT NULL,
|
|
2337
|
+
updatedAt TEXT NOT NULL
|
|
2338
|
+
);
|
|
2339
|
+
|
|
2340
|
+
CREATE TABLE IF NOT EXISTS work_items (
|
|
2341
|
+
id TEXT PRIMARY KEY,
|
|
2342
|
+
caseId TEXT NOT NULL REFERENCES cases(id),
|
|
2343
|
+
findingFingerprint TEXT,
|
|
2344
|
+
kind TEXT NOT NULL,
|
|
2345
|
+
title TEXT NOT NULL,
|
|
2346
|
+
owner TEXT,
|
|
2347
|
+
status TEXT NOT NULL DEFAULT 'backlog',
|
|
2348
|
+
summary TEXT,
|
|
2349
|
+
dependsOn TEXT,
|
|
2350
|
+
createdAt TEXT NOT NULL,
|
|
2351
|
+
updatedAt TEXT NOT NULL
|
|
2352
|
+
);
|
|
2353
|
+
|
|
2354
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
2355
|
+
id TEXT PRIMARY KEY,
|
|
2356
|
+
caseId TEXT NOT NULL REFERENCES cases(id),
|
|
2357
|
+
findingFingerprint TEXT,
|
|
2358
|
+
workItemId TEXT,
|
|
2359
|
+
kind TEXT NOT NULL,
|
|
2360
|
+
label TEXT NOT NULL,
|
|
2361
|
+
content TEXT,
|
|
2362
|
+
metadata TEXT,
|
|
2363
|
+
createdAt TEXT NOT NULL,
|
|
2364
|
+
updatedAt TEXT NOT NULL
|
|
2365
|
+
);
|
|
2366
|
+
|
|
2367
|
+
CREATE TABLE IF NOT EXISTS triage_memories (
|
|
2368
|
+
id TEXT PRIMARY KEY,
|
|
2369
|
+
scope TEXT NOT NULL,
|
|
2370
|
+
scope_value TEXT,
|
|
2371
|
+
category TEXT NOT NULL,
|
|
2372
|
+
pattern TEXT NOT NULL,
|
|
2373
|
+
reasoning TEXT NOT NULL,
|
|
2374
|
+
created_at INTEGER NOT NULL,
|
|
2375
|
+
applied_count INTEGER NOT NULL DEFAULT 0
|
|
2376
|
+
);
|
|
2377
|
+
|
|
2378
|
+
CREATE TABLE IF NOT EXISTS workers (
|
|
2379
|
+
id TEXT PRIMARY KEY,
|
|
2380
|
+
role TEXT NOT NULL DEFAULT 'orchestrator',
|
|
2381
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
2382
|
+
label TEXT NOT NULL,
|
|
2383
|
+
currentCaseId TEXT,
|
|
2384
|
+
currentWorkItemId TEXT,
|
|
2385
|
+
currentScanId TEXT,
|
|
2386
|
+
pid INTEGER,
|
|
2387
|
+
host TEXT,
|
|
2388
|
+
lastError TEXT,
|
|
2389
|
+
heartbeatAt TEXT NOT NULL,
|
|
2390
|
+
startedAt TEXT NOT NULL,
|
|
2391
|
+
updatedAt TEXT NOT NULL
|
|
2392
|
+
);
|
|
2393
|
+
|
|
2394
|
+
CREATE TABLE IF NOT EXISTS persistent_credentials (
|
|
2395
|
+
id TEXT PRIMARY KEY,
|
|
2396
|
+
credentialKind TEXT NOT NULL,
|
|
2397
|
+
valueHash TEXT NOT NULL,
|
|
2398
|
+
valuePreview TEXT NOT NULL,
|
|
2399
|
+
context TEXT,
|
|
2400
|
+
target TEXT,
|
|
2401
|
+
firstScanId TEXT,
|
|
2402
|
+
firstFindingId TEXT,
|
|
2403
|
+
firstSource TEXT,
|
|
2404
|
+
firstTurn INTEGER,
|
|
2405
|
+
lastScanId TEXT,
|
|
2406
|
+
timesSeen INTEGER NOT NULL DEFAULT 1,
|
|
2407
|
+
firstSeenAt TEXT NOT NULL,
|
|
2408
|
+
lastSeenAt TEXT NOT NULL,
|
|
2409
|
+
UNIQUE(credentialKind, valueHash)
|
|
2410
|
+
);
|
|
2411
|
+
|
|
2412
|
+
CREATE TABLE IF NOT EXISTS trust_graph_edges (
|
|
2413
|
+
id TEXT PRIMARY KEY,
|
|
2414
|
+
srcKind TEXT NOT NULL,
|
|
2415
|
+
srcId TEXT NOT NULL,
|
|
2416
|
+
dstKind TEXT NOT NULL,
|
|
2417
|
+
dstId TEXT NOT NULL,
|
|
2418
|
+
relation TEXT NOT NULL,
|
|
2419
|
+
scanId TEXT,
|
|
2420
|
+
findingId TEXT,
|
|
2421
|
+
confidence REAL,
|
|
2422
|
+
note TEXT,
|
|
2423
|
+
createdAt TEXT NOT NULL,
|
|
2424
|
+
updatedAt TEXT NOT NULL,
|
|
2425
|
+
UNIQUE(srcKind, srcId, dstKind, dstId, relation)
|
|
2426
|
+
);
|
|
2427
|
+
`;
|
|
2428
|
+
var SCHEMA_INDEXES_SQL = `
|
|
2429
|
+
CREATE INDEX IF NOT EXISTS idx_findings_scanId ON findings(scanId);
|
|
2430
|
+
CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
|
|
2431
|
+
CREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);
|
|
2432
|
+
CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status);
|
|
2433
|
+
CREATE INDEX IF NOT EXISTS idx_findings_fingerprint ON findings(fingerprint);
|
|
2434
|
+
CREATE INDEX IF NOT EXISTS idx_findings_triageStatus ON findings(triageStatus);
|
|
2435
|
+
CREATE INDEX IF NOT EXISTS idx_findings_workflowStatus ON findings(workflowStatus);
|
|
2436
|
+
CREATE INDEX IF NOT EXISTS idx_attack_results_scanId ON attack_results(scanId);
|
|
2437
|
+
CREATE INDEX IF NOT EXISTS idx_targets_url ON targets(url);
|
|
2438
|
+
CREATE INDEX IF NOT EXISTS idx_verdicts_findingId ON verdicts(findingId);
|
|
2439
|
+
CREATE INDEX IF NOT EXISTS idx_events_scanId ON pipeline_events(scanId);
|
|
2440
|
+
CREATE INDEX IF NOT EXISTS idx_events_stage ON pipeline_events(stage);
|
|
2441
|
+
CREATE INDEX IF NOT EXISTS idx_events_findingId ON pipeline_events(findingId);
|
|
2442
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_scanId ON agent_sessions(scanId);
|
|
2443
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_role ON agent_sessions(agentRole);
|
|
2444
|
+
CREATE INDEX IF NOT EXISTS idx_cases_target ON cases(target);
|
|
2445
|
+
CREATE INDEX IF NOT EXISTS idx_cases_status ON cases(status);
|
|
2446
|
+
CREATE INDEX IF NOT EXISTS idx_work_items_caseId ON work_items(caseId);
|
|
2447
|
+
CREATE INDEX IF NOT EXISTS idx_work_items_fingerprint ON work_items(findingFingerprint);
|
|
2448
|
+
CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status);
|
|
2449
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_caseId ON artifacts(caseId);
|
|
2450
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_fingerprint ON artifacts(findingFingerprint);
|
|
2451
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_workItemId ON artifacts(workItemId);
|
|
2452
|
+
CREATE INDEX IF NOT EXISTS idx_workers_status ON workers(status);
|
|
2453
|
+
CREATE INDEX IF NOT EXISTS idx_workers_heartbeat ON workers(heartbeatAt);
|
|
2454
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON triage_memories(category, scope);
|
|
2455
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON triage_memories(scope, scope_value);
|
|
2456
|
+
CREATE INDEX IF NOT EXISTS idx_pcred_kind_hash ON persistent_credentials(credentialKind, valueHash);
|
|
2457
|
+
CREATE INDEX IF NOT EXISTS idx_pcred_target ON persistent_credentials(target);
|
|
2458
|
+
CREATE INDEX IF NOT EXISTS idx_pcred_kind ON persistent_credentials(credentialKind);
|
|
2459
|
+
CREATE INDEX IF NOT EXISTS idx_tge_src ON trust_graph_edges(srcKind, srcId);
|
|
2460
|
+
CREATE INDEX IF NOT EXISTS idx_tge_dst ON trust_graph_edges(dstKind, dstId);
|
|
2461
|
+
CREATE INDEX IF NOT EXISTS idx_tge_relation ON trust_graph_edges(relation);
|
|
2462
|
+
`;
|
|
2463
|
+
function normalizeWorkflowStatus(value, fallback) {
|
|
2464
|
+
if (value && findingWorkflowStatuses.includes(value)) {
|
|
2465
|
+
return value;
|
|
2466
|
+
}
|
|
2467
|
+
if (fallback?.triageStatus === "accepted" || fallback?.status === "reported" || fallback?.status === "fixed") {
|
|
2468
|
+
return "done";
|
|
2469
|
+
}
|
|
2470
|
+
if (fallback?.triageStatus === "suppressed" || fallback?.status === "false-positive") {
|
|
2471
|
+
return "cancelled";
|
|
2472
|
+
}
|
|
2473
|
+
if (fallback?.status && ["verified", "confirmed", "scored"].includes(fallback.status)) {
|
|
2474
|
+
return "human_review";
|
|
2475
|
+
}
|
|
2476
|
+
return "backlog";
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// packages/db/dist/run-storage.js
|
|
2480
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2481
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync } from "node:fs";
|
|
2482
|
+
import { dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
|
|
2483
|
+
var STATE_DB_FILE = "state.db";
|
|
2484
|
+
var REPORT_FILE = "report.json";
|
|
2485
|
+
function normalizeRunId(value) {
|
|
2486
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) {
|
|
2487
|
+
throw new Error(`Invalid 0sec run id ${JSON.stringify(value)}.`);
|
|
2488
|
+
}
|
|
2489
|
+
return value;
|
|
2490
|
+
}
|
|
2491
|
+
function resolveOsecRunStorage(options = {}) {
|
|
2492
|
+
const env = options.env ?? process.env;
|
|
2493
|
+
const cloudRunId = env["0SEC_CLOUD_SCAN_ID"]?.trim() || void 0;
|
|
2494
|
+
let runId = normalizeRunId(options.runId ?? cloudRunId ?? randomUUID2());
|
|
2495
|
+
const stateDir = homeStateDir(options.homeDir);
|
|
2496
|
+
const configuredRunDir = env["0SEC_RUN_DIR"]?.trim() || void 0;
|
|
2497
|
+
const configuredDbPath = options.dbPath ?? (env["0SEC_DB_PATH"]?.trim() || void 0);
|
|
2498
|
+
let runDir = configuredRunDir ? resolve(configuredRunDir) : runStateDir(runId, options.homeDir);
|
|
2499
|
+
if (options.resume && !configuredRunDir && !configuredDbPath && !existsSync2(runDir)) {
|
|
2500
|
+
const runsDir = join2(stateDir, "runs");
|
|
2501
|
+
if (existsSync2(runsDir)) {
|
|
2502
|
+
const matches = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith(runId));
|
|
2503
|
+
if (matches.length > 1) {
|
|
2504
|
+
throw new Error(`0sec run id prefix '${runId}' is ambiguous.`);
|
|
2505
|
+
}
|
|
2506
|
+
const match = matches[0];
|
|
2507
|
+
if (match) {
|
|
2508
|
+
runId = normalizeRunId(match.name);
|
|
2509
|
+
runDir = join2(runsDir, runId);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (!isAbsolute(runDir)) {
|
|
2514
|
+
throw new Error(`0sec run directory must be absolute: ${JSON.stringify(runDir)}.`);
|
|
2515
|
+
}
|
|
2516
|
+
let dbPath = configuredDbPath ? configuredDbPath === ":memory:" ? configuredDbPath : resolve(configuredDbPath) : join2(runDir, STATE_DB_FILE);
|
|
2517
|
+
const configuredReportPath = env["0SEC_REPORT_PATH"]?.trim() || void 0;
|
|
2518
|
+
const reportPath = configuredReportPath ? resolve(configuredReportPath) : !configuredDbPath || configuredRunDir ? join2(runDir, REPORT_FILE) : void 0;
|
|
2519
|
+
if (options.resume && !configuredDbPath && !existsSync2(dbPath)) {
|
|
2520
|
+
const legacyDbPath = join2(stateDir, "0sec.db");
|
|
2521
|
+
if (existsSync2(legacyDbPath))
|
|
2522
|
+
dbPath = legacyDbPath;
|
|
2523
|
+
}
|
|
2524
|
+
if (!configuredDbPath || configuredRunDir || reportPath) {
|
|
2525
|
+
mkdirSync2(runDir, { recursive: true, mode: 448 });
|
|
2526
|
+
}
|
|
2527
|
+
if (dbPath !== ":memory:") {
|
|
2528
|
+
mkdirSync2(dirname2(dbPath), { recursive: true, mode: 448 });
|
|
2529
|
+
}
|
|
2530
|
+
if (reportPath) {
|
|
2531
|
+
mkdirSync2(dirname2(reportPath), { recursive: true, mode: 448 });
|
|
2532
|
+
}
|
|
2533
|
+
return {
|
|
2534
|
+
runId,
|
|
2535
|
+
runDir,
|
|
2536
|
+
dbPath,
|
|
2537
|
+
...reportPath ? { reportPath } : {}
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
function listOsecRunDatabasePaths(homeDir) {
|
|
2541
|
+
const runsDir = join2(homeStateDir(homeDir), "runs");
|
|
2542
|
+
if (!existsSync2(runsDir))
|
|
2543
|
+
return [];
|
|
2544
|
+
return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join2(runsDir, entry.name, STATE_DB_FILE)).filter((path) => existsSync2(path));
|
|
2545
|
+
}
|
|
2546
|
+
function writeOsecRunReport(storage, report) {
|
|
2547
|
+
const reportPath = storage.reportPath;
|
|
2548
|
+
if (!reportPath)
|
|
2549
|
+
return;
|
|
2550
|
+
const body = `${JSON.stringify(report)}
|
|
2551
|
+
`;
|
|
2552
|
+
const temporaryPath = join2(dirname2(reportPath), `.${REPORT_FILE}.${process.pid}.${randomUUID2()}.tmp`);
|
|
2553
|
+
try {
|
|
2554
|
+
writeFileSync(temporaryPath, body, { encoding: "utf8", mode: 384 });
|
|
2555
|
+
renameSync2(temporaryPath, reportPath);
|
|
2556
|
+
chmodSync(reportPath, 384);
|
|
2557
|
+
} finally {
|
|
2558
|
+
if (existsSync2(temporaryPath)) {
|
|
2559
|
+
rmSync2(temporaryPath, { force: true });
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
export {
|
|
2565
|
+
findingStatuses,
|
|
2566
|
+
findingTriageStatuses,
|
|
2567
|
+
caseStatuses,
|
|
2568
|
+
workItemKinds,
|
|
2569
|
+
workItemStatuses,
|
|
2570
|
+
artifactKinds,
|
|
2571
|
+
workerStatuses,
|
|
2572
|
+
credentialKinds,
|
|
2573
|
+
scans,
|
|
2574
|
+
targets,
|
|
2575
|
+
findings,
|
|
2576
|
+
attackResults,
|
|
2577
|
+
verdicts,
|
|
2578
|
+
pipelineEvents,
|
|
2579
|
+
agentSessions,
|
|
2580
|
+
cases,
|
|
2581
|
+
workItems,
|
|
2582
|
+
artifacts,
|
|
2583
|
+
triageMemories,
|
|
2584
|
+
workers,
|
|
2585
|
+
persistentCredentials,
|
|
2586
|
+
trustGraphEdges,
|
|
2587
|
+
resolveOsecDbPath,
|
|
2588
|
+
resetOsecDatabase,
|
|
2589
|
+
repairOsecDatabase,
|
|
2590
|
+
osecDB,
|
|
2591
|
+
parseFindingVerificationResult,
|
|
2592
|
+
parseFindingReviewAnnotation,
|
|
2593
|
+
restoreFindingReviewFields,
|
|
2594
|
+
resolveOsecRunStorage,
|
|
2595
|
+
listOsecRunDatabasePaths,
|
|
2596
|
+
writeOsecRunReport
|
|
2597
|
+
};
|