0sec-cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0sec.js +322 -0
- package/README.md +63 -52
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-ECQG7454.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
- package/chunks/assumption-mining-BVMMUZHB.js +79 -0
- package/chunks/chunk-2CJ776PV.js +2430 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-2SANI5RH.js +4728 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-3QFDYBZQ.js +5108 -0
- package/chunks/chunk-47TQJWDH.js +3729 -0
- package/chunks/chunk-4Y4KQJXE.js +600 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-A6CLR72I.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-CLHCDHP4.js +471 -0
- package/chunks/chunk-D6S3IIPG.js +518 -0
- package/chunks/chunk-DN25OQQA.js +1070 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-H44E2CRN.js +193 -0
- package/chunks/chunk-HDWV7PGI.js +596 -0
- package/chunks/chunk-HE7LCA7G.js +381 -0
- package/chunks/chunk-HT6P7RY3.js +2597 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IORC6MQX.js +72828 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-K26SZ37E.js +5735 -0
- package/chunks/chunk-KKGE5RQE.js +1094 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-KTQDLNSR.js +1732 -0
- package/chunks/chunk-LOHTE223.js +2572 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MMLDQR4H.js +852 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-O462Y7P2.js +33019 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-QJQKHO7G.js +917 -0
- package/chunks/chunk-QKICO43A.js +808 -0
- package/chunks/chunk-QOKTUOU2.js +102 -0
- package/chunks/chunk-RDZYQQLW.js +596 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SNKTC4BP.js +1422 -0
- package/chunks/chunk-SOG2U7B3.js +662 -0
- package/chunks/chunk-SZJCPG2I.js +110 -0
- package/chunks/chunk-UJ4IK5PO.js +2010 -0
- package/chunks/chunk-UM3ZNQIM.js +9329 -0
- package/chunks/chunk-VGRDNSHA.js +3249 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WKNNZVJS.js +2798 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/commands-U3AMV5ZU.js +16795 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-26NGQFKO.js +16 -0
- package/chunks/disclose-A6AEIEDA.js +132 -0
- package/chunks/dist-FDKALV4R.js +2772 -0
- package/chunks/dist-GL66KMUX.js +74 -0
- package/chunks/dist-JR67XKYO.js +159 -0
- package/chunks/eval-runner-DDLE5RQ3.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-XP23ISN7.js +98 -0
- package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
- package/chunks/exploit-climb-2FAUP6XU.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-HWOYFKFY.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-XPELSPQQ.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-TENE3R7T.js +44 -0
- package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-NTONUDP7.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-ZUWAUWBC.js +62 -0
- package/chunks/pipeline-FCARFZU3.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-3Q7QJIOZ.js +13 -0
- package/chunks/replay-runner-RRR4E2A7.js +39 -0
- package/chunks/run-ESNN4V5W.js +32632 -0
- package/chunks/runtime-62ZQAH7H.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-BCFQYDCE.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-6KM6F6MD.js +13 -0
- package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
allowlistedChildEnv
|
|
6
|
+
} from "./chunk-KLTNTE2Z.js";
|
|
7
|
+
|
|
8
|
+
// packages/core/dist/stages/npm-detectors/osv-lookup.js
|
|
9
|
+
var DEFAULT_ENDPOINT = "https://api.osv.dev/v1/querybatch";
|
|
10
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
11
|
+
var DEFAULT_RETRIES = 1;
|
|
12
|
+
var RETRY_BACKOFF_MS = 400;
|
|
13
|
+
var OsvLookupError = class extends Error {
|
|
14
|
+
cause;
|
|
15
|
+
constructor(message, cause) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
this.name = "OsvLookupError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function deriveForkSiblings(name) {
|
|
22
|
+
const out = /* @__PURE__ */ new Set();
|
|
23
|
+
const dotted = /^([a-z0-9][a-z0-9-]*)\.[a-z0-9.-]+$/.exec(name);
|
|
24
|
+
if (dotted)
|
|
25
|
+
out.add(dotted[1]);
|
|
26
|
+
const scoped = /^@([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9._-]*)$/.exec(name);
|
|
27
|
+
if (scoped && scoped[1] !== "types")
|
|
28
|
+
out.add(scoped[2]);
|
|
29
|
+
out.delete(name);
|
|
30
|
+
return [...out];
|
|
31
|
+
}
|
|
32
|
+
function createOsvAdvisoryLookup(options = {}) {
|
|
33
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
34
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
35
|
+
const retries = options.retries ?? DEFAULT_RETRIES;
|
|
36
|
+
const deriveSiblings = options.deriveSiblings ?? deriveForkSiblings;
|
|
37
|
+
return async (name, version, _cwe) => {
|
|
38
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
39
|
+
if (typeof fetchImpl !== "function") {
|
|
40
|
+
throw new OsvLookupError("no fetch implementation available for OSV lookup");
|
|
41
|
+
}
|
|
42
|
+
const siblings = deriveSiblings(name).filter((s) => s && s !== name);
|
|
43
|
+
const queries = [
|
|
44
|
+
{ package: { ecosystem: "npm", name }, ...version ? { version } : {} },
|
|
45
|
+
...siblings.map((s) => ({ package: { ecosystem: "npm", name: s } }))
|
|
46
|
+
];
|
|
47
|
+
const data = await postBatch(fetchImpl, endpoint, { queries }, timeoutMs, retries);
|
|
48
|
+
const results = data.results ?? [];
|
|
49
|
+
const advisories = [];
|
|
50
|
+
for (const v of results[0]?.vulns ?? []) {
|
|
51
|
+
if (v?.id)
|
|
52
|
+
advisories.push(v.id);
|
|
53
|
+
}
|
|
54
|
+
siblings.forEach((sib, i) => {
|
|
55
|
+
for (const v of results[i + 1]?.vulns ?? []) {
|
|
56
|
+
if (v?.id)
|
|
57
|
+
advisories.push(`sibling:${sib}:${v.id}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return [...new Set(advisories)];
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function postBatch(fetchImpl, endpoint, body, timeoutMs, retries) {
|
|
64
|
+
let lastErr;
|
|
65
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetchImpl(endpoint, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
signal: controller.signal
|
|
74
|
+
});
|
|
75
|
+
if (res.status === 429 || res.status >= 500) {
|
|
76
|
+
lastErr = new OsvLookupError(`OSV transient status ${res.status}`);
|
|
77
|
+
if (attempt < retries) {
|
|
78
|
+
await delay(RETRY_BACKOFF_MS * (attempt + 1));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw lastErr;
|
|
82
|
+
}
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new OsvLookupError(`OSV lookup failed: HTTP ${res.status}`);
|
|
85
|
+
}
|
|
86
|
+
const json = await res.json();
|
|
87
|
+
if (!json || typeof json !== "object" || !("results" in json)) {
|
|
88
|
+
throw new OsvLookupError("OSV lookup returned a malformed response (no `results`)");
|
|
89
|
+
}
|
|
90
|
+
return json;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
lastErr = err;
|
|
93
|
+
const transient = isAbortOrNetwork(err);
|
|
94
|
+
if (transient && attempt < retries) {
|
|
95
|
+
await delay(RETRY_BACKOFF_MS * (attempt + 1));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (err instanceof OsvLookupError)
|
|
99
|
+
throw err;
|
|
100
|
+
throw new OsvLookupError(`OSV lookup errored: ${errMsg(err)}`, err);
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw new OsvLookupError(`OSV lookup exhausted retries: ${errMsg(lastErr)}`, lastErr);
|
|
106
|
+
}
|
|
107
|
+
function isAbortOrNetwork(err) {
|
|
108
|
+
if (err instanceof OsvLookupError)
|
|
109
|
+
return false;
|
|
110
|
+
const name = err?.name;
|
|
111
|
+
return name === "AbortError" || name === "TypeError" || err instanceof Error;
|
|
112
|
+
}
|
|
113
|
+
function delay(ms) {
|
|
114
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
115
|
+
}
|
|
116
|
+
function errMsg(e) {
|
|
117
|
+
return e instanceof Error ? e.message.split("\n")[0].slice(0, 160) : String(e);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// packages/core/dist/stages/npm-detectors/probe.js
|
|
121
|
+
import { createRequire } from "node:module";
|
|
122
|
+
import { pathToFileURL } from "node:url";
|
|
123
|
+
function inProcessProbe(pkg, baseDir, note) {
|
|
124
|
+
const req = createRequire(pathToFileURL(baseDir.endsWith("/") ? baseDir : `${baseDir}/`).href);
|
|
125
|
+
return {
|
|
126
|
+
pkg,
|
|
127
|
+
baseDir,
|
|
128
|
+
load(moduleId) {
|
|
129
|
+
try {
|
|
130
|
+
return req(moduleId);
|
|
131
|
+
} catch {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
note
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function staticProbe(pkg, modules, note) {
|
|
139
|
+
return {
|
|
140
|
+
pkg,
|
|
141
|
+
baseDir: "/dev/null",
|
|
142
|
+
load(moduleId) {
|
|
143
|
+
return Object.prototype.hasOwnProperty.call(modules, moduleId) ? modules[moduleId] : void 0;
|
|
144
|
+
},
|
|
145
|
+
note
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// packages/core/dist/stages/npm-detectors/sandbox-probe.js
|
|
150
|
+
import { execFile } from "node:child_process";
|
|
151
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
152
|
+
import { tmpdir } from "node:os";
|
|
153
|
+
import { join } from "node:path";
|
|
154
|
+
import { fileURLToPath } from "node:url";
|
|
155
|
+
var DEFAULT_INSTALL_TIMEOUT_MS = 18e4;
|
|
156
|
+
var DEFAULT_RUN_TIMEOUT_MS = 12e4;
|
|
157
|
+
function defaultHarnessPath() {
|
|
158
|
+
return fileURLToPath(new URL("./sandbox-harness.js", import.meta.url));
|
|
159
|
+
}
|
|
160
|
+
function localSandboxProvider() {
|
|
161
|
+
return {
|
|
162
|
+
async create() {
|
|
163
|
+
const workdir = await mkdtemp(join(tmpdir(), "0sec-npm-dyn-"));
|
|
164
|
+
return {
|
|
165
|
+
workdir,
|
|
166
|
+
run(cmd, args, runOpts) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
execFile(cmd, args, {
|
|
169
|
+
cwd: runOpts?.cwd ?? workdir,
|
|
170
|
+
timeout: runOpts?.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS,
|
|
171
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
172
|
+
// This child runs UNTRUSTED npm package code (install + harness
|
|
173
|
+
// exec). Inheriting process.env would hand every provider /
|
|
174
|
+
// cloud credential to attacker-authored package code via one
|
|
175
|
+
// `process.env` read. Build from the allowlist instead; a fresh
|
|
176
|
+
// npm install needs only PATH/HOME/TMPDIR, all carried there.
|
|
177
|
+
env: allowlistedChildEnv(runOpts?.env ?? {})
|
|
178
|
+
}, (err, stdout, stderr) => {
|
|
179
|
+
const anyErr = err;
|
|
180
|
+
if (anyErr && typeof anyErr.code !== "number") {
|
|
181
|
+
reject(anyErr);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
resolve({
|
|
185
|
+
stdout: stdout?.toString() ?? "",
|
|
186
|
+
stderr: stderr?.toString() ?? "",
|
|
187
|
+
exitCode: typeof anyErr?.code === "number" ? anyErr.code : 0
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
async dispose() {
|
|
193
|
+
await rm(workdir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function parseHarnessResult(stdout) {
|
|
200
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
201
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
202
|
+
const line = lines[i];
|
|
203
|
+
if (!line.startsWith("{"))
|
|
204
|
+
continue;
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(line);
|
|
207
|
+
if (parsed && Array.isArray(parsed.outcomes)) {
|
|
208
|
+
return { outcomes: parsed.outcomes, warnings: Array.isArray(parsed.warnings) ? parsed.warnings : [] };
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
function createSandboxPackageRunner(options = {}) {
|
|
216
|
+
const provider = options.provider ?? localSandboxProvider();
|
|
217
|
+
const harnessPath = options.harnessPath ?? defaultHarnessPath();
|
|
218
|
+
const nodeBin = options.nodeBin ?? process.execPath;
|
|
219
|
+
const installTimeoutMs = options.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
|
|
220
|
+
const runTimeoutMs = options.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
|
|
221
|
+
const log = options.log ?? (() => {
|
|
222
|
+
});
|
|
223
|
+
return async function sandboxRunner(pkg, detectorIds) {
|
|
224
|
+
let session;
|
|
225
|
+
try {
|
|
226
|
+
session = await provider.create();
|
|
227
|
+
} catch (e) {
|
|
228
|
+
log(`SKIP ${pkg.name}: sandbox create failed: ${errMsg2(e)}`);
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
let result;
|
|
232
|
+
try {
|
|
233
|
+
const spec = pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
|
|
234
|
+
const install = await session.run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--no-package-lock", "--prefix", session.workdir, spec], { cwd: session.workdir, timeoutMs: installTimeoutMs });
|
|
235
|
+
if (install.exitCode !== 0) {
|
|
236
|
+
log(`SKIP ${pkg.name}: npm install exited ${install.exitCode}: ${firstLine(install.stderr)}`);
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
const config = JSON.stringify({
|
|
240
|
+
installDir: session.workdir,
|
|
241
|
+
pkg,
|
|
242
|
+
detectorIds,
|
|
243
|
+
offlineDedup: options.offlineDedup ?? false
|
|
244
|
+
});
|
|
245
|
+
const harness = await session.run(nodeBin, [harnessPath, config], {
|
|
246
|
+
cwd: session.workdir,
|
|
247
|
+
timeoutMs: runTimeoutMs
|
|
248
|
+
});
|
|
249
|
+
if (harness.exitCode !== 0) {
|
|
250
|
+
log(`SKIP ${pkg.name}: harness exited ${harness.exitCode}: ${firstLine(harness.stderr)}`);
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
const parsed = parseHarnessResult(harness.stdout);
|
|
254
|
+
if (!parsed) {
|
|
255
|
+
log(`SKIP ${pkg.name}: harness produced no parseable result`);
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
result = parsed;
|
|
259
|
+
} catch (e) {
|
|
260
|
+
log(`SKIP ${pkg.name}: sandbox run threw: ${errMsg2(e)}`);
|
|
261
|
+
result = void 0;
|
|
262
|
+
} finally {
|
|
263
|
+
try {
|
|
264
|
+
await session.dispose();
|
|
265
|
+
} catch (e) {
|
|
266
|
+
log(`WARN ${pkg.name}: sandbox dispose failed: ${errMsg2(e)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function errMsg2(e) {
|
|
273
|
+
return e instanceof Error ? e.message.split("\n")[0].slice(0, 200) : String(e);
|
|
274
|
+
}
|
|
275
|
+
function firstLine(s) {
|
|
276
|
+
return (s.split("\n").find(Boolean) ?? "").slice(0, 200);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export {
|
|
280
|
+
OsvLookupError,
|
|
281
|
+
deriveForkSiblings,
|
|
282
|
+
createOsvAdvisoryLookup,
|
|
283
|
+
inProcessProbe,
|
|
284
|
+
staticProbe,
|
|
285
|
+
localSandboxProvider,
|
|
286
|
+
createSandboxPackageRunner
|
|
287
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
LlmApiRuntime
|
|
6
|
+
} from "./chunk-2SANI5RH.js";
|
|
7
|
+
|
|
8
|
+
// packages/core/dist/stages/variant-candidates.js
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
// packages/core/dist/stages/hunt-reachability.js
|
|
12
|
+
var UNREACHABLE_PATH_PREFIXES = [
|
|
13
|
+
// ── Not built on COS (kernelctf-config.ts COS_CONFIG_STATE) ──
|
|
14
|
+
"net/kcm/",
|
|
15
|
+
// CONFIG_AF_KCM not-built — the original "kcm lesson"
|
|
16
|
+
"net/bluetooth/",
|
|
17
|
+
// CONFIG_BT not-built
|
|
18
|
+
"net/can/",
|
|
19
|
+
// CONFIG_CAN not-built
|
|
20
|
+
"fs/f2fs/",
|
|
21
|
+
// CONFIG_F2FS_FS not-built
|
|
22
|
+
// ── Exotic/unbuilt drivers (observed this session; same story, not yet
|
|
23
|
+
// in kernelctf-config.ts's token map) ──
|
|
24
|
+
"drivers/infiniband/",
|
|
25
|
+
// RXE/infiniband — not built on COS
|
|
26
|
+
"drivers/net/wireless/",
|
|
27
|
+
// mwifiex etc. — not built on COS
|
|
28
|
+
"drivers/media/",
|
|
29
|
+
// exotic media drivers — not built on COS
|
|
30
|
+
"drivers/hid/",
|
|
31
|
+
// hidraw — not built on COS
|
|
32
|
+
"drivers/usb/gadget/",
|
|
33
|
+
// USB gadget — not built / not a zero-cap network path
|
|
34
|
+
"drivers/usb/atm/",
|
|
35
|
+
// uea/ueagle-atm — not built on COS
|
|
36
|
+
"drivers/xen/",
|
|
37
|
+
// xen paravirt — COS is not a Xen guest
|
|
38
|
+
"drivers/staging/",
|
|
39
|
+
// staging tree — not built on COS, churny/unstable
|
|
40
|
+
// ── Exotic filesystems: the trigger is mount(2), which needs
|
|
41
|
+
// CAP_SYS_ADMIN a zero-cap kernelCTF attacker doesn't have (mirrors
|
|
42
|
+
// kernelctf-config.ts's ext4/xfs/btrfs/ntfs3/fuse `needsCap:
|
|
43
|
+
// CAP_SYS_ADMIN` entries); these ones aren't built on COS at all either ──
|
|
44
|
+
"fs/reiserfs/",
|
|
45
|
+
"fs/hpfs/",
|
|
46
|
+
"fs/sysv/",
|
|
47
|
+
"fs/ntfs/",
|
|
48
|
+
// legacy ntfs (distinct from the ntfs3 module)
|
|
49
|
+
"fs/jfs/"
|
|
50
|
+
];
|
|
51
|
+
var REACHABLE_PATH_PREFIXES = [
|
|
52
|
+
"crypto/",
|
|
53
|
+
// CONFIG_CRYPTO_USER_API_{AEAD,SKCIPHER,HASH} — AF_ALG, zero-cap
|
|
54
|
+
"net/unix/",
|
|
55
|
+
// CONFIG_UNIX — AF_UNIX, zero-cap
|
|
56
|
+
"net/core/",
|
|
57
|
+
// core socket/skb plumbing, zero-cap from any AF_* socket
|
|
58
|
+
"net/ipv4/",
|
|
59
|
+
// core IPv4, zero-cap
|
|
60
|
+
"net/ipv6/",
|
|
61
|
+
// core IPv6, zero-cap
|
|
62
|
+
// CONFIG_PACKET — kernelctf-config.ts marks needsCap CAP_NET_RAW for
|
|
63
|
+
// AF_PACKET itself; still listed reachable here (worth the finder budget).
|
|
64
|
+
"net/packet/",
|
|
65
|
+
"net/vmw_vsock/",
|
|
66
|
+
// CONFIG_VSOCKETS(+_LOOPBACK) — AF_VSOCK loopback, zero-cap
|
|
67
|
+
"net/tls/",
|
|
68
|
+
// CONFIG_TLS — kTLS, zero-cap
|
|
69
|
+
"net/xfrm/",
|
|
70
|
+
// IPsec policy/state — reachable via standard AF_INET{,6} sockets
|
|
71
|
+
"net/netlink/",
|
|
72
|
+
// AF_NETLINK core — zero-cap socket family
|
|
73
|
+
"ipc/",
|
|
74
|
+
// CONFIG_SYSVIPC — System-V IPC (msg/sem/shm), zero-cap
|
|
75
|
+
// CONFIG_EXT4_FS / CONFIG_XFS_FS — kernelctf-config.ts marks needsCap
|
|
76
|
+
// CAP_SYS_ADMIN for the *mount* trigger; still listed reachable here (core
|
|
77
|
+
// FS code is real audit surface, worth the finder budget).
|
|
78
|
+
"fs/ext4/",
|
|
79
|
+
"fs/xfs/",
|
|
80
|
+
// unprivileged_bpf_disabled is commonly runtime-off; listed reachable here
|
|
81
|
+
// rather than dropped outright (flag the caveat in the finder hint instead).
|
|
82
|
+
"kernel/bpf/",
|
|
83
|
+
"security/keys/"
|
|
84
|
+
// CONFIG_KEYS — keyrings / add_key(2), zero-cap
|
|
85
|
+
];
|
|
86
|
+
function hasPathSegmentPrefix(path, prefix) {
|
|
87
|
+
const idx = path.indexOf(prefix);
|
|
88
|
+
if (idx === -1)
|
|
89
|
+
return false;
|
|
90
|
+
return idx === 0 || path[idx - 1] === "/";
|
|
91
|
+
}
|
|
92
|
+
function classifyPathReachability(path) {
|
|
93
|
+
const norm = path.replace(/\\/g, "/");
|
|
94
|
+
for (const prefix of UNREACHABLE_PATH_PREFIXES)
|
|
95
|
+
if (hasPathSegmentPrefix(norm, prefix))
|
|
96
|
+
return "unreachable";
|
|
97
|
+
for (const prefix of REACHABLE_PATH_PREFIXES)
|
|
98
|
+
if (hasPathSegmentPrefix(norm, prefix))
|
|
99
|
+
return "reachable";
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
function applyReachabilityGate(paths, opts) {
|
|
103
|
+
if (!opts.reachableOnly && !opts.reachablePrefer)
|
|
104
|
+
return { paths, unreachableCount: 0 };
|
|
105
|
+
const classified = paths.map((path) => ({ path, cls: classifyPathReachability(path) }));
|
|
106
|
+
const unreachableCount = classified.filter((c) => c.cls === "unreachable").length;
|
|
107
|
+
if (opts.reachableOnly) {
|
|
108
|
+
return { paths: classified.filter((c) => c.cls === "reachable").map((c) => c.path), unreachableCount };
|
|
109
|
+
}
|
|
110
|
+
const rank = { reachable: 0, unknown: 1, unreachable: 2 };
|
|
111
|
+
const sorted = [...classified].sort((a, b) => rank[a.cls] - rank[b.cls]).map((c) => c.path);
|
|
112
|
+
return { paths: sorted, unreachableCount };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// packages/core/dist/stages/variant-candidates.js
|
|
116
|
+
var clip = (s, n) => s.length > n ? s.slice(0, n) + `
|
|
117
|
+
...[truncated ${s.length - n} chars]` : s;
|
|
118
|
+
function fixedPathsFromDiff(diff) {
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const line of diff.split("\n")) {
|
|
121
|
+
const m = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
122
|
+
if (m && m[1] !== "/dev/null")
|
|
123
|
+
out.push(m[1].trim());
|
|
124
|
+
}
|
|
125
|
+
return [...new Set(out)];
|
|
126
|
+
}
|
|
127
|
+
function grepFiles(pattern, sourceRoot, includeGlobs) {
|
|
128
|
+
const args = ["-rlE", ...includeGlobs.map((g) => `--include=${g}`), "--", pattern, "."];
|
|
129
|
+
try {
|
|
130
|
+
const out = execFileSync("grep", args, { cwd: sourceRoot, encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 6e4 });
|
|
131
|
+
return out.split("\n").map((s) => s.replace(/^\.\//, "").trim()).filter(Boolean);
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function generateVariantCandidates(input) {
|
|
137
|
+
const log = input.log ?? (() => {
|
|
138
|
+
});
|
|
139
|
+
const warnings = [];
|
|
140
|
+
const maxCandidates = input.maxCandidates ?? 40;
|
|
141
|
+
const includeGlobs = input.includeGlobs ?? ["*.c", "*.h", "*.cc", "*.cpp"];
|
|
142
|
+
let diff = input.fix.diff ?? "";
|
|
143
|
+
if (!diff && input.fix.commit) {
|
|
144
|
+
try {
|
|
145
|
+
diff = execFileSync("git", ["-C", input.sourceRoot, "show", "--no-color", input.fix.commit], {
|
|
146
|
+
encoding: "utf8",
|
|
147
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
148
|
+
timeout: 3e4
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw new Error(`could not read fix commit ${input.fix.commit}: ${String(e).slice(0, 160)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!diff.trim())
|
|
155
|
+
throw new Error("variant hunt needs fix.diff or fix.commit");
|
|
156
|
+
const fixedPaths = fixedPathsFromDiff(diff);
|
|
157
|
+
const tool = {
|
|
158
|
+
name: "emit_variant_plan",
|
|
159
|
+
description: "Emit the variant-hunt plan derived from the fix diff.",
|
|
160
|
+
input_schema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
bugClass: { type: "string", description: "The bug class the fix closed, e.g. 'missing bounds check before a TLV copy'." },
|
|
164
|
+
pattern: { type: "string", description: "The exact unguarded code shape (the sink) and how the fix guarded it." },
|
|
165
|
+
grepPatterns: {
|
|
166
|
+
type: "array",
|
|
167
|
+
items: { type: "string" },
|
|
168
|
+
description: "2-6 grep ERE patterns that locate the SAME sink shape at OTHER call-sites/files (the variant sites where the fix was NOT applied). Match the sink/call, not the added guard."
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
required: ["bugClass", "pattern", "grepPatterns"]
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const system = "You are a kernel/security VARIANT-HUNT analyst. Given a security fix diff, identify the bug class and the unguarded code SHAPE (the sink) the fix added a guard around. Then write grep EXTENDED-REGEX patterns that find the SAME bug CLASS at OTHER call-sites across the tree \u2014 where an INCOMPLETE fix or an independent instance lives.\nCRITICAL \u2014 match the CLASS, not the original site: the patched file is EXCLUDED from results, so a pattern that hardcodes the original's specific identifiers (field names like `sensf_res`, struct names like `digital_*`, the variable `resp`) will match ONLY the original and yield ZERO variants. WILDCARD the identifiers. Match the dangerous SHAPE \u2014 e.g. a length-controlled copy into a fixed buffer regardless of names: `memcpy\\([^,]+,[^,]+,[^)]*->len\\)`, or an unchecked `->len`/`->size` used as a copy/index bound. Match the sink, NOT the guard that was added.\nEmit 3-6 patterns ranging from moderately specific (the same struct-field-copy shape with wildcarded names) to general (any length-bounded memcpy/copy_from of that family). Aim for tens of hits to triage, not zero and not thousands. Call emit_variant_plan.";
|
|
175
|
+
const messages = [{ role: "user", content: [{ type: "text", text: `## Fix diff
|
|
176
|
+
${clip(diff, 24e3)}` }] }];
|
|
177
|
+
const rt = new LlmApiRuntime({ type: "api", ...input.model ? { model: input.model } : {}, timeout: 24e4 });
|
|
178
|
+
let plan = null;
|
|
179
|
+
try {
|
|
180
|
+
const res = await rt.executeNative(system, messages, [tool], { onThinking() {
|
|
181
|
+
}, onDelta() {
|
|
182
|
+
}, onText() {
|
|
183
|
+
}, onUsage() {
|
|
184
|
+
} });
|
|
185
|
+
const call = (res.content ?? []).find((b) => b.type === "tool_use" && b.name === "emit_variant_plan");
|
|
186
|
+
if (call?.input)
|
|
187
|
+
plan = call.input;
|
|
188
|
+
} catch (e) {
|
|
189
|
+
throw new Error(`variant-plan LLM call failed: ${String(e).slice(0, 200)}`);
|
|
190
|
+
}
|
|
191
|
+
if (!plan || !Array.isArray(plan.grepPatterns) || plan.grepPatterns.length === 0) {
|
|
192
|
+
throw new Error("model did not emit a usable variant plan");
|
|
193
|
+
}
|
|
194
|
+
log(`[variant] bug class: ${plan.bugClass}`);
|
|
195
|
+
log(`[variant] ${plan.grepPatterns.length} grep pattern(s): ${plan.grepPatterns.join(" | ")}`);
|
|
196
|
+
const fixedSet = new Set(fixedPaths);
|
|
197
|
+
const hits = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const pat of plan.grepPatterns) {
|
|
199
|
+
const files = grepFiles(pat, input.sourceRoot, includeGlobs);
|
|
200
|
+
if (files.length === 0)
|
|
201
|
+
warnings.push(`pattern matched nothing (or invalid): ${pat}`);
|
|
202
|
+
for (const f of files)
|
|
203
|
+
if (!fixedSet.has(f))
|
|
204
|
+
hits.set(f, (hits.get(f) ?? 0) + 1);
|
|
205
|
+
}
|
|
206
|
+
const ranked = [...hits.entries()].sort((a, b) => b[1] - a[1]).map(([path]) => path);
|
|
207
|
+
const gated = input.reachableOnly || input.reachablePrefer ? applyReachabilityGate(ranked, { reachableOnly: input.reachableOnly, reachablePrefer: input.reachablePrefer }) : { paths: ranked, unreachableCount: 0 };
|
|
208
|
+
if (gated.unreachableCount > 0) {
|
|
209
|
+
const verb = input.reachableOnly ? "dropped" : "deprioritized";
|
|
210
|
+
const msg = `${verb} ${gated.unreachableCount} unreachable candidate(s) (not built/zero-cap on kernelCTF COS \u2014 see hunt-reachability.ts)`;
|
|
211
|
+
warnings.push(msg);
|
|
212
|
+
log(`[variant] ${msg}`);
|
|
213
|
+
}
|
|
214
|
+
if (gated.paths.length > maxCandidates) {
|
|
215
|
+
warnings.push(`capped candidates ${gated.paths.length} -> ${maxCandidates} (raise maxCandidates to widen)`);
|
|
216
|
+
}
|
|
217
|
+
const candidates = gated.paths.slice(0, maxCandidates).map((path) => ({
|
|
218
|
+
path,
|
|
219
|
+
hint: `Variant site for: ${plan.bugClass}. Check whether ${plan.pattern} is guarded here.`
|
|
220
|
+
}));
|
|
221
|
+
log(`[variant] ${candidates.length} candidate site(s) after dedup/exclude`);
|
|
222
|
+
return {
|
|
223
|
+
brief: { bugClass: plan.bugClass, pattern: plan.pattern, fixReference: input.fix.reference ?? input.fix.commit },
|
|
224
|
+
candidates,
|
|
225
|
+
grepPatterns: plan.grepPatterns,
|
|
226
|
+
fixedPaths,
|
|
227
|
+
warnings
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
classifyPathReachability,
|
|
233
|
+
applyReachabilityGate,
|
|
234
|
+
generateVariantCandidates
|
|
235
|
+
};
|