0sec-cli 0.15.0 → 0.18.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 +327 -0
- package/README.md +68 -48
- 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-SMH6V4PD.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-HQ7HLP6V.js +34 -0
- package/chunks/assumption-mining-5SNIVVDN.js +79 -0
- package/chunks/chunk-242TMK6G.js +110 -0
- package/chunks/chunk-2JCCA2JL.js +381 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-3KPWWJCI.js +1422 -0
- package/chunks/chunk-3MOLBTLS.js +953 -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-5JI7L7KV.js +74240 -0
- package/chunks/chunk-6L26OBWI.js +1094 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-72CISA2D.js +2597 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-AD7WXDH6.js +3250 -0
- package/chunks/chunk-ADOLI6DT.js +102 -0
- package/chunks/chunk-AHTBZC3E.js +596 -0
- package/chunks/chunk-ASUR522M.js +518 -0
- package/chunks/chunk-ATATQACO.js +471 -0
- package/chunks/chunk-BATRQBOR.js +1732 -0
- package/chunks/chunk-BFR2CDV5.js +5735 -0
- package/chunks/chunk-BFRB6HB2.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-BVAZO4WA.js +193 -0
- package/chunks/chunk-BZNEY2ZS.js +33225 -0
- package/chunks/chunk-C2WXWFBB.js +1070 -0
- package/chunks/chunk-CMKXP5RR.js +917 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-GMT2AZKM.js +2439 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-JQHOLLTD.js +3729 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-N7BOUYEV.js +852 -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-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-SVJEDVYK.js +808 -0
- package/chunks/chunk-TSK2AG6J.js +5108 -0
- package/chunks/chunk-U5FLXGHT.js +2010 -0
- package/chunks/chunk-UCYRA73C.js +662 -0
- package/chunks/chunk-UR4ELX2Y.js +4784 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WEBMRWEG.js +9337 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-WXQ6BJ6A.js +596 -0
- package/chunks/chunk-XM5NLHYU.js +600 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/chunk-YYRBQVFE.js +2572 -0
- package/chunks/chunk-Z5FA2XOX.js +2798 -0
- package/chunks/commands-TBC2F5CE.js +16999 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-NU63MYZM.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-KFWOAUUB.js +16 -0
- package/chunks/disclose-EKTWSCGW.js +132 -0
- package/chunks/dist-4MAW5X6L.js +74 -0
- package/chunks/dist-4YICS2J2.js +2776 -0
- package/chunks/dist-FMGSR3DW.js +159 -0
- package/chunks/eval-runner-7ZF472KV.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-45YQOHPJ.js +98 -0
- package/chunks/exploit-autoclimb-UZGEMGYT.js +81 -0
- package/chunks/exploit-climb-SG23KB2G.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-7ODOLOWU.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-MMKUOETU.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-7U6CWHXD.js +44 -0
- package/chunks/kernel-vm-runner-3JAU4O6J.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-RJCJPOUS.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-OC2VZN2X.js +62 -0
- package/chunks/pipeline-BZK5BLJR.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-LQRCS5OY.js +13 -0
- package/chunks/replay-runner-45G3VU7X.js +39 -0
- package/chunks/run-D56MPDKY.js +32716 -0
- package/chunks/runtime-G7YWIO3B.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-GZ4A4KYT.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-YERCYMPD.js +13 -0
- package/chunks/web-recon-prepass-JJGPEOLE.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,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-UR4ELX2Y.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
|
+
};
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
collectScopeFiles
|
|
6
|
+
} from "./chunk-53G27VPS.js";
|
|
7
|
+
|
|
8
|
+
// packages/core/dist/specdrift/extract.js
|
|
9
|
+
var MODAL_RE = /\b(MUST(?:\s+NOT)?|SHALL(?:\s+NOT)?|SHOULD(?:\s+NOT)?|MAY|REQUIRED|OPTIONAL)\b/;
|
|
10
|
+
var RANGE_RE = /\b(?:between|minimum|maximum|at least|at most|less than|greater than|[0-9]+\s*(?:-|to)\s*[0-9]+|0x[0-9a-f]+)\b/i;
|
|
11
|
+
var LENGTH_RE = /\b(length|size|bytes?|octets?|bits?|payload|frame size|field)\b/i;
|
|
12
|
+
var STATE_RE = /\b(state|transition|handshake|stream|session|before|after|until|when|while|once)\b/i;
|
|
13
|
+
var REJECT_RE = /\b(reject(?:ed|s|ing)?|error|invalid|malformed|abort(?:ed|s|ing)?|terminate(?:d|s|ing)?|discard(?:ed|s|ing)?|fail(?:ed|s|ing)?|close(?:d|s|ing)?)\b/i;
|
|
14
|
+
var ORDER_RE = /\b(before|after|first|then|prior to|followed by|precede|order)\b/i;
|
|
15
|
+
var CANON_RE = /\b(canonical|normalize|normalise|case-sensitive|case-insensitive|duplicate|unique)\b/i;
|
|
16
|
+
function hasNormativeSignal(text) {
|
|
17
|
+
return MODAL_RE.test(text) || REJECT_RE.test(text) || STATE_RE.test(text) && (RANGE_RE.test(text) || LENGTH_RE.test(text));
|
|
18
|
+
}
|
|
19
|
+
function slug(s) {
|
|
20
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || "rule";
|
|
21
|
+
}
|
|
22
|
+
function splitLines(specText) {
|
|
23
|
+
const out = [];
|
|
24
|
+
let section;
|
|
25
|
+
const lines = specText.replace(/\r\n/g, "\n").split("\n");
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const raw = lines[i] ?? "";
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
const mdHeading = /^#{1,6}\s+(.+)$/.exec(trimmed);
|
|
30
|
+
const numberedHeading = /^(\d+(?:\.\d+)*\.?\s+.+)$/.exec(trimmed);
|
|
31
|
+
if (mdHeading?.[1])
|
|
32
|
+
section = mdHeading[1].trim();
|
|
33
|
+
else if (numberedHeading?.[1] && trimmed.length < 120)
|
|
34
|
+
section = numberedHeading[1].trim();
|
|
35
|
+
out.push({ n: i + 1, text: raw, ...section ? { section } : {} });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function sentences(lines) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let buf = "";
|
|
42
|
+
let start = 1;
|
|
43
|
+
let section;
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const trimmed = line.text.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
continue;
|
|
48
|
+
if (/^#{1,6}\s+/.test(trimmed) || /^(\d+(?:\.\d+)*\.?\s+.+)$/.test(trimmed))
|
|
49
|
+
continue;
|
|
50
|
+
if (!buf && !hasNormativeSignal(trimmed))
|
|
51
|
+
continue;
|
|
52
|
+
if (!buf) {
|
|
53
|
+
start = line.n;
|
|
54
|
+
section = line.section;
|
|
55
|
+
}
|
|
56
|
+
buf = buf ? `${buf} ${trimmed}` : trimmed;
|
|
57
|
+
if (/[.!?]$/.test(trimmed) || buf.length > 420) {
|
|
58
|
+
out.push({ n: start, text: buf, ...section ? { section } : {} });
|
|
59
|
+
buf = "";
|
|
60
|
+
section = void 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (buf)
|
|
64
|
+
out.push({ n: start, text: buf, ...section ? { section } : {} });
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function kindFor(text) {
|
|
68
|
+
if (REJECT_RE.test(text))
|
|
69
|
+
return "rejection";
|
|
70
|
+
if (STATE_RE.test(text))
|
|
71
|
+
return "state";
|
|
72
|
+
if (RANGE_RE.test(text))
|
|
73
|
+
return "range";
|
|
74
|
+
if (LENGTH_RE.test(text))
|
|
75
|
+
return "length";
|
|
76
|
+
if (ORDER_RE.test(text))
|
|
77
|
+
return "ordering";
|
|
78
|
+
if (CANON_RE.test(text))
|
|
79
|
+
return "canonicalization";
|
|
80
|
+
return "requirement";
|
|
81
|
+
}
|
|
82
|
+
function relevanceFor(kind, text) {
|
|
83
|
+
if (/\b(auth|encrypt|signature|certificate|key|nonce|token|permission|credential)\b/i.test(text))
|
|
84
|
+
return "high";
|
|
85
|
+
if (kind === "state" || kind === "rejection")
|
|
86
|
+
return "high";
|
|
87
|
+
if (kind === "range" || kind === "length")
|
|
88
|
+
return "medium";
|
|
89
|
+
return "low";
|
|
90
|
+
}
|
|
91
|
+
function subjectFor(text) {
|
|
92
|
+
const match = /(?:the|a|an)\s+([A-Za-z0-9_.-]+(?:\s+[A-Za-z0-9_.-]+){0,4})\s+(?:MUST|SHALL|SHOULD|MAY|is|required|contains|has)\b/i.exec(text);
|
|
93
|
+
return match?.[1]?.trim();
|
|
94
|
+
}
|
|
95
|
+
function summarize(text) {
|
|
96
|
+
return text.replace(/\s+/g, " ").slice(0, 180).replace(/[.;:,\s]+$/g, "");
|
|
97
|
+
}
|
|
98
|
+
function extractSpecInvariants(opts) {
|
|
99
|
+
const max = opts.maxInvariants ?? 40;
|
|
100
|
+
const warnings = [];
|
|
101
|
+
const lines = splitLines(opts.specText);
|
|
102
|
+
const candidates = sentences(lines).filter((s) => hasNormativeSignal(s.text)).slice(0, max);
|
|
103
|
+
if (candidates.length === max)
|
|
104
|
+
warnings.push(`capped invariants at ${max}; raise --max-invariants to widen extraction`);
|
|
105
|
+
if (candidates.length === 0)
|
|
106
|
+
warnings.push("no normative invariants found; spec may need OCR cleanup or LLM extraction");
|
|
107
|
+
const invariants = candidates.map((c, idx) => {
|
|
108
|
+
const kind = kindFor(c.text);
|
|
109
|
+
const summary = summarize(c.text);
|
|
110
|
+
return {
|
|
111
|
+
id: `inv-${String(idx + 1).padStart(3, "0")}-${slug(summary)}`,
|
|
112
|
+
kind,
|
|
113
|
+
summary,
|
|
114
|
+
rule: c.text,
|
|
115
|
+
...subjectFor(c.text) ? { subject: subjectFor(c.text) } : {},
|
|
116
|
+
citations: [{ spec: opts.specName, ...c.section ? { section: c.section } : {}, lineStart: c.n, lineEnd: c.n, text: c.text }],
|
|
117
|
+
securityRelevance: relevanceFor(kind, c.text)
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
mode: "specdrift",
|
|
122
|
+
stage: "extract",
|
|
123
|
+
spec: opts.specName,
|
|
124
|
+
invariants,
|
|
125
|
+
warnings
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// packages/core/dist/specdrift/map.js
|
|
130
|
+
import { readFileSync } from "node:fs";
|
|
131
|
+
import { relative } from "node:path";
|
|
132
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
133
|
+
"the",
|
|
134
|
+
"and",
|
|
135
|
+
"that",
|
|
136
|
+
"this",
|
|
137
|
+
"with",
|
|
138
|
+
"from",
|
|
139
|
+
"when",
|
|
140
|
+
"while",
|
|
141
|
+
"must",
|
|
142
|
+
"shall",
|
|
143
|
+
"should",
|
|
144
|
+
"not",
|
|
145
|
+
"may",
|
|
146
|
+
"been",
|
|
147
|
+
"have",
|
|
148
|
+
"has",
|
|
149
|
+
"are",
|
|
150
|
+
"is",
|
|
151
|
+
"be",
|
|
152
|
+
"for",
|
|
153
|
+
"into",
|
|
154
|
+
"whose",
|
|
155
|
+
"their",
|
|
156
|
+
"there",
|
|
157
|
+
"then",
|
|
158
|
+
"than",
|
|
159
|
+
"frame",
|
|
160
|
+
"field",
|
|
161
|
+
"message",
|
|
162
|
+
"payload",
|
|
163
|
+
"implementation",
|
|
164
|
+
"implementations"
|
|
165
|
+
]);
|
|
166
|
+
var KIND_HINTS = {
|
|
167
|
+
range: [/\b(?:len|length|size|limit|max|min|range|bounds?)\b/i, /[<>]=?|\bbetween\b/i],
|
|
168
|
+
length: [/\b(?:len|length|size|payload|bytes?|octets?)\b/i, /[<>]=?/],
|
|
169
|
+
ordering: [/\b(?:before|after|state|phase|order|seq|sequence)\b/i],
|
|
170
|
+
state: [/\b(?:state|phase|handshake|session|stream|open|closed|init)\b/i],
|
|
171
|
+
rejection: [/\b(?:reject|invalid|error|fail|abort|discard|close|return|throw)\b/i],
|
|
172
|
+
canonicalization: [/\b(?:canonical|normalize|normalise|lower|upper|duplicate|unique)\b/i],
|
|
173
|
+
requirement: [/\b(?:validate|check|require|assert|if|return|throw)\b/i]
|
|
174
|
+
};
|
|
175
|
+
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
176
|
+
".c",
|
|
177
|
+
".h",
|
|
178
|
+
".cc",
|
|
179
|
+
".cpp",
|
|
180
|
+
".cxx",
|
|
181
|
+
".hpp",
|
|
182
|
+
".rs",
|
|
183
|
+
".go",
|
|
184
|
+
".java",
|
|
185
|
+
".kt",
|
|
186
|
+
".py",
|
|
187
|
+
".js",
|
|
188
|
+
".ts",
|
|
189
|
+
".jsx",
|
|
190
|
+
".tsx"
|
|
191
|
+
]);
|
|
192
|
+
function termsFor(invariant) {
|
|
193
|
+
const raw = `${invariant.subject ?? ""} ${invariant.summary} ${invariant.rule}`.toLowerCase().replace(/[`*_]/g, " ").match(/[a-z][a-z0-9-]{2,}/g) ?? [];
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const term of raw) {
|
|
196
|
+
const normalized = term.replace(/-/g, "");
|
|
197
|
+
if (STOPWORDS.has(term) || STOPWORDS.has(normalized))
|
|
198
|
+
continue;
|
|
199
|
+
if (!out.includes(term))
|
|
200
|
+
out.push(term);
|
|
201
|
+
if (out.length >= 12)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function snippet(lines, line) {
|
|
207
|
+
const start = Math.max(1, line - 2);
|
|
208
|
+
const end = Math.min(lines.length, line + 2);
|
|
209
|
+
const text = lines.slice(start - 1, end).map((l, idx) => `${start + idx}: ${l}`).join("\n");
|
|
210
|
+
return { lineStart: start, lineEnd: end, text };
|
|
211
|
+
}
|
|
212
|
+
function scoreLine(invariant, line, terms) {
|
|
213
|
+
const lowered = line.toLowerCase();
|
|
214
|
+
const matchedTerms = terms.filter((term) => lowered.includes(term));
|
|
215
|
+
let score = matchedTerms.length;
|
|
216
|
+
for (const hint of KIND_HINTS[invariant.kind])
|
|
217
|
+
if (hint.test(line))
|
|
218
|
+
score += 2;
|
|
219
|
+
if (/\b(if|switch|case|return|throw|goto|break|continue)\b/.test(line))
|
|
220
|
+
score += 1;
|
|
221
|
+
if (/^\s*(\/\/|\*|#)/.test(line))
|
|
222
|
+
score -= 2;
|
|
223
|
+
return { score, matchedTerms };
|
|
224
|
+
}
|
|
225
|
+
function mapInvariantsToImplementation(opts) {
|
|
226
|
+
const warnings = [];
|
|
227
|
+
const files = collectScopeFiles(opts.sourceRoot, { maxFiles: opts.maxFiles ?? 400, extensions: SOURCE_EXTS });
|
|
228
|
+
if (files.length === (opts.maxFiles ?? 400))
|
|
229
|
+
warnings.push(`source file scan capped at ${files.length}; raise --max-files to widen mapping`);
|
|
230
|
+
const candidates = [];
|
|
231
|
+
const maxPerInvariant = opts.maxCandidatesPerInvariant ?? 5;
|
|
232
|
+
for (const invariant of opts.invariants) {
|
|
233
|
+
const terms = termsFor(invariant);
|
|
234
|
+
if (terms.length === 0)
|
|
235
|
+
continue;
|
|
236
|
+
const ranked = [];
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
let source;
|
|
239
|
+
try {
|
|
240
|
+
source = readFileSync(file, "utf8");
|
|
241
|
+
} catch {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const lines = source.split(/\r?\n/);
|
|
245
|
+
for (let i = 0; i < lines.length; i++) {
|
|
246
|
+
const { score, matchedTerms } = scoreLine(invariant, lines[i] ?? "", terms);
|
|
247
|
+
if (score < 3 || matchedTerms.length === 0)
|
|
248
|
+
continue;
|
|
249
|
+
const snip = snippet(lines, i + 1);
|
|
250
|
+
ranked.push({
|
|
251
|
+
invariantId: invariant.id,
|
|
252
|
+
file: relative(opts.sourceRoot, file),
|
|
253
|
+
lineStart: snip.lineStart,
|
|
254
|
+
lineEnd: snip.lineEnd,
|
|
255
|
+
snippet: snip.text,
|
|
256
|
+
matchedTerms,
|
|
257
|
+
reason: `${invariant.kind} invariant terms matched implementation code`,
|
|
258
|
+
confidence: Math.min(0.95, 0.25 + score * 0.08),
|
|
259
|
+
status: "candidate",
|
|
260
|
+
rawScore: score
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
ranked.sort((a, b) => b.rawScore - a.rawScore || a.file.localeCompare(b.file) || a.lineStart - b.lineStart);
|
|
265
|
+
candidates.push(...ranked.slice(0, maxPerInvariant).map(({ rawScore: _rawScore, ...candidate }) => candidate));
|
|
266
|
+
}
|
|
267
|
+
if (opts.invariants.length > 0 && candidates.length === 0)
|
|
268
|
+
warnings.push("no implementation candidates matched extracted invariants");
|
|
269
|
+
return { candidates, warnings };
|
|
270
|
+
}
|
|
271
|
+
function runSpecdriftScan(opts) {
|
|
272
|
+
const extracted = extractSpecInvariants({
|
|
273
|
+
specName: opts.specName,
|
|
274
|
+
specText: opts.specText,
|
|
275
|
+
...opts.maxInvariants ? { maxInvariants: opts.maxInvariants } : {}
|
|
276
|
+
});
|
|
277
|
+
const mapped = mapInvariantsToImplementation({
|
|
278
|
+
sourceRoot: opts.sourceRoot,
|
|
279
|
+
invariants: extracted.invariants,
|
|
280
|
+
...opts.maxFiles ? { maxFiles: opts.maxFiles } : {},
|
|
281
|
+
...opts.maxCandidatesPerInvariant ? { maxCandidatesPerInvariant: opts.maxCandidatesPerInvariant } : {}
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
mode: "specdrift",
|
|
285
|
+
stage: "scan",
|
|
286
|
+
spec: opts.specName,
|
|
287
|
+
source: opts.sourceRoot,
|
|
288
|
+
invariants: extracted.invariants,
|
|
289
|
+
candidates: mapped.candidates,
|
|
290
|
+
warnings: [...extracted.warnings, ...mapped.warnings]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// packages/core/dist/specdrift/plan.js
|
|
295
|
+
function adapterFor(invariant, candidate) {
|
|
296
|
+
const file = candidate.file.toLowerCase();
|
|
297
|
+
if (/\b(kernel|net|drivers)\b/.test(file))
|
|
298
|
+
return "kernel-repro";
|
|
299
|
+
if (invariant.kind === "state" || invariant.kind === "ordering")
|
|
300
|
+
return "stateful-transcript";
|
|
301
|
+
if (/\.(c|h|cc|cpp|cxx|rs|go)$/i.test(file) && (invariant.kind === "range" || invariant.kind === "length" || invariant.kind === "rejection"))
|
|
302
|
+
return "raw-bytes";
|
|
303
|
+
if (/\.(js|ts|py|java|kt)$/i.test(file))
|
|
304
|
+
return "unit-test";
|
|
305
|
+
return "request-response";
|
|
306
|
+
}
|
|
307
|
+
function questionFor(invariant, candidate) {
|
|
308
|
+
const subject = invariant.subject ? ` for ${invariant.subject}` : "";
|
|
309
|
+
if (invariant.kind === "rejection") {
|
|
310
|
+
return `Does ${candidate.file}:${candidate.lineStart} reject inputs that violate the spec rule${subject}: ${invariant.summary}?`;
|
|
311
|
+
}
|
|
312
|
+
if (invariant.kind === "state" || invariant.kind === "ordering") {
|
|
313
|
+
return `Can an input transcript reach ${candidate.file}:${candidate.lineStart} in an order/state forbidden by the spec rule${subject}: ${invariant.summary}?`;
|
|
314
|
+
}
|
|
315
|
+
if (invariant.kind === "range" || invariant.kind === "length") {
|
|
316
|
+
return `Does ${candidate.file}:${candidate.lineStart} enforce the spec bound${subject}: ${invariant.summary}?`;
|
|
317
|
+
}
|
|
318
|
+
return `Does ${candidate.file}:${candidate.lineStart} implement the spec invariant${subject}: ${invariant.summary}?`;
|
|
319
|
+
}
|
|
320
|
+
function planSpecdriftHypotheses(opts) {
|
|
321
|
+
const byInvariant = new Map(opts.scan.invariants.map((inv) => [inv.id, inv]));
|
|
322
|
+
const max = opts.maxHypotheses ?? 20;
|
|
323
|
+
const hypotheses = [];
|
|
324
|
+
for (const candidate of opts.scan.candidates) {
|
|
325
|
+
const invariant = byInvariant.get(candidate.invariantId);
|
|
326
|
+
if (!invariant)
|
|
327
|
+
continue;
|
|
328
|
+
const suggestedAdapter = adapterFor(invariant, candidate);
|
|
329
|
+
hypotheses.push({
|
|
330
|
+
id: `hyp-${String(hypotheses.length + 1).padStart(3, "0")}-${invariant.id.replace(/^inv-\d+-/, "").slice(0, 40)}`,
|
|
331
|
+
invariantId: invariant.id,
|
|
332
|
+
candidateFile: candidate.file,
|
|
333
|
+
candidateLineStart: candidate.lineStart,
|
|
334
|
+
candidateLineEnd: candidate.lineEnd,
|
|
335
|
+
question: questionFor(invariant, candidate),
|
|
336
|
+
suggestedAdapter,
|
|
337
|
+
rationale: `Spec invariant (${invariant.kind}, ${invariant.securityRelevance} relevance) matched ${candidate.matchedTerms.join(", ")} in candidate code. This is a hypothesis only; adapter execution or skeptic review must prove/refute behavior.`,
|
|
338
|
+
status: "hypothesis",
|
|
339
|
+
confidence: Math.min(0.9, candidate.confidence * (invariant.securityRelevance === "high" ? 1 : 0.85))
|
|
340
|
+
});
|
|
341
|
+
if (hypotheses.length >= max)
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
return hypotheses;
|
|
345
|
+
}
|
|
346
|
+
function runSpecdriftPlan(opts) {
|
|
347
|
+
const scan = runSpecdriftScan(opts);
|
|
348
|
+
return {
|
|
349
|
+
...scan,
|
|
350
|
+
stage: "plan",
|
|
351
|
+
hypotheses: planSpecdriftHypotheses({ scan, ...opts.maxHypotheses ? { maxHypotheses: opts.maxHypotheses } : {} })
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export {
|
|
356
|
+
extractSpecInvariants,
|
|
357
|
+
mapInvariantsToImplementation,
|
|
358
|
+
runSpecdriftScan,
|
|
359
|
+
planSpecdriftHypotheses,
|
|
360
|
+
runSpecdriftPlan
|
|
361
|
+
};
|