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.
Files changed (66) hide show
  1. package/README.md +64 -0
  2. package/bin/fchek.js +107 -0
  3. package/lib/api.js +110 -0
  4. package/lib/audit.js +211 -0
  5. package/lib/bench.js +248 -0
  6. package/lib/config.js +191 -0
  7. package/lib/context.js +356 -0
  8. package/lib/convention.js +526 -0
  9. package/lib/coverage.js +604 -0
  10. package/lib/db.js +135 -0
  11. package/lib/deps-check.js +264 -0
  12. package/lib/deps.js +374 -0
  13. package/lib/docker.js +84 -0
  14. package/lib/doctor.js +149 -0
  15. package/lib/dom.js +226 -0
  16. package/lib/fuzz.js +470 -0
  17. package/lib/git.js +290 -0
  18. package/lib/goto.js +544 -0
  19. package/lib/launch.js +182 -0
  20. package/lib/lint.js +624 -0
  21. package/lib/new_features.test.js +181 -0
  22. package/lib/output.js +46 -0
  23. package/lib/port.js +173 -0
  24. package/lib/process.js +228 -0
  25. package/lib/profile.js +453 -0
  26. package/lib/python.js +41 -0
  27. package/lib/race.js +186 -0
  28. package/lib/registry.js +179 -0
  29. package/lib/repl.js +135 -0
  30. package/lib/run.js +403 -0
  31. package/lib/screenshot.js +152 -0
  32. package/lib/secrets.js +257 -0
  33. package/lib/state.js +219 -0
  34. package/lib/test.js +471 -0
  35. package/lib/vuln.js +253 -0
  36. package/lib/watch.js +240 -0
  37. package/lib/winlog.js +123 -0
  38. package/package.json +27 -0
  39. package/skills/ACTIVATE.md +274 -0
  40. package/skills/README.md +163 -0
  41. package/skills/agent.md +444 -0
  42. package/skills/api.md +47 -0
  43. package/skills/bench.md +117 -0
  44. package/skills/context.md +116 -0
  45. package/skills/convention.md +143 -0
  46. package/skills/coverage.md +99 -0
  47. package/skills/csharp.md +97 -0
  48. package/skills/db.md +66 -0
  49. package/skills/deps-check.md +135 -0
  50. package/skills/deps.md +143 -0
  51. package/skills/docker.md +61 -0
  52. package/skills/dom.md +56 -0
  53. package/skills/fuzz.md +167 -0
  54. package/skills/goto.md +111 -0
  55. package/skills/lint.md +123 -0
  56. package/skills/port.md +57 -0
  57. package/skills/profile.md +91 -0
  58. package/skills/race.md +117 -0
  59. package/skills/repl.md +81 -0
  60. package/skills/rules.md +318 -0
  61. package/skills/run.md +135 -0
  62. package/skills/secrets.md +170 -0
  63. package/skills/security.md +360 -0
  64. package/skills/state.md +261 -0
  65. package/skills/vuln.md +57 -0
  66. 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 };