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,600 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
agenticScan
|
|
6
|
+
} from "./chunk-IORC6MQX.js";
|
|
7
|
+
import {
|
|
8
|
+
source_default
|
|
9
|
+
} from "./chunk-SF4KZ4O3.js";
|
|
10
|
+
import {
|
|
11
|
+
runAgentLoop
|
|
12
|
+
} from "./chunk-K26SZ37E.js";
|
|
13
|
+
import {
|
|
14
|
+
getToolsForRole
|
|
15
|
+
} from "./chunk-O462Y7P2.js";
|
|
16
|
+
import {
|
|
17
|
+
osecDB
|
|
18
|
+
} from "./chunk-HT6P7RY3.js";
|
|
19
|
+
import {
|
|
20
|
+
createRuntime
|
|
21
|
+
} from "./chunk-MMLDQR4H.js";
|
|
22
|
+
import {
|
|
23
|
+
LlmApiRuntime
|
|
24
|
+
} from "./chunk-2SANI5RH.js";
|
|
25
|
+
|
|
26
|
+
// packages/cli/src/commands/orchestrate.ts
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { hostname } from "node:os";
|
|
29
|
+
var EXECUTABLE_KINDS = [
|
|
30
|
+
"surface_map",
|
|
31
|
+
"hypothesis",
|
|
32
|
+
"poc_build",
|
|
33
|
+
"blind_verify",
|
|
34
|
+
"consensus"
|
|
35
|
+
];
|
|
36
|
+
var KIND_PRIORITY = {
|
|
37
|
+
surface_map: 0,
|
|
38
|
+
hypothesis: 1,
|
|
39
|
+
poc_build: 2,
|
|
40
|
+
blind_verify: 3,
|
|
41
|
+
consensus: 4,
|
|
42
|
+
human_review: 5
|
|
43
|
+
};
|
|
44
|
+
function sleep(ms) {
|
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
|
+
}
|
|
47
|
+
function findingRowsToFindings(rows) {
|
|
48
|
+
return rows.map((row) => ({
|
|
49
|
+
id: row.id,
|
|
50
|
+
templateId: row.templateId,
|
|
51
|
+
title: row.title,
|
|
52
|
+
description: row.description,
|
|
53
|
+
severity: row.severity,
|
|
54
|
+
category: row.category,
|
|
55
|
+
status: row.status,
|
|
56
|
+
evidence: {
|
|
57
|
+
request: row.evidenceRequest,
|
|
58
|
+
response: row.evidenceResponse,
|
|
59
|
+
analysis: row.evidenceAnalysis ?? void 0
|
|
60
|
+
},
|
|
61
|
+
timestamp: row.timestamp
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
function parseTargetForExecution(target, scanMode) {
|
|
65
|
+
if (target.startsWith("web:")) {
|
|
66
|
+
return {
|
|
67
|
+
target: target.slice("web:".length),
|
|
68
|
+
targetType: "web-app",
|
|
69
|
+
mode: "web"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
73
|
+
return {
|
|
74
|
+
target,
|
|
75
|
+
targetType: "url",
|
|
76
|
+
mode: scanMode ?? "deep"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (target.startsWith("mcp://")) {
|
|
80
|
+
return {
|
|
81
|
+
target,
|
|
82
|
+
targetType: "url",
|
|
83
|
+
mode: "mcp"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (target.startsWith("scan:")) {
|
|
87
|
+
return {
|
|
88
|
+
target: target.slice("scan:".length),
|
|
89
|
+
targetType: "url",
|
|
90
|
+
mode: scanMode ?? "deep"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
target,
|
|
95
|
+
targetType: null,
|
|
96
|
+
mode: scanMode ?? "deep"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function isRunnable(item, workItemsById, workItemsByCaseId) {
|
|
100
|
+
if (!EXECUTABLE_KINDS.includes(item.kind)) return false;
|
|
101
|
+
if (item.status !== "todo") return false;
|
|
102
|
+
const dependency = item.dependsOn ? workItemsById.get(item.dependsOn) : null;
|
|
103
|
+
if (item.dependsOn && dependency?.status !== "done") return false;
|
|
104
|
+
const siblings = workItemsByCaseId.get(item.caseId) ?? [];
|
|
105
|
+
if (siblings.some((candidate) => candidate.id !== item.id && candidate.status === "in_progress")) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
function buildWorkerRecord(args) {
|
|
111
|
+
return {
|
|
112
|
+
id: args.id,
|
|
113
|
+
role: "orchestrator",
|
|
114
|
+
status: args.status,
|
|
115
|
+
label: args.label,
|
|
116
|
+
currentCaseId: args.currentCaseId ?? null,
|
|
117
|
+
currentWorkItemId: args.currentWorkItemId ?? null,
|
|
118
|
+
currentScanId: args.currentScanId ?? null,
|
|
119
|
+
pid: process.pid,
|
|
120
|
+
host: hostname(),
|
|
121
|
+
lastError: args.lastError ?? null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function touchWorker(dbPath, worker) {
|
|
125
|
+
const db = new osecDB(dbPath);
|
|
126
|
+
try {
|
|
127
|
+
db.upsertWorker(worker);
|
|
128
|
+
} finally {
|
|
129
|
+
db.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function stopSupersededWorkers(dbPath, label, workerId) {
|
|
133
|
+
const db = new osecDB(dbPath);
|
|
134
|
+
try {
|
|
135
|
+
return db.stopWorkersByLabel(label, workerId);
|
|
136
|
+
} finally {
|
|
137
|
+
db.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function recoverStaleWorkers(dbPath, staleAfterMs = 3e4) {
|
|
141
|
+
const db = new osecDB(dbPath);
|
|
142
|
+
try {
|
|
143
|
+
const workers = db.listWorkers(100);
|
|
144
|
+
const workItems = db.listWorkItems({ limit: 5e3 });
|
|
145
|
+
const workItemsById = new Map(workItems.map((item) => [item.id, item]));
|
|
146
|
+
let recovered = 0;
|
|
147
|
+
for (const worker of workers) {
|
|
148
|
+
if (worker.status === "stopped") continue;
|
|
149
|
+
const heartbeatAge = Date.now() - Date.parse(worker.heartbeatAt);
|
|
150
|
+
if (Number.isNaN(heartbeatAge) || heartbeatAge < staleAfterMs) continue;
|
|
151
|
+
if (worker.currentWorkItemId) {
|
|
152
|
+
const item = workItemsById.get(worker.currentWorkItemId);
|
|
153
|
+
if (item && item.status === "in_progress") {
|
|
154
|
+
db.upsertWorkItem({
|
|
155
|
+
...item,
|
|
156
|
+
status: "todo",
|
|
157
|
+
summary: `Recovered after stale worker ${worker.label} stopped heartbeating.`
|
|
158
|
+
});
|
|
159
|
+
recovered += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
db.upsertWorker({
|
|
163
|
+
id: worker.id,
|
|
164
|
+
role: "orchestrator",
|
|
165
|
+
status: "error",
|
|
166
|
+
label: worker.label,
|
|
167
|
+
currentCaseId: null,
|
|
168
|
+
currentWorkItemId: null,
|
|
169
|
+
currentScanId: null,
|
|
170
|
+
pid: worker.pid ?? null,
|
|
171
|
+
host: worker.host ?? null,
|
|
172
|
+
lastError: `Heartbeat expired after ${Math.round(heartbeatAge / 1e3)}s.`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return recovered;
|
|
176
|
+
} finally {
|
|
177
|
+
db.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function findRunnableCandidates(dbPath, limit) {
|
|
181
|
+
const db = new osecDB(dbPath);
|
|
182
|
+
try {
|
|
183
|
+
const allWorkItems = db.listWorkItems({ limit: 5e3 });
|
|
184
|
+
const workItemsById = new Map(allWorkItems.map((item) => [item.id, item]));
|
|
185
|
+
const workItemsByCaseId = /* @__PURE__ */ new Map();
|
|
186
|
+
for (const item of allWorkItems) {
|
|
187
|
+
const list = workItemsByCaseId.get(item.caseId) ?? [];
|
|
188
|
+
list.push(item);
|
|
189
|
+
workItemsByCaseId.set(item.caseId, list);
|
|
190
|
+
}
|
|
191
|
+
const runnable = allWorkItems.filter((item) => isRunnable(item, workItemsById, workItemsByCaseId)).sort((left, right) => {
|
|
192
|
+
const leftPriority = KIND_PRIORITY[left.kind] ?? 999;
|
|
193
|
+
const rightPriority = KIND_PRIORITY[right.kind] ?? 999;
|
|
194
|
+
if (leftPriority !== rightPriority) return leftPriority - rightPriority;
|
|
195
|
+
return new Date(left.updatedAt).getTime() - new Date(right.updatedAt).getTime();
|
|
196
|
+
});
|
|
197
|
+
const queuedByCase = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const item of runnable) {
|
|
199
|
+
if (!queuedByCase.has(item.caseId)) queuedByCase.set(item.caseId, item);
|
|
200
|
+
}
|
|
201
|
+
const selected = [...queuedByCase.values()].slice(0, limit);
|
|
202
|
+
const candidates = [];
|
|
203
|
+
for (const item of selected) {
|
|
204
|
+
const caseRecord = db.getCase(item.caseId);
|
|
205
|
+
if (!caseRecord) continue;
|
|
206
|
+
const familyScanId = item.findingFingerprint ? db.getRelatedFindings(item.findingFingerprint)?.[0]?.scanId : null;
|
|
207
|
+
const scanId = familyScanId ?? caseRecord.latestScanId;
|
|
208
|
+
if (!scanId) continue;
|
|
209
|
+
const scan = db.getScan(scanId);
|
|
210
|
+
if (!scan || scan.status === "running") continue;
|
|
211
|
+
const resolved = parseTargetForExecution(scan.target, scan.mode);
|
|
212
|
+
if (!resolved.targetType) continue;
|
|
213
|
+
candidates.push({
|
|
214
|
+
item,
|
|
215
|
+
caseRecord,
|
|
216
|
+
scan,
|
|
217
|
+
target: resolved.target,
|
|
218
|
+
targetType: resolved.targetType,
|
|
219
|
+
mode: resolved.mode
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return candidates;
|
|
223
|
+
} finally {
|
|
224
|
+
db.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function summarizeFindingsBySeverity(findings) {
|
|
228
|
+
const summary = {
|
|
229
|
+
totalFindings: findings.length,
|
|
230
|
+
totalAttacks: 0,
|
|
231
|
+
critical: 0,
|
|
232
|
+
high: 0,
|
|
233
|
+
medium: 0,
|
|
234
|
+
low: 0,
|
|
235
|
+
info: 0
|
|
236
|
+
};
|
|
237
|
+
for (const finding of findings) {
|
|
238
|
+
if (finding.severity === "critical") summary.critical += 1;
|
|
239
|
+
else if (finding.severity === "high") summary.high += 1;
|
|
240
|
+
else if (finding.severity === "medium") summary.medium += 1;
|
|
241
|
+
else if (finding.severity === "low") summary.low += 1;
|
|
242
|
+
else summary.info += 1;
|
|
243
|
+
}
|
|
244
|
+
return summary;
|
|
245
|
+
}
|
|
246
|
+
function reconcileCandidateOutcome(dbPath, candidate, result) {
|
|
247
|
+
const db = new osecDB(dbPath);
|
|
248
|
+
try {
|
|
249
|
+
const current = db.listWorkItems({ caseId: candidate.item.caseId, limit: 5e3 }).find((item) => item.id === candidate.item.id) ?? candidate.item;
|
|
250
|
+
if (result.ok) {
|
|
251
|
+
if (current.status === "in_progress") {
|
|
252
|
+
db.upsertWorkItem({
|
|
253
|
+
...current,
|
|
254
|
+
status: "done",
|
|
255
|
+
summary: current.summary ?? "Completed by autonomous worker."
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
db.logEvent({
|
|
259
|
+
scanId: candidate.scan.id,
|
|
260
|
+
stage: candidate.item.kind,
|
|
261
|
+
eventType: "worker_completed",
|
|
262
|
+
findingId: candidate.item.findingFingerprint ? db.getRelatedFindings(candidate.item.findingFingerprint)?.[0]?.id : void 0,
|
|
263
|
+
agentRole: candidate.item.owner ?? void 0,
|
|
264
|
+
payload: {
|
|
265
|
+
caseId: candidate.caseRecord.id,
|
|
266
|
+
workItemId: candidate.item.id,
|
|
267
|
+
kind: candidate.item.kind,
|
|
268
|
+
fingerprint: candidate.item.findingFingerprint ?? null,
|
|
269
|
+
status: "done"
|
|
270
|
+
},
|
|
271
|
+
timestamp: Date.now()
|
|
272
|
+
});
|
|
273
|
+
const findings = db.getFindings(candidate.scan.id);
|
|
274
|
+
db.completeScan(candidate.scan.id, summarizeFindingsBySeverity(findings));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (current.status === "in_progress") {
|
|
278
|
+
db.upsertWorkItem({
|
|
279
|
+
...current,
|
|
280
|
+
status: "blocked",
|
|
281
|
+
summary: result.message
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
db.logEvent({
|
|
285
|
+
scanId: candidate.scan.id,
|
|
286
|
+
stage: candidate.item.kind,
|
|
287
|
+
eventType: "worker_failed",
|
|
288
|
+
findingId: candidate.item.findingFingerprint ? db.getRelatedFindings(candidate.item.findingFingerprint)?.[0]?.id : void 0,
|
|
289
|
+
agentRole: candidate.item.owner ?? void 0,
|
|
290
|
+
payload: {
|
|
291
|
+
caseId: candidate.caseRecord.id,
|
|
292
|
+
workItemId: candidate.item.id,
|
|
293
|
+
kind: candidate.item.kind,
|
|
294
|
+
fingerprint: candidate.item.findingFingerprint ?? null,
|
|
295
|
+
status: "blocked",
|
|
296
|
+
error: result.message
|
|
297
|
+
},
|
|
298
|
+
timestamp: Date.now()
|
|
299
|
+
});
|
|
300
|
+
db.failScan(candidate.scan.id, result.message);
|
|
301
|
+
} finally {
|
|
302
|
+
db.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function claimCandidate(dbPath, workerId, label, candidate) {
|
|
306
|
+
const db = new osecDB(dbPath);
|
|
307
|
+
try {
|
|
308
|
+
const claimed = db.claimWorkItem(candidate.item.id, workerId, {
|
|
309
|
+
owner: label,
|
|
310
|
+
summary: candidate.item.findingFingerprint ? `Claimed family work by ${label}; resuming the parent scan as a family-aware bridge.` : `Claimed by ${label} for autonomous execution.`
|
|
311
|
+
});
|
|
312
|
+
if (!claimed) return false;
|
|
313
|
+
db.reopenScan(candidate.scan.id);
|
|
314
|
+
db.logEvent({
|
|
315
|
+
scanId: candidate.scan.id,
|
|
316
|
+
stage: candidate.item.kind,
|
|
317
|
+
eventType: "worker_claimed",
|
|
318
|
+
findingId: candidate.item.findingFingerprint ? db.getRelatedFindings(candidate.item.findingFingerprint)?.[0]?.id : void 0,
|
|
319
|
+
agentRole: label,
|
|
320
|
+
payload: {
|
|
321
|
+
caseId: candidate.caseRecord.id,
|
|
322
|
+
workItemId: candidate.item.id,
|
|
323
|
+
kind: candidate.item.kind,
|
|
324
|
+
fingerprint: candidate.item.findingFingerprint ?? null,
|
|
325
|
+
owner: label
|
|
326
|
+
},
|
|
327
|
+
timestamp: Date.now()
|
|
328
|
+
});
|
|
329
|
+
db.upsertWorker(
|
|
330
|
+
buildWorkerRecord({
|
|
331
|
+
id: workerId,
|
|
332
|
+
label,
|
|
333
|
+
status: "running",
|
|
334
|
+
currentCaseId: candidate.caseRecord.id,
|
|
335
|
+
currentWorkItemId: candidate.item.id,
|
|
336
|
+
currentScanId: candidate.scan.id
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
return true;
|
|
340
|
+
} finally {
|
|
341
|
+
db.close();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function runCandidate(candidate, opts) {
|
|
345
|
+
if (candidate.item.findingFingerprint) {
|
|
346
|
+
return runFamilyCandidate(candidate, opts);
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const report = await agenticScan({
|
|
350
|
+
config: {
|
|
351
|
+
target: candidate.target,
|
|
352
|
+
depth: candidate.scan.depth,
|
|
353
|
+
format: "json",
|
|
354
|
+
runtime: opts.runtime ?? candidate.scan.runtime ?? "auto",
|
|
355
|
+
mode: candidate.mode,
|
|
356
|
+
timeout: parseInt(opts.timeout ?? "30000", 10),
|
|
357
|
+
verbose: false,
|
|
358
|
+
apiKey: opts.apiKey,
|
|
359
|
+
model: opts.model
|
|
360
|
+
},
|
|
361
|
+
dbPath: opts.dbPath,
|
|
362
|
+
resumeScanId: candidate.scan.id
|
|
363
|
+
});
|
|
364
|
+
console.log(
|
|
365
|
+
source_default.green(
|
|
366
|
+
` completed ${candidate.item.kind}: ${report.summary.totalFindings} findings, ${report.summary.critical} critical, ${report.summary.high} high`
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
return { ok: true };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
+
console.log(source_default.red(` failed ${candidate.item.kind}: ${message}`));
|
|
373
|
+
return { ok: false, message };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function familyAttackPrompt(target, kind, fingerprint, latest) {
|
|
377
|
+
return `You are 0sec's family execution agent.
|
|
378
|
+
|
|
379
|
+
Target: ${target}
|
|
380
|
+
Finding family fingerprint: ${fingerprint}
|
|
381
|
+
Current work item: ${kind}
|
|
382
|
+
|
|
383
|
+
Representative family finding:
|
|
384
|
+
- Title: ${latest.title}
|
|
385
|
+
- Category: ${latest.category}
|
|
386
|
+
- Severity: ${latest.severity}
|
|
387
|
+
- Existing request: ${latest.evidence.request.slice(0, 500)}
|
|
388
|
+
- Existing response: ${latest.evidence.response.slice(0, 500)}
|
|
389
|
+
- Existing analysis: ${(latest.evidence.analysis ?? "none").slice(0, 500)}
|
|
390
|
+
|
|
391
|
+
Your job:
|
|
392
|
+
1. Reproduce and refine this specific family only.
|
|
393
|
+
2. Focus on stronger evidence, cleaner reproduction, and better exploit framing.
|
|
394
|
+
3. Use http_request and send_prompt against the live target.
|
|
395
|
+
4. If you discover stronger or more precise evidence, call save_finding.
|
|
396
|
+
5. Query existing findings to avoid pointless duplication.
|
|
397
|
+
6. Call done with a concise summary of what changed for this family.
|
|
398
|
+
|
|
399
|
+
Do not broadly scan unrelated attack categories. Stay on this family.`;
|
|
400
|
+
}
|
|
401
|
+
function familyVerifyPrompt(target, findings) {
|
|
402
|
+
const findingList = findings.map(
|
|
403
|
+
(f, index) => `${index + 1}. ID: ${f.id}
|
|
404
|
+
Title: ${f.title}
|
|
405
|
+
Category: ${f.category}
|
|
406
|
+
Severity: ${f.severity}
|
|
407
|
+
Request: ${f.evidence.request.slice(0, 220)}
|
|
408
|
+
Response: ${f.evidence.response.slice(0, 220)}`
|
|
409
|
+
).join("\n\n");
|
|
410
|
+
return `You are 0sec's blind family verification agent.
|
|
411
|
+
|
|
412
|
+
Target: ${target}
|
|
413
|
+
|
|
414
|
+
You must independently verify ONLY these family findings:
|
|
415
|
+
|
|
416
|
+
${findingList || "No findings supplied."}
|
|
417
|
+
|
|
418
|
+
For each finding:
|
|
419
|
+
1. Replay the evidence against the target.
|
|
420
|
+
2. If the issue reproduces, call update_finding with status "confirmed".
|
|
421
|
+
3. If it does not reproduce after a few focused retries, call update_finding with status "false-positive".
|
|
422
|
+
4. Do not invent new unrelated findings here.
|
|
423
|
+
5. Call done with a concise verification summary.`;
|
|
424
|
+
}
|
|
425
|
+
async function runFamilyCandidate(candidate, opts) {
|
|
426
|
+
const fingerprint = candidate.item.findingFingerprint;
|
|
427
|
+
if (!fingerprint) {
|
|
428
|
+
return { ok: false, message: "Family candidate is missing a fingerprint." };
|
|
429
|
+
}
|
|
430
|
+
const db = new osecDB(opts.dbPath);
|
|
431
|
+
try {
|
|
432
|
+
const rows = db.getRelatedFindings(fingerprint);
|
|
433
|
+
if (rows.length === 0) {
|
|
434
|
+
return { ok: false, message: `No persisted findings found for family ${fingerprint}.` };
|
|
435
|
+
}
|
|
436
|
+
const findings = findingRowsToFindings(rows);
|
|
437
|
+
const latest = findings[0];
|
|
438
|
+
const runtime = !opts.runtime || opts.runtime === "auto" || opts.runtime === "api" ? new LlmApiRuntime({
|
|
439
|
+
type: "api",
|
|
440
|
+
timeout: parseInt(opts.timeout ?? "30000", 10),
|
|
441
|
+
apiKey: opts.apiKey,
|
|
442
|
+
model: opts.model
|
|
443
|
+
}) : createRuntime({
|
|
444
|
+
type: opts.runtime,
|
|
445
|
+
timeout: parseInt(opts.timeout ?? "30000", 10),
|
|
446
|
+
apiKey: opts.apiKey,
|
|
447
|
+
model: opts.model
|
|
448
|
+
});
|
|
449
|
+
if (candidate.item.kind === "consensus") {
|
|
450
|
+
const latestRow = rows[0];
|
|
451
|
+
const consensus = latestRow ? db.computeConsensus(latestRow.id) : "pending";
|
|
452
|
+
if (consensus === "verified" || consensus === "false-positive") {
|
|
453
|
+
db.updateFindingWorkflowByFingerprint(fingerprint, "human_review", candidate.item.owner ?? null);
|
|
454
|
+
} else {
|
|
455
|
+
db.upsertWorkItem({
|
|
456
|
+
...candidate.item,
|
|
457
|
+
status: "blocked",
|
|
458
|
+
owner: candidate.item.owner ?? "consensus-agent",
|
|
459
|
+
summary: consensus === "disputed" ? "Verifier votes disagree. Route this family into agent review instead of retrying consensus." : "Consensus could not complete because verifier evidence is incomplete."
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return { ok: true };
|
|
463
|
+
}
|
|
464
|
+
const role = candidate.item.kind === "blind_verify" ? "verify" : "attack";
|
|
465
|
+
const systemPrompt = role === "verify" ? familyVerifyPrompt(candidate.target, findings) : familyAttackPrompt(candidate.target, candidate.item.kind, fingerprint, latest);
|
|
466
|
+
await runAgentLoop({
|
|
467
|
+
config: {
|
|
468
|
+
role,
|
|
469
|
+
systemPrompt,
|
|
470
|
+
tools: getToolsForRole(role),
|
|
471
|
+
maxTurns: role === "verify" ? 8 : 10,
|
|
472
|
+
target: candidate.target,
|
|
473
|
+
scanId: candidate.scan.id
|
|
474
|
+
},
|
|
475
|
+
runtime,
|
|
476
|
+
db
|
|
477
|
+
});
|
|
478
|
+
return { ok: true };
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
481
|
+
} finally {
|
|
482
|
+
db.close();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function registerOrchestrateCommand(program) {
|
|
486
|
+
program.command("orchestrate").description("Run the autonomous verification worker against persisted queued case work").option("--db-path <path>", "Path to SQLite database").option("--limit <n>", "Maximum queued cases to claim per pass", "1").option("--runtime <runtime>", "Runtime override: auto, claude, codex, gemini, api").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--api-key <key>", "API key for LLM provider").option("-m, --model <model>", "LLM model to use").option("--watch", "Run as a persistent daemon loop", false).option("--poll-interval <ms>", "Idle poll interval for watch mode", "5000").option("--label <name>", "Operator-facing worker label").action(async (opts) => {
|
|
487
|
+
const workerId = `orchestrator:${randomUUID()}`;
|
|
488
|
+
const label = opts.label?.trim() || `${hostname()}:${process.pid}`;
|
|
489
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "1", 10));
|
|
490
|
+
const pollInterval = Math.max(1e3, parseInt(opts.pollInterval ?? "5000", 10));
|
|
491
|
+
let stopping = false;
|
|
492
|
+
let heartbeat = null;
|
|
493
|
+
let workerState = {
|
|
494
|
+
status: "idle",
|
|
495
|
+
currentCaseId: null,
|
|
496
|
+
currentWorkItemId: null,
|
|
497
|
+
currentScanId: null,
|
|
498
|
+
lastError: null
|
|
499
|
+
};
|
|
500
|
+
const updateWorker = (status, state) => {
|
|
501
|
+
workerState = {
|
|
502
|
+
status,
|
|
503
|
+
currentCaseId: state?.currentCaseId ?? null,
|
|
504
|
+
currentWorkItemId: state?.currentWorkItemId ?? null,
|
|
505
|
+
currentScanId: state?.currentScanId ?? null,
|
|
506
|
+
lastError: state?.lastError ?? null
|
|
507
|
+
};
|
|
508
|
+
touchWorker(
|
|
509
|
+
opts.dbPath,
|
|
510
|
+
buildWorkerRecord({
|
|
511
|
+
id: workerId,
|
|
512
|
+
label,
|
|
513
|
+
status: workerState.status,
|
|
514
|
+
currentCaseId: workerState.currentCaseId,
|
|
515
|
+
currentWorkItemId: workerState.currentWorkItemId,
|
|
516
|
+
currentScanId: workerState.currentScanId,
|
|
517
|
+
lastError: workerState.lastError
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
const stop = () => {
|
|
522
|
+
stopping = true;
|
|
523
|
+
};
|
|
524
|
+
process.once("SIGINT", stop);
|
|
525
|
+
process.once("SIGTERM", stop);
|
|
526
|
+
stopSupersededWorkers(opts.dbPath, label, workerId);
|
|
527
|
+
updateWorker("idle");
|
|
528
|
+
heartbeat = setInterval(() => {
|
|
529
|
+
try {
|
|
530
|
+
touchWorker(
|
|
531
|
+
opts.dbPath,
|
|
532
|
+
buildWorkerRecord({
|
|
533
|
+
id: workerId,
|
|
534
|
+
label,
|
|
535
|
+
status: workerState.status,
|
|
536
|
+
currentCaseId: workerState.currentCaseId,
|
|
537
|
+
currentWorkItemId: workerState.currentWorkItemId,
|
|
538
|
+
currentScanId: workerState.currentScanId,
|
|
539
|
+
lastError: workerState.lastError
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}, Math.min(pollInterval, 5e3));
|
|
545
|
+
console.log(source_default.red.bold("\u25C6 0sec") + source_default.gray(opts.watch ? ` daemon ${label} online` : ` worker ${label} starting`));
|
|
546
|
+
try {
|
|
547
|
+
do {
|
|
548
|
+
if (stopping) break;
|
|
549
|
+
const recovered = recoverStaleWorkers(opts.dbPath);
|
|
550
|
+
if (recovered > 0) {
|
|
551
|
+
console.log(source_default.yellow(`Recovered ${recovered} stale work item${recovered === 1 ? "" : "s"} from dead workers.`));
|
|
552
|
+
}
|
|
553
|
+
updateWorker("claiming");
|
|
554
|
+
const candidates = findRunnableCandidates(opts.dbPath, limit);
|
|
555
|
+
if (candidates.length === 0) {
|
|
556
|
+
updateWorker(opts.watch ? "sleeping" : "idle");
|
|
557
|
+
if (!opts.watch) {
|
|
558
|
+
console.log(source_default.gray("No runnable autonomous case work found."));
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
await sleep(pollInterval);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
for (const candidate of candidates) {
|
|
565
|
+
if (stopping) break;
|
|
566
|
+
const claimed = claimCandidate(opts.dbPath, workerId, label, candidate);
|
|
567
|
+
if (!claimed) continue;
|
|
568
|
+
console.log("");
|
|
569
|
+
console.log(source_default.white(candidate.caseRecord.target));
|
|
570
|
+
console.log(
|
|
571
|
+
source_default.gray(
|
|
572
|
+
` running ${candidate.item.kind}${candidate.item.findingFingerprint ? ` for family ${candidate.item.findingFingerprint.slice(0, 10)}` : ""} via ${label}`
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
const result = await runCandidate(candidate, opts);
|
|
576
|
+
reconcileCandidateOutcome(opts.dbPath, candidate, result);
|
|
577
|
+
if (!result.ok) {
|
|
578
|
+
updateWorker("error", {
|
|
579
|
+
currentCaseId: candidate.caseRecord.id,
|
|
580
|
+
currentWorkItemId: candidate.item.id,
|
|
581
|
+
currentScanId: candidate.scan.id,
|
|
582
|
+
lastError: result.message
|
|
583
|
+
});
|
|
584
|
+
} else {
|
|
585
|
+
updateWorker("idle");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!opts.watch) break;
|
|
589
|
+
} while (!stopping);
|
|
590
|
+
} finally {
|
|
591
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
592
|
+
updateWorker("stopped");
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export {
|
|
598
|
+
recoverStaleWorkers,
|
|
599
|
+
registerOrchestrateCommand
|
|
600
|
+
};
|