0sec-cli 0.17.0 → 0.21.4
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 → 0.js} +92 -79
- package/LICENSE +1 -1
- package/README.md +77 -37
- package/chunks/adapt-loop-IGBRQAXO.js +18 -0
- package/chunks/{adgraph-JLGA6RYI.js → adgraph-AUDQTBYA.js} +5 -5
- package/chunks/agent/skills/frameworks/entra-id.yaml +2 -2
- package/chunks/agent/skills/techniques/assumption-mining.yaml +2 -2
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +2 -2
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +1 -1
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +1 -1
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +2 -2
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +1 -1
- package/chunks/agent/skills/techniques/poc-verification.yaml +1 -1
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +20 -38
- package/chunks/{artifact-scraper-GJEZMOHF.js → artifact-scraper-LNQJUORS.js} +6 -6
- package/chunks/{assumption-mining-BVMMUZHB.js → assumption-mining-4MJTP6H5.js} +24 -26
- package/chunks/{chunk-P6WKNFWX.js → chunk-2OQOQ2FZ.js} +8 -8
- package/chunks/{chunk-5G3ZXFBW.js → chunk-2WNCI664.js} +3 -3
- package/chunks/{chunk-HDWV7PGI.js → chunk-2WSXFFJZ.js} +4 -4
- package/chunks/{chunk-DW5UWPFY.js → chunk-4VGLO2YS.js} +6 -5
- package/chunks/{chunk-SNKTC4BP.js → chunk-5T6D5BVY.js} +88 -88
- package/chunks/{chunk-OEFNRYI2.js → chunk-6GHSR47F.js} +3 -6
- package/chunks/chunk-6YNKRUMC.js +4474 -0
- package/chunks/{chunk-O462Y7P2.js → chunk-ANDKY54G.js} +22317 -19616
- package/chunks/{chunk-53G27VPS.js → chunk-AZ7L3HFD.js} +7 -2
- package/chunks/{chunk-2SANI5RH.js → chunk-BACXRTQ4.js} +691 -318
- package/chunks/{chunk-SOG2U7B3.js → chunk-CAJJRUTV.js} +4 -4
- package/chunks/{chunk-57ZENEX2.js → chunk-CGHCEN7W.js} +3 -3
- package/chunks/{chunk-VGRDNSHA.js → chunk-CL5KAUM6.js} +17 -16
- package/chunks/{chunk-QOKTUOU2.js → chunk-CXKDWFNO.js} +6 -6
- package/chunks/{chunk-RWONANDA.js → chunk-DQTNI3KY.js} +5 -5
- package/chunks/{chunk-K26SZ37E.js → chunk-E2UOY5PJ.js} +288 -100
- package/chunks/{chunk-CLHCDHP4.js → chunk-ER3SUMYJ.js} +40 -18
- package/chunks/{chunk-IORC6MQX.js → chunk-FAH6V4QM.js} +31964 -32279
- package/chunks/{chunk-6LKRLK2R.js → chunk-FHC2B7LN.js} +3 -3
- package/chunks/{chunk-4Y4KQJXE.js → chunk-FWAYJ2IV.js} +16 -16
- package/chunks/{chunk-WVTBZEQO.js → chunk-G34QP2XW.js} +8 -8
- package/chunks/{chunk-MYVT64FN.js → chunk-GHWODZMR.js} +2 -2
- package/chunks/{chunk-F3WBKITT.js → chunk-GIQKESMN.js} +10 -10
- package/chunks/{chunk-2RMLOJVB.js → chunk-GVD6SJGH.js} +2 -2
- package/chunks/{chunk-RDZYQQLW.js → chunk-HBKCGJAV.js} +7 -7
- package/chunks/{chunk-SF4KZ4O3.js → chunk-HLFS6WV2.js} +2 -2
- package/chunks/chunk-HPQZHOIU.js +67 -0
- package/chunks/{chunk-3QFDYBZQ.js → chunk-IMGWPNQM.js} +231 -1308
- package/chunks/{chunk-3MOLBTLS.js → chunk-K2PWUPUI.js} +2 -2
- package/chunks/{chunk-MMLDQR4H.js → chunk-NDJU5ZZ7.js} +14 -14
- package/chunks/{chunk-H44E2CRN.js → chunk-NOPBXZUV.js} +3 -3
- package/chunks/{chunk-HE7LCA7G.js → chunk-O6SD4A36.js} +3 -3
- package/chunks/{chunk-SAFFWQW4.js → chunk-P6YG6TEZ.js} +6 -6
- package/chunks/{chunk-KKGE5RQE.js → chunk-PC6RCKQV.js} +198 -11
- package/chunks/{chunk-LP3HHYQU.js → chunk-PD2BBBKT.js} +2 -2
- package/chunks/{chunk-BKZFDZ23.js → chunk-QCUFBVIZ.js} +3 -3
- package/chunks/{chunk-KLTNTE2Z.js → chunk-QZVG3BG4.js} +14 -14
- package/chunks/{chunk-D6S3IIPG.js → chunk-RF5QGKLR.js} +9 -9
- package/chunks/{chunk-LOHTE223.js → chunk-SBMB4OL3.js} +9 -9
- package/chunks/{chunk-IOL7D5YV.js → chunk-SQCBHFWN.js} +3 -3
- package/chunks/{chunk-A6CLR72I.js → chunk-TF46UWFU.js} +3 -3
- package/chunks/chunk-TH2LW437.js +7829 -0
- package/chunks/chunk-TRH2PJSN.js +547 -0
- package/chunks/{chunk-RJWOYEOG.js → chunk-UEX7PFSE.js} +5 -5
- package/chunks/{chunk-VQG4FT5D.js → chunk-UFHN6RIU.js} +2 -2
- package/chunks/{chunk-WU6AFRAZ.js → chunk-V36HVTUL.js} +12 -12
- package/chunks/{chunk-IR537GON.js → chunk-VCSFCHAJ.js} +2 -2
- package/chunks/{chunk-YLMN3N25.js → chunk-VIT5ALXF.js} +3 -3
- package/chunks/{chunk-UJ4IK5PO.js → chunk-VOAXLO4A.js} +10 -10
- package/chunks/{chunk-UM3ZNQIM.js → chunk-W4JOXE4N.js} +100 -84
- package/chunks/{chunk-DN25OQQA.js → chunk-WF5W75WY.js} +25 -25
- package/chunks/{chunk-QKICO43A.js → chunk-WPQLBN4S.js} +5 -5
- package/chunks/{chunk-HT6P7RY3.js → chunk-WZISK5VC.js} +37 -35
- package/chunks/{chunk-QJQKHO7G.js → chunk-Y5H6E24H.js} +3 -3
- package/chunks/{chunk-KTQDLNSR.js → chunk-YA4PUM4H.js} +41 -41
- package/chunks/{chunk-47TQJWDH.js → chunk-YHWWF4QY.js} +9 -9
- package/chunks/{chunk-2CJ776PV.js → chunk-YPOA3W4O.js} +48 -39
- package/chunks/{chunk-7DQEV5QI.js → chunk-Z3IKZX3X.js} +4 -4
- package/chunks/codex-models-TYYV4JNN.js +15 -0
- package/chunks/{commands-U3AMV5ZU.js → commands-WF5RG4I2.js} +5620 -2513
- package/chunks/corpus-v1.json +2 -2
- package/chunks/data/appsec-archetypes.json +2 -2
- package/chunks/data/chromium-archetypes.json +1 -1
- package/chunks/data/freebsd-archetypes.json +1 -1
- package/chunks/data/kernel-archetypes.json +2 -2
- package/chunks/db-U3WQZVAO.js +16 -0
- package/chunks/{disclose-A6AEIEDA.js → disclose-67GZCDBO.js} +5 -5
- package/chunks/{dist-JR67XKYO.js → dist-ACC3DZUZ.js} +11 -5
- package/chunks/{dist-GL66KMUX.js → dist-EVTN3UTT.js} +6 -6
- package/chunks/{dist-FDKALV4R.js → dist-F6X62TQK.js} +290 -327
- package/chunks/dist-KEULE5IQ.js +21914 -0
- package/chunks/dist-XZB53PS5.js +53 -0
- package/chunks/eval-runner-SA6OUGHZ.js +26 -0
- package/chunks/example-manifest.json +2 -2
- package/chunks/{exploit-agent-XP23ISN7.js → exploit-agent-36Q7XQFP.js} +4 -4
- package/chunks/{exploit-autoclimb-E6T7RRSC.js → exploit-autoclimb-J4OTSTZ6.js} +6 -6
- package/chunks/{exploit-climb-2FAUP6XU.js → exploit-climb-PYEJDIC4.js} +12 -12
- package/chunks/fix-J2KBRJDV.js +12 -0
- package/chunks/{github-issues-MJ6OYOOU.js → github-issues-IGTTWM7V.js} +7 -7
- package/chunks/harness-GDUMKGSE.js +22 -0
- package/chunks/http-conformance-U5F6GKYG.js +11 -0
- package/chunks/http-sender-I5XFFY35.js +10 -0
- package/chunks/hunt-scan-UDJSCISU.js +36 -0
- package/chunks/{identity-6ZAIIWOR.js → identity-JTIVJSV7.js} +5 -5
- package/chunks/{kernel-primitive-TENE3R7T.js → kernel-primitive-F2IXUFAX.js} +6 -6
- package/chunks/{kernel-vm-runner-4F6QSNFY.js → kernel-vm-runner-276LHIOV.js} +6 -6
- package/chunks/memsafety-scan-NHUOGTEX.js +16 -0
- package/chunks/{native-loop-NTONUDP7.js → native-loop-3BSYVDUI.js} +17 -18
- package/chunks/{npm-detectors-KB5Y5ZFX.js → npm-detectors-VIYJXKFQ.js} +6 -6
- package/chunks/npm-dynamic-discovery-5J7FT3XM.js +13 -0
- package/chunks/orchestrate-DHBFRTSI.js +59 -0
- package/chunks/{pre-recon-cve-66EB6G4M.js → pre-recon-cve-X2K2P5YQ.js} +4 -4
- package/chunks/prepare-KYHGFWCJ.js +13 -0
- package/chunks/process-PNMC36JI.js +15 -0
- package/chunks/{replay-runner-RRR4E2A7.js → replay-runner-LK2XFIVG.js} +7 -7
- package/chunks/{run-ESNN4V5W.js → run-CBGE6DRQ.js} +2401 -1810
- package/chunks/{runtime-62ZQAH7H.js → runtime-PXO5G6UV.js} +11 -11
- package/chunks/runtime-XFWJTBUO.js +12 -0
- package/chunks/{scan-stream-FHI2FYZE.js → scan-stream-BMOCPKQX.js} +7 -7
- package/chunks/{scope-BI7BF4ZY.js → scope-NJHIDDTN.js} +4 -4
- package/chunks/{session-store-BCFQYDCE.js → session-store-46WBZM22.js} +6 -6
- package/chunks/source-files-REHOM44I.js +12 -0
- package/chunks/{specdrift-WCLH6TTQ.js → specdrift-AFS54WEJ.js} +5 -5
- package/chunks/token-KRWP5JYA.js +72 -0
- package/chunks/token-util-DWTL33S3.js +8 -0
- package/chunks/variant-candidates-UH2WC2HA.js +13 -0
- package/chunks/{web-recon-prepass-DPVCBRZA.js → web-recon-prepass-SFXU44SS.js} +9 -9
- package/dashboard/assets/desktop-BT7RkC4q.js +32 -0
- package/dashboard/assets/desktop-OaBI0E0e.css +1 -0
- package/dashboard/assets/{findings-page-CACW9nw2.js → findings-page-B4rwF6fZ.js} +2 -2
- package/dashboard/assets/{format-PoISqsES.js → format-LNQSSpn6.js} +1 -1
- package/dashboard/assets/live-page-DoNwpPAO.js +1 -0
- package/dashboard/assets/{meta-tile-CcskIV_o.js → meta-tile-BMtBC-Qq.js} +1 -1
- package/dashboard/assets/operations-CMFVrXEe.css +2 -0
- package/dashboard/assets/operations-app-ER0gg7GN.js +2 -0
- package/dashboard/assets/{operations-D8nUP5_m.js → operations-e2CGYKB1.js} +2 -2
- package/dashboard/assets/{overview-page-DTGS8dPo.js → overview-page-CmeWH17A.js} +1 -1
- package/dashboard/assets/{page-header-MFVvEtZW.js → page-header-CIJyycfz.js} +1 -1
- package/dashboard/assets/scans-page-CuhzWhHx.js +1 -0
- package/dashboard/assets/{table-BYS-nIVA.js → table-BgsSBdke.js} +1 -1
- package/dashboard/assets/{tabs-DivxbhQy.js → tabs-vn6v7Bk6.js} +1 -1
- package/dashboard/desktop.html +3 -3
- package/dashboard/index.html +3 -3
- package/package.json +11 -11
- package/chunks/adapt-loop-ECQG7454.js +0 -18
- package/chunks/appsec-catalog-CBWHGTEL.js +0 -24
- package/chunks/chunk-H2FFLZNK.js +0 -3
- package/chunks/chunk-QI233I24.js +0 -333
- package/chunks/chunk-RRMJC3ZE.js +0 -105
- package/chunks/chunk-SZJCPG2I.js +0 -110
- package/chunks/chunk-WKNNZVJS.js +0 -2798
- package/chunks/cost-ledger-ZCKMBJEN.js +0 -13
- package/chunks/db-26NGQFKO.js +0 -16
- package/chunks/eval-runner-DDLE5RQ3.js +0 -27
- package/chunks/fix-IG7LYXH3.js +0 -12
- package/chunks/harness-HWOYFKFY.js +0 -22
- package/chunks/http-conformance-DU66MZIU.js +0 -11
- package/chunks/http-sender-GWH2IYEA.js +0 -10
- package/chunks/hunt-scan-XPELSPQQ.js +0 -38
- package/chunks/memsafety-scan-J7SECV5W.js +0 -16
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +0 -13
- package/chunks/orchestrate-ZUWAUWBC.js +0 -62
- package/chunks/pipeline-FCARFZU3.js +0 -14
- package/chunks/prepare-MYY743TK.js +0 -13
- package/chunks/process-3Q7QJIOZ.js +0 -13
- package/chunks/runtime-J7PLXZNM.js +0 -12
- package/chunks/source-files-PWRQR6LY.js +0 -12
- package/chunks/variant-candidates-6KM6F6MD.js +0 -13
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/desktop-CXTQonNm.js +0 -32
- package/dashboard/assets/desktop-NOekk4QS.css +0 -1
- package/dashboard/assets/live-page-DrgQqQK5.js +0 -1
- package/dashboard/assets/operations-BKE8T-vY.css +0 -2
- package/dashboard/assets/operations-app-CQQhG54n.js +0 -2
- package/dashboard/assets/scans-page-ChVtq8Us.js +0 -1
|
@@ -1,55 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire as
|
|
3
|
-
const require =
|
|
2
|
+
import { createRequire as __0CreateRequire } from "node:module";
|
|
3
|
+
const require = __0CreateRequire(import.meta.url);
|
|
4
4
|
import {
|
|
5
5
|
isValidSessionId,
|
|
6
6
|
listSessions,
|
|
7
7
|
loadSession
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NOPBXZUV.js";
|
|
9
9
|
import {
|
|
10
10
|
consolePresentationOutput,
|
|
11
11
|
getSettings,
|
|
12
12
|
maybeLoadCodexAuth
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-YPOA3W4O.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
source_default
|
|
16
|
+
} from "./chunk-HLFS6WV2.js";
|
|
17
|
+
import {
|
|
18
|
+
createConsoleJevRuntime,
|
|
16
19
|
createConsoleSession,
|
|
17
|
-
createEvolvedFinder,
|
|
18
20
|
inspectLensRegistry,
|
|
19
21
|
lastGreen,
|
|
20
22
|
loadLedger,
|
|
21
23
|
loadManifest,
|
|
22
24
|
makeFinderLensProbe,
|
|
23
25
|
retireArchetype,
|
|
24
|
-
runLensSynthesisLoop
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
source_default
|
|
28
|
-
} from "./chunk-SF4KZ4O3.js";
|
|
29
|
-
import {
|
|
30
|
-
stampDeploymentContext
|
|
31
|
-
} from "./chunk-K26SZ37E.js";
|
|
26
|
+
runLensSynthesisLoop,
|
|
27
|
+
toToolContextJevRuntime
|
|
28
|
+
} from "./chunk-FAH6V4QM.js";
|
|
32
29
|
import {
|
|
33
30
|
harvestMissesFromLedgerEntry,
|
|
34
|
-
loadEvolutionConfigFile,
|
|
35
31
|
mergeHarvestedMisses
|
|
36
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-ANDKY54G.js";
|
|
33
|
+
import {
|
|
34
|
+
eventBus
|
|
35
|
+
} from "./chunk-CXKDWFNO.js";
|
|
37
36
|
import {
|
|
38
37
|
listOsecRunDatabasePaths,
|
|
39
38
|
osecDB,
|
|
40
|
-
resolveOsecDbPath
|
|
41
|
-
|
|
42
|
-
writeOsecRunReport
|
|
43
|
-
} from "./chunk-HT6P7RY3.js";
|
|
44
|
-
import {
|
|
45
|
-
loadAppsecFinderLenses
|
|
46
|
-
} from "./chunk-QI233I24.js";
|
|
47
|
-
import {
|
|
48
|
-
ScanCostLedger
|
|
49
|
-
} from "./chunk-SZJCPG2I.js";
|
|
39
|
+
resolveOsecDbPath
|
|
40
|
+
} from "./chunk-WZISK5VC.js";
|
|
50
41
|
import {
|
|
51
42
|
redactSensitiveHeaders
|
|
52
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-W4JOXE4N.js";
|
|
53
44
|
import {
|
|
54
45
|
CloudAuthMissingError,
|
|
55
46
|
CloudClient,
|
|
@@ -59,13 +50,11 @@ import {
|
|
|
59
50
|
CloudUnauthorizedError,
|
|
60
51
|
DEFAULT_CLOUD_HOST,
|
|
61
52
|
loadCloudCredentials
|
|
62
|
-
} from "./chunk-
|
|
63
|
-
import {
|
|
64
|
-
eventBus
|
|
65
|
-
} from "./chunk-QOKTUOU2.js";
|
|
53
|
+
} from "./chunk-BACXRTQ4.js";
|
|
66
54
|
import {
|
|
67
55
|
SEVERITY_RANK,
|
|
68
56
|
VERSION,
|
|
57
|
+
cloudStateDir,
|
|
69
58
|
createAuditReportDocument,
|
|
70
59
|
createReviewReportDocument,
|
|
71
60
|
createScanReportDocument,
|
|
@@ -73,12 +62,12 @@ import {
|
|
|
73
62
|
formatZodError,
|
|
74
63
|
homeStateDir,
|
|
75
64
|
severityRank
|
|
76
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-PC6RCKQV.js";
|
|
77
66
|
|
|
78
67
|
// packages/cli/src/utils.ts
|
|
79
68
|
import { gzipSync } from "zlib";
|
|
80
69
|
async function getRuntimeAvailability() {
|
|
81
|
-
const { detectAvailableRuntimes, LlmApiRuntime } = await import("./dist-
|
|
70
|
+
const { detectAvailableRuntimes, LlmApiRuntime } = await import("./dist-F6X62TQK.js");
|
|
82
71
|
const apiRuntimeDiagnostics = new LlmApiRuntime({ type: "api", timeout: 5e3 }).getConfigurationDiagnostics();
|
|
83
72
|
const hasApiKey = apiRuntimeDiagnostics.valid;
|
|
84
73
|
const availableRuntimes = [...await detectAvailableRuntimes()];
|
|
@@ -110,1094 +99,14 @@ async function checkRuntimeAvailability(runtime) {
|
|
|
110
99
|
} else {
|
|
111
100
|
console.log(source_default.yellow(" Warning: No API key or local agent runtime detected. AI analysis will be skipped."));
|
|
112
101
|
}
|
|
113
|
-
console.log(source_default.gray(" Provider credentials:
|
|
102
|
+
console.log(source_default.gray(" Provider credentials: ZERO_CHATGPT_ACCESS_TOKEN | ZERO_CHATGPT_OAUTH_REFRESH_TOKEN, OPENROUTER_API_KEY, ANTHROPIC_API_KEY, AZURE_OPENAI_API_KEY, OPENAI_API_KEY"));
|
|
114
103
|
console.log("");
|
|
115
104
|
}
|
|
116
105
|
function buildShareUrl(report) {
|
|
117
106
|
const json = JSON.stringify(report);
|
|
118
107
|
const compressed = gzipSync(Buffer.from(json, "utf-8"));
|
|
119
108
|
const b64 = compressed.toString("base64url");
|
|
120
|
-
return `https://
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// packages/cli/src/commands/hunt.ts
|
|
124
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
125
|
-
import { resolve } from "node:path";
|
|
126
|
-
function leadToCandidateFinding(finding, bugClass, seedRef, candidatePath) {
|
|
127
|
-
const evidence = finding.evidence ?? {};
|
|
128
|
-
const provenance = `Variant-hunt LEAD (bug class: ${bugClass}; seed: ${seedRef}). Surfaced by the recency hunt and gated by the adversarial skeptic \u2014 a HYPOTHESIS, not a confirmed bug. Verify the real sink + upstream-fix status (novelty) before any disclosure.`;
|
|
129
|
-
if (candidatePath) stampDeploymentContext(finding, candidatePath);
|
|
130
|
-
return {
|
|
131
|
-
...finding,
|
|
132
|
-
// LEADS are never confirmed/sendable: force candidate status so the cloud
|
|
133
|
-
// ingests them as verify candidates, never as confirmed findings.
|
|
134
|
-
status: "discovered",
|
|
135
|
-
templateId: "recency-hunt-lead",
|
|
136
|
-
evidence: {
|
|
137
|
-
request: evidence.request ?? "",
|
|
138
|
-
response: evidence.response ?? "",
|
|
139
|
-
analysis: evidence.analysis ? `${evidence.analysis}
|
|
140
|
-
|
|
141
|
-
${provenance}` : provenance
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
function parsePositive(flag, raw, dflt) {
|
|
146
|
-
if (raw === void 0) return dflt;
|
|
147
|
-
const n = parseInt(raw, 10);
|
|
148
|
-
if (!Number.isFinite(n) || n <= 0) throw new Error(`invalid ${flag} '${raw}' (expected positive integer)`);
|
|
149
|
-
return n;
|
|
150
|
-
}
|
|
151
|
-
var IMPACT_CEILINGS = ["dos-only", "info-leak", "oob-write", "uaf-control"];
|
|
152
|
-
function parseCeiling(raw) {
|
|
153
|
-
const v = raw.trim();
|
|
154
|
-
if (!IMPACT_CEILINGS.includes(v)) {
|
|
155
|
-
throw new Error(`invalid --prove-min-ceiling '${raw}' (expected one of: ${IMPACT_CEILINGS.join(", ")})`);
|
|
156
|
-
}
|
|
157
|
-
return v;
|
|
158
|
-
}
|
|
159
|
-
function parseNonNegative(flag, raw, dflt) {
|
|
160
|
-
if (raw === void 0) return dflt;
|
|
161
|
-
const n = parseInt(raw, 10);
|
|
162
|
-
if (!Number.isFinite(n) || n < 0) throw new Error(`invalid ${flag} '${raw}' (expected non-negative integer)`);
|
|
163
|
-
return n;
|
|
164
|
-
}
|
|
165
|
-
function parseEnvBestOfN(raw) {
|
|
166
|
-
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
167
|
-
const n = Number.parseInt(raw, 10);
|
|
168
|
-
return Number.isFinite(n) && n > 1 ? n : void 0;
|
|
169
|
-
}
|
|
170
|
-
function parseEnvPositiveInt(raw) {
|
|
171
|
-
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
172
|
-
const n = Number.parseInt(raw, 10);
|
|
173
|
-
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
174
|
-
}
|
|
175
|
-
async function runHunt(opts) {
|
|
176
|
-
const {
|
|
177
|
-
generateVariantCandidates,
|
|
178
|
-
runHuntScan,
|
|
179
|
-
makeSkepticVerifier,
|
|
180
|
-
loadKnownNegativesFromEnv,
|
|
181
|
-
buildInvariantHuntContext,
|
|
182
|
-
buildGraphSliceHuntContext,
|
|
183
|
-
localMirrors,
|
|
184
|
-
syncLoreMirror,
|
|
185
|
-
makeLloreJudge,
|
|
186
|
-
makeHuntProveStage,
|
|
187
|
-
prepare,
|
|
188
|
-
getCloudSinkConfig,
|
|
189
|
-
postFinding
|
|
190
|
-
} = await import("./dist-FDKALV4R.js");
|
|
191
|
-
const log = opts.log ?? (() => {
|
|
192
|
-
});
|
|
193
|
-
const runtime = opts.runtime ?? "api";
|
|
194
|
-
const seedDiff = readFileSync(resolve(opts.seedPath), "utf8");
|
|
195
|
-
const prepared = await prepare(opts.sourceRoot, "source-code", { ...opts.timeoutMs ? { timeout: opts.timeoutMs } : {} }, (e) => {
|
|
196
|
-
if (e.message) log(`[hunt:source] ${e.message}`);
|
|
197
|
-
});
|
|
198
|
-
const sourceRoot = resolve(prepared.resolvedTarget);
|
|
199
|
-
const noveltyRoot = opts.novelty?.rootDir ?? process.env["0SEC_LORE_MIRROR_ROOT"] ?? "/root/lore-mirror";
|
|
200
|
-
const noveltyLists = opts.novelty?.lists ?? (process.env["0SEC_LORE_LISTS"] ?? "linux-media").split(",").map((s) => s.trim()).filter(Boolean);
|
|
201
|
-
const noveltyRecentEpochs = opts.novelty?.recentEpochs ?? 1;
|
|
202
|
-
const noveltyWarnings = [];
|
|
203
|
-
const sinkCfg = getCloudSinkConfig();
|
|
204
|
-
const savedCloudSink = process.env["0SEC_CLOUD_SINK"];
|
|
205
|
-
if (sinkCfg) delete process.env["0SEC_CLOUD_SINK"];
|
|
206
|
-
try {
|
|
207
|
-
let noveltyMirrors = [];
|
|
208
|
-
if (opts.novelty && noveltyLists.length > 0) {
|
|
209
|
-
try {
|
|
210
|
-
noveltyMirrors = opts.novelty.sync ? await syncLoreMirror({
|
|
211
|
-
rootDir: noveltyRoot,
|
|
212
|
-
lists: noveltyLists,
|
|
213
|
-
recentEpochs: noveltyRecentEpochs,
|
|
214
|
-
log
|
|
215
|
-
}) : localMirrors(noveltyRoot, noveltyLists);
|
|
216
|
-
} catch (err) {
|
|
217
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
218
|
-
noveltyWarnings.push(`hunt: novelty sync failed: ${reason.slice(0, 160)}`);
|
|
219
|
-
log(`[hunt] ${noveltyWarnings[noveltyWarnings.length - 1]}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (opts.novelty && noveltyMirrors.length === 0) {
|
|
223
|
-
const message = `novelty requested but no lore mirrors found under ${noveltyRoot} for ${noveltyLists.join(",") || "(no lists)"}`;
|
|
224
|
-
return {
|
|
225
|
-
exitCode: 3,
|
|
226
|
-
result: {
|
|
227
|
-
mode: "hunt",
|
|
228
|
-
seed: opts.ref ?? opts.seedPath,
|
|
229
|
-
candidates: 0,
|
|
230
|
-
novelty: { enabled: true, required: true, mirrors: [] },
|
|
231
|
-
warnings: [...noveltyWarnings, `hunt: ${message}; aborting fail-closed`],
|
|
232
|
-
note: "Discovery did not run because requested novelty evidence was unavailable."
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const skipCandidates = opts.skipCandidates ?? 0;
|
|
237
|
-
const maxCandidates = opts.maxCandidates ?? 40;
|
|
238
|
-
const reachableOnly = opts.reachableOnly ?? process.env.HUNT_REACHABLE_ONLY === "1";
|
|
239
|
-
const reachablePrefer = opts.reachablePrefer ?? (opts.methodology ? true : process.env.HUNT_REACHABLE_PREFER === "1");
|
|
240
|
-
let invariantCtx = null;
|
|
241
|
-
const invariantWarnings = [];
|
|
242
|
-
if (opts.invariant) {
|
|
243
|
-
try {
|
|
244
|
-
invariantCtx = await buildInvariantHuntContext({ sourceRoot, seedDiff, runtime, log });
|
|
245
|
-
if (!invariantCtx) {
|
|
246
|
-
invariantWarnings.push("hunt: --invariant set but no subsystem scope/files derivable from the seed \u2014 continuing without invariant context");
|
|
247
|
-
log(`[hunt] ${invariantWarnings[invariantWarnings.length - 1]}`);
|
|
248
|
-
}
|
|
249
|
-
} catch (err) {
|
|
250
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
251
|
-
invariantWarnings.push(`hunt: invariant-model stage failed, continuing without it: ${reason.slice(0, 160)}`);
|
|
252
|
-
log(`[hunt] ${invariantWarnings[invariantWarnings.length - 1]}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
let graphSliceCtx = null;
|
|
256
|
-
const graphSliceWarnings = [];
|
|
257
|
-
if (opts.graphSlice) {
|
|
258
|
-
try {
|
|
259
|
-
graphSliceCtx = buildGraphSliceHuntContext({
|
|
260
|
-
sourceRoot,
|
|
261
|
-
seedDiff,
|
|
262
|
-
...opts.cpgPath ? { cpgPath: opts.cpgPath } : {},
|
|
263
|
-
log,
|
|
264
|
-
...opts.opsHarvestSourceFiles !== void 0 ? { opsHarvestSourceFiles: opts.opsHarvestSourceFiles } : {},
|
|
265
|
-
...opts.graphSliceHops !== void 0 ? { hops: opts.graphSliceHops } : {}
|
|
266
|
-
});
|
|
267
|
-
if (!graphSliceCtx) {
|
|
268
|
-
graphSliceWarnings.push("hunt: --graph-slice set but no CPG/scope/slice derivable \u2014 continuing with the flat-text finder");
|
|
269
|
-
log(`[hunt] ${graphSliceWarnings[graphSliceWarnings.length - 1]}`);
|
|
270
|
-
}
|
|
271
|
-
} catch (err) {
|
|
272
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
273
|
-
graphSliceWarnings.push(`hunt: graph-slice stage failed, continuing without it: ${reason.slice(0, 160)}`);
|
|
274
|
-
log(`[hunt] ${graphSliceWarnings[graphSliceWarnings.length - 1]}`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const plan = await generateVariantCandidates({
|
|
278
|
-
sourceRoot,
|
|
279
|
-
fix: { diff: seedDiff, reference: opts.ref ?? opts.seedPath },
|
|
280
|
-
runtime,
|
|
281
|
-
maxCandidates: skipCandidates + maxCandidates,
|
|
282
|
-
...opts.models ? { models: opts.models } : {},
|
|
283
|
-
...reachableOnly ? { reachableOnly } : {},
|
|
284
|
-
...reachablePrefer ? { reachablePrefer } : {},
|
|
285
|
-
log
|
|
286
|
-
});
|
|
287
|
-
const selectedCandidates = plan.candidates.slice(skipCandidates, skipCandidates + maxCandidates);
|
|
288
|
-
if (selectedCandidates.length === 0) {
|
|
289
|
-
return {
|
|
290
|
-
exitCode: 2,
|
|
291
|
-
result: {
|
|
292
|
-
mode: "hunt",
|
|
293
|
-
seed: opts.ref ?? opts.seedPath,
|
|
294
|
-
bug_class: plan.brief.bugClass,
|
|
295
|
-
grep_patterns: plan.grepPatterns,
|
|
296
|
-
candidates: 0,
|
|
297
|
-
skipped_candidates: Math.min(skipCandidates, plan.candidates.length),
|
|
298
|
-
warnings: [...noveltyWarnings, ...plan.warnings],
|
|
299
|
-
note: plan.candidates.length === 0 ? "no candidate sites generated \u2014 seed too narrow or surface already clean" : "no candidate sites left after --skip-candidates"
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
const candidates = selectedCandidates.map((c) => ({ ...c, path: `${sourceRoot}/${c.path}` }));
|
|
304
|
-
const attemptsPerCandidate = opts.attemptsPerCandidate ?? parseEnvBestOfN(process.env.HUNT_BEST_OF_N) ?? (opts.methodology ? 4 : void 0);
|
|
305
|
-
const judgeTopK = opts.judgeTopK ?? parseEnvPositiveInt(process.env.HUNT_JUDGE_TOP_K) ?? (opts.methodology ? 2 : void 0);
|
|
306
|
-
const judgeModel = opts.judgeModel ?? process.env.HUNT_JUDGE_MODEL;
|
|
307
|
-
const baseBrief = opts.methodology ? {
|
|
308
|
-
...plan.brief,
|
|
309
|
-
pattern: `${plan.brief.pattern}
|
|
310
|
-
|
|
311
|
-
METHODOLOGY LENSES: map the full object lifecycle across handlers, callbacks, error/teardown paths and concurrent sessions; trace buffer/page provenance across zero-copy and in-place subsystem boundaries; look for a fix applied to one sibling path but bypassed by another. Refute capability, configuration and state-machine reachability before claiming impact. Treat source reasoning as a hypothesis until a sanitizer-backed reproducer proves it.`
|
|
312
|
-
} : plan.brief;
|
|
313
|
-
let huntPattern = baseBrief.pattern;
|
|
314
|
-
if (invariantCtx) huntPattern = `${huntPattern}
|
|
315
|
-
|
|
316
|
-
${invariantCtx.promptBlock}`;
|
|
317
|
-
if (graphSliceCtx) huntPattern = `${huntPattern}
|
|
318
|
-
|
|
319
|
-
${graphSliceCtx.promptBlock}`;
|
|
320
|
-
const huntBrief = huntPattern === baseBrief.pattern ? baseBrief : { ...baseBrief, pattern: huntPattern };
|
|
321
|
-
const res = await runHuntScan({
|
|
322
|
-
sourceRoot,
|
|
323
|
-
candidates,
|
|
324
|
-
brief: huntBrief,
|
|
325
|
-
runtime,
|
|
326
|
-
concurrency: opts.concurrency ?? 4,
|
|
327
|
-
...opts.models ? { models: opts.models } : {},
|
|
328
|
-
...attemptsPerCandidate ? { attemptsPerCandidate } : {},
|
|
329
|
-
...judgeTopK ? { judgeTopK } : {},
|
|
330
|
-
...judgeModel ? { judgeModel } : {},
|
|
331
|
-
// The skeptic is handed `models[0]` — i.e. by default the refute pass runs
|
|
332
|
-
// on the SAME MODEL that found the bug, which is the correlated-error
|
|
333
|
-
// problem in #661. `finderModels` lets the cross-family selector avoid
|
|
334
|
-
// every family in the finder fan-out (not just the first) and swap the
|
|
335
|
-
// refuter to a different family when one is configured; when only one
|
|
336
|
-
// provider is configured it passes through to exactly this model. `log`
|
|
337
|
-
// makes that decision visible in the run log instead of implicit.
|
|
338
|
-
...opts.verify === false ? {} : {
|
|
339
|
-
verify: makeSkepticVerifier({
|
|
340
|
-
sourceRoot,
|
|
341
|
-
runtime,
|
|
342
|
-
...opts.models?.[0] ? { model: opts.models[0] } : {},
|
|
343
|
-
...opts.models && opts.models.length > 0 ? { finderModels: opts.models } : {},
|
|
344
|
-
negatives: loadKnownNegativesFromEnv(),
|
|
345
|
-
log
|
|
346
|
-
})
|
|
347
|
-
},
|
|
348
|
-
// PROVE stage (#1119) — the terminal gate. Only wired on --exploitability,
|
|
349
|
-
// because it boots REAL QEMU per confirmed finding. runHuntScan itself
|
|
350
|
-
// ignores it (with a warning) when `verify` is absent, so --no-verify
|
|
351
|
-
// --exploitability degrades safely instead of booting anything.
|
|
352
|
-
...opts.exploitability ? {
|
|
353
|
-
exploitability: makeHuntProveStage({
|
|
354
|
-
// Cheap VM-free pre-filter: never spend a QEMU slot on a dos-only bug.
|
|
355
|
-
escalation: opts.proveMinCeiling ? { minCeiling: opts.proveMinCeiling } : {},
|
|
356
|
-
onVerdict: (f, _c, v) => log(
|
|
357
|
-
`[hunt:prove] ${f.title}: maxObserved=${v.maxObservedClass} upgraded=${v.upgraded} reachesPrivesc=${v.reachesPrivesc} proven=${v.provenExploitability}`
|
|
358
|
-
),
|
|
359
|
-
onEscalation: (f, _c, v) => log(`[hunt:prove] ${f.title}: impact ceiling ${v.ceiling} (${v.basis}, conf ${v.confidence})`),
|
|
360
|
-
log
|
|
361
|
-
})
|
|
362
|
-
} : {},
|
|
363
|
-
...opts.novelty && noveltyMirrors.length > 0 ? {
|
|
364
|
-
novelty: {
|
|
365
|
-
mirrors: noveltyMirrors,
|
|
366
|
-
...opts.novelty.model ? { judge: makeLloreJudge({ model: opts.novelty.model }) } : {}
|
|
367
|
-
}
|
|
368
|
-
} : {},
|
|
369
|
-
log
|
|
370
|
-
});
|
|
371
|
-
const gated = opts.verify !== false;
|
|
372
|
-
const leads = gated ? res.confirmed : res.findings;
|
|
373
|
-
let ingested = 0;
|
|
374
|
-
if (sinkCfg) {
|
|
375
|
-
const seedRef = opts.ref ?? opts.seedPath;
|
|
376
|
-
const pathForId = /* @__PURE__ */ new Map();
|
|
377
|
-
for (const rec of res.records) pathForId.set(rec.finding.id, rec.candidatePath);
|
|
378
|
-
for (const lead of leads) {
|
|
379
|
-
const candidatePath = pathForId.get(lead.id);
|
|
380
|
-
await postFinding(leadToCandidateFinding(lead, plan.brief.bugClass, seedRef, candidatePath), sinkCfg);
|
|
381
|
-
ingested++;
|
|
382
|
-
}
|
|
383
|
-
log(`[hunt] posted ${ingested} lead(s) to the cloud-sink as candidate findings`);
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
exitCode: leads.length > 0 ? 0 : 1,
|
|
387
|
-
result: {
|
|
388
|
-
mode: "hunt",
|
|
389
|
-
seed: opts.ref ?? opts.seedPath,
|
|
390
|
-
bug_class: plan.brief.bugClass,
|
|
391
|
-
source: sourceRoot,
|
|
392
|
-
candidate_sites: selectedCandidates.map((c) => c.path),
|
|
393
|
-
skipped_candidates: skipCandidates,
|
|
394
|
-
scanned: res.scanned,
|
|
395
|
-
findings: res.findings.length,
|
|
396
|
-
confirmed: gated ? res.confirmed.length : null,
|
|
397
|
-
novelty: opts.novelty ? {
|
|
398
|
-
enabled: true,
|
|
399
|
-
required: true,
|
|
400
|
-
root: noveltyRoot,
|
|
401
|
-
lists: noveltyLists,
|
|
402
|
-
mirrors: noveltyMirrors.map((m) => ({ list: m.list, epoch: m.epoch, dir: m.dir })),
|
|
403
|
-
duplicates: res.duplicates.map((d) => ({
|
|
404
|
-
title: d.finding.title,
|
|
405
|
-
matches: d.novelty.duplicates
|
|
406
|
-
}))
|
|
407
|
-
} : { enabled: false },
|
|
408
|
-
leads: leads.map((f) => ({
|
|
409
|
-
title: f.title,
|
|
410
|
-
severity: f.severity,
|
|
411
|
-
analysis: f.evidence.analysis ?? ""
|
|
412
|
-
})),
|
|
413
|
-
dropped: res.dropped.map((d) => ({
|
|
414
|
-
title: d.finding.title,
|
|
415
|
-
severity: d.finding.severity,
|
|
416
|
-
candidatePath: d.candidatePath,
|
|
417
|
-
lensId: d.lensId,
|
|
418
|
-
dropReason: d.dropReason,
|
|
419
|
-
detail: d.detail
|
|
420
|
-
})),
|
|
421
|
-
invariant: invariantCtx ? {
|
|
422
|
-
enabled: true,
|
|
423
|
-
subsystem: invariantCtx.subsystem,
|
|
424
|
-
model_path: invariantCtx.modelPath,
|
|
425
|
-
model_loaded: invariantCtx.modelLoaded,
|
|
426
|
-
objects: invariantCtx.model.objects.length,
|
|
427
|
-
violations: invariantCtx.violations.length
|
|
428
|
-
} : { enabled: false },
|
|
429
|
-
graph_slice: graphSliceCtx ? {
|
|
430
|
-
enabled: true,
|
|
431
|
-
subsystem: graphSliceCtx.subsystem,
|
|
432
|
-
cpg_path: graphSliceCtx.cpgPath,
|
|
433
|
-
target_functions: graphSliceCtx.targetFunctions,
|
|
434
|
-
resolved_targets: graphSliceCtx.resolvedTargets,
|
|
435
|
-
ops_edges: graphSliceCtx.opsEdges,
|
|
436
|
-
functions: graphSliceCtx.stats.functions,
|
|
437
|
-
files: graphSliceCtx.stats.files.length,
|
|
438
|
-
call_edges: graphSliceCtx.stats.callEdges,
|
|
439
|
-
slice_chars: graphSliceCtx.stats.chars
|
|
440
|
-
} : { enabled: false },
|
|
441
|
-
ingested: sinkCfg ? ingested : null,
|
|
442
|
-
gated,
|
|
443
|
-
methodology: opts.methodology === true,
|
|
444
|
-
warnings: [...invariantWarnings, ...graphSliceWarnings, ...noveltyWarnings, ...plan.warnings, ...res.warnings].slice(0, 10),
|
|
445
|
-
note: opts.novelty ? "LEADS, not confirmed 0-days. Novelty-duplicate leads were dropped when lore mirrors matched; still verify the real sink before disclosure." : "LEADS, not confirmed 0-days. Verify the real sink + upstream-fix (novelty) before disclosure."
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
} finally {
|
|
449
|
-
if (savedCloudSink !== void 0) process.env["0SEC_CLOUD_SINK"] = savedCloudSink;
|
|
450
|
-
prepared.cleanup();
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
async function huntAction(opts) {
|
|
454
|
-
if (!opts.source) throw new Error("missing required flag: --source <kernel/src tree>");
|
|
455
|
-
if (!opts.seed) throw new Error("missing required flag: --seed <fix diff/patch to hunt variants of>");
|
|
456
|
-
const outcome = await runHunt({
|
|
457
|
-
sourceRoot: opts.source,
|
|
458
|
-
seedPath: opts.seed,
|
|
459
|
-
...opts.ref ? { ref: opts.ref } : {},
|
|
460
|
-
concurrency: parsePositive("--concurrency", opts.concurrency, 4),
|
|
461
|
-
skipCandidates: parseNonNegative("--skip-candidates", opts.skipCandidates, 0),
|
|
462
|
-
...opts.models ? { models: opts.models.split(",").map((s) => s.trim()).filter(Boolean) } : {},
|
|
463
|
-
...opts.reachableOnly ? { reachableOnly: true } : {},
|
|
464
|
-
...opts.reachablePrefer ? { reachablePrefer: true } : {},
|
|
465
|
-
verify: opts.verify,
|
|
466
|
-
...opts.novelty || opts.noveltyRequired || opts.methodology ? {
|
|
467
|
-
novelty: {
|
|
468
|
-
...opts.noveltyRoot ? { rootDir: opts.noveltyRoot } : {},
|
|
469
|
-
...opts.noveltyLists ? { lists: opts.noveltyLists.split(",").map((s) => s.trim()).filter(Boolean) } : {},
|
|
470
|
-
recentEpochs: parsePositive("--novelty-recent-epochs", opts.noveltyRecentEpochs, 1),
|
|
471
|
-
sync: opts.noveltySync === true || opts.methodology === true,
|
|
472
|
-
...opts.noveltyModel ? { model: opts.noveltyModel } : {},
|
|
473
|
-
required: opts.noveltyRequired === true || opts.methodology === true
|
|
474
|
-
}
|
|
475
|
-
} : {},
|
|
476
|
-
methodology: opts.methodology === true,
|
|
477
|
-
invariant: opts.invariant === true,
|
|
478
|
-
graphSlice: opts.graphSlice === true,
|
|
479
|
-
...opts.cpg ? { cpgPath: opts.cpg } : {},
|
|
480
|
-
...opts.opsHarvest !== void 0 ? {
|
|
481
|
-
opsHarvestSourceFiles: opts.opsHarvest.split(",").map((path) => path.trim()).filter(Boolean)
|
|
482
|
-
} : {},
|
|
483
|
-
...opts.graphSliceHops ? { graphSliceHops: parsePositive("--graph-slice-hops", opts.graphSliceHops, 3) } : {},
|
|
484
|
-
exploitability: opts.exploitability === true,
|
|
485
|
-
...opts.proveMinCeiling ? { proveMinCeiling: parseCeiling(opts.proveMinCeiling) } : {},
|
|
486
|
-
...opts.runtime ? { runtime: opts.runtime } : {},
|
|
487
|
-
timeoutMs: parsePositive("--timeout", opts.timeout, 6e5),
|
|
488
|
-
log: (m) => process.stderr.write(m + "\n")
|
|
489
|
-
});
|
|
490
|
-
const json = JSON.stringify(outcome.result, null, 2);
|
|
491
|
-
if (opts.output) writeFileSync(resolve(opts.output), json + "\n", "utf8");
|
|
492
|
-
else process.stdout.write(json + "\n");
|
|
493
|
-
process.exitCode = outcome.exitCode;
|
|
494
|
-
}
|
|
495
|
-
function registerHuntCommand(program) {
|
|
496
|
-
program.command("hunt").description(
|
|
497
|
-
"Hunt a bug CLASS across a source tree, seeded by a proven fix: generate variant candidate sites from the fix, fan finders out over them, and gate each finding through an adversarial skeptic. Emits LEADS to verify (not confirmed 0-days). Exit 0=lead(s), 1=none, 2=no candidates, 3=error."
|
|
498
|
-
).requiredOption("--source <path>", "Source tree to hunt in (e.g. a linux checkout)").requiredOption("--seed <path>", "Fix diff / .patch whose bug class to hunt variants of").option("--ref <name>", "Provenance label for the seed (e.g. the CVE / commit)").option("--concurrency <N>", "Max finders in flight (default 4)").option("--max-candidates <N>", "Cap candidate sites hunted (default 40)").option("--skip-candidates <N>", "Skip the first N ranked candidate sites before hunting (default 0)").option("--models <a,b>", "Comma-separated finder models for diversity (default: provider default)").option("--reachable-only", "Restrict candidates to paths built + zero-cap reachable on the kernelCTF COS target (default: HUNT_REACHABLE_ONLY env)").option("--reachable-prefer", "Sort kernelCTF-reachable candidates first, without dropping any (default: HUNT_REACHABLE_PREFER env)").option("--no-verify", "Skip the skeptic gate (emit all raw findings \u2014 triage only, never disclosure)").option("--novelty", "Require lore.kernel.org duplicate suppression; abort before discovery when evidence is unavailable").option("--novelty-root <path>", "Lore mirror root (default: 0SEC_LORE_MIRROR_ROOT or /root/lore-mirror)").option("--novelty-lists <a,b>", "Comma-separated lore lists to search (default: 0SEC_LORE_LISTS or linux-media)").option("--novelty-recent-epochs <N>", "Newest public-inbox epochs to sync per list when --novelty-sync is set (default 1)").option("--novelty-sync", "Clone/fetch lore mirrors before running the novelty gate").option("--novelty-model <model>", "Optional model override for the lore duplicate judge").option("--novelty-required", "Legacy alias; --novelty already aborts when evidence is unavailable").option("--methodology", "Use the kernel-LPE methodology preset: lifecycle/provenance lenses, best-of-4, top-2 skeptic gate, reachable-first").option("--invariant", "Engine A: build (or load) the seed-touched subsystem's stored invariant model and inject its rules + deterministic violation hypotheses into every finder prompt").option("--graph-slice", "Load the seed-touched subsystem's pre-exported Joern CPG and inject a compact interprocedural reachability slice around the fix site into every finder prompt (needs scripts/provision-cpg.sh; fail-open to flat-text)").option("--cpg <path>", "Explicit CPG graphson JSON path for --graph-slice (default: <source>/.0sec/cpg/<subsystem>.json)").option(
|
|
499
|
-
"--ops-harvest <paths>",
|
|
500
|
-
"[--graph-slice] Comma-separated repo-relative C files to harvest static ops-struct initializers from; overrides a precomputed .ops.json"
|
|
501
|
-
).option(
|
|
502
|
-
"--graph-slice-hops <N>",
|
|
503
|
-
"[--graph-slice] Call-graph radius around the seed functions (default 3; use 8 for the exp527 known answer)"
|
|
504
|
-
).option(
|
|
505
|
-
"--exploitability",
|
|
506
|
-
"PROVE stage: after the skeptic+prover gate, run the execution-verified exploitability oracle on each confirmed finding (GREBE diversify + SCAVY differential). BOOTS REAL QEMU VMs \u2014 requires staged kernel-VM artifacts and is ignored under --no-verify. Never rejects a finding; it stamps a proven verdict and gates the weaponize budget."
|
|
507
|
-
).option(
|
|
508
|
-
"--prove-min-ceiling <ceiling>",
|
|
509
|
-
"[--exploitability] Minimum assessed impact ceiling worth a VM slot: dos-only|info-leak|oob-write|uaf-control (default info-leak \u2014 filters out dos-only before QEMU is touched)"
|
|
510
|
-
).option("--output <path>", "Write the hunt result JSON to this path instead of stdout").option("--runtime <mode>", "Engine runtime (default api)").option("--timeout <ms>", "Accepted cloud agent timeout budget in milliseconds", "600000").action(async (opts) => {
|
|
511
|
-
try {
|
|
512
|
-
await huntAction(opts);
|
|
513
|
-
} catch (err) {
|
|
514
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
515
|
-
const json = JSON.stringify({ mode: "hunt", error: reason }, null, 2);
|
|
516
|
-
if (opts.output) {
|
|
517
|
-
try {
|
|
518
|
-
writeFileSync(resolve(opts.output), json + "\n", "utf8");
|
|
519
|
-
} catch {
|
|
520
|
-
process.stderr.write(json + "\n");
|
|
521
|
-
}
|
|
522
|
-
} else process.stdout.write(json + "\n");
|
|
523
|
-
process.exitCode = 3;
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// packages/cli/src/commands/deep-review.ts
|
|
529
|
-
import { readFileSync as readFileSync2, realpathSync, statSync } from "node:fs";
|
|
530
|
-
import { resolve as resolve2, join, sep, relative } from "node:path";
|
|
531
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
532
|
-
var DEEP_REVIEW_FILE_CAP = 5e3;
|
|
533
|
-
var DEFAULT_MAX_CANDIDATES = 8;
|
|
534
|
-
function defaultMaxCandidates() {
|
|
535
|
-
const raw = process.env["0SEC_DEEP_REVIEW_MAX_CANDIDATES"];
|
|
536
|
-
if (!raw) return DEFAULT_MAX_CANDIDATES;
|
|
537
|
-
const n = Number.parseInt(raw, 10);
|
|
538
|
-
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_CANDIDATES;
|
|
539
|
-
}
|
|
540
|
-
function scaleMaxCandidates(fileCount) {
|
|
541
|
-
if (fileCount > 2e3) return 20;
|
|
542
|
-
if (fileCount > 500) return 16;
|
|
543
|
-
if (fileCount > 200) return 12;
|
|
544
|
-
return DEFAULT_MAX_CANDIDATES;
|
|
545
|
-
}
|
|
546
|
-
var DEFAULT_ATTEMPTS = 1;
|
|
547
|
-
function defaultAttempts() {
|
|
548
|
-
const raw = process.env["0SEC_DEEP_REVIEW_ATTEMPTS"];
|
|
549
|
-
if (!raw) return DEFAULT_ATTEMPTS;
|
|
550
|
-
const n = Number.parseInt(raw, 10);
|
|
551
|
-
return Number.isFinite(n) && n > 0 ? n : DEFAULT_ATTEMPTS;
|
|
552
|
-
}
|
|
553
|
-
function defaultModels() {
|
|
554
|
-
const raw = process.env["0SEC_DEEP_REVIEW_MODELS"];
|
|
555
|
-
if (!raw) return void 0;
|
|
556
|
-
const models = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
557
|
-
return models.length > 0 ? models : void 0;
|
|
558
|
-
}
|
|
559
|
-
var DEFAULT_CONCURRENCY = 8;
|
|
560
|
-
var genericFinderLenses = [
|
|
561
|
-
{
|
|
562
|
-
id: "memory-safety",
|
|
563
|
-
challengeHint: "Hunt MEMORY-SAFETY bugs only: out-of-bounds read/write, use-after-free, double-free, uninitialized read, integer overflow feeding an allocation or index, and unchecked length/size arithmetic before a copy. Cite the exact unguarded sink (file:line) and the attacker-controlled path that reaches it."
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
id: "input-validation",
|
|
567
|
-
challengeHint: "Hunt INPUT-VALIDATION and injection bugs only: untrusted input reaching a dangerous sink without validation \u2014 command/SQL/path/template injection, unsafe deserialization, SSRF, unchecked redirects, or a parser that trusts attacker-supplied structure. Trace the taint from the entry point to the sink."
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
id: "auth-logic",
|
|
571
|
-
challengeHint: "Hunt AUTHZ / LOGIC bugs only: a missing or wrong permission/ownership check, a broken state machine, a TOCTOU race, an off-by-one or boundary error in a security-relevant decision, or a check that can be bypassed. Prove the guard is absent or mis-scoped, not merely that the keywords appear."
|
|
572
|
-
},
|
|
573
|
-
{
|
|
574
|
-
id: "secrets-crypto",
|
|
575
|
-
challengeHint: "Hunt SECRETS / CRYPTO misuse only: hardcoded credentials, a weak/predictable RNG used for security, a broken or misused cryptographic primitive (ECB, static IV/nonce, missing MAC verification, non-constant-time compare), or a signature/token check that can be forged or replayed. Cite the exact misuse."
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
id: "cross-component",
|
|
579
|
-
challengeHint: "Hunt CROSS-COMPONENT bugs only: a bug whose exploit path spans two or more subsystems touching DIFFERENT trust boundaries \u2014 e.g. an API layer that passes unvalidated user input to a kernel driver, a storage layer whose consistency assumptions are violated by a separate async path, or a UX component whose state bleeds into an auth decision in a sibling module. Trace the path ACROSS the boundary and prove the attacker can reach BOTH sides. Ignore bugs fully contained within a single component."
|
|
580
|
-
}
|
|
581
|
-
];
|
|
582
|
-
function lensSetDigest(lenses) {
|
|
583
|
-
return createHash("sha256").update(JSON.stringify(lenses.map(({ id, challengeHint }) => ({ id, challengeHint })))).digest("hex");
|
|
584
|
-
}
|
|
585
|
-
function createDefaultFinderLenses() {
|
|
586
|
-
return [...genericFinderLenses, ...loadAppsecFinderLenses()];
|
|
587
|
-
}
|
|
588
|
-
var defaultFinderLenses = createDefaultFinderLenses();
|
|
589
|
-
var defaultVerifyLenses = [
|
|
590
|
-
{
|
|
591
|
-
id: "reachability",
|
|
592
|
-
challengeHint: "REACHABILITY: is the vulnerable code actually reachable by an attacker from a real entry point (exported/public API, request handler, CLI, parser) with no gate it cannot pass? Trace the concrete path from the entry to the sink. If the path is dead code, disabled, or privileged-only, refute it."
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
id: "completeness",
|
|
596
|
-
challengeHint: "COMPLETENESS: is the 'missing' check actually enforced elsewhere on the path \u2014 a validation earlier in the flow, a guard in the only caller, a wrapper that sanitizes first? Read the whole call path and the full file/module. If the guard is present AND correctly scoped, refute it; keep it only if genuinely absent or mis-scoped."
|
|
597
|
-
},
|
|
598
|
-
{
|
|
599
|
-
id: "novelty-known-issue",
|
|
600
|
-
challengeHint: "NOVELTY / KNOWN-GUARD: is the standard guard for this class present and correct (a framework escaper, a safe API, a bounds check, a working auth middleware)? Is this a well-known already-mitigated pattern in this codebase or its dependencies? If a correct standard guard covers it, refute it."
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
id: "scope",
|
|
604
|
-
challengeHint: "SCOPE / IMPACT: does exploiting this actually corrupt memory, leak secrets, escalate privilege, execute code, or move value / break a security invariant? A cosmetic missing check with no real impact is info/low, not a high-severity finding \u2014 refute it as such."
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
id: "deployment-context",
|
|
608
|
-
challengeHint: "DEPLOYMENT CONTEXT: is this finding's vulnerable code path actually deployed to a production runtime, or is it restricted to development, test, or build-only surfaces? \n\nYour job is to examine the finding's evidence AND your own reading of the candidate source to decide: can an attacker reach this code from a production entry point? \n\n- If the code is in a test file, a dev-only config (seeds, .dev.vars, dev-server), or build scaffolding (webpack, CI, vendored deps) with NO trust-boundary crossing from prod, refute it as dev/test/build-only.\n- If you find a trust-boundary bypass \u2014 a prod-routed entry point that reaches this code despite it being in a dev/test/build file \u2014 DO NOT refute; the finding is valid per the HackerOne rule.\n- If you are UNCERTAIN (the evidence is ambiguous or you cannot assess the deployment topology), ABSTAIN by returning an error. Never falsely refute a finding with weak evidence.\nPRIORITY: the mechanical path heuristic (file path classification) is the DETERMINISTIC FLOOR and overrides this lens on conflict. If the heuristic already tagged the finding as dev_only/test_only/build_only, the severity cap has already been applied \u2014 this lens exists to catch cases the heuristic misses (ambiguous paths, model-extracted evidence, cross-boundary reachability)."
|
|
609
|
-
}
|
|
610
|
-
];
|
|
611
|
-
function selectProfileLenses(profile, sets, defaultFinderSnapshot = defaultFinderLenses) {
|
|
612
|
-
switch ((profile ?? "").trim().toLowerCase()) {
|
|
613
|
-
case "evm-onchain":
|
|
614
|
-
return { finderLenses: sets.evmFinderLenses, verifyLenses: sets.evmVerifyLenses, matchedProfile: "evm-onchain" };
|
|
615
|
-
case "solana-onchain":
|
|
616
|
-
return { finderLenses: sets.solanaFinderLenses, verifyLenses: sets.solanaVerifyLenses, matchedProfile: "solana-onchain" };
|
|
617
|
-
case "cardano-onchain":
|
|
618
|
-
return { finderLenses: sets.cardanoFinderLenses, verifyLenses: sets.cardanoVerifyLenses, matchedProfile: "cardano-onchain" };
|
|
619
|
-
case "cairo-onchain":
|
|
620
|
-
return { finderLenses: sets.cairoFinderLenses, verifyLenses: sets.cairoVerifyLenses, matchedProfile: "cairo-onchain" };
|
|
621
|
-
case "move-onchain":
|
|
622
|
-
return { finderLenses: sets.moveFinderLenses, verifyLenses: sets.moveVerifyLenses, matchedProfile: "move-onchain" };
|
|
623
|
-
default:
|
|
624
|
-
return { finderLenses: defaultFinderSnapshot, verifyLenses: defaultVerifyLenses, matchedProfile: "default" };
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
var NATIVE_SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
628
|
-
"c",
|
|
629
|
-
"h",
|
|
630
|
-
"cc",
|
|
631
|
-
"cpp",
|
|
632
|
-
"cxx",
|
|
633
|
-
"hpp",
|
|
634
|
-
"hh",
|
|
635
|
-
"hxx",
|
|
636
|
-
"rs"
|
|
637
|
-
]);
|
|
638
|
-
var MANAGED_WEB_EXTS = /* @__PURE__ */ new Set([
|
|
639
|
-
"cs",
|
|
640
|
-
"csproj",
|
|
641
|
-
"sln",
|
|
642
|
-
"vbproj",
|
|
643
|
-
"fsproj",
|
|
644
|
-
// .NET / C#
|
|
645
|
-
"ts",
|
|
646
|
-
"tsx",
|
|
647
|
-
"js",
|
|
648
|
-
"jsx",
|
|
649
|
-
"mjs",
|
|
650
|
-
"cjs",
|
|
651
|
-
"vue",
|
|
652
|
-
"svelte",
|
|
653
|
-
// JS/TS front+back
|
|
654
|
-
"java",
|
|
655
|
-
"kt",
|
|
656
|
-
"kts",
|
|
657
|
-
"scala",
|
|
658
|
-
// JVM
|
|
659
|
-
"py",
|
|
660
|
-
"rb",
|
|
661
|
-
"php",
|
|
662
|
-
"go",
|
|
663
|
-
// scripting / Go
|
|
664
|
-
"html",
|
|
665
|
-
"htm",
|
|
666
|
-
"cshtml",
|
|
667
|
-
"razor",
|
|
668
|
-
"aspx",
|
|
669
|
-
"jsp"
|
|
670
|
-
// web templates
|
|
671
|
-
]);
|
|
672
|
-
function fileExt(path) {
|
|
673
|
-
const base = path.split(/[/\\]+/).pop() ?? path;
|
|
674
|
-
const dot = base.lastIndexOf(".");
|
|
675
|
-
return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
676
|
-
}
|
|
677
|
-
function detectStack(files) {
|
|
678
|
-
let native = 0;
|
|
679
|
-
let managed = 0;
|
|
680
|
-
for (const f of files) {
|
|
681
|
-
const ext = fileExt(f);
|
|
682
|
-
if (NATIVE_SOURCE_EXTS.has(ext)) native++;
|
|
683
|
-
else if (MANAGED_WEB_EXTS.has(ext)) managed++;
|
|
684
|
-
}
|
|
685
|
-
if (native === 0 && managed === 0) return "unknown";
|
|
686
|
-
if (native === 0) return "managed-web";
|
|
687
|
-
if (managed === 0) return "native";
|
|
688
|
-
return "mixed";
|
|
689
|
-
}
|
|
690
|
-
var MEMORY_SAFETY_LENS_ID = "memory-safety";
|
|
691
|
-
function selectDefaultFinderLensesForStack(files, defaultFinder) {
|
|
692
|
-
if (detectStack(files) === "managed-web") {
|
|
693
|
-
return defaultFinder.filter((l) => l.id !== MEMORY_SAFETY_LENS_ID);
|
|
694
|
-
}
|
|
695
|
-
return defaultFinder;
|
|
696
|
-
}
|
|
697
|
-
var EVM_EXCLUDE_DIR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
698
|
-
"test",
|
|
699
|
-
"tests",
|
|
700
|
-
"mock",
|
|
701
|
-
"mocks",
|
|
702
|
-
"script",
|
|
703
|
-
"scripts",
|
|
704
|
-
// Foundry deploy scripts
|
|
705
|
-
"lib",
|
|
706
|
-
// Foundry vendored deps (forge-std / openzeppelin / solmate …)
|
|
707
|
-
"node_modules"
|
|
708
|
-
// Hardhat/JS vendored deps (also skipped by the core walker)
|
|
709
|
-
]);
|
|
710
|
-
function isNonProtocolEvmPath(absPath, scopeRoot) {
|
|
711
|
-
const rel = relative(scopeRoot, absPath);
|
|
712
|
-
if (rel === "" || rel.startsWith("..")) return false;
|
|
713
|
-
const segments = rel.split(/[/\\]+/).filter(Boolean);
|
|
714
|
-
if (segments.length === 0) return false;
|
|
715
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
716
|
-
if (EVM_EXCLUDE_DIR_SEGMENTS.has(segments[i].toLowerCase())) return true;
|
|
717
|
-
}
|
|
718
|
-
const fileName = segments[segments.length - 1].toLowerCase();
|
|
719
|
-
if (fileName.endsWith(".t.sol")) return true;
|
|
720
|
-
if (fileName.includes(".test.") || fileName.includes(".spec.")) return true;
|
|
721
|
-
return false;
|
|
722
|
-
}
|
|
723
|
-
function topLevelModule(absPath, scopeRoot) {
|
|
724
|
-
const rel = relative(scopeRoot, absPath);
|
|
725
|
-
if (rel === "" || rel.startsWith("..")) return ".";
|
|
726
|
-
const segments = rel.split(/[/\\]+/).filter(Boolean);
|
|
727
|
-
return segments.length > 1 ? segments[0] : ".";
|
|
728
|
-
}
|
|
729
|
-
function selectCandidatesModuleSpread(entries, scopeRoot, maxCandidates) {
|
|
730
|
-
const cmp = (a, b) => b.size - a.size || a.p.localeCompare(b.p);
|
|
731
|
-
const byModule = /* @__PURE__ */ new Map();
|
|
732
|
-
for (const e of entries) {
|
|
733
|
-
const key = topLevelModule(e.p, scopeRoot);
|
|
734
|
-
let group = byModule.get(key);
|
|
735
|
-
if (!group) byModule.set(key, group = []);
|
|
736
|
-
group.push(e);
|
|
737
|
-
}
|
|
738
|
-
const groups = [...byModule.values()];
|
|
739
|
-
for (const g of groups) g.sort(cmp);
|
|
740
|
-
groups.sort((a, b) => cmp(a[0], b[0]));
|
|
741
|
-
const cap = Math.max(1, maxCandidates);
|
|
742
|
-
const out = [];
|
|
743
|
-
for (let round = 0; out.length < cap; round++) {
|
|
744
|
-
let advanced = false;
|
|
745
|
-
for (const g of groups) {
|
|
746
|
-
if (round >= g.length) continue;
|
|
747
|
-
out.push(g[round].p);
|
|
748
|
-
advanced = true;
|
|
749
|
-
if (out.length >= cap) break;
|
|
750
|
-
}
|
|
751
|
-
if (!advanced) break;
|
|
752
|
-
}
|
|
753
|
-
return out;
|
|
754
|
-
}
|
|
755
|
-
function enumerateDeepReviewCandidates(scopeRoot, helpers, opts) {
|
|
756
|
-
const fileCap = opts.fileCap ?? DEEP_REVIEW_FILE_CAP;
|
|
757
|
-
const exclude = opts.exclude ?? (() => false);
|
|
758
|
-
const fileSize = helpers.fileSize ?? ((p) => {
|
|
759
|
-
try {
|
|
760
|
-
return statSync(p).size;
|
|
761
|
-
} catch {
|
|
762
|
-
return 0;
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
const totalFiles = helpers.countScopeFilesUpTo(scopeRoot, fileCap);
|
|
766
|
-
const overCap = totalFiles > fileCap;
|
|
767
|
-
const files = helpers.collectScopeFiles(scopeRoot, { maxFiles: fileCap });
|
|
768
|
-
const entries = files.filter((p) => !exclude(p)).map((p) => ({ p, size: fileSize(p) }));
|
|
769
|
-
const candidates = selectCandidatesModuleSpread(entries, scopeRoot, opts.maxCandidates);
|
|
770
|
-
return { candidates, totalFiles, overCap };
|
|
771
|
-
}
|
|
772
|
-
async function runDeepReview(opts) {
|
|
773
|
-
const defaultFinderSnapshot = createDefaultFinderLenses();
|
|
774
|
-
const evolvedFinder = opts.evolutionConfig ? await createEvolvedFinder(opts.evolutionConfig, `deep-review-${randomUUID()}`) : void 0;
|
|
775
|
-
const {
|
|
776
|
-
runHuntScan,
|
|
777
|
-
makeMultiLensVerifier,
|
|
778
|
-
prepare,
|
|
779
|
-
collectScopeFiles,
|
|
780
|
-
countScopeFilesUpTo,
|
|
781
|
-
getCloudSinkConfig,
|
|
782
|
-
postFinding,
|
|
783
|
-
evmFinderLenses,
|
|
784
|
-
evmVerifyLenses,
|
|
785
|
-
solanaFinderLenses,
|
|
786
|
-
solanaVerifyLenses,
|
|
787
|
-
cardanoFinderLenses,
|
|
788
|
-
cardanoVerifyLenses,
|
|
789
|
-
cairoFinderLenses,
|
|
790
|
-
cairoVerifyLenses,
|
|
791
|
-
moveFinderLenses,
|
|
792
|
-
moveVerifyLenses
|
|
793
|
-
} = await import("./dist-FDKALV4R.js");
|
|
794
|
-
const costLedger = new ScanCostLedger();
|
|
795
|
-
const scanStartedAt = Date.now();
|
|
796
|
-
const log = opts.log ?? (() => {
|
|
797
|
-
});
|
|
798
|
-
const runtime = opts.runtime ?? "api";
|
|
799
|
-
const explicitMaxCandidates = opts.maxCandidates;
|
|
800
|
-
const hasEnvMaxOverride = !!process.env["0SEC_DEEP_REVIEW_MAX_CANDIDATES"];
|
|
801
|
-
const baseMaxCandidates = explicitMaxCandidates ?? defaultMaxCandidates();
|
|
802
|
-
if (evolvedFinder && opts.models?.length) throw new Error("--models selects native finders and cannot be combined with --evolution-config");
|
|
803
|
-
const models = evolvedFinder ? ["evolved-source"] : opts.models ?? defaultModels();
|
|
804
|
-
const attemptsPerCandidate = opts.attemptsPerCandidate ?? defaultAttempts();
|
|
805
|
-
const prepared = await prepare(opts.target, "source-code", { ...opts.timeoutMs ? { timeout: opts.timeoutMs } : {} }, (e) => {
|
|
806
|
-
if (e.message) log(`[deep-review:source] ${e.message}`);
|
|
807
|
-
});
|
|
808
|
-
const sourceRoot = resolve2(prepared.resolvedTarget);
|
|
809
|
-
let scopeRoot = sourceRoot;
|
|
810
|
-
if (opts.subsystem && opts.subsystem.trim() !== "") {
|
|
811
|
-
const scoped = resolve2(join(sourceRoot, opts.subsystem));
|
|
812
|
-
if (scoped !== sourceRoot && !scoped.startsWith(sourceRoot + sep)) {
|
|
813
|
-
prepared.cleanup();
|
|
814
|
-
throw new Error(`--subsystem '${opts.subsystem}' escapes the source tree`);
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
const realRoot = realpathSync(sourceRoot);
|
|
818
|
-
const realScoped = realpathSync(scoped);
|
|
819
|
-
if (realScoped !== realRoot && !realScoped.startsWith(realRoot + sep)) {
|
|
820
|
-
throw new Error(`--subsystem '${opts.subsystem}' escapes the source tree`);
|
|
821
|
-
}
|
|
822
|
-
scopeRoot = scoped;
|
|
823
|
-
} catch (error) {
|
|
824
|
-
prepared.cleanup();
|
|
825
|
-
throw error;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const sinkCfg = getCloudSinkConfig();
|
|
829
|
-
const savedCloudSink = process.env["0SEC_CLOUD_SINK"];
|
|
830
|
-
if (sinkCfg) delete process.env["0SEC_CLOUD_SINK"];
|
|
831
|
-
try {
|
|
832
|
-
const isEvmProfile = (opts.profile ?? "").trim().toLowerCase() === "evm-onchain";
|
|
833
|
-
const totalFiles = countScopeFilesUpTo(scopeRoot, DEEP_REVIEW_FILE_CAP);
|
|
834
|
-
const maxCandidates = explicitMaxCandidates ?? (hasEnvMaxOverride ? baseMaxCandidates : scaleMaxCandidates(totalFiles));
|
|
835
|
-
const overCap = totalFiles > DEEP_REVIEW_FILE_CAP;
|
|
836
|
-
if (explicitMaxCandidates === void 0 && maxCandidates !== baseMaxCandidates) {
|
|
837
|
-
log(`[deep-review] auto-scaled candidate cap from ${baseMaxCandidates} to ${maxCandidates} (${totalFiles} scope files)`);
|
|
838
|
-
}
|
|
839
|
-
let lanes = null;
|
|
840
|
-
let allocateAcrossLanes;
|
|
841
|
-
if (opts.useThreatModel) {
|
|
842
|
-
const threatModule = await import("./dist-FDKALV4R.js");
|
|
843
|
-
allocateAcrossLanes = threatModule.allocateCandidatesAcrossLanes;
|
|
844
|
-
const rtType = runtime !== "auto" ? runtime : "api";
|
|
845
|
-
const plannerRuntime = threatModule.createRuntime({
|
|
846
|
-
type: rtType,
|
|
847
|
-
timeout: opts.timeoutMs ?? 6e5,
|
|
848
|
-
model: models?.[0]
|
|
849
|
-
});
|
|
850
|
-
lanes = await threatModule.runThreatModelPlanner(sourceRoot, plannerRuntime, log);
|
|
851
|
-
if (!lanes || lanes.length === 0) {
|
|
852
|
-
log("[threat-model] no lanes produced \u2014 continuing with default selection");
|
|
853
|
-
} else {
|
|
854
|
-
log(`[threat-model] planner returned ${lanes.length} lane(s) \u2014 will allocate candidates across boundaries`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (overCap) {
|
|
858
|
-
return {
|
|
859
|
-
exitCode: 2,
|
|
860
|
-
result: {
|
|
861
|
-
mode: "deep_review",
|
|
862
|
-
source: sourceRoot,
|
|
863
|
-
subsystem: opts.subsystem ?? null,
|
|
864
|
-
scope_files: totalFiles,
|
|
865
|
-
note: `scope exceeds the ${DEEP_REVIEW_FILE_CAP}-file review cap \u2014 narrow it with --subsystem <path> to a specific directory.`
|
|
866
|
-
}
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
const { candidates: initialCandidates } = enumerateDeepReviewCandidates(
|
|
870
|
-
scopeRoot,
|
|
871
|
-
{ collectScopeFiles, countScopeFilesUpTo },
|
|
872
|
-
{
|
|
873
|
-
maxCandidates,
|
|
874
|
-
fileCap: DEEP_REVIEW_FILE_CAP,
|
|
875
|
-
...isEvmProfile ? { exclude: (p) => isNonProtocolEvmPath(p, scopeRoot) } : {}
|
|
876
|
-
}
|
|
877
|
-
);
|
|
878
|
-
const candidatePaths = initialCandidates.slice();
|
|
879
|
-
if (lanes && lanes.length > 0 && allocateAcrossLanes) {
|
|
880
|
-
const allFiles = collectScopeFiles(scopeRoot, { maxFiles: DEEP_REVIEW_FILE_CAP });
|
|
881
|
-
const exclude = isEvmProfile ? (p) => isNonProtocolEvmPath(p, scopeRoot) : () => false;
|
|
882
|
-
const entries = allFiles.filter((p) => !exclude(p)).map((p) => {
|
|
883
|
-
try {
|
|
884
|
-
return { p, size: statSync(p).size };
|
|
885
|
-
} catch {
|
|
886
|
-
return { p, size: 0 };
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
const laneCandidates = allocateAcrossLanes(entries, lanes, scopeRoot, maxCandidates, log);
|
|
890
|
-
if (laneCandidates.length > 0) {
|
|
891
|
-
const prevModules = new Set(initialCandidates.map((p) => relative(scopeRoot, p).split(/[/\\]+/)[0]));
|
|
892
|
-
const laneModules = new Set(laneCandidates.map((p) => relative(scopeRoot, p).split(/[/\\]+/)[0]));
|
|
893
|
-
log(`[deep-review] threat-model re-allocated ${initialCandidates.length} \u2192 ${laneCandidates.length} candidate(s); modules ${prevModules.size} \u2192 ${laneModules.size}`);
|
|
894
|
-
candidatePaths.length = 0;
|
|
895
|
-
candidatePaths.push(...laneCandidates);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
if (candidatePaths.length === 0) {
|
|
899
|
-
return {
|
|
900
|
-
exitCode: 2,
|
|
901
|
-
result: {
|
|
902
|
-
mode: "deep_review",
|
|
903
|
-
source: sourceRoot,
|
|
904
|
-
subsystem: opts.subsystem ?? null,
|
|
905
|
-
scope_files: totalFiles,
|
|
906
|
-
candidates: 0,
|
|
907
|
-
note: "no reviewable source files found under the scope (empty tree or all filtered)."
|
|
908
|
-
}
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
const { finderLenses: selectedFinderLenses, verifyLenses, matchedProfile } = selectProfileLenses(opts.profile, {
|
|
912
|
-
evmFinderLenses,
|
|
913
|
-
evmVerifyLenses,
|
|
914
|
-
solanaFinderLenses,
|
|
915
|
-
solanaVerifyLenses,
|
|
916
|
-
cardanoFinderLenses,
|
|
917
|
-
cardanoVerifyLenses,
|
|
918
|
-
cairoFinderLenses,
|
|
919
|
-
cairoVerifyLenses,
|
|
920
|
-
moveFinderLenses,
|
|
921
|
-
moveVerifyLenses
|
|
922
|
-
}, defaultFinderSnapshot);
|
|
923
|
-
const finderLenses = matchedProfile === "default" ? selectDefaultFinderLensesForStack(candidatePaths, selectedFinderLenses) : selectedFinderLenses;
|
|
924
|
-
const lensDigest = lensSetDigest(finderLenses);
|
|
925
|
-
const scanId = randomUUID();
|
|
926
|
-
let sourceRevisionDigest = null;
|
|
927
|
-
let sourceFingerprintWarning;
|
|
928
|
-
try {
|
|
929
|
-
const files = [...candidatePaths].sort().map((path) => ({
|
|
930
|
-
path: relative(sourceRoot, path),
|
|
931
|
-
digest: createHash("sha256").update(readFileSync2(path)).digest("hex")
|
|
932
|
-
}));
|
|
933
|
-
sourceRevisionDigest = `sha256:${createHash("sha256").update(JSON.stringify(files)).digest("hex")}`;
|
|
934
|
-
} catch (error) {
|
|
935
|
-
sourceFingerprintWarning = `learning feedback skipped: reviewed inputs could not be fingerprinted (${error instanceof Error ? error.message : String(error)})`;
|
|
936
|
-
log(`[deep-review] ${sourceFingerprintWarning}`);
|
|
937
|
-
}
|
|
938
|
-
log(
|
|
939
|
-
`[deep-review] ${candidatePaths.length} candidate file(s) (of ${totalFiles}${overCap ? "+" : ""}) \xD7 ${finderLenses.length} finder-lens(es); verify quorum over ${verifyLenses.length} lens(es); profile=${matchedProfile}; lens-snapshot=${lensDigest.slice(0, 12)}`
|
|
940
|
-
);
|
|
941
|
-
const verify = makeMultiLensVerifier(verifyLenses, {
|
|
942
|
-
sourceRoot,
|
|
943
|
-
runtime,
|
|
944
|
-
...!evolvedFinder && models?.[0] ? { model: models[0] } : {},
|
|
945
|
-
// Every verify lens is a refute pass on `models[0]` — the same model the
|
|
946
|
-
// finders ran on. Passing the full finder set lets the cross-family
|
|
947
|
-
// selector move the refutation off ALL finder families when a second
|
|
948
|
-
// provider is configured (#661), and `log` records what it decided —
|
|
949
|
-
// including "it could not", which is the case worth knowing about.
|
|
950
|
-
...opts.costCeilingUsd !== void 0 ? { costCeilingUsd: opts.costCeilingUsd } : {},
|
|
951
|
-
costLedger,
|
|
952
|
-
...!evolvedFinder && models && models.length > 0 ? { finderModels: models } : {},
|
|
953
|
-
...opts.quorum ? { quorum: opts.quorum } : {},
|
|
954
|
-
log
|
|
955
|
-
});
|
|
956
|
-
const postedIds = /* @__PURE__ */ new Set();
|
|
957
|
-
let ingested = 0;
|
|
958
|
-
const provenance = `deep_review (${matchedProfile} lenses)`;
|
|
959
|
-
const postLead = async (lead) => {
|
|
960
|
-
if (!sinkCfg || postedIds.has(lead.id)) return;
|
|
961
|
-
postedIds.add(lead.id);
|
|
962
|
-
await postFinding(leadToCandidateFinding(lead, `deep-review:${matchedProfile}`, provenance), sinkCfg);
|
|
963
|
-
ingested++;
|
|
964
|
-
};
|
|
965
|
-
const res = await runHuntScan({
|
|
966
|
-
sourceRoot,
|
|
967
|
-
candidates: candidatePaths.map((path) => ({ path })),
|
|
968
|
-
lenses: finderLenses,
|
|
969
|
-
runtime,
|
|
970
|
-
...evolvedFinder ? { finder: (input) => evolvedFinder.find(sourceRoot, input) } : {},
|
|
971
|
-
concurrency: opts.concurrency ?? DEFAULT_CONCURRENCY,
|
|
972
|
-
...opts.costCeilingUsd !== void 0 ? { costCeilingUsd: opts.costCeilingUsd } : {},
|
|
973
|
-
costLedger,
|
|
974
|
-
attemptsPerCandidate,
|
|
975
|
-
...models ? { models } : {},
|
|
976
|
-
verify,
|
|
977
|
-
...sinkCfg ? { onConfirmed: postLead } : {},
|
|
978
|
-
log
|
|
979
|
-
});
|
|
980
|
-
const leads = res.confirmed;
|
|
981
|
-
if (sourceFingerprintWarning) res.warnings.push(sourceFingerprintWarning);
|
|
982
|
-
if (sourceRevisionDigest) {
|
|
983
|
-
try {
|
|
984
|
-
for (const gap of res.incompleteCoverage ?? []) {
|
|
985
|
-
captureObservation({
|
|
986
|
-
classHint: gap.lensId || "generic-finder-timeout",
|
|
987
|
-
sinkPattern: "",
|
|
988
|
-
exampleFileLine: gap.file,
|
|
989
|
-
whyMissed: `finder ${gap.reason}; budget ${gap.budgetMs}ms`,
|
|
990
|
-
source: "incomplete-coverage",
|
|
991
|
-
scanId,
|
|
992
|
-
sourceRevisionDigest,
|
|
993
|
-
consent: false,
|
|
994
|
-
detectorLensId: gap.lensId || void 0,
|
|
995
|
-
detectorLensVersionDigest: gap.lensVersionDigest
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
for (const dropped of res.dropped ?? []) {
|
|
999
|
-
if (dropped.dropReason === "verify_refuted" || dropped.dropReason === "novelty_duplicate" || dropped.dropReason === "judge_truncation" || dropped.dropReason === "late_resolution") continue;
|
|
1000
|
-
captureObservation({
|
|
1001
|
-
classHint: dropped.lensId || "unknown-lens",
|
|
1002
|
-
sinkPattern: "",
|
|
1003
|
-
exampleFileLine: dropped.candidatePath,
|
|
1004
|
-
whyMissed: `dropped: ${dropped.dropReason}`,
|
|
1005
|
-
source: "incomplete-coverage",
|
|
1006
|
-
scanId,
|
|
1007
|
-
sourceRevisionDigest,
|
|
1008
|
-
consent: false,
|
|
1009
|
-
detectorLensId: dropped.lensId || void 0,
|
|
1010
|
-
detectorLensVersionDigest: dropped.lensVersionDigest
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
const warning = `learning feedback was not persisted: ${error instanceof Error ? error.message : String(error)}`;
|
|
1015
|
-
res.warnings.push(warning);
|
|
1016
|
-
log(`[deep-review] ${warning}`);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
if (sinkCfg) {
|
|
1020
|
-
for (const lead of leads) await postLead(lead);
|
|
1021
|
-
log(`[deep-review] posted ${ingested} lead(s) to the cloud-sink as candidate findings (incremental)`);
|
|
1022
|
-
}
|
|
1023
|
-
const sweptNothing = res.scanned > 0 && typeof res.finderCompleted === "number" && res.finderCompleted === 0;
|
|
1024
|
-
const cost = costLedger.costBreakdown();
|
|
1025
|
-
eventBus.emit("scan_completed", {
|
|
1026
|
-
exit_reason: sweptNothing ? "failed" : res.costCeilingExceeded ? "cost_exceeded" : "completed",
|
|
1027
|
-
findings: res.findings.length,
|
|
1028
|
-
findings_count: res.findings.length,
|
|
1029
|
-
duration_ms: Date.now() - scanStartedAt,
|
|
1030
|
-
summary: res.costCeilingExceeded ? `Deep review stopped at the $${opts.costCeilingUsd?.toFixed(2) ?? "configured"} cost ceiling.` : `Deep review completed: ${res.scanned} finder runs, ${res.findings.length} findings, ${leads.length} leads.`,
|
|
1031
|
-
...cost ? { cost_usd: cost.costUsd, cost_breakdown: cost.breakdown } : {}
|
|
1032
|
-
});
|
|
1033
|
-
if (sweptNothing) {
|
|
1034
|
-
return {
|
|
1035
|
-
exitCode: 3,
|
|
1036
|
-
result: {
|
|
1037
|
-
mode: "deep_review",
|
|
1038
|
-
profile: matchedProfile,
|
|
1039
|
-
scan_id: scanId,
|
|
1040
|
-
source_revision_digest: sourceRevisionDigest,
|
|
1041
|
-
lens_snapshot_digest: lensDigest,
|
|
1042
|
-
...evolvedFinder ? { evolution_version_id: evolvedFinder.versionId, evolution_compute_cost_usd: evolvedFinder.computeCostUsd } : {},
|
|
1043
|
-
source: sourceRoot,
|
|
1044
|
-
subsystem: opts.subsystem ?? null,
|
|
1045
|
-
scope_files: totalFiles,
|
|
1046
|
-
candidates: candidatePaths.length,
|
|
1047
|
-
scanned: res.scanned,
|
|
1048
|
-
finder_completed: res.finderCompleted,
|
|
1049
|
-
finder_timed_out: res.finderTimedOut ?? null,
|
|
1050
|
-
finder_errored: res.finderErrored ?? null,
|
|
1051
|
-
error: `every finder run failed (0 of ${res.scanned} completed) \u2014 no coverage was produced; treating as a backend/LLM failure, not a clean 0-finding result.`,
|
|
1052
|
-
warnings: res.warnings.slice(0, 10)
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
const completedAt = /* @__PURE__ */ new Date();
|
|
1057
|
-
const candidateFindings = leads.map((lead) => ({
|
|
1058
|
-
...lead,
|
|
1059
|
-
status: "discovered"
|
|
1060
|
-
}));
|
|
1061
|
-
const report = {
|
|
1062
|
-
target: opts.target,
|
|
1063
|
-
scanDepth: "deep",
|
|
1064
|
-
startedAt: new Date(scanStartedAt).toISOString(),
|
|
1065
|
-
completedAt: completedAt.toISOString(),
|
|
1066
|
-
durationMs: completedAt.getTime() - scanStartedAt,
|
|
1067
|
-
summary: {
|
|
1068
|
-
totalAttacks: res.scanned,
|
|
1069
|
-
totalFindings: candidateFindings.length,
|
|
1070
|
-
critical: candidateFindings.filter((finding) => finding.severity === "critical").length,
|
|
1071
|
-
high: candidateFindings.filter((finding) => finding.severity === "high").length,
|
|
1072
|
-
medium: candidateFindings.filter((finding) => finding.severity === "medium").length,
|
|
1073
|
-
low: candidateFindings.filter((finding) => finding.severity === "low").length,
|
|
1074
|
-
info: candidateFindings.filter((finding) => finding.severity === "info").length
|
|
1075
|
-
},
|
|
1076
|
-
findings: candidateFindings,
|
|
1077
|
-
warnings: res.warnings.slice(0, 10).map((message) => ({
|
|
1078
|
-
stage: "attack",
|
|
1079
|
-
message
|
|
1080
|
-
})),
|
|
1081
|
-
...res.costCeilingExceeded ? { costCeilingExceeded: true, exitReason: "cost_ceiling_exceeded" } : {}
|
|
1082
|
-
};
|
|
1083
|
-
return {
|
|
1084
|
-
exitCode: 0,
|
|
1085
|
-
report,
|
|
1086
|
-
result: {
|
|
1087
|
-
mode: "deep_review",
|
|
1088
|
-
profile: matchedProfile,
|
|
1089
|
-
scan_id: scanId,
|
|
1090
|
-
source_revision_digest: sourceRevisionDigest,
|
|
1091
|
-
source: sourceRoot,
|
|
1092
|
-
subsystem: opts.subsystem ?? null,
|
|
1093
|
-
scope_files: totalFiles,
|
|
1094
|
-
lens_snapshot_digest: lensDigest,
|
|
1095
|
-
...evolvedFinder ? { evolution_version_id: evolvedFinder.versionId, evolution_compute_cost_usd: evolvedFinder.computeCostUsd } : {},
|
|
1096
|
-
candidates: candidatePaths.length,
|
|
1097
|
-
finder_lenses: finderLenses.map((l) => l.id),
|
|
1098
|
-
verify_lenses: verifyLenses.map((l) => l.id),
|
|
1099
|
-
scanned: res.scanned,
|
|
1100
|
-
findings: res.findings.length,
|
|
1101
|
-
confirmed: leads.length,
|
|
1102
|
-
leads: leads.map((f) => ({
|
|
1103
|
-
title: f.title,
|
|
1104
|
-
severity: f.severity,
|
|
1105
|
-
analysis: f.evidence.analysis ?? ""
|
|
1106
|
-
})),
|
|
1107
|
-
dropped: res.dropped.map((d) => ({
|
|
1108
|
-
title: d.finding.title,
|
|
1109
|
-
severity: d.finding.severity,
|
|
1110
|
-
candidatePath: d.candidatePath,
|
|
1111
|
-
lensId: d.lensId,
|
|
1112
|
-
dropReason: d.dropReason,
|
|
1113
|
-
detail: d.detail
|
|
1114
|
-
})),
|
|
1115
|
-
ingested: sinkCfg ? ingested : null,
|
|
1116
|
-
warnings: res.warnings.slice(0, 10),
|
|
1117
|
-
note: "LEADS, not confirmed bugs. Each survived the multi-lens refute quorum; verify the real sink + impact before disclosure."
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
const cost = costLedger.costBreakdown();
|
|
1122
|
-
eventBus.emit("scan_completed", {
|
|
1123
|
-
exit_reason: "failed",
|
|
1124
|
-
findings: 0,
|
|
1125
|
-
findings_count: 0,
|
|
1126
|
-
duration_ms: Date.now() - scanStartedAt,
|
|
1127
|
-
summary: `Deep review failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1128
|
-
...cost ? { cost_usd: cost.costUsd, cost_breakdown: cost.breakdown } : {}
|
|
1129
|
-
});
|
|
1130
|
-
throw error;
|
|
1131
|
-
} finally {
|
|
1132
|
-
if (savedCloudSink !== void 0) process.env["0SEC_CLOUD_SINK"] = savedCloudSink;
|
|
1133
|
-
prepared.cleanup();
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
function parsePositive2(flag, raw, dflt) {
|
|
1137
|
-
if (raw === void 0) return dflt;
|
|
1138
|
-
const n = parseInt(raw, 10);
|
|
1139
|
-
if (!Number.isFinite(n) || n <= 0) throw new Error(`invalid ${flag} '${raw}' (expected positive integer)`);
|
|
1140
|
-
return n;
|
|
1141
|
-
}
|
|
1142
|
-
async function deepReviewAction(target, opts) {
|
|
1143
|
-
if (!target || target.trim() === "") throw new Error("missing required argument: <target> (source tree path or git URL)");
|
|
1144
|
-
const ceilingSource = opts.costCeiling ?? process.env["0SEC_COST_CEILING_USD"];
|
|
1145
|
-
let costCeilingUsd;
|
|
1146
|
-
if (ceilingSource !== void 0 && ceilingSource !== "") {
|
|
1147
|
-
const parsed = Number(ceilingSource);
|
|
1148
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1149
|
-
throw new Error(`invalid --cost-ceiling '${ceilingSource}' (expected positive USD amount)`);
|
|
1150
|
-
}
|
|
1151
|
-
costCeilingUsd = parsed;
|
|
1152
|
-
}
|
|
1153
|
-
const { writeFileSync: writeFileSync3 } = await import("node:fs");
|
|
1154
|
-
const outcome = await runDeepReview({
|
|
1155
|
-
target,
|
|
1156
|
-
...costCeilingUsd !== void 0 ? { costCeilingUsd } : {},
|
|
1157
|
-
...opts.profile ? { profile: opts.profile } : {},
|
|
1158
|
-
...opts.subsystem ? { subsystem: opts.subsystem } : {},
|
|
1159
|
-
// Flag > env > single provider-default model. Only pass `models` when the
|
|
1160
|
-
// flag is set; otherwise let runDeepReview resolve $0SEC_DEEP_REVIEW_MODELS.
|
|
1161
|
-
...opts.models ? { models: opts.models.split(",").map((s) => s.trim()).filter(Boolean) } : {},
|
|
1162
|
-
...opts.attempts ? { attemptsPerCandidate: parsePositive2("--attempts", opts.attempts, defaultAttempts()) } : {},
|
|
1163
|
-
concurrency: parsePositive2("--concurrency", opts.concurrency, DEFAULT_CONCURRENCY),
|
|
1164
|
-
maxCandidates: parsePositive2("--max-candidates", opts.maxCandidates, defaultMaxCandidates()),
|
|
1165
|
-
...opts.quorum ? { quorum: parsePositive2("--quorum", opts.quorum, 1) } : {},
|
|
1166
|
-
...opts.threatModel ? { useThreatModel: true } : {},
|
|
1167
|
-
...opts.runtime ? { runtime: opts.runtime } : {},
|
|
1168
|
-
...opts.evolutionConfig ? { evolutionConfig: loadEvolutionConfigFile(opts.evolutionConfig) } : {},
|
|
1169
|
-
timeoutMs: parsePositive2("--timeout", opts.timeout, 6e5),
|
|
1170
|
-
log: (m) => process.stderr.write(m + "\n")
|
|
1171
|
-
});
|
|
1172
|
-
if (outcome.report) {
|
|
1173
|
-
const storage = resolveOsecRunStorage();
|
|
1174
|
-
writeOsecRunReport(storage, outcome.report);
|
|
1175
|
-
}
|
|
1176
|
-
const json = JSON.stringify(outcome.result, null, 2);
|
|
1177
|
-
if (opts.output) writeFileSync3(resolve2(opts.output), json + "\n", "utf8");
|
|
1178
|
-
else process.stdout.write(json + "\n");
|
|
1179
|
-
process.exitCode = outcome.exitCode;
|
|
1180
|
-
}
|
|
1181
|
-
function registerDeepReviewCommand(program) {
|
|
1182
|
-
program.command("deep-review").description(
|
|
1183
|
-
"Seedless DEPTH review of a source tree: enumerate candidate files, re-hunt each through the profile's specialized finder lenses, and gate survivors through the multi-lens verify quorum. Emits LEADS to verify (not confirmed bugs). Exit 0=sweep completed (with or without leads), 2=skipped (no files / over the review cap), 3=error (bad flags / unreadable target / all finders failed)."
|
|
1184
|
-
).argument("<target>", "Source tree to review (a local path or a git URL)").option("--profile <p>", "Lens profile: evm-onchain | solana-onchain | cardano-onchain | cairo-onchain | move-onchain (else a generic default lens set)").option("--subsystem <path>", "Narrow the review scope to a subdirectory (respects the 5000-file review cap)").option("--evolution-config <path>", "Use the active source finder with private local execution receipts").option("--models <a,b>", "Comma-separated finder models for diversity (default: single provider model, or $0SEC_DEEP_REVIEW_MODELS)").option("--attempts <N>", `Finder attempts per candidate\xD7lens\xD7model, best-of-N (default ${DEFAULT_ATTEMPTS}, or $0SEC_DEEP_REVIEW_ATTEMPTS)`).option("--concurrency <N>", `Max finders in flight (default ${DEFAULT_CONCURRENCY})`).option("--cost-ceiling <usd>", "Hard scan-wide USD ceiling. Overrides $0SEC_COST_CEILING_USD.").option("--max-candidates <N>", `Cap candidate files hunted, largest-first (default ${DEFAULT_MAX_CANDIDATES}, or $0SEC_DEEP_REVIEW_MAX_CANDIDATES)`).option("--threat-model", "Enable pre-selection threat-model planner pass (trust-boundary lanes); default OFF").option("--quorum <N>", "Multi-lens verify quorum (default: majority of the verify-lens count)").option("--format <fmt>", "Output format (json)", "json").option("--output <path>", "Write the result JSON to this path instead of stdout").option("--runtime <mode>", "Engine runtime (default api)").option("--timeout <ms>", "Cloud agent timeout budget in milliseconds", "600000").action(async (target, opts) => {
|
|
1185
|
-
try {
|
|
1186
|
-
await deepReviewAction(target, opts);
|
|
1187
|
-
} catch (err) {
|
|
1188
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1189
|
-
const json = JSON.stringify({ mode: "deep_review", error: reason }, null, 2);
|
|
1190
|
-
if (opts.output) {
|
|
1191
|
-
const { writeFileSync: writeFileSync3 } = await import("node:fs");
|
|
1192
|
-
try {
|
|
1193
|
-
writeFileSync3(resolve2(opts.output), json + "\n", "utf8");
|
|
1194
|
-
} catch {
|
|
1195
|
-
process.stderr.write(json + "\n");
|
|
1196
|
-
}
|
|
1197
|
-
} else process.stdout.write(json + "\n");
|
|
1198
|
-
process.exitCode = 3;
|
|
1199
|
-
}
|
|
1200
|
-
});
|
|
109
|
+
return `https://0.security/r#${b64}`;
|
|
1201
110
|
}
|
|
1202
111
|
|
|
1203
112
|
// packages/cli/src/finding-handoff.ts
|
|
@@ -1285,7 +194,7 @@ function shellQuote(value) {
|
|
|
1285
194
|
}
|
|
1286
195
|
function buildFindingConsoleCommand(finding, dbPath, intent = "investigate") {
|
|
1287
196
|
const args = [
|
|
1288
|
-
"
|
|
197
|
+
"0 console",
|
|
1289
198
|
"--finding",
|
|
1290
199
|
shellQuote(finding.id),
|
|
1291
200
|
"--finding-intent",
|
|
@@ -1296,7 +205,7 @@ function buildFindingConsoleCommand(finding, dbPath, intent = "investigate") {
|
|
|
1296
205
|
}
|
|
1297
206
|
|
|
1298
207
|
// packages/cli/src/finding-focus.ts
|
|
1299
|
-
import { resolve
|
|
208
|
+
import { resolve } from "node:path";
|
|
1300
209
|
import { existsSync } from "node:fs";
|
|
1301
210
|
function parseJson(value) {
|
|
1302
211
|
if (value == null) return void 0;
|
|
@@ -1308,11 +217,11 @@ function parseJson(value) {
|
|
|
1308
217
|
}
|
|
1309
218
|
}
|
|
1310
219
|
function databasePaths(dbPath) {
|
|
1311
|
-
if (dbPath?.trim()) return [
|
|
220
|
+
if (dbPath?.trim()) return [resolve(dbPath)];
|
|
1312
221
|
return [...new Set([
|
|
1313
222
|
...listOsecRunDatabasePaths(),
|
|
1314
223
|
resolveOsecDbPath()
|
|
1315
|
-
].map((path) =>
|
|
224
|
+
].map((path) => resolve(path)))];
|
|
1316
225
|
}
|
|
1317
226
|
function findingFromRow(row, reviewFields) {
|
|
1318
227
|
const id = typeof row.id === "string" ? row.id : "";
|
|
@@ -1408,9 +317,9 @@ function loadFindingFocus(id, options = {}) {
|
|
|
1408
317
|
}
|
|
1409
318
|
|
|
1410
319
|
// packages/cli/src/commands/lens-synth.ts
|
|
1411
|
-
import { createHash
|
|
1412
|
-
import { readFileSync
|
|
1413
|
-
import { resolve as
|
|
320
|
+
import { createHash } from "node:crypto";
|
|
321
|
+
import { readFileSync } from "node:fs";
|
|
322
|
+
import { resolve as resolve2 } from "node:path";
|
|
1414
323
|
function isRecord(value) {
|
|
1415
324
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1416
325
|
}
|
|
@@ -1466,10 +375,10 @@ function parseMissInputFile(raw) {
|
|
|
1466
375
|
return { misses, corpus };
|
|
1467
376
|
}
|
|
1468
377
|
async function harvestBenchMisses(fromBench, manifestPath) {
|
|
1469
|
-
const ledger = await loadLedger(
|
|
378
|
+
const ledger = await loadLedger(resolve2(fromBench));
|
|
1470
379
|
const entry = lastGreen(ledger) ?? ledger.entries[ledger.entries.length - 1];
|
|
1471
380
|
if (!entry) throw new Error(`--from-bench: ledger ${fromBench} has no entries to harvest`);
|
|
1472
|
-
const manifest = await loadManifest(
|
|
381
|
+
const manifest = await loadManifest(resolve2(manifestPath));
|
|
1473
382
|
const result = harvestMissesFromLedgerEntry(entry, manifest);
|
|
1474
383
|
return { misses: result.misses, result };
|
|
1475
384
|
}
|
|
@@ -1487,7 +396,7 @@ async function runLensSynthesisInput(input, opts, deps = {}) {
|
|
|
1487
396
|
...deps.model ? { model: deps.model } : {},
|
|
1488
397
|
...opts.model ? { modelId: opts.model } : {},
|
|
1489
398
|
probe,
|
|
1490
|
-
...opts.registry ? { registryPath:
|
|
399
|
+
...opts.registry ? { registryPath: resolve2(opts.registry) } : {},
|
|
1491
400
|
maxRegistrations: opts.maxRegister ?? 1,
|
|
1492
401
|
dryRun: !opts.promote,
|
|
1493
402
|
trials,
|
|
@@ -1496,7 +405,7 @@ async function runLensSynthesisInput(input, opts, deps = {}) {
|
|
|
1496
405
|
});
|
|
1497
406
|
}
|
|
1498
407
|
async function runLensSynthCommand(opts, deps = {}) {
|
|
1499
|
-
const raw = JSON.parse(
|
|
408
|
+
const raw = JSON.parse(readFileSync(resolve2(opts.missInput), "utf8"));
|
|
1500
409
|
let input = parseMissInputFile(raw);
|
|
1501
410
|
if (opts.fromBench) {
|
|
1502
411
|
if (!opts.manifest) throw new Error("--from-bench requires --manifest");
|
|
@@ -1507,7 +416,7 @@ async function runLensSynthCommand(opts, deps = {}) {
|
|
|
1507
416
|
return runLensSynthesisInput(input, opts, deps);
|
|
1508
417
|
}
|
|
1509
418
|
async function watchLensSynthCommand(opts, deps = {}) {
|
|
1510
|
-
const inputPath =
|
|
419
|
+
const inputPath = resolve2(opts.missInput);
|
|
1511
420
|
const log = deps.log ?? (() => {
|
|
1512
421
|
});
|
|
1513
422
|
const requestedInterval = opts.pollIntervalMs ?? 2e3;
|
|
@@ -1516,8 +425,8 @@ async function watchLensSynthCommand(opts, deps = {}) {
|
|
|
1516
425
|
let previousRevision;
|
|
1517
426
|
while (!deps.signal?.aborted) {
|
|
1518
427
|
try {
|
|
1519
|
-
const bytes =
|
|
1520
|
-
const revision =
|
|
428
|
+
const bytes = readFileSync(inputPath);
|
|
429
|
+
const revision = createHash("sha256").update(bytes).digest("hex");
|
|
1521
430
|
if (revision !== previousRevision) {
|
|
1522
431
|
previousRevision = revision;
|
|
1523
432
|
const input = parseMissInputFile(JSON.parse(bytes.toString("utf8")));
|
|
@@ -1584,9 +493,9 @@ function formatLensRegistryStatus(status) {
|
|
|
1584
493
|
return lines.join("\n");
|
|
1585
494
|
}
|
|
1586
495
|
function registerLensSynthCommand(program) {
|
|
1587
|
-
program.command("lens-synth").description("Evolve appsec finder coverage from curated misses; promotion is corpus-gated and active reviews stay pinned").option("--miss-input <path>", "curated miss-input JSON ({ misses, corpus })").option("--registry <path>", "durable overlay path (default: ~/.
|
|
496
|
+
program.command("lens-synth").description("Evolve appsec finder coverage from curated misses; promotion is corpus-gated and active reviews stay pinned").option("--miss-input <path>", "curated miss-input JSON ({ misses, corpus })").option("--registry <path>", "durable overlay path (default: ~/.0/lenses/appsec-archetypes.json)").option("--max-register <n>", "cap promoted champions per input revision", (value) => Number.parseInt(value, 10)).option("-m, --model <id>", "synthesis model override").option("--promote", "persist a validated champion to the durable overlay", false).option("--trials <n>", "repeated validation trials (2\u201310; default 2)", Number).option("--from-bench <ledger>", "harvest the champion's false-negatives from a benchmark ledger into the curated misses (requires --manifest)").option("--manifest <path>", "bench manifest path (ground-truth vuln class + sink); required with --from-bench").option("--watch", "poll the miss-input and process each new content revision", false).option("--poll-interval <ms>", "watch polling interval (minimum 100ms)", "2000").option("--status", "show the active durable overlay and promotion ledger", false).option("--rollback <lens-id>", "retire one previously promoted overlay lens").option("--json", "print machine-readable output", false).action(async (opts) => {
|
|
1588
497
|
try {
|
|
1589
|
-
const registry = opts.registry ?
|
|
498
|
+
const registry = opts.registry ? resolve2(opts.registry) : void 0;
|
|
1590
499
|
if (opts.status && opts.rollback) throw new Error("--status and --rollback cannot be combined");
|
|
1591
500
|
if (opts.status) {
|
|
1592
501
|
const status = inspectLensRegistry(registry);
|
|
@@ -1625,7 +534,7 @@ function registerLensSynthCommand(program) {
|
|
|
1625
534
|
promote: Boolean(opts.promote),
|
|
1626
535
|
trials: trialCount(opts.trials),
|
|
1627
536
|
...opts.fromBench ? { fromBench: String(opts.fromBench) } : {},
|
|
1628
|
-
...opts.manifest ? { manifest:
|
|
537
|
+
...opts.manifest ? { manifest: resolve2(String(opts.manifest)) } : {}
|
|
1629
538
|
};
|
|
1630
539
|
const printResult = (result) => {
|
|
1631
540
|
process.stdout.write(
|
|
@@ -1671,9 +580,9 @@ function registerLensSynthCommand(program) {
|
|
|
1671
580
|
|
|
1672
581
|
// packages/cli/src/commands/auth.ts
|
|
1673
582
|
import { spawn } from "node:child_process";
|
|
1674
|
-
import { mkdirSync, writeFileSync
|
|
583
|
+
import { mkdirSync, writeFileSync, chmodSync, unlinkSync, existsSync as existsSync2 } from "node:fs";
|
|
1675
584
|
import { homedir, platform } from "node:os";
|
|
1676
|
-
import { join
|
|
585
|
+
import { join } from "node:path";
|
|
1677
586
|
import { randomBytes } from "node:crypto";
|
|
1678
587
|
import { setTimeout as delay } from "node:timers/promises";
|
|
1679
588
|
var EXIT_OK = 0;
|
|
@@ -1681,19 +590,19 @@ var EXIT_USER_ERROR = 1;
|
|
|
1681
590
|
var EXIT_AUTH = 2;
|
|
1682
591
|
var EXIT_NET = 3;
|
|
1683
592
|
function registerAuthCommand(program) {
|
|
1684
|
-
const auth = program.command("auth").description("
|
|
1685
|
-
auth.command("login").description("Log in to
|
|
593
|
+
const auth = program.command("auth").description("0-cloud authentication");
|
|
594
|
+
auth.command("login").description("Log in to 0-cloud (opens browser; --token to paste directly)").option("--host <url>", "Cloud host (defaults to ZERO_CLOUD_HOST or production)").option("--token <value>", "Skip the browser flow and persist this token directly").action(async (opts) => {
|
|
1686
595
|
await runLogin(opts);
|
|
1687
596
|
});
|
|
1688
|
-
auth.command("logout").description("Delete
|
|
597
|
+
auth.command("logout").description("Delete credentials for the current Cloud profile").action(() => {
|
|
1689
598
|
runLogout({});
|
|
1690
599
|
});
|
|
1691
|
-
auth.command("status").description("Verify
|
|
600
|
+
auth.command("status").description("Verify 0-cloud credentials and account access").action(async () => {
|
|
1692
601
|
await runStatus({});
|
|
1693
602
|
});
|
|
1694
603
|
}
|
|
1695
604
|
async function hostedBrowserLoginFlow(opts = {}) {
|
|
1696
|
-
const host = normaliseHostArg(opts.host ?? DEFAULT_CLOUD_HOST);
|
|
605
|
+
const host = normaliseHostArg(opts.host ?? process.env["ZERO_CLOUD_HOST"] ?? DEFAULT_CLOUD_HOST);
|
|
1697
606
|
if (host === null) {
|
|
1698
607
|
return { ok: false, error: "Cloud host must be an http(s) URL without credentials, query or fragment." };
|
|
1699
608
|
}
|
|
@@ -1718,7 +627,7 @@ async function hostedBrowserLoginFlow(opts = {}) {
|
|
|
1718
627
|
return { ok: false, error: "Sign-in timed out. Try again.", recoverable: true };
|
|
1719
628
|
};
|
|
1720
629
|
try {
|
|
1721
|
-
opts.onStatus?.("opening", "Opening browser for
|
|
630
|
+
opts.onStatus?.("opening", "Opening browser for 0cloud sign-in.", loginUrl);
|
|
1722
631
|
signal.throwIfAborted();
|
|
1723
632
|
try {
|
|
1724
633
|
await awaitLoginStep(Promise.resolve().then(() => {
|
|
@@ -1776,9 +685,9 @@ async function hostedBrowserLoginFlow(opts = {}) {
|
|
|
1776
685
|
try {
|
|
1777
686
|
persistCredentials(host, token, opts.homeDir);
|
|
1778
687
|
} catch {
|
|
1779
|
-
return { ok: false, error: "Could not save
|
|
688
|
+
return { ok: false, error: "Could not save 0cloud credentials." };
|
|
1780
689
|
}
|
|
1781
|
-
opts.onStatus?.("ready", "Signed in to
|
|
690
|
+
opts.onStatus?.("ready", "Signed in to 0cloud.");
|
|
1782
691
|
return { ok: true, host };
|
|
1783
692
|
}
|
|
1784
693
|
if (status === "pending") continue;
|
|
@@ -1789,7 +698,7 @@ async function hostedBrowserLoginFlow(opts = {}) {
|
|
|
1789
698
|
if (res.status === 410) return { ok: false, error: "Sign-in request expired. Try again.", recoverable: true };
|
|
1790
699
|
return {
|
|
1791
700
|
ok: false,
|
|
1792
|
-
error: `
|
|
701
|
+
error: `0cloud sign-in unavailable (HTTP ${res.status}). Use your own provider or try again later.`,
|
|
1793
702
|
recoverable: res.status === 429 || res.status >= 500
|
|
1794
703
|
};
|
|
1795
704
|
} finally {
|
|
@@ -1807,7 +716,7 @@ async function hostedBrowserLoginFlow(opts = {}) {
|
|
|
1807
716
|
}
|
|
1808
717
|
}
|
|
1809
718
|
async function awaitLoginStep(operation, signal) {
|
|
1810
|
-
return new Promise((
|
|
719
|
+
return new Promise((resolve4, reject) => {
|
|
1811
720
|
const onAbort = () => {
|
|
1812
721
|
signal.removeEventListener("abort", onAbort);
|
|
1813
722
|
reject(signal.reason);
|
|
@@ -1815,7 +724,7 @@ async function awaitLoginStep(operation, signal) {
|
|
|
1815
724
|
operation.then(
|
|
1816
725
|
(value) => {
|
|
1817
726
|
signal.removeEventListener("abort", onAbort);
|
|
1818
|
-
|
|
727
|
+
resolve4(value);
|
|
1819
728
|
},
|
|
1820
729
|
(error) => {
|
|
1821
730
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -1827,7 +736,7 @@ async function awaitLoginStep(operation, signal) {
|
|
|
1827
736
|
});
|
|
1828
737
|
}
|
|
1829
738
|
async function runLogin(opts) {
|
|
1830
|
-
const host = normaliseHostArg(opts.host ?? DEFAULT_CLOUD_HOST);
|
|
739
|
+
const host = normaliseHostArg(opts.host ?? process.env["ZERO_CLOUD_HOST"] ?? DEFAULT_CLOUD_HOST);
|
|
1831
740
|
if (host === null) {
|
|
1832
741
|
consolePresentationOutput.stderr(source_default.red("Error: Cloud host must be an http(s) URL without credentials, query or fragment."), "auth.login.host-error");
|
|
1833
742
|
process.exitCode = EXIT_USER_ERROR;
|
|
@@ -1843,7 +752,7 @@ async function runLogin(opts) {
|
|
|
1843
752
|
try {
|
|
1844
753
|
persistCredentials(host, tok, opts.homeDir);
|
|
1845
754
|
} catch {
|
|
1846
|
-
consolePresentationOutput.stderr(source_default.red("Could not save
|
|
755
|
+
consolePresentationOutput.stderr(source_default.red("Could not save 0cloud credentials."), "auth.login.save-error");
|
|
1847
756
|
process.exitCode = EXIT_USER_ERROR;
|
|
1848
757
|
return;
|
|
1849
758
|
}
|
|
@@ -1882,8 +791,9 @@ async function runLogin(opts) {
|
|
|
1882
791
|
}
|
|
1883
792
|
function runLogout(opts) {
|
|
1884
793
|
const home = opts.homeDir ?? homedir();
|
|
1885
|
-
const
|
|
1886
|
-
const
|
|
794
|
+
const stateDir = cloudStateDir(opts.homeDir);
|
|
795
|
+
const osecPath = join(stateDir, "cloud.env");
|
|
796
|
+
const cloudCredsPath = join(home, ".0cloud", "credentials.json");
|
|
1887
797
|
let deletedAny = false;
|
|
1888
798
|
try {
|
|
1889
799
|
unlinkSync(osecPath);
|
|
@@ -1896,15 +806,17 @@ function runLogout(opts) {
|
|
|
1896
806
|
return;
|
|
1897
807
|
}
|
|
1898
808
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
809
|
+
if (stateDir === homeStateDir(opts.homeDir)) {
|
|
810
|
+
try {
|
|
811
|
+
unlinkSync(cloudCredsPath);
|
|
812
|
+
deletedAny = true;
|
|
813
|
+
} catch (err) {
|
|
814
|
+
const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
815
|
+
if (code !== "ENOENT") {
|
|
816
|
+
consolePresentationOutput.stderr(source_default.red(`Could not delete ${cloudCredsPath}: ${err instanceof Error ? err.message : String(err)}`), "auth.logout.delete-error");
|
|
817
|
+
process.exitCode = EXIT_USER_ERROR;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
1908
820
|
}
|
|
1909
821
|
}
|
|
1910
822
|
if (deletedAny) {
|
|
@@ -1931,7 +843,7 @@ async function runStatus(opts) {
|
|
|
1931
843
|
}
|
|
1932
844
|
const client = new CloudClient({ ...creds, fetchImpl: opts.fetchImpl });
|
|
1933
845
|
try {
|
|
1934
|
-
await client.
|
|
846
|
+
await client.getInferenceAccount();
|
|
1935
847
|
consolePresentationOutput.stdout(`OK (host=${creds.host})`, "auth.status.ok");
|
|
1936
848
|
process.exitCode = EXIT_OK;
|
|
1937
849
|
} catch (err) {
|
|
@@ -1975,23 +887,24 @@ function normaliseHostArg(host) {
|
|
|
1975
887
|
function persistCredentials(host, token, homeDirOverride) {
|
|
1976
888
|
if (!validCloudToken(token)) throw new Error("Invalid Cloud credential");
|
|
1977
889
|
const home = homeDirOverride ?? homedir();
|
|
1978
|
-
const dir =
|
|
890
|
+
const dir = cloudStateDir(homeDirOverride);
|
|
1979
891
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1980
|
-
const path =
|
|
1981
|
-
const body = `#
|
|
892
|
+
const path = join(dir, "cloud.env");
|
|
893
|
+
const body = `# 0-cloud credentials. Managed by \`0 auth\`.
|
|
1982
894
|
# DO NOT commit this file or share its contents.
|
|
1983
|
-
|
|
1984
|
-
|
|
895
|
+
ZERO_CLOUD_HOST=${host}
|
|
896
|
+
ZERO_CLOUD_TOKEN=${token}
|
|
1985
897
|
`;
|
|
1986
|
-
|
|
898
|
+
writeFileSync(path, body, { mode: 384 });
|
|
1987
899
|
chmodSync(path, 384);
|
|
1988
900
|
if (!existsSync2(path)) {
|
|
1989
901
|
return;
|
|
1990
902
|
}
|
|
903
|
+
if (dir !== homeStateDir(homeDirOverride)) return;
|
|
1991
904
|
try {
|
|
1992
|
-
const cloudDir =
|
|
905
|
+
const cloudDir = join(home, ".0cloud");
|
|
1993
906
|
mkdirSync(cloudDir, { recursive: true });
|
|
1994
|
-
const cloudCredsPath =
|
|
907
|
+
const cloudCredsPath = join(cloudDir, "credentials.json");
|
|
1995
908
|
const trimmedHost = host.replace(/\/+$/, "");
|
|
1996
909
|
const apiEndpoint = trimmedHost.endsWith("/api") ? trimmedHost : `${trimmedHost}/api`;
|
|
1997
910
|
const cloudCreds = JSON.stringify(
|
|
@@ -2005,7 +918,7 @@ function persistCredentials(host, token, homeDirOverride) {
|
|
|
2005
918
|
null,
|
|
2006
919
|
2
|
|
2007
920
|
) + "\n";
|
|
2008
|
-
|
|
921
|
+
writeFileSync(cloudCredsPath, cloudCreds, { mode: 384 });
|
|
2009
922
|
chmodSync(cloudCredsPath, 384);
|
|
2010
923
|
} catch {
|
|
2011
924
|
}
|
|
@@ -2029,7 +942,7 @@ function extractToken(body) {
|
|
|
2029
942
|
}
|
|
2030
943
|
function defaultOpenBrowser(url) {
|
|
2031
944
|
const plat = platform();
|
|
2032
|
-
return new Promise((
|
|
945
|
+
return new Promise((resolve4, reject) => {
|
|
2033
946
|
const options = { detached: true, stdio: "ignore", shell: false };
|
|
2034
947
|
const child = plat === "darwin" ? spawn("open", [url], options) : plat === "win32" ? spawn("powershell.exe", [
|
|
2035
948
|
"-NoProfile",
|
|
@@ -2040,14 +953,14 @@ function defaultOpenBrowser(url) {
|
|
|
2040
953
|
child.once("error", reject);
|
|
2041
954
|
child.once("spawn", () => {
|
|
2042
955
|
child.unref();
|
|
2043
|
-
|
|
956
|
+
resolve4();
|
|
2044
957
|
});
|
|
2045
958
|
});
|
|
2046
959
|
}
|
|
2047
960
|
|
|
2048
961
|
// packages/cli/src/commands/run.ts
|
|
2049
962
|
import { writeFile } from "node:fs/promises";
|
|
2050
|
-
import { join as
|
|
963
|
+
import { join as join2, resolve as resolve3 } from "node:path";
|
|
2051
964
|
import { tmpdir } from "node:os";
|
|
2052
965
|
import { execFile } from "node:child_process";
|
|
2053
966
|
import { existsSync as existsSync3 } from "node:fs";
|
|
@@ -2177,7 +1090,7 @@ var EVIDENCE_BLOCK_LIMIT = 4e3;
|
|
|
2177
1090
|
var POC_STEP_LIMIT = 20;
|
|
2178
1091
|
function formatMarkdown(report) {
|
|
2179
1092
|
const lines = [];
|
|
2180
|
-
lines.push("#
|
|
1093
|
+
lines.push("# 0 Scan Report");
|
|
2181
1094
|
lines.push("");
|
|
2182
1095
|
lines.push(`| Field | Value |`);
|
|
2183
1096
|
lines.push(`|-------|-------|`);
|
|
@@ -2434,7 +1347,7 @@ function formatHtml(report) {
|
|
|
2434
1347
|
<head>
|
|
2435
1348
|
<meta charset="UTF-8">
|
|
2436
1349
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2437
|
-
<title>
|
|
1350
|
+
<title>0 Report \u2014 ${escapeHtml(report.target)}</title>
|
|
2438
1351
|
<style>
|
|
2439
1352
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2440
1353
|
body {
|
|
@@ -2698,7 +1611,7 @@ function formatHtml(report) {
|
|
|
2698
1611
|
<div class="header">
|
|
2699
1612
|
<div class="header-top">
|
|
2700
1613
|
<div>
|
|
2701
|
-
<div class="brand">
|
|
1614
|
+
<div class="brand">ZERO</div>
|
|
2702
1615
|
<div class="brand-sub">Security Scan Report</div>
|
|
2703
1616
|
</div>
|
|
2704
1617
|
<div class="risk-badge" style="background:${risk.color};color:#fff">${risk.label} RISK</div>
|
|
@@ -2739,7 +1652,7 @@ function formatHtml(report) {
|
|
|
2739
1652
|
</div>
|
|
2740
1653
|
|
|
2741
1654
|
<div class="footer">
|
|
2742
|
-
Generated by
|
|
1655
|
+
Generated by 0 · ${escapeHtml((/* @__PURE__ */ new Date()).toISOString())}
|
|
2743
1656
|
</div>
|
|
2744
1657
|
</body>
|
|
2745
1658
|
</html>`;
|
|
@@ -2875,9 +1788,9 @@ function formatSarif(report) {
|
|
|
2875
1788
|
{
|
|
2876
1789
|
tool: {
|
|
2877
1790
|
driver: {
|
|
2878
|
-
name: "
|
|
1791
|
+
name: "0",
|
|
2879
1792
|
version: VERSION,
|
|
2880
|
-
informationUri: "https://github.com/0sec-labs/
|
|
1793
|
+
informationUri: "https://github.com/0sec-labs/0",
|
|
2881
1794
|
rules: Array.from(rulesMap.values())
|
|
2882
1795
|
}
|
|
2883
1796
|
},
|
|
@@ -2953,13 +1866,13 @@ function truncate(text, maxLen) {
|
|
|
2953
1866
|
async function generatePdfReport(report, outputPath) {
|
|
2954
1867
|
const require_ = createRequire(import.meta.url);
|
|
2955
1868
|
const PDFDocument = require_("pdfkit");
|
|
2956
|
-
return new Promise((
|
|
1869
|
+
return new Promise((resolve4, reject) => {
|
|
2957
1870
|
const doc = new PDFDocument({
|
|
2958
1871
|
size: "LETTER",
|
|
2959
1872
|
margins: { top: PAGE_MARGIN, bottom: PAGE_MARGIN, left: PAGE_MARGIN, right: PAGE_MARGIN },
|
|
2960
1873
|
info: {
|
|
2961
|
-
Title: `
|
|
2962
|
-
Author: "
|
|
1874
|
+
Title: `0 Pentest Report - ${report.target}`,
|
|
1875
|
+
Author: "0",
|
|
2963
1876
|
Subject: "Automated Penetration Test Report",
|
|
2964
1877
|
CreationDate: /* @__PURE__ */ new Date()
|
|
2965
1878
|
},
|
|
@@ -2967,7 +1880,7 @@ async function generatePdfReport(report, outputPath) {
|
|
|
2967
1880
|
});
|
|
2968
1881
|
const stream = createWriteStream(outputPath);
|
|
2969
1882
|
stream.on("error", reject);
|
|
2970
|
-
stream.on("finish",
|
|
1883
|
+
stream.on("finish", resolve4);
|
|
2971
1884
|
doc.pipe(stream);
|
|
2972
1885
|
renderCoverPage(doc, report);
|
|
2973
1886
|
renderExecutiveSummary(doc, report);
|
|
@@ -2980,7 +1893,7 @@ async function generatePdfReport(report, outputPath) {
|
|
|
2980
1893
|
}
|
|
2981
1894
|
function renderCoverPage(doc, report) {
|
|
2982
1895
|
doc.rect(0, 0, 612, 792).fill(COLORS.primary);
|
|
2983
|
-
doc.fontSize(48).font("Helvetica-Bold").fillColor(COLORS.accent).text("
|
|
1896
|
+
doc.fontSize(48).font("Helvetica-Bold").fillColor(COLORS.accent).text("0", PAGE_MARGIN, 200, { align: "center" });
|
|
2984
1897
|
doc.fontSize(14).font("Helvetica").fillColor(COLORS.white).text("Automated Penetration Test Report", PAGE_MARGIN, 260, { align: "center" });
|
|
2985
1898
|
doc.moveTo(200, 300).lineTo(412, 300).strokeColor(COLORS.accent).lineWidth(2).stroke();
|
|
2986
1899
|
doc.fontSize(18).font("Helvetica-Bold").fillColor(COLORS.white).text(report.target, PAGE_MARGIN, 330, { align: "center" });
|
|
@@ -3107,7 +2020,7 @@ function renderMethodology(doc, report) {
|
|
|
3107
2020
|
doc.addPage();
|
|
3108
2021
|
sectionTitle(doc, "Methodology");
|
|
3109
2022
|
doc.fontSize(10).font("Helvetica").fillColor(COLORS.text).text(
|
|
3110
|
-
"This report was generated by
|
|
2023
|
+
"This report was generated by 0, a fully autonomous agentic pentesting framework. The tool uses AI-driven attack generation and multi-agent consensus verification to identify security vulnerabilities.",
|
|
3111
2024
|
PAGE_MARGIN,
|
|
3112
2025
|
doc.y + 10,
|
|
3113
2026
|
{ width: CONTENT_WIDTH }
|
|
@@ -3184,7 +2097,7 @@ function renderFooters(doc) {
|
|
|
3184
2097
|
doc.switchToPage(i);
|
|
3185
2098
|
if (i === 0) continue;
|
|
3186
2099
|
doc.fontSize(8).font("Helvetica").fillColor(COLORS.textLight).text(
|
|
3187
|
-
`
|
|
2100
|
+
`0 Pentest Report | Page ${i + 1} of ${pageCount} | CONFIDENTIAL`,
|
|
3188
2101
|
PAGE_MARGIN,
|
|
3189
2102
|
760,
|
|
3190
2103
|
{ width: CONTENT_WIDTH, align: "center" }
|
|
@@ -3194,7 +2107,7 @@ function renderFooters(doc) {
|
|
|
3194
2107
|
|
|
3195
2108
|
// packages/cli/src/formatters/replay.ts
|
|
3196
2109
|
function sleep(ms) {
|
|
3197
|
-
return new Promise((
|
|
2110
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3198
2111
|
}
|
|
3199
2112
|
function stripAnsi(s) {
|
|
3200
2113
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -3383,7 +2296,7 @@ function buildReplayLines(data) {
|
|
|
3383
2296
|
const duration = data.durationMs < 1e3 ? `${data.durationMs}ms` : `${(data.durationMs / 1e3).toFixed(1)}s`;
|
|
3384
2297
|
lines.push({
|
|
3385
2298
|
text: boxRow(
|
|
3386
|
-
source_default.white(`Completed in ${duration}`) + source_default.dim(" \u2192 ") + source_default.gray("./
|
|
2299
|
+
source_default.white(`Completed in ${duration}`) + source_default.dim(" \u2192 ") + source_default.gray("./0-report.json")
|
|
3387
2300
|
),
|
|
3388
2301
|
delay: MED
|
|
3389
2302
|
});
|
|
@@ -3394,7 +2307,7 @@ async function renderReplay(data) {
|
|
|
3394
2307
|
const lines = buildReplayLines(data);
|
|
3395
2308
|
process.stdout.write("\n");
|
|
3396
2309
|
process.stdout.write(
|
|
3397
|
-
source_default.red.bold(" \u25C6
|
|
2310
|
+
source_default.red.bold(" \u25C6 0") + source_default.gray(" attack replay") + "\n"
|
|
3398
2311
|
);
|
|
3399
2312
|
process.stdout.write(
|
|
3400
2313
|
source_default.dim(" " + "\u2500".repeat(BOX_WIDTH + 1)) + "\n"
|
|
@@ -3620,7 +2533,7 @@ async function loadCoreModule() {
|
|
|
3620
2533
|
if (!coreModulePromise) {
|
|
3621
2534
|
const srcUrl = new URL("../../../core/src/index.ts", import.meta.url);
|
|
3622
2535
|
const srcExists = process.versions.bun && existsSync3(fileURLToPath(srcUrl));
|
|
3623
|
-
coreModulePromise = srcExists ? import(srcUrl.href) : import("./dist-
|
|
2536
|
+
coreModulePromise = srcExists ? import(srcUrl.href) : import("./dist-F6X62TQK.js");
|
|
3624
2537
|
}
|
|
3625
2538
|
return coreModulePromise;
|
|
3626
2539
|
}
|
|
@@ -3664,15 +2577,15 @@ function getTargetType(report, opts) {
|
|
|
3664
2577
|
return report?.targetType ?? opts.targetType;
|
|
3665
2578
|
}
|
|
3666
2579
|
function emitResultLine(payload) {
|
|
3667
|
-
if (process.env["
|
|
3668
|
-
console.log(`
|
|
2580
|
+
if (process.env["ZERO_EMIT_RESULT_LINE"] !== "1" && !process.env["ZERO_CLOUD_SINK"]) return;
|
|
2581
|
+
console.log(`ZERO_RESULT=${JSON.stringify(payload)}`);
|
|
3669
2582
|
}
|
|
3670
2583
|
function getCloudFinalSinkConfig() {
|
|
3671
|
-
if (process.env["
|
|
3672
|
-
const sinkUrl = process.env["
|
|
3673
|
-
const scanId = process.env["
|
|
2584
|
+
if (process.env["ZERO_FEATURE_CLOUD_SINK"] === "0") return null;
|
|
2585
|
+
const sinkUrl = process.env["ZERO_CLOUD_SINK"]?.trim();
|
|
2586
|
+
const scanId = process.env["ZERO_CLOUD_SCAN_ID"]?.trim();
|
|
3674
2587
|
if (!sinkUrl || !scanId) return null;
|
|
3675
|
-
const token = process.env["
|
|
2588
|
+
const token = process.env["ZERO_CLOUD_TOKEN"]?.trim() || void 0;
|
|
3676
2589
|
return { sinkUrl, scanId, token };
|
|
3677
2590
|
}
|
|
3678
2591
|
function isCostBreakdownEntry(value) {
|
|
@@ -3752,7 +2665,7 @@ async function postFinalResultToCloud(report) {
|
|
|
3752
2665
|
const url = `${config.sinkUrl.replace(/\/+$/, "")}/scans/${encodeURIComponent(config.scanId)}/findings`;
|
|
3753
2666
|
const headers = {
|
|
3754
2667
|
"Content-Type": "application/json",
|
|
3755
|
-
"X-
|
|
2668
|
+
"X-0-Scan-Id": config.scanId
|
|
3756
2669
|
};
|
|
3757
2670
|
if (config.token) headers.Authorization = `Bearer ${config.token}`;
|
|
3758
2671
|
try {
|
|
@@ -3764,37 +2677,16 @@ async function postFinalResultToCloud(report) {
|
|
|
3764
2677
|
if (!res.ok) {
|
|
3765
2678
|
const text = await res.text().catch(() => "");
|
|
3766
2679
|
process.stderr.write(
|
|
3767
|
-
`[
|
|
2680
|
+
`[0 cloud-sink] report POST ${url} returned ${res.status}: ${text.slice(0, 200)}
|
|
3768
2681
|
`
|
|
3769
2682
|
);
|
|
3770
2683
|
}
|
|
3771
2684
|
} catch (err) {
|
|
3772
2685
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3773
|
-
process.stderr.write(`[
|
|
2686
|
+
process.stderr.write(`[0 cloud-sink] report POST ${url} failed: ${msg}
|
|
3774
2687
|
`);
|
|
3775
2688
|
}
|
|
3776
2689
|
}
|
|
3777
|
-
function skippedLensReviewReport(target, message) {
|
|
3778
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3779
|
-
return {
|
|
3780
|
-
target,
|
|
3781
|
-
scanDepth: "deep",
|
|
3782
|
-
startedAt: now,
|
|
3783
|
-
completedAt: now,
|
|
3784
|
-
durationMs: 0,
|
|
3785
|
-
summary: {
|
|
3786
|
-
totalAttacks: 0,
|
|
3787
|
-
totalFindings: 0,
|
|
3788
|
-
critical: 0,
|
|
3789
|
-
high: 0,
|
|
3790
|
-
medium: 0,
|
|
3791
|
-
low: 0,
|
|
3792
|
-
info: 0
|
|
3793
|
-
},
|
|
3794
|
-
findings: [],
|
|
3795
|
-
warnings: [{ stage: "attack", message }]
|
|
3796
|
-
};
|
|
3797
|
-
}
|
|
3798
2690
|
async function runUnified(opts) {
|
|
3799
2691
|
const { target, depth, format, runtime, timeout } = opts;
|
|
3800
2692
|
const core = await loadCoreModule();
|
|
@@ -3804,7 +2696,7 @@ async function runUnified(opts) {
|
|
|
3804
2696
|
console.error(source_default.red("--branch-from requires --resume <run-id>"));
|
|
3805
2697
|
process.exit(2);
|
|
3806
2698
|
}
|
|
3807
|
-
const { branchJournal } = await import("./dist-
|
|
2699
|
+
const { branchJournal } = await import("./dist-F6X62TQK.js");
|
|
3808
2700
|
const result = branchJournal({
|
|
3809
2701
|
runId: effectiveResumeScanId,
|
|
3810
2702
|
fromEntry: opts.branchFromEntry
|
|
@@ -3840,7 +2732,7 @@ async function runUnified(opts) {
|
|
|
3840
2732
|
console.error(source_default.red(`Unknown runtime '${runtime}'. Valid: ${validRuntimes.join(", ")}`));
|
|
3841
2733
|
process.exit(2);
|
|
3842
2734
|
}
|
|
3843
|
-
const directCodexProviderConfigured = runtime === "codex" && (!!process.env["
|
|
2735
|
+
const directCodexProviderConfigured = runtime === "codex" && (!!process.env["ZERO_CHATGPT_ACCESS_TOKEN"]?.trim() || !!process.env["ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"]?.trim());
|
|
3844
2736
|
if (runtime !== "api" && runtime !== "auto" && !directCodexProviderConfigured) {
|
|
3845
2737
|
const rt = core.createRuntime({ type: runtime, timeout });
|
|
3846
2738
|
const available = await rt.isAvailable();
|
|
@@ -3859,9 +2751,9 @@ async function runUnified(opts) {
|
|
|
3859
2751
|
if (opts.sessionUiFactory) {
|
|
3860
2752
|
inkUI = await opts.sessionUiFactory({ target, depth, mode });
|
|
3861
2753
|
} else {
|
|
3862
|
-
const { isBunRuntime } = await import("./runtime-
|
|
2754
|
+
const { isBunRuntime } = await import("./runtime-XFWJTBUO.js");
|
|
3863
2755
|
if (isBunRuntime()) {
|
|
3864
|
-
const { createOpenTuiSession } = await import("./run-
|
|
2756
|
+
const { createOpenTuiSession } = await import("./run-CBGE6DRQ.js");
|
|
3865
2757
|
const availability = await getRuntimeAvailability();
|
|
3866
2758
|
inkUI = await createOpenTuiSession({
|
|
3867
2759
|
target,
|
|
@@ -3875,7 +2767,7 @@ async function runUnified(opts) {
|
|
|
3875
2767
|
model: opts.model
|
|
3876
2768
|
});
|
|
3877
2769
|
} else {
|
|
3878
|
-
const { renderScanStream } = await import("./scan-stream-
|
|
2770
|
+
const { renderScanStream } = await import("./scan-stream-BMOCPKQX.js");
|
|
3879
2771
|
inkUI = renderScanStream({ version: VERSION, target, depth, mode });
|
|
3880
2772
|
}
|
|
3881
2773
|
}
|
|
@@ -3883,44 +2775,8 @@ async function runUnified(opts) {
|
|
|
3883
2775
|
getPendingUserMessages = inkUI.getPendingUserMessages;
|
|
3884
2776
|
}
|
|
3885
2777
|
try {
|
|
3886
|
-
let runnerExitCode = 0;
|
|
3887
2778
|
let report;
|
|
3888
|
-
if (opts.targetType === "
|
|
3889
|
-
eventHandler({
|
|
3890
|
-
type: "stage:start",
|
|
3891
|
-
stage: "source-analysis",
|
|
3892
|
-
message: "capturing the validated finder-lens snapshot"
|
|
3893
|
-
});
|
|
3894
|
-
const outcome = await runDeepReview({
|
|
3895
|
-
target,
|
|
3896
|
-
profile: opts.reviewProfile,
|
|
3897
|
-
subsystem: opts.subsystem,
|
|
3898
|
-
models: opts.model ? [opts.model] : void 0,
|
|
3899
|
-
runtime,
|
|
3900
|
-
timeoutMs: timeout,
|
|
3901
|
-
costCeilingUsd: opts.costCeilingUsd,
|
|
3902
|
-
log: (message) => eventHandler({
|
|
3903
|
-
type: "stage:start",
|
|
3904
|
-
stage: "attack",
|
|
3905
|
-
message
|
|
3906
|
-
})
|
|
3907
|
-
});
|
|
3908
|
-
runnerExitCode = outcome.exitCode;
|
|
3909
|
-
const lensReport = outcome.report ?? skippedLensReviewReport(
|
|
3910
|
-
target,
|
|
3911
|
-
typeof outcome.result === "object" && outcome.result !== null && "note" in outcome.result ? String(outcome.result.note) : `lens review ended with exit code ${outcome.exitCode}`
|
|
3912
|
-
);
|
|
3913
|
-
report = lensReport;
|
|
3914
|
-
writeOsecRunReport(
|
|
3915
|
-
resolveOsecRunStorage({ ...opts.dbPath ? { dbPath: opts.dbPath } : {} }),
|
|
3916
|
-
lensReport
|
|
3917
|
-
);
|
|
3918
|
-
eventHandler({
|
|
3919
|
-
type: "stage:end",
|
|
3920
|
-
stage: "attack",
|
|
3921
|
-
message: outcome.exitCode === 0 ? "validated finder-lens review completed" : `validated finder-lens review ended with exit code ${outcome.exitCode}`
|
|
3922
|
-
});
|
|
3923
|
-
} else if (opts.targetType === "url" || opts.targetType === "web-app") {
|
|
2779
|
+
if (opts.targetType === "url" || opts.targetType === "web-app") {
|
|
3924
2780
|
report = await core.agenticScan({
|
|
3925
2781
|
config: {
|
|
3926
2782
|
target,
|
|
@@ -3995,7 +2851,7 @@ async function runUnified(opts) {
|
|
|
3995
2851
|
} else {
|
|
3996
2852
|
if (format === "html" || format === "pdf") {
|
|
3997
2853
|
const extension = format === "pdf" ? "pdf" : "html";
|
|
3998
|
-
const filePath = opts.reportPath ?
|
|
2854
|
+
const filePath = opts.reportPath ? resolve3(opts.reportPath) : join2(tmpdir(), `0-report-${Date.now()}.${extension}`);
|
|
3999
2855
|
if (format === "pdf") {
|
|
4000
2856
|
await generatePdfReport(toScanReport(reportAny), filePath);
|
|
4001
2857
|
} else {
|
|
@@ -4015,7 +2871,7 @@ async function runUnified(opts) {
|
|
|
4015
2871
|
console.log("");
|
|
4016
2872
|
console.log(source_default.gray(" --tui post-scan view is no longer bundled in the npm package."));
|
|
4017
2873
|
console.log(source_default.gray(" Install the standalone binary for the full OpenTUI experience:"));
|
|
4018
|
-
console.log(source_default.gray(" curl -fsSL https://raw.githubusercontent.com/0sec-labs/
|
|
2874
|
+
console.log(source_default.gray(" curl -fsSL https://raw.githubusercontent.com/0sec-labs/0/main/install.sh | bash"));
|
|
4019
2875
|
console.log("");
|
|
4020
2876
|
}
|
|
4021
2877
|
if (crossValidatedLeads) {
|
|
@@ -4025,14 +2881,14 @@ async function runUnified(opts) {
|
|
|
4025
2881
|
printCostSummary(scanCompletedCost);
|
|
4026
2882
|
}
|
|
4027
2883
|
unsubscribeCost();
|
|
4028
|
-
let exitCode =
|
|
2884
|
+
let exitCode = 0;
|
|
4029
2885
|
const estimatedCostUsd = getEstimatedCost(reportAny);
|
|
4030
2886
|
const usage = getUsage(reportAny);
|
|
4031
2887
|
if (opts.emit === "pr") {
|
|
4032
2888
|
const findings = report.findings ?? [];
|
|
4033
2889
|
const core2 = await loadCoreModule();
|
|
4034
|
-
const repoRoot = opts.targetType === "source-code" && existsSync3(target) ?
|
|
4035
|
-
const outDir = opts.emitOutDir ??
|
|
2890
|
+
const repoRoot = opts.targetType === "source-code" && existsSync3(target) ? resolve3(target) : process.cwd();
|
|
2891
|
+
const outDir = opts.emitOutDir ?? join2(tmpdir(), `0-emit-${Date.now()}`);
|
|
4036
2892
|
console.log(source_default.blue(`[emit pr] ${findings.length} finding(s); repo=${repoRoot} base=${opts.emitPrBase ?? "main"}${opts.emitPrDryRun ? " (dry-run)" : ""}`));
|
|
4037
2893
|
const emitReport = await core2.emitFindingsAsPRs(findings, {
|
|
4038
2894
|
repoRoot,
|
|
@@ -4063,7 +2919,7 @@ async function runUnified(opts) {
|
|
|
4063
2919
|
if (findings.length === 0) {
|
|
4064
2920
|
console.log(source_default.yellow("No findings to export."));
|
|
4065
2921
|
} else {
|
|
4066
|
-
const { exportToGitHubIssues } = await import("./github-issues-
|
|
2922
|
+
const { exportToGitHubIssues } = await import("./github-issues-IGTTWM7V.js");
|
|
4067
2923
|
console.log(source_default.blue(`Exporting ${findings.length} finding(s) to GitHub Issues on ${repo}...`));
|
|
4068
2924
|
const result = await exportToGitHubIssues(findings, repo);
|
|
4069
2925
|
console.log(
|
|
@@ -4071,7 +2927,7 @@ async function runUnified(opts) {
|
|
|
4071
2927
|
);
|
|
4072
2928
|
}
|
|
4073
2929
|
}
|
|
4074
|
-
const ceilingRaw = process.env["
|
|
2930
|
+
const ceilingRaw = process.env["ZERO_COST_CEILING_USD"]?.trim();
|
|
4075
2931
|
if (ceilingRaw) {
|
|
4076
2932
|
const ceiling = Number(ceilingRaw);
|
|
4077
2933
|
if (Number.isFinite(ceiling) && estimatedCostUsd !== void 0 && estimatedCostUsd > ceiling) {
|
|
@@ -4389,14 +3245,6 @@ var SLASH_COMMANDS = [
|
|
|
4389
3245
|
description: "Replay a previous turn or session",
|
|
4390
3246
|
tuiOnly: true
|
|
4391
3247
|
},
|
|
4392
|
-
// ── mode ────────────────────────────────────────────────────────────────
|
|
4393
|
-
{
|
|
4394
|
-
name: "mode",
|
|
4395
|
-
aliases: [],
|
|
4396
|
-
category: "mode",
|
|
4397
|
-
description: "Set Standard (approve actions), Co-pilot (in scope), YOLO (public-network autonomy), or Recon (passive)",
|
|
4398
|
-
usage: "/mode [standard|copilot|yolo|recon]"
|
|
4399
|
-
},
|
|
4400
3248
|
{
|
|
4401
3249
|
name: "resume",
|
|
4402
3250
|
aliases: ["sessions"],
|
|
@@ -4408,7 +3256,7 @@ var SLASH_COMMANDS = [
|
|
|
4408
3256
|
name: "providers",
|
|
4409
3257
|
aliases: [],
|
|
4410
3258
|
category: "system",
|
|
4411
|
-
description: "Connection:
|
|
3259
|
+
description: "Connection: 0cloud, API keys, or provider subscriptions",
|
|
4412
3260
|
usage: "/providers",
|
|
4413
3261
|
tuiOnly: true
|
|
4414
3262
|
},
|
|
@@ -4423,7 +3271,7 @@ var SLASH_COMMANDS = [
|
|
|
4423
3271
|
name: "feedback",
|
|
4424
3272
|
aliases: [],
|
|
4425
3273
|
category: "system",
|
|
4426
|
-
description: "Record feedback about
|
|
3274
|
+
description: "Record feedback about 0 to a local file you control, with optional HTTPS submission",
|
|
4427
3275
|
usage: "/feedback <message> | /feedback submit <message> | /feedback send | /feedback cancel"
|
|
4428
3276
|
},
|
|
4429
3277
|
{
|
|
@@ -4452,7 +3300,7 @@ var SLASH_COMMANDS = [
|
|
|
4452
3300
|
{
|
|
4453
3301
|
name: "model",
|
|
4454
3302
|
aliases: ["models"],
|
|
4455
|
-
category: "
|
|
3303
|
+
category: "session",
|
|
4456
3304
|
description: "Show or select a model (TUI: applies to New chat)",
|
|
4457
3305
|
usage: "/model [id]"
|
|
4458
3306
|
},
|
|
@@ -4503,7 +3351,7 @@ var SLASH_COMMANDS = [
|
|
|
4503
3351
|
name: "connect",
|
|
4504
3352
|
aliases: ["login", "auth"],
|
|
4505
3353
|
category: "navigation",
|
|
4506
|
-
description: "Connect to
|
|
3354
|
+
description: "Connect to 0cloud, or use your own API key or provider subscription",
|
|
4507
3355
|
usage: "/connect",
|
|
4508
3356
|
tuiOnly: true
|
|
4509
3357
|
},
|
|
@@ -4605,7 +3453,7 @@ function filterCommands(query) {
|
|
|
4605
3453
|
}
|
|
4606
3454
|
|
|
4607
3455
|
// packages/cli/src/console-session.ts
|
|
4608
|
-
import { randomUUID
|
|
3456
|
+
import { randomUUID } from "node:crypto";
|
|
4609
3457
|
|
|
4610
3458
|
// packages/cli/src/conversation-history.ts
|
|
4611
3459
|
var MAX_PAGE_CHARS = 16e3;
|
|
@@ -4677,10 +3525,10 @@ function createConversationHistory(options = {}) {
|
|
|
4677
3525
|
|
|
4678
3526
|
// packages/cli/src/dev-engine-updates.ts
|
|
4679
3527
|
import { execFile as execFile2 } from "node:child_process";
|
|
4680
|
-
import { createHash as
|
|
4681
|
-
import { appendFileSync, readFileSync as
|
|
3528
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3529
|
+
import { appendFileSync, readFileSync as readFileSync2, readdirSync, realpathSync, statSync } from "node:fs";
|
|
4682
3530
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
4683
|
-
import { isAbsolute, join as
|
|
3531
|
+
import { isAbsolute, join as join3, relative, sep } from "node:path";
|
|
4684
3532
|
import { pathToFileURL } from "node:url";
|
|
4685
3533
|
import { isDeepStrictEqual, promisify } from "node:util";
|
|
4686
3534
|
var execute = promisify(execFile2);
|
|
@@ -4708,12 +3556,12 @@ function assertSessionContract(session) {
|
|
|
4708
3556
|
}
|
|
4709
3557
|
}
|
|
4710
3558
|
function sourceDigest(root) {
|
|
4711
|
-
const hash =
|
|
3559
|
+
const hash = createHash2("sha256");
|
|
4712
3560
|
const visit = (directory) => {
|
|
4713
3561
|
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
4714
3562
|
for (const entry of entries) {
|
|
4715
3563
|
if (entry.name.startsWith(".") || Object.hasOwn(ignoredSourceNames, entry.name)) continue;
|
|
4716
|
-
const path =
|
|
3564
|
+
const path = join3(directory, entry.name);
|
|
4717
3565
|
if (entry.isDirectory()) {
|
|
4718
3566
|
visit(path);
|
|
4719
3567
|
continue;
|
|
@@ -4721,23 +3569,23 @@ function sourceDigest(root) {
|
|
|
4721
3569
|
if (!entry.isFile() && !entry.isSymbolicLink() || /\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name) || entry.name.endsWith(".d.ts")) continue;
|
|
4722
3570
|
let readable = path;
|
|
4723
3571
|
if (entry.isSymbolicLink()) {
|
|
4724
|
-
readable =
|
|
4725
|
-
const target =
|
|
4726
|
-
if (target === ".." || target.startsWith(`..${
|
|
3572
|
+
readable = realpathSync(path);
|
|
3573
|
+
const target = relative(root, readable);
|
|
3574
|
+
if (target === ".." || target.startsWith(`..${sep}`) || isAbsolute(target) || !statSync(readable).isFile()) {
|
|
4727
3575
|
throw new Error(`Source links must resolve to files inside the core source tree: ${path}`);
|
|
4728
3576
|
}
|
|
4729
3577
|
}
|
|
4730
|
-
hash.update(
|
|
3578
|
+
hash.update(relative(root, path)).update("\0").update(readFileSync2(readable)).update("\0");
|
|
4731
3579
|
}
|
|
4732
3580
|
};
|
|
4733
3581
|
visit(root);
|
|
4734
3582
|
return hash.digest("hex");
|
|
4735
3583
|
}
|
|
4736
3584
|
function withDevEngineUpdates(initial, config, closeStore) {
|
|
4737
|
-
const requestedRoot = process.env["
|
|
4738
|
-
const root = requestedRoot ?
|
|
4739
|
-
if (root && JSON.parse(
|
|
4740
|
-
throw new Error("
|
|
3585
|
+
const requestedRoot = process.env["ZERO_DEV_SOURCE_ROOT"];
|
|
3586
|
+
const root = requestedRoot ? realpathSync(requestedRoot) : void 0;
|
|
3587
|
+
if (root && JSON.parse(readFileSync2(join3(root, "packages/core/package.json"), "utf8")).name !== "@0/core") {
|
|
3588
|
+
throw new Error("ZERO_DEV_SOURCE_ROOT must identify a 0 development checkout");
|
|
4741
3589
|
}
|
|
4742
3590
|
let current = initial;
|
|
4743
3591
|
let activeDigest;
|
|
@@ -4763,15 +3611,15 @@ function withDevEngineUpdates(initial, config, closeStore) {
|
|
|
4763
3611
|
let committed = false;
|
|
4764
3612
|
let pendingHarnessSnapshot;
|
|
4765
3613
|
try {
|
|
4766
|
-
const source =
|
|
3614
|
+
const source = join3(root, "packages/core/src");
|
|
4767
3615
|
const digest = sourceDigest(source);
|
|
4768
3616
|
if (digest === activeDigest || digest === rejectedDigest) return;
|
|
4769
3617
|
rejectedDigest = digest;
|
|
4770
3618
|
notify("Building changed development engine source; the current session remains active until handoff.");
|
|
4771
|
-
const generationRoot =
|
|
3619
|
+
const generationRoot = join3(root, "packages/core/.0/dev-engines");
|
|
4772
3620
|
await mkdir(generationRoot, { recursive: true, mode: 448 });
|
|
4773
|
-
directory = await mkdtemp(
|
|
4774
|
-
const result = await execute(process.execPath, [
|
|
3621
|
+
directory = await mkdtemp(join3(generationRoot, "generation-"));
|
|
3622
|
+
const result = await execute(process.execPath, [join3(root, "scripts/build-dev-engine.mjs"), source, directory], {
|
|
4775
3623
|
cwd: root,
|
|
4776
3624
|
timeout: 12e4,
|
|
4777
3625
|
maxBuffer: 2 * 1024 * 1024,
|
|
@@ -4801,7 +3649,7 @@ function withDevEngineUpdates(initial, config, closeStore) {
|
|
|
4801
3649
|
if (!isDeepStrictEqual(comparableCheckpoint(candidate.exportCheckpoint()), comparableCheckpoint(checkpoint))) {
|
|
4802
3650
|
throw new Error("Candidate engine did not preserve the session checkpoint");
|
|
4803
3651
|
}
|
|
4804
|
-
const candidateBus = await import(pathToFileURL(
|
|
3652
|
+
const candidateBus = await import(pathToFileURL(join3(directory, "runtime/events/bus.js")).href);
|
|
4805
3653
|
signal?.throwIfAborted();
|
|
4806
3654
|
if (closingStarted || !getSettings().allowDevSourceUpdates) throw new Error("Development engine update cancelled before handoff");
|
|
4807
3655
|
if (!isDeepStrictEqual(current.exportCheckpoint(), checkpoint)) throw new Error("Session changed during candidate preparation; keeping the current engine");
|
|
@@ -4879,7 +3727,7 @@ function withDevEngineUpdates(initial, config, closeStore) {
|
|
|
4879
3727
|
};
|
|
4880
3728
|
const devlog = (o) => {
|
|
4881
3729
|
try {
|
|
4882
|
-
appendFileSync(process.env["
|
|
3730
|
+
appendFileSync(process.env["ZERO_TUI_LOG"] ?? "/tmp/0-tui.log", JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), kind: "dev-engine", ...o }) + "\n");
|
|
4883
3731
|
} catch {
|
|
4884
3732
|
}
|
|
4885
3733
|
};
|
|
@@ -4946,9 +3794,25 @@ function withDevEngineUpdates(initial, config, closeStore) {
|
|
|
4946
3794
|
}
|
|
4947
3795
|
|
|
4948
3796
|
// packages/cli/src/console-session.ts
|
|
3797
|
+
var CONSOLE_JEV_FEATURES = /* @__PURE__ */ new Set([
|
|
3798
|
+
"browser",
|
|
3799
|
+
"memory",
|
|
3800
|
+
"dedupe",
|
|
3801
|
+
"redteam",
|
|
3802
|
+
"kernel",
|
|
3803
|
+
"crash",
|
|
3804
|
+
"radar",
|
|
3805
|
+
"foxguard"
|
|
3806
|
+
]);
|
|
3807
|
+
function consoleJevRuntimeFromEnvironment() {
|
|
3808
|
+
const features = [...new Set(
|
|
3809
|
+
(process.env["ZERO_JEV_FEATURES"] ?? "").split(",").map((feature) => feature.trim()).filter((feature) => CONSOLE_JEV_FEATURES.has(feature))
|
|
3810
|
+
)];
|
|
3811
|
+
return toToolContextJevRuntime(createConsoleJevRuntime({ provider: "direct", features }));
|
|
3812
|
+
}
|
|
4949
3813
|
function createLocalConsoleSession(config, dbPath) {
|
|
4950
3814
|
const db = new osecDB(dbPath);
|
|
4951
|
-
const scanId = config.scanId ?? `console-${
|
|
3815
|
+
const scanId = config.scanId ?? `console-${randomUUID()}`;
|
|
4952
3816
|
let ownsScan = false;
|
|
4953
3817
|
try {
|
|
4954
3818
|
if (!db.getScan(scanId)) {
|
|
@@ -4964,7 +3828,8 @@ function createLocalConsoleSession(config, dbPath) {
|
|
|
4964
3828
|
...config,
|
|
4965
3829
|
scanId,
|
|
4966
3830
|
db,
|
|
4967
|
-
conversationHistory: config.conversationHistory ?? createConversationHistory()
|
|
3831
|
+
conversationHistory: config.conversationHistory ?? createConversationHistory(),
|
|
3832
|
+
jevRuntime: config.jevRuntime ?? consoleJevRuntimeFromEnvironment()
|
|
4968
3833
|
};
|
|
4969
3834
|
const session = createConsoleSession(engineConfig);
|
|
4970
3835
|
return withDevEngineUpdates(session, engineConfig, (completed) => {
|
|
@@ -4984,6 +3849,46 @@ function createLocalConsoleSession(config, dbPath) {
|
|
|
4984
3849
|
}
|
|
4985
3850
|
}
|
|
4986
3851
|
|
|
3852
|
+
// packages/cli/src/tui/hosted-balance.ts
|
|
3853
|
+
function fmtUsd(usd) {
|
|
3854
|
+
return usd === null ? "unavailable" : `$${usd}`;
|
|
3855
|
+
}
|
|
3856
|
+
function hostedBalanceState(account) {
|
|
3857
|
+
if (!account) return { status: "unavailable" };
|
|
3858
|
+
const { included, prepaid } = account;
|
|
3859
|
+
const parts = [included.state === "none" ? "no included allowance" : included.state !== "unavailable" && included.usedPercent !== null ? `${included.usedPercent}% used` : "unavailable"];
|
|
3860
|
+
if (included.state !== "unavailable" && included.resetsAt) {
|
|
3861
|
+
parts.push(`resets ${included.resetsAt.slice(0, 10)}`);
|
|
3862
|
+
}
|
|
3863
|
+
if (prepaid.fallbackEnabled) parts.push(`prepaid ${fmtUsd(prepaid.balanceUsd)}`);
|
|
3864
|
+
if (account.state !== "ready") {
|
|
3865
|
+
parts.push(`${account.state}${account.reason ? `: ${account.reason}` : ""}`);
|
|
3866
|
+
} else if (!account.admission.eligible) {
|
|
3867
|
+
parts.push("access blocked");
|
|
3868
|
+
}
|
|
3869
|
+
return { status: "ready", display: parts.join(" \xB7 ") };
|
|
3870
|
+
}
|
|
3871
|
+
function formatHostedBalance(state) {
|
|
3872
|
+
if (state.status === "loading") return "Usage: Loading\u2026";
|
|
3873
|
+
if (state.status === "unavailable") return "Usage: unavailable";
|
|
3874
|
+
return `Usage: ${state.display}`;
|
|
3875
|
+
}
|
|
3876
|
+
function formatBalanceDetail(account) {
|
|
3877
|
+
if (!account) return " Usage unavailable (unsupported account data).\n";
|
|
3878
|
+
const { included, prepaid } = account;
|
|
3879
|
+
const lines = [included.state === "none" ? " Usage: no included allowance" : included.state !== "unavailable" && included.usedPercent !== null ? ` Usage: ${included.usedPercent}% used` : " Usage: unavailable"];
|
|
3880
|
+
if (included.state !== "unavailable" && included.resetsAt) {
|
|
3881
|
+
lines.push(` Resets: ${included.resetsAt}`);
|
|
3882
|
+
}
|
|
3883
|
+
if (prepaid.fallbackEnabled) lines.push(` Prepaid balance: ${fmtUsd(prepaid.balanceUsd)}`);
|
|
3884
|
+
if (account.state !== "ready" || !account.admission.eligible) {
|
|
3885
|
+
lines.push(` Access: ${account.state === "ready" ? "blocked" : account.state}`);
|
|
3886
|
+
const reason = account.admission.reason ?? account.reason;
|
|
3887
|
+
if (reason) lines.push(` Reason: ${reason}`);
|
|
3888
|
+
}
|
|
3889
|
+
return lines.join("\n") + "\n";
|
|
3890
|
+
}
|
|
3891
|
+
|
|
4987
3892
|
// packages/cli/src/tui/codex-device-auth.ts
|
|
4988
3893
|
import { spawn as spawn2 } from "node:child_process";
|
|
4989
3894
|
var MAX_VISIBLE_LINES = 8;
|
|
@@ -5001,22 +3906,39 @@ function defaultSpawnCodexDeviceAuth(command, args, options) {
|
|
|
5001
3906
|
function startCodexDeviceAuth(options) {
|
|
5002
3907
|
const env = options.env ?? process.env;
|
|
5003
3908
|
const launch = options.spawn ?? defaultSpawnCodexDeviceAuth;
|
|
3909
|
+
const openBrowser = options.openBrowser ?? defaultOpenBrowser;
|
|
5004
3910
|
const lines = [];
|
|
5005
3911
|
let pending = "";
|
|
5006
3912
|
let settled = false;
|
|
5007
3913
|
let cancelled = false;
|
|
3914
|
+
let browserOpened = false;
|
|
5008
3915
|
const publish = (phase, message) => {
|
|
5009
3916
|
options.onUpdate({ phase, lines: [...lines], message });
|
|
5010
3917
|
};
|
|
3918
|
+
const tryOpenBrowser = (text) => {
|
|
3919
|
+
if (browserOpened) return;
|
|
3920
|
+
const rawUrl = text.match(/https:\/\/auth\.openai\.com\/codex\/device[^\s\x1b]*/)?.[0];
|
|
3921
|
+
if (!rawUrl) return;
|
|
3922
|
+
const url = rawUrl.replace(/[),.;]+$/, "");
|
|
3923
|
+
browserOpened = true;
|
|
3924
|
+
void Promise.resolve(openBrowser(url)).catch(() => {
|
|
3925
|
+
lines.push("Could not open a browser automatically; open the URL above.");
|
|
3926
|
+
if (lines.length > MAX_VISIBLE_LINES) lines.shift();
|
|
3927
|
+
publish("running", "Complete the ChatGPT Codex device sign-in in your browser.");
|
|
3928
|
+
});
|
|
3929
|
+
};
|
|
5011
3930
|
const pushOutput = (chunk) => {
|
|
5012
|
-
const
|
|
5013
|
-
|
|
3931
|
+
const text = chunk.toString("utf8");
|
|
3932
|
+
tryOpenBrowser(text);
|
|
3933
|
+
const raw = `${pending}${text}`;
|
|
3934
|
+
const parts = raw.split(/\r?\n/);
|
|
5014
3935
|
pending = parts.pop() ?? "";
|
|
5015
3936
|
for (const line of parts) {
|
|
5016
|
-
const value = line
|
|
3937
|
+
const value = sanitizeTuiText(line);
|
|
5017
3938
|
if (value.length === 0) continue;
|
|
5018
3939
|
lines.push(value);
|
|
5019
3940
|
if (lines.length > MAX_VISIBLE_LINES) lines.shift();
|
|
3941
|
+
tryOpenBrowser(value);
|
|
5020
3942
|
}
|
|
5021
3943
|
publish("running", "Complete the ChatGPT Codex device sign-in in your browser.");
|
|
5022
3944
|
};
|
|
@@ -5052,7 +3974,7 @@ function startCodexDeviceAuth(options) {
|
|
|
5052
3974
|
return;
|
|
5053
3975
|
}
|
|
5054
3976
|
maybeLoadCodexAuth({ env, home: options.homeDir, force: true });
|
|
5055
|
-
if (!env["
|
|
3977
|
+
if (!env["ZERO_CHATGPT_ACCESS_TOKEN"] && !env["ZERO_CHATGPT_OAUTH_REFRESH_TOKEN"]) {
|
|
5056
3978
|
finish("failed", "Codex completed without a readable ChatGPT subscription credential.");
|
|
5057
3979
|
return;
|
|
5058
3980
|
}
|
|
@@ -5077,8 +3999,6 @@ export {
|
|
|
5077
3999
|
TIMELINE_FORMATS,
|
|
5078
4000
|
isTimelineFormat,
|
|
5079
4001
|
formatTimeline,
|
|
5080
|
-
registerHuntCommand,
|
|
5081
|
-
registerDeepReviewCommand,
|
|
5082
4002
|
resolveFindingChatIntent,
|
|
5083
4003
|
buildFindingChatPrompt,
|
|
5084
4004
|
buildFindingConsoleCommand,
|
|
@@ -5099,10 +4019,13 @@ export {
|
|
|
5099
4019
|
watchLensSynthCommand,
|
|
5100
4020
|
registerLensSynthCommand,
|
|
5101
4021
|
createLocalConsoleSession,
|
|
5102
|
-
|
|
4022
|
+
hostedBalanceState,
|
|
4023
|
+
formatHostedBalance,
|
|
4024
|
+
formatBalanceDetail,
|
|
5103
4025
|
registerAuthCommand,
|
|
5104
4026
|
hostedBrowserLoginFlow,
|
|
5105
4027
|
runLogin,
|
|
5106
4028
|
defaultOpenBrowser,
|
|
4029
|
+
startCodexDeviceAuth,
|
|
5107
4030
|
runUnified
|
|
5108
4031
|
};
|