fchek 1.0.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/README.md +64 -0
- package/bin/fchek.js +107 -0
- package/lib/api.js +110 -0
- package/lib/audit.js +211 -0
- package/lib/bench.js +248 -0
- package/lib/config.js +191 -0
- package/lib/context.js +356 -0
- package/lib/convention.js +526 -0
- package/lib/coverage.js +604 -0
- package/lib/db.js +135 -0
- package/lib/deps-check.js +264 -0
- package/lib/deps.js +374 -0
- package/lib/docker.js +84 -0
- package/lib/doctor.js +149 -0
- package/lib/dom.js +226 -0
- package/lib/fuzz.js +470 -0
- package/lib/git.js +290 -0
- package/lib/goto.js +544 -0
- package/lib/launch.js +182 -0
- package/lib/lint.js +624 -0
- package/lib/new_features.test.js +181 -0
- package/lib/output.js +46 -0
- package/lib/port.js +173 -0
- package/lib/process.js +228 -0
- package/lib/profile.js +453 -0
- package/lib/python.js +41 -0
- package/lib/race.js +186 -0
- package/lib/registry.js +179 -0
- package/lib/repl.js +135 -0
- package/lib/run.js +403 -0
- package/lib/screenshot.js +152 -0
- package/lib/secrets.js +257 -0
- package/lib/state.js +219 -0
- package/lib/test.js +471 -0
- package/lib/vuln.js +253 -0
- package/lib/watch.js +240 -0
- package/lib/winlog.js +123 -0
- package/package.json +27 -0
- package/skills/ACTIVATE.md +274 -0
- package/skills/README.md +163 -0
- package/skills/agent.md +444 -0
- package/skills/api.md +47 -0
- package/skills/bench.md +117 -0
- package/skills/context.md +116 -0
- package/skills/convention.md +143 -0
- package/skills/coverage.md +99 -0
- package/skills/csharp.md +97 -0
- package/skills/db.md +66 -0
- package/skills/deps-check.md +135 -0
- package/skills/deps.md +143 -0
- package/skills/docker.md +61 -0
- package/skills/dom.md +56 -0
- package/skills/fuzz.md +167 -0
- package/skills/goto.md +111 -0
- package/skills/lint.md +123 -0
- package/skills/port.md +57 -0
- package/skills/profile.md +91 -0
- package/skills/race.md +117 -0
- package/skills/repl.md +81 -0
- package/skills/rules.md +318 -0
- package/skills/run.md +135 -0
- package/skills/secrets.md +170 -0
- package/skills/security.md +360 -0
- package/skills/state.md +261 -0
- package/skills/vuln.md +57 -0
- package/skills/windows.md +320 -0
package/lib/dom.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { URL } = require('url');
|
|
7
|
+
const { output, ok, fail } = require('./output');
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
fchek dom <url_or_file> <selector> [--attr=<attr>]
|
|
11
|
+
|
|
12
|
+
Query DOM elements from local HTML files or remote websites.
|
|
13
|
+
Supports basic selectors like tag names (e.g., div, a), classes (e.g., .btn), and IDs (e.g., #header).
|
|
14
|
+
Use --attr to extract a specific attribute value instead of inner text/HTML.
|
|
15
|
+
`.trim();
|
|
16
|
+
|
|
17
|
+
// Lightweight HTML Parser
|
|
18
|
+
function parseHTML(html) {
|
|
19
|
+
const nodes = [];
|
|
20
|
+
const tagRegex = /<(\/?)([a-zA-Z0-9\-]+)([^>]*)>/g;
|
|
21
|
+
let match;
|
|
22
|
+
let lastIndex = 0;
|
|
23
|
+
const stack = [];
|
|
24
|
+
|
|
25
|
+
const root = { tag: 'root', attrs: {}, children: [], text: '', html: html };
|
|
26
|
+
let current = root;
|
|
27
|
+
|
|
28
|
+
function parseAttributes(attrStr) {
|
|
29
|
+
const attrs = {};
|
|
30
|
+
const attrRegex = /([a-zA-Z0-9\-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
31
|
+
let attrMatch;
|
|
32
|
+
while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
|
|
33
|
+
const name = attrMatch[1];
|
|
34
|
+
const val = attrMatch[2] || attrMatch[3] || attrMatch[4] || '';
|
|
35
|
+
attrs[name] = val;
|
|
36
|
+
}
|
|
37
|
+
return attrs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
while ((match = tagRegex.exec(html)) !== null) {
|
|
41
|
+
const [fullTag, isClosing, tagName, attrStr] = match;
|
|
42
|
+
const startIndex = match.index;
|
|
43
|
+
|
|
44
|
+
// Save text node before this tag
|
|
45
|
+
const textVal = html.substring(lastIndex, startIndex).trim();
|
|
46
|
+
if (textVal && current) {
|
|
47
|
+
current.children.push({
|
|
48
|
+
tag: 'text',
|
|
49
|
+
text: textVal,
|
|
50
|
+
html: textVal,
|
|
51
|
+
attrs: {},
|
|
52
|
+
children: []
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isClosing) {
|
|
57
|
+
// Find matching tag in stack
|
|
58
|
+
if (stack.length > 0) {
|
|
59
|
+
const popped = stack.pop();
|
|
60
|
+
if (popped.tag === tagName.toLowerCase()) {
|
|
61
|
+
popped.html = html.substring(popped.startIndex, startIndex + fullTag.length);
|
|
62
|
+
// Recalculate text for this node
|
|
63
|
+
popped.text = popped.children.map(c => c.text || '').join(' ').trim();
|
|
64
|
+
current = stack[stack.length - 1] || root;
|
|
65
|
+
} else {
|
|
66
|
+
// Mismatch, push back
|
|
67
|
+
stack.push(popped);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Self-closing tags check
|
|
72
|
+
const isSelfClosing = attrStr.endsWith('/') || ['img', 'br', 'hr', 'input', 'meta', 'link'].includes(tagName.toLowerCase());
|
|
73
|
+
const node = {
|
|
74
|
+
tag: tagName.toLowerCase(),
|
|
75
|
+
attrs: parseAttributes(attrStr),
|
|
76
|
+
children: [],
|
|
77
|
+
text: '',
|
|
78
|
+
html: '',
|
|
79
|
+
startIndex
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
current.children.push(node);
|
|
83
|
+
|
|
84
|
+
if (!isSelfClosing) {
|
|
85
|
+
stack.push(node);
|
|
86
|
+
current = node;
|
|
87
|
+
} else {
|
|
88
|
+
node.html = fullTag;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lastIndex = tagRegex.lastIndex;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const remainingText = html.substring(lastIndex).trim();
|
|
96
|
+
if (remainingText) {
|
|
97
|
+
root.children.push({
|
|
98
|
+
tag: 'text',
|
|
99
|
+
text: remainingText,
|
|
100
|
+
html: remainingText,
|
|
101
|
+
attrs: {},
|
|
102
|
+
children: []
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return root.children;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findNodes(nodes, selector) {
|
|
110
|
+
const matches = [];
|
|
111
|
+
const target = selector.toLowerCase().trim();
|
|
112
|
+
|
|
113
|
+
function traverse(node) {
|
|
114
|
+
let isMatch = false;
|
|
115
|
+
|
|
116
|
+
if (target.startsWith('.')) {
|
|
117
|
+
const cls = target.slice(1);
|
|
118
|
+
if (node.attrs.class && node.attrs.class.toLowerCase().split(/\s+/).includes(cls)) {
|
|
119
|
+
isMatch = true;
|
|
120
|
+
}
|
|
121
|
+
} else if (target.startsWith('#')) {
|
|
122
|
+
const id = target.slice(1);
|
|
123
|
+
if (node.attrs.id && node.attrs.id.toLowerCase() === id) {
|
|
124
|
+
isMatch = true;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
if (node.tag === target) {
|
|
128
|
+
isMatch = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isMatch) {
|
|
133
|
+
matches.push(node);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const child of node.children) {
|
|
137
|
+
traverse(child);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const node of nodes) {
|
|
142
|
+
traverse(node);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return matches;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fetchUrl(urlStr) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const parsed = new URL(urlStr);
|
|
151
|
+
const protocol = parsed.protocol === 'https:' ? https : http;
|
|
152
|
+
protocol.get(urlStr, { timeout: 10000 }, (res) => {
|
|
153
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
154
|
+
// Handle redirect
|
|
155
|
+
resolve(fetchUrl(new URL(res.headers.location, urlStr).toString()));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (res.statusCode !== 200) {
|
|
159
|
+
reject(new Error(`Server returned status code ${res.statusCode}`));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
let data = '';
|
|
163
|
+
res.setEncoding('utf8');
|
|
164
|
+
res.on('data', chunk => data += chunk);
|
|
165
|
+
res.on('end', () => resolve(data));
|
|
166
|
+
}).on('error', reject);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function run(args) {
|
|
171
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
172
|
+
console.log(HELP);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const source = args.find(a => !a.startsWith('-'));
|
|
177
|
+
const selector = args.find(a => a !== source && !a.startsWith('-'));
|
|
178
|
+
|
|
179
|
+
if (!source || !selector) {
|
|
180
|
+
output(fail('Please specify both an HTML source (file/URL) and a selector', 'dom'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let attrName = null;
|
|
185
|
+
for (const arg of args) {
|
|
186
|
+
if (arg.startsWith('--attr=')) {
|
|
187
|
+
attrName = arg.split('=')[1];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
let html = '';
|
|
193
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
194
|
+
html = await fetchUrl(source);
|
|
195
|
+
} else {
|
|
196
|
+
if (!fs.existsSync(source)) {
|
|
197
|
+
output(fail(`HTML file does not exist: ${source}`, 'dom'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
html = fs.readFileSync(source, 'utf8');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const parsedTree = parseHTML(html);
|
|
204
|
+
const nodes = findNodes(parsedTree, selector);
|
|
205
|
+
|
|
206
|
+
const formattedMatches = nodes.map(n => {
|
|
207
|
+
const res = {
|
|
208
|
+
tag: n.tag,
|
|
209
|
+
attributes: n.attrs,
|
|
210
|
+
};
|
|
211
|
+
if (attrName) {
|
|
212
|
+
res.attribute_value = n.attrs[attrName] || null;
|
|
213
|
+
} else {
|
|
214
|
+
res.text = n.text;
|
|
215
|
+
res.html = n.html;
|
|
216
|
+
}
|
|
217
|
+
return res;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
output(ok({ source, selector, count: formattedMatches.length, matches: formattedMatches }, 'dom'));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
output(fail(`DOM query failed: ${err.message}`, 'dom'));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { run };
|
package/lib/fuzz.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fuzz.js — fuzzing on minimal setup
|
|
5
|
+
*
|
|
6
|
+
* C/C++ → AFL++ (afl-fuzz) — Linux/macOS
|
|
7
|
+
* Rust → cargo-fuzz (libFuzzer)
|
|
8
|
+
* Python → Atheris (Google's libFuzzer-based Python fuzzer)
|
|
9
|
+
* Go → go-fuzz or native go test -fuzz
|
|
10
|
+
*
|
|
11
|
+
* Fuzzing finds inputs that crash/hang/trigger unexpected behavior.
|
|
12
|
+
* Run for a limited time (default: 60s) and report findings.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { spawnSync, spawn, execSync } = require('child_process');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { output, ok, fail } = require('./output');
|
|
20
|
+
|
|
21
|
+
const HELP = `
|
|
22
|
+
fchek fuzz <file> [--duration=60] [--target=<fuzz_target>] [--corpus=<dir>]
|
|
23
|
+
|
|
24
|
+
Run a fuzzer on your code and report crashes / interesting inputs found.
|
|
25
|
+
Finds edge cases you'd never write by hand.
|
|
26
|
+
|
|
27
|
+
Supported:
|
|
28
|
+
.c / .cpp → AFL++ (apt install afl++)
|
|
29
|
+
.rs / Cargo → cargo-fuzz (cargo install cargo-fuzz)
|
|
30
|
+
.py → Atheris (pip install atheris)
|
|
31
|
+
.go → native go test -fuzz (Go 1.18+)
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--duration=60 Run fuzzer for N seconds (default: 60)
|
|
35
|
+
--target=<name> Fuzzing target name (Rust/Go: fuzz target function)
|
|
36
|
+
--corpus=<dir> Seed corpus directory (initial inputs)
|
|
37
|
+
--no-compile Skip compilation step (use existing binary)
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
fchek fuzz parse_input.c --duration=120
|
|
41
|
+
fchek fuzz . --target=fuzz_parse --duration=60 (Rust)
|
|
42
|
+
fchek fuzz fuzz_target.py --duration=30 (Python)
|
|
43
|
+
fchek fuzz . --target=FuzzParseInput --duration=60 (Go)
|
|
44
|
+
|
|
45
|
+
IMPORTANT: Fuzzing is CPU-intensive. Start with --duration=30 to test setup.
|
|
46
|
+
`.trim();
|
|
47
|
+
|
|
48
|
+
const DEFAULT_DURATION = 60;
|
|
49
|
+
|
|
50
|
+
function commandExists(cmd) {
|
|
51
|
+
try {
|
|
52
|
+
execSync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
|
|
53
|
+
return true;
|
|
54
|
+
} catch { return false; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── C/C++ via AFL++ ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function fuzzC(file, duration, corpus, timeoutMs) {
|
|
60
|
+
if (process.platform === 'win32') {
|
|
61
|
+
return output(fail('AFL++ is not supported on Windows. Use WSL.'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const aflCompiler = commandExists('afl-clang-fast++') ? 'afl-clang-fast++'
|
|
65
|
+
: commandExists('afl-g++-fast') ? 'afl-g++-fast'
|
|
66
|
+
: commandExists('afl-clang++') ? 'afl-clang++'
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
if (!aflCompiler) {
|
|
70
|
+
return output(fail(
|
|
71
|
+
'AFL++ compiler not found.\n' +
|
|
72
|
+
'Install: apt install afl++ OR brew install afl++'
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ext = path.extname(file).toLowerCase();
|
|
77
|
+
const outBin = path.join(os.tmpdir(), `fchek_fuzz_${path.basename(file, ext)}`);
|
|
78
|
+
|
|
79
|
+
// Compile with AFL++ instrumentation
|
|
80
|
+
const compile = spawnSync(
|
|
81
|
+
aflCompiler,
|
|
82
|
+
['-o', outBin, '-fsanitize=address', file],
|
|
83
|
+
{ encoding: 'utf8', env: { ...process.env, AFL_USE_ASAN: '1' } }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (compile.status !== 0) {
|
|
87
|
+
return output(fail(`AFL++ compilation failed:\n${compile.stderr}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Setup corpus
|
|
91
|
+
const corpusDir = corpus || path.join(os.tmpdir(), 'fchek_corpus');
|
|
92
|
+
const outputDir = path.join(os.tmpdir(), 'fchek_findings');
|
|
93
|
+
fs.mkdirSync(corpusDir, { recursive: true });
|
|
94
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// Seed: create a minimal input if corpus is empty
|
|
97
|
+
if (fs.readdirSync(corpusDir).length === 0) {
|
|
98
|
+
fs.writeFileSync(path.join(corpusDir, 'seed0'), 'hello\n');
|
|
99
|
+
fs.writeFileSync(path.join(corpusDir, 'seed1'), '0\n');
|
|
100
|
+
fs.writeFileSync(path.join(corpusDir, 'seed2'), '\x00\xff\xfe');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Run AFL++ with time limit
|
|
104
|
+
const aflRes = spawnSync(
|
|
105
|
+
'afl-fuzz',
|
|
106
|
+
['-i', corpusDir, '-o', outputDir, '-V', String(duration), '--', outBin, '@@'],
|
|
107
|
+
{
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
timeout: (duration + 30) * 1000, // buffer of 30s for startup
|
|
110
|
+
env: {
|
|
111
|
+
...process.env,
|
|
112
|
+
AFL_NO_UI: '1', // no terminal UI, just text
|
|
113
|
+
AFL_SKIP_CPUFREQ: '1',
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Parse AFL++ results
|
|
119
|
+
const crashes = countAflFindings(path.join(outputDir, 'default', 'crashes'));
|
|
120
|
+
const hangs = countAflFindings(path.join(outputDir, 'default', 'hangs'));
|
|
121
|
+
const queueLen = countAflFindings(path.join(outputDir, 'default', 'queue'));
|
|
122
|
+
|
|
123
|
+
try { fs.unlinkSync(outBin); } catch {}
|
|
124
|
+
|
|
125
|
+
output(ok({
|
|
126
|
+
file: path.resolve(file),
|
|
127
|
+
lang: 'c',
|
|
128
|
+
tool: 'AFL++',
|
|
129
|
+
duration_s: duration,
|
|
130
|
+
crashes_found: crashes,
|
|
131
|
+
hangs_found: hangs,
|
|
132
|
+
queue_entries: queueLen,
|
|
133
|
+
verdict: crashes > 0 ? 'crashes_found' : hangs > 0 ? 'hangs_found' : 'clean',
|
|
134
|
+
findings_dir: outputDir,
|
|
135
|
+
afl_output: (aflRes.stderr || '').slice(0, 2000),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function countAflFindings(dir) {
|
|
140
|
+
try {
|
|
141
|
+
const files = fs.readdirSync(dir).filter(f => !f.startsWith('README'));
|
|
142
|
+
return files.length;
|
|
143
|
+
} catch { return 0; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Rust via cargo-fuzz ─────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function fuzzRust(target, duration, fuzzTarget, timeoutMs) {
|
|
149
|
+
if (!commandExists('cargo')) {
|
|
150
|
+
return output(fail('cargo not found. Install Rust: https://rustup.rs'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cwd = path.resolve(target === '.' ? process.cwd() : target);
|
|
154
|
+
|
|
155
|
+
// Check if cargo-fuzz is installed
|
|
156
|
+
const versionRes = spawnSync('cargo', ['fuzz', '--version'], { encoding: 'utf8', timeout: 5000 });
|
|
157
|
+
if (versionRes.status !== 0) {
|
|
158
|
+
return output(fail(
|
|
159
|
+
'cargo-fuzz not found.\n' +
|
|
160
|
+
'Install: cargo install cargo-fuzz\n' +
|
|
161
|
+
'Note: requires nightly Rust: rustup default nightly'
|
|
162
|
+
));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// List available fuzz targets if none specified
|
|
166
|
+
if (!fuzzTarget) {
|
|
167
|
+
const listRes = spawnSync('cargo', ['fuzz', 'list'], { encoding: 'utf8', cwd, timeout: 10_000 });
|
|
168
|
+
const targets = (listRes.stdout || '').trim().split('\n').filter(Boolean);
|
|
169
|
+
|
|
170
|
+
if (targets.length === 0) {
|
|
171
|
+
return output(fail(
|
|
172
|
+
'No fuzz targets found. Create one:\n' +
|
|
173
|
+
' cargo fuzz add fuzz_target_1\n' +
|
|
174
|
+
'Then add your fuzzing logic in fuzz/fuzz_targets/fuzz_target_1.rs'
|
|
175
|
+
));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (targets.length === 1) {
|
|
179
|
+
fuzzTarget = targets[0];
|
|
180
|
+
} else {
|
|
181
|
+
// Use first target but report all available
|
|
182
|
+
return output(fail(
|
|
183
|
+
`Multiple fuzz targets found. Specify one with --target=<name>.\n` +
|
|
184
|
+
`Available: ${targets.join(', ')}`
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Run fuzzing for duration seconds
|
|
190
|
+
const runRes = spawnSync(
|
|
191
|
+
'cargo',
|
|
192
|
+
['fuzz', 'run', fuzzTarget, '--', `-max_total_time=${duration}`],
|
|
193
|
+
{
|
|
194
|
+
encoding: 'utf8',
|
|
195
|
+
cwd,
|
|
196
|
+
timeout: (duration + 60) * 1000,
|
|
197
|
+
env: { ...process.env, RUSTFLAGS: '-C opt-level=0' },
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const raw = (runRes.stdout || '') + (runRes.stderr || '');
|
|
202
|
+
|
|
203
|
+
// Parse libFuzzer output
|
|
204
|
+
const crashMatch = raw.match(/SUMMARY: AddressSanitizer: (.+)/);
|
|
205
|
+
const execsMatch = raw.match(/exec\/s:\s+(\d+)/);
|
|
206
|
+
const covMatch = raw.match(/cov:\s+(\d+)/);
|
|
207
|
+
|
|
208
|
+
const crashed = crashMatch !== null || raw.includes('CRASHED') || raw.includes('AddressSanitizer');
|
|
209
|
+
|
|
210
|
+
output(ok({
|
|
211
|
+
target: cwd,
|
|
212
|
+
lang: 'rust',
|
|
213
|
+
tool: 'cargo-fuzz (libFuzzer)',
|
|
214
|
+
fuzz_target: fuzzTarget,
|
|
215
|
+
duration_s: duration,
|
|
216
|
+
crashed: crashed,
|
|
217
|
+
crash_reason: crashMatch?.[1] ?? null,
|
|
218
|
+
execs_per_sec: execsMatch ? parseInt(execsMatch[1]) : null,
|
|
219
|
+
coverage_points: covMatch ? parseInt(covMatch[1]) : null,
|
|
220
|
+
verdict: crashed ? 'crash_found' : 'clean',
|
|
221
|
+
raw_output: raw.slice(-2000), // last 2000 chars most relevant
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Python via Atheris ───────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function fuzzPython(file, duration, timeoutMs) {
|
|
228
|
+
const py = ['python3', 'python'].find(p => {
|
|
229
|
+
const r = spawnSync(p, ['--version'], { stdio: 'ignore', timeout: 3000 });
|
|
230
|
+
return r && r.status === 0;
|
|
231
|
+
});
|
|
232
|
+
if (!py) return output(fail('Python not found. Install: https://python.org'));
|
|
233
|
+
|
|
234
|
+
// Check atheris
|
|
235
|
+
const atherisCheck = spawnSync(py, ['-c', 'import atheris'], { encoding: 'utf8', timeout: 5000 });
|
|
236
|
+
if (atherisCheck.status !== 0) {
|
|
237
|
+
return output(fail(
|
|
238
|
+
'atheris not found. Install: pip install atheris\n' +
|
|
239
|
+
'Note: atheris uses libFuzzer — requires Linux or macOS with clang.'
|
|
240
|
+
));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Wrap the target file in an atheris harness if it doesn't have one
|
|
244
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
245
|
+
const hasAtheris = content.includes('atheris') || content.includes('FuzzedDataProvider');
|
|
246
|
+
|
|
247
|
+
let runFile = file;
|
|
248
|
+
let autoHarnessWarning = null;
|
|
249
|
+
|
|
250
|
+
if (!hasAtheris) {
|
|
251
|
+
// Try to find entry points in the module
|
|
252
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
253
|
+
const hasParse = /\bdef\s+parse\s*\(/.test(content);
|
|
254
|
+
const hasProcess = /\bdef\s+process\s*\(/.test(content);
|
|
255
|
+
const hasDecode = /\bdef\s+decode\s*\(/.test(content);
|
|
256
|
+
const hasLoad = /\bdef\s+load\s*\(/.test(content);
|
|
257
|
+
|
|
258
|
+
if (!hasParse && !hasProcess && !hasDecode && !hasLoad) {
|
|
259
|
+
// No known entry point found — harness will fuzz a no-op
|
|
260
|
+
autoHarnessWarning =
|
|
261
|
+
'WARNING: auto-harness did not find a known entry point (parse/process/decode/load). ' +
|
|
262
|
+
'The fuzzer will run but test nothing meaningful. ' +
|
|
263
|
+
'Write a manual harness with FuzzedDataProvider for accurate results. ' +
|
|
264
|
+
'See: https://github.com/google/atheris#using-atheris';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Generate a minimal harness that tries all detected functions
|
|
268
|
+
const entryPoints = [];
|
|
269
|
+
if (hasParse) entryPoints.push(`if hasattr(mod, 'parse'): mod.parse(fdp.ConsumeUnicodeNoSurrogates(128))`);
|
|
270
|
+
if (hasProcess) entryPoints.push(`if hasattr(mod, 'process'): mod.process(fdp.ConsumeBytes(256))`);
|
|
271
|
+
if (hasDecode) entryPoints.push(`if hasattr(mod, 'decode'): mod.decode(fdp.ConsumeBytes(256))`);
|
|
272
|
+
if (hasLoad) entryPoints.push(`if hasattr(mod, 'load'): mod.load(fdp.ConsumeUnicodeNoSurrogates(256))`);
|
|
273
|
+
|
|
274
|
+
if (entryPoints.length === 0) {
|
|
275
|
+
// Last resort: fuzz every callable that takes one argument
|
|
276
|
+
entryPoints.push(
|
|
277
|
+
`for name in dir(mod):\n` +
|
|
278
|
+
` fn = getattr(mod, name)\n` +
|
|
279
|
+
` if callable(fn) and not name.startswith('_'):\n` +
|
|
280
|
+
` try: fn(fdp.ConsumeUnicodeNoSurrogates(64))\n` +
|
|
281
|
+
` except: pass`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Write harness to tmpfile — avoids any -c / sys.argv issues
|
|
286
|
+
const harnessPath = path.join(os.tmpdir(), 'fchek_atheris_' + Date.now() + '.py');
|
|
287
|
+
const entryPointsCode = entryPoints.join('\n ');
|
|
288
|
+
const absFilePath = path.resolve(file).replace(/\\/g, '/');
|
|
289
|
+
const harness = [
|
|
290
|
+
'import atheris',
|
|
291
|
+
'import sys',
|
|
292
|
+
'import importlib.util',
|
|
293
|
+
'',
|
|
294
|
+
'spec = importlib.util.spec_from_file_location("target", ' + JSON.stringify(path.resolve(file)) + ')',
|
|
295
|
+
'mod = importlib.util.module_from_spec(spec)',
|
|
296
|
+
'try:',
|
|
297
|
+
' spec.loader.exec_module(mod)',
|
|
298
|
+
'except Exception:',
|
|
299
|
+
' pass',
|
|
300
|
+
'',
|
|
301
|
+
'@atheris.instrument_func',
|
|
302
|
+
'def TestOneInput(data):',
|
|
303
|
+
' fdp = atheris.FuzzedDataProvider(data)',
|
|
304
|
+
' try:',
|
|
305
|
+
' ' + entryPointsCode,
|
|
306
|
+
' except (ValueError, TypeError, KeyError, IndexError, UnicodeDecodeError, OverflowError):',
|
|
307
|
+
' pass',
|
|
308
|
+
'',
|
|
309
|
+
'atheris.Setup(sys.argv, TestOneInput)',
|
|
310
|
+
'atheris.Fuzz()',
|
|
311
|
+
].join('\n');
|
|
312
|
+
fs.writeFileSync(harnessPath, harness, 'utf8');
|
|
313
|
+
runFile = harnessPath;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const res = spawnSync(
|
|
317
|
+
py,
|
|
318
|
+
[runFile, '-max_total_time=' + String(duration)],
|
|
319
|
+
{
|
|
320
|
+
encoding: 'utf8',
|
|
321
|
+
timeout: (duration + 30) * 1000,
|
|
322
|
+
env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' },
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const raw = (res.stdout || '') + (res.stderr || '');
|
|
327
|
+
const crashed = raw.includes('CRASH') || raw.includes('Traceback') && raw.includes('SUMMARY');
|
|
328
|
+
const execsM = raw.match(/exec\/s:\s+(\d+)/);
|
|
329
|
+
const covM = raw.match(/cov:\s+(\d+)/);
|
|
330
|
+
|
|
331
|
+
if (runFile !== file) try { fs.unlinkSync(runFile); } catch {}
|
|
332
|
+
|
|
333
|
+
output(ok({
|
|
334
|
+
file: path.resolve(file),
|
|
335
|
+
lang: 'python',
|
|
336
|
+
tool: 'atheris (libFuzzer)',
|
|
337
|
+
duration_s: duration,
|
|
338
|
+
auto_harness: !hasAtheris,
|
|
339
|
+
auto_harness_warning: autoHarnessWarning,
|
|
340
|
+
crashed,
|
|
341
|
+
execs_per_sec: execsM ? parseInt(execsM[1]) : null,
|
|
342
|
+
coverage_points: covM ? parseInt(covM[1]) : null,
|
|
343
|
+
verdict: crashed ? 'crash_found' : 'clean',
|
|
344
|
+
raw_output: raw.slice(-2000),
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ─── Go — native go test -fuzz ────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function fuzzGo(target, duration, fuzzTarget, timeoutMs) {
|
|
351
|
+
if (!commandExists('go')) {
|
|
352
|
+
return output(fail('go not found. Install: https://go.dev'));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const cwd = path.resolve(target === '.' ? process.cwd() : target);
|
|
356
|
+
|
|
357
|
+
// Check Go version >= 1.18
|
|
358
|
+
const verRes = spawnSync('go', ['version'], { encoding: 'utf8', timeout: 5000 });
|
|
359
|
+
const verMatch = (verRes.stdout || '').match(/go(\d+)\.(\d+)/);
|
|
360
|
+
if (!verMatch || parseInt(verMatch[1]) < 1 || (parseInt(verMatch[1]) === 1 && parseInt(verMatch[2]) < 18)) {
|
|
361
|
+
return output(fail('go test -fuzz requires Go 1.18+. Update Go: https://go.dev'));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!fuzzTarget) {
|
|
365
|
+
// Try to auto-detect fuzz functions from test files
|
|
366
|
+
const testFiles = findGoTestFiles(cwd);
|
|
367
|
+
const targets = [];
|
|
368
|
+
for (const tf of testFiles) {
|
|
369
|
+
const content = fs.readFileSync(tf, 'utf8');
|
|
370
|
+
const matches = [...content.matchAll(/func (Fuzz\w+)/g)];
|
|
371
|
+
targets.push(...matches.map(m => m[1]));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (targets.length === 0) {
|
|
375
|
+
return output(fail(
|
|
376
|
+
'No Fuzz* functions found in _test.go files.\n' +
|
|
377
|
+
'Create one:\n' +
|
|
378
|
+
' func FuzzParseInput(f *testing.F) {\n' +
|
|
379
|
+
' f.Add("initial_seed")\n' +
|
|
380
|
+
' f.Fuzz(func(t *testing.T, s string) { ParseInput(s) })\n' +
|
|
381
|
+
' }'
|
|
382
|
+
));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (targets.length > 1) {
|
|
386
|
+
return output(fail(
|
|
387
|
+
`Multiple Fuzz targets found. Specify with --target=<name>.\n` +
|
|
388
|
+
`Available: ${targets.join(', ')}`
|
|
389
|
+
));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fuzzTarget = targets[0];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const res = spawnSync(
|
|
396
|
+
'go',
|
|
397
|
+
['test', '-fuzz', fuzzTarget, `-fuzztime=${duration}s`, './...'],
|
|
398
|
+
{ encoding: 'utf8', cwd, timeout: (duration + 30) * 1000 }
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const raw = (res.stdout || '') + (res.stderr || '');
|
|
402
|
+
const crashed = res.status !== 0 || raw.includes('FAIL') && raw.includes('panic:');
|
|
403
|
+
const failingInput = raw.match(/Failing input written to testdata\/fuzz\/.+/)?.[0] ?? null;
|
|
404
|
+
|
|
405
|
+
output(ok({
|
|
406
|
+
target: cwd,
|
|
407
|
+
lang: 'go',
|
|
408
|
+
tool: 'go test -fuzz',
|
|
409
|
+
fuzz_target: fuzzTarget,
|
|
410
|
+
duration_s: duration,
|
|
411
|
+
crashed,
|
|
412
|
+
failing_input: failingInput,
|
|
413
|
+
verdict: crashed ? 'crash_found' : 'clean',
|
|
414
|
+
raw_output: raw.slice(-2000),
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function findGoTestFiles(dir) {
|
|
419
|
+
const results = [];
|
|
420
|
+
try {
|
|
421
|
+
for (const name of fs.readdirSync(dir)) {
|
|
422
|
+
if (name.endsWith('_test.go')) results.push(path.join(dir, name));
|
|
423
|
+
}
|
|
424
|
+
} catch {}
|
|
425
|
+
return results;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
async function run(args) {
|
|
431
|
+
if (args.length === 0 || args[0] === '--help') {
|
|
432
|
+
console.log(HELP);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const target = args[0];
|
|
437
|
+
const duration = parseInt(
|
|
438
|
+
(args.find(a => a.startsWith('--duration=')) ?? `--duration=${DEFAULT_DURATION}`).replace('--duration=', ''),
|
|
439
|
+
10
|
|
440
|
+
);
|
|
441
|
+
const fuzzTarget = (args.find(a => a.startsWith('--target=')) ?? '').replace('--target=', '') || null;
|
|
442
|
+
const corpus = (args.find(a => a.startsWith('--corpus=')) ?? '').replace('--corpus=', '') || null;
|
|
443
|
+
|
|
444
|
+
if (!fs.existsSync(target)) {
|
|
445
|
+
return output(fail(`Not found: ${target}`));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const ext = path.extname(target).toLowerCase();
|
|
449
|
+
const isDir = fs.statSync(target).isDirectory();
|
|
450
|
+
|
|
451
|
+
if (['.c', '.cpp', '.cc'].includes(ext)) return fuzzC(target, duration, corpus, (duration + 60) * 1000);
|
|
452
|
+
if (ext === '.py') return fuzzPython(target, duration, (duration + 60) * 1000);
|
|
453
|
+
if (ext === '.rs' || (isDir && fs.existsSync(path.join(path.resolve(target), 'Cargo.toml')))) {
|
|
454
|
+
return fuzzRust(target, duration, fuzzTarget, (duration + 120) * 1000);
|
|
455
|
+
}
|
|
456
|
+
if (ext === '.go' || (isDir && fs.existsSync(path.join(path.resolve(target), 'go.mod')))) {
|
|
457
|
+
return fuzzGo(target, duration, fuzzTarget, (duration + 60) * 1000);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Fallback "." — detect by project markers
|
|
461
|
+
if (isDir) {
|
|
462
|
+
const cwd = path.resolve(target);
|
|
463
|
+
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return fuzzRust(target, duration, fuzzTarget, (duration + 120) * 1000);
|
|
464
|
+
if (fs.existsSync(path.join(cwd, 'go.mod'))) return fuzzGo(target, duration, fuzzTarget, (duration + 60) * 1000);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
output(fail(`Cannot detect language for: ${target}. Supported: .c .cpp .rs .py .go`));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
module.exports = { run };
|