recon-generate 0.0.17 β†’ 0.0.19

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/dist/info.js CHANGED
@@ -34,25 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runInfo = void 0;
37
- const child_process_1 = require("child_process");
38
37
  const fs = __importStar(require("fs/promises"));
39
38
  const path = __importStar(require("path"));
40
39
  const solc_typed_ast_1 = require("solc-typed-ast");
41
40
  const utils_1 = require("./utils");
42
41
  const z3Solver_1 = require("./z3Solver");
43
42
  const call_tree_builder_1 = require("./analyzer/call-tree-builder");
44
- const runCmd = (cmd, cwd) => {
45
- return new Promise((resolve, reject) => {
46
- (0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
47
- if (err) {
48
- reject(new Error(stderr || err.message));
49
- }
50
- else {
51
- resolve();
52
- }
53
- });
54
- });
55
- };
56
43
  const loadLatestBuildInfo = async (foundryRoot) => {
57
44
  var _a;
58
45
  const outDir = path.join(foundryRoot, 'out');
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Recon Expander
4
+ *
5
+ * Generates CFGs and coverage-map.json for coverage-directed fuzzing.
6
+ * Auto-detects Foundry project in current directory or parent directories.
7
+ *
8
+ * Usage: recon-expander
9
+ */
10
+ export interface SourcemapOptions {
11
+ outputDir?: string;
12
+ verbose?: boolean;
13
+ }
14
+ export declare function runSourcemap(foundryRoot: string, options?: SourcemapOptions): Promise<void>;
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Recon Expander
5
+ *
6
+ * Generates CFGs and coverage-map.json for coverage-directed fuzzing.
7
+ * Auto-detects Foundry project in current directory or parent directories.
8
+ *
9
+ * Usage: recon-expander
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.runSourcemap = runSourcemap;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const solc_typed_ast_1 = require("solc-typed-ast");
49
+ const utils_1 = require("./utils");
50
+ const call_tree_builder_1 = require("./analyzer/call-tree-builder");
51
+ const cfg_1 = require("./cfg");
52
+ // Functions to exclude from CFG analysis (harness setup, not coverage targets)
53
+ const EXCLUDED_FUNCTIONS = new Set([
54
+ 'setUp', 'targetContracts', 'targetSelectors', 'targetSenders',
55
+ 'targetInterfaces', 'targetArtifacts', 'targetArtifactSelectors',
56
+ 'excludeContracts', 'excludeSelectors', 'excludeSenders',
57
+ 'excludeArtifacts', 'excludeArtifactSelectors', 'vm', 'kevm', 'stdstore',
58
+ 'stdChainsInitialized', 'stdChains'
59
+ ]);
60
+ // ============================================================================
61
+ // Project Detection
62
+ // ============================================================================
63
+ function findFoundryProject(startDir = process.cwd()) {
64
+ let dir = startDir;
65
+ while (dir !== path.dirname(dir)) {
66
+ if (fs.existsSync(path.join(dir, 'foundry.toml'))) {
67
+ return dir;
68
+ }
69
+ dir = path.dirname(dir);
70
+ }
71
+ return null;
72
+ }
73
+ function findBuildInfo(projectDir) {
74
+ const buildInfoDir = path.join(projectDir, 'out', 'build-info');
75
+ if (!fs.existsSync(buildInfoDir))
76
+ return null;
77
+ const files = fs.readdirSync(buildInfoDir).filter(f => f.endsWith('.json'));
78
+ if (files.length === 0)
79
+ return null;
80
+ // Return the most recent build-info file
81
+ const sorted = files.sort((a, b) => {
82
+ const statA = fs.statSync(path.join(buildInfoDir, a));
83
+ const statB = fs.statSync(path.join(buildInfoDir, b));
84
+ return statB.mtimeMs - statA.mtimeMs;
85
+ });
86
+ return path.join(buildInfoDir, sorted[0]);
87
+ }
88
+ function loadBuildInfo(buildInfoPath) {
89
+ const content = fs.readFileSync(buildInfoPath, 'utf-8');
90
+ const buildInfo = JSON.parse(content);
91
+ return buildInfo.output || buildInfo;
92
+ }
93
+ function getAllSourceContracts(sourceUnits, astData) {
94
+ const results = [];
95
+ const sources = astData.sources || {};
96
+ for (const su of sourceUnits) {
97
+ const sourcePath = Object.keys(sources).find(key => {
98
+ var _a;
99
+ const src = sources[key];
100
+ return ((_a = src === null || src === void 0 ? void 0 : src.ast) === null || _a === void 0 ? void 0 : _a.id) === su.id;
101
+ }) || '';
102
+ // Only include contracts from src/ directory
103
+ if (!sourcePath.startsWith('src/'))
104
+ continue;
105
+ for (const contract of su.vContracts) {
106
+ if (contract.kind === solc_typed_ast_1.ContractKind.Interface || contract.abstract)
107
+ continue;
108
+ if (contract.kind === solc_typed_ast_1.ContractKind.Library)
109
+ continue;
110
+ results.push({ contract, sourcePath });
111
+ }
112
+ }
113
+ return results;
114
+ }
115
+ // ============================================================================
116
+ // Call Tree Building
117
+ // ============================================================================
118
+ function buildCallTreesForContract(contract) {
119
+ const callTrees = [];
120
+ const inheritedFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', false).reverse();
121
+ const processedSignatures = new Set();
122
+ for (const func of [...contract.vFunctions, ...inheritedFunctions]) {
123
+ if (!func.implemented || !func.vBody)
124
+ continue;
125
+ if (EXCLUDED_FUNCTIONS.has(func.name))
126
+ continue;
127
+ const signature = (0, utils_1.getSignature)(func);
128
+ if (processedSignatures.has(signature))
129
+ continue;
130
+ processedSignatures.add(signature);
131
+ if (func.visibility === solc_typed_ast_1.FunctionVisibility.Public ||
132
+ func.visibility === solc_typed_ast_1.FunctionVisibility.External) {
133
+ const context = {
134
+ nodeCounter: { value: 0 },
135
+ contract,
136
+ callStack: [],
137
+ activeNodes: new Map()
138
+ };
139
+ const callTree = (0, call_tree_builder_1.buildCallTree)(func, [], undefined, undefined, context);
140
+ callTrees.push({ contract: contract.name, function: func, callTree });
141
+ }
142
+ }
143
+ return callTrees;
144
+ }
145
+ async function runSourcemap(foundryRoot, options = {}) {
146
+ var _a, _b;
147
+ const { verbose = false } = options;
148
+ console.log(`\nπŸš€ Recon-Expander: Coverage Map Generation`);
149
+ console.log('═'.repeat(60));
150
+ const projectDir = foundryRoot;
151
+ const outputDir = options.outputDir || path.join(projectDir, '.recon');
152
+ if (verbose) {
153
+ console.log(`[VERBOSE] Foundry root: ${foundryRoot}`);
154
+ console.log(`[VERBOSE] Output dir: ${outputDir}`);
155
+ }
156
+ console.log(`πŸ“ Project: ${projectDir}`);
157
+ console.log(`πŸ“‚ Output: ${outputDir}`);
158
+ const startTime = Date.now();
159
+ // Find build info
160
+ const buildInfoPath = findBuildInfo(projectDir);
161
+ if (!buildInfoPath) {
162
+ throw new Error(`No build-info found in ${projectDir}/out/build-info/. Run "forge build" first to generate build artifacts.`);
163
+ }
164
+ console.log(`πŸ“‚ Found build-info: ${path.basename(buildInfoPath)}`);
165
+ // Load AST
166
+ const astData = loadBuildInfo(buildInfoPath);
167
+ const reader = new solc_typed_ast_1.ASTReader();
168
+ const skipPatterns = ['forge-std/', 'lib/forge-std/'];
169
+ const filteredSources = {};
170
+ for (const [key, content] of Object.entries(astData.sources)) {
171
+ const shouldSkip = skipPatterns.some(p => key.includes(p));
172
+ if (shouldSkip)
173
+ continue;
174
+ const ast = content.ast || content.legacyAST;
175
+ if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
176
+ filteredSources[key] = content;
177
+ }
178
+ }
179
+ const sourceUnits = reader.read({ sources: filteredSources });
180
+ // Get all source contracts
181
+ const srcContracts = getAllSourceContracts(sourceUnits, astData);
182
+ console.log(`\n🎯 Found ${srcContracts.length} contracts under src/`);
183
+ // Create output directory
184
+ if (!fs.existsSync(outputDir)) {
185
+ fs.mkdirSync(outputDir, { recursive: true });
186
+ }
187
+ // Step 1: Build CFGs
188
+ console.log(`\n${'─'.repeat(60)}`);
189
+ console.log(`πŸ“Š Step 1/3: Generating Control Flow Graphs...`);
190
+ console.log(`${'─'.repeat(60)}`);
191
+ let totalFunctions = 0;
192
+ let totalPaths = 0;
193
+ let totalEdges = 0;
194
+ const results = [];
195
+ for (const { contract } of srcContracts) {
196
+ try {
197
+ const callTrees = buildCallTreesForContract(contract);
198
+ if (callTrees.length === 0)
199
+ continue;
200
+ const { module, sexpr } = (0, cfg_1.buildAndEmitCFG)(contract, callTrees);
201
+ const summary = (0, cfg_1.getCoverageSummary)(module);
202
+ const outFile = path.join(outputDir, `${contract.name}.cfg.sexp`);
203
+ fs.writeFileSync(outFile, sexpr);
204
+ totalFunctions += module.functions.length;
205
+ totalPaths += summary.totalPaths;
206
+ totalEdges += summary.totalEdges;
207
+ results.push({ name: contract.name, funcs: module.functions.length, paths: summary.totalPaths });
208
+ }
209
+ catch (e) {
210
+ console.log(` ⚠️ Skipped ${contract.name}: ${e.message}`);
211
+ }
212
+ }
213
+ console.log(` βœ… ${results.length} contracts, ${totalFunctions} functions, ${totalPaths} paths`);
214
+ // Write aggregate manifest
215
+ const aggregateManifest = {
216
+ generated: new Date().toISOString(),
217
+ totalContracts: results.length,
218
+ totalFunctions,
219
+ totalPaths,
220
+ totalEdges,
221
+ contracts: results.map(r => ({ name: r.name, functions: r.funcs, paths: r.paths }))
222
+ };
223
+ fs.writeFileSync(path.join(outputDir, 'manifest.json'), JSON.stringify(aggregateManifest, null, 2));
224
+ // Step 2: Generate Branch Mappings
225
+ console.log(`\n${'─'.repeat(60)}`);
226
+ console.log(`πŸ—ΊοΈ Step 2/3: Generating Branch Mappings...`);
227
+ console.log(`${'─'.repeat(60)}`);
228
+ const { loadBuildInfoSourceMaps, generateBranchMapping } = await Promise.resolve().then(() => __importStar(require('./cfg/sourcemap')));
229
+ const sourceMaps = loadBuildInfoSourceMaps(buildInfoPath);
230
+ const contractsToProcess = Array.from(sourceMaps.keys()).filter(name => {
231
+ const mapData = sourceMaps.get(name);
232
+ return mapData && mapData.sourceFile.startsWith('src/');
233
+ });
234
+ // Build source contents for line number calculation
235
+ const sourceContents = new Map();
236
+ const sourceList = astData.sourceList || Object.keys(astData.sources || {});
237
+ for (let i = 0; i < sourceList.length; i++) {
238
+ const sourcePath = sourceList[i];
239
+ const fullPath = path.join(projectDir, sourcePath);
240
+ if (fs.existsSync(fullPath)) {
241
+ sourceContents.set(i, fs.readFileSync(fullPath, 'utf-8'));
242
+ }
243
+ }
244
+ const allBranches = [];
245
+ let totalBranches = 0;
246
+ for (const contractName of contractsToProcess) {
247
+ const mapData = sourceMaps.get(contractName);
248
+ if (!mapData)
249
+ continue;
250
+ const ast = (_b = (_a = astData.sources) === null || _a === void 0 ? void 0 : _a[mapData.sourceFile]) === null || _b === void 0 ? void 0 : _b.ast;
251
+ const branchMapping = generateBranchMapping(contractName, mapData.bytecode, mapData.sourceMap, mapData.sourceFile, ast, undefined, sourceContents);
252
+ allBranches.push(branchMapping);
253
+ totalBranches += branchMapping.branches.length;
254
+ }
255
+ const branchManifest = {
256
+ generated: new Date().toISOString(),
257
+ totalContracts: contractsToProcess.length,
258
+ totalBranches,
259
+ contracts: allBranches
260
+ };
261
+ fs.writeFileSync(path.join(outputDir, 'branches.json'), JSON.stringify(branchManifest, null, 2));
262
+ console.log(` βœ… ${contractsToProcess.length} contracts, ${totalBranches} branches`);
263
+ // Step 3: Generate Edge Mappings and Unified Coverage Map
264
+ console.log(`\n${'─'.repeat(60)}`);
265
+ console.log(`πŸ”— Step 3/3: Generating Coverage Map...`);
266
+ console.log(`${'─'.repeat(60)}`);
267
+ let edgeMappingsData = null;
268
+ const cfgFiles = fs.readdirSync(outputDir).filter(f => f.endsWith('.cfg.sexp'));
269
+ const contractNames = cfgFiles.map(f => f.replace('.cfg.sexp', ''));
270
+ if (contractNames.length > 0) {
271
+ try {
272
+ const { generateEdgeMappings } = await Promise.resolve().then(() => __importStar(require('./cfg/edge-mapper')));
273
+ const result = await generateEdgeMappings(outputDir, contractNames);
274
+ console.log(` πŸ“ ${result.totalContracts} contracts, ${result.totalEdges} edges`);
275
+ const edgeMappingsPath = path.join(outputDir, 'edge-mappings.json');
276
+ if (fs.existsSync(edgeMappingsPath)) {
277
+ edgeMappingsData = JSON.parse(fs.readFileSync(edgeMappingsPath, 'utf-8'));
278
+ }
279
+ }
280
+ catch (e) {
281
+ console.log(` ⚠️ Edge mapping failed: ${e.message}`);
282
+ }
283
+ }
284
+ // Generate unified coverage map
285
+ try {
286
+ const { createUnifiedFromMemory, writeUnifiedCoverageMap } = await Promise.resolve().then(() => __importStar(require('./cfg/unified-coverage')));
287
+ const unifiedManifest = createUnifiedFromMemory(branchManifest, edgeMappingsData);
288
+ writeUnifiedCoverageMap(outputDir, unifiedManifest);
289
+ console.log(` πŸ“ˆ coverage-map.json: ${unifiedManifest.totalBranches} branches, ${unifiedManifest.totalEdges} edges`);
290
+ }
291
+ catch (e) {
292
+ console.log(` ⚠️ Unified coverage map failed: ${e.message}`);
293
+ }
294
+ // Final summary
295
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
296
+ console.log(`\n${'═'.repeat(60)}`);
297
+ console.log(`βœ… Done in ${elapsed}s`);
298
+ console.log(`${'═'.repeat(60)}`);
299
+ console.log(`\nπŸ“ Output: ${outputDir}/`);
300
+ console.log(` β”œβ”€β”€ coverage-map.json ← Fuzzer reads this`);
301
+ console.log(` β”œβ”€β”€ *.cfg.sexp CFG for solver`);
302
+ console.log(` └── manifest.json Stats`);
303
+ console.log(`\nπŸ’‘ Usage: recon <project> --cfgDirectedEnable=true`);
304
+ }
305
+ // CLI entry point when run directly
306
+ if (require.main === module) {
307
+ const projectDir = findFoundryProject();
308
+ if (!projectDir) {
309
+ console.error(`❌ No Foundry project found (no foundry.toml)`);
310
+ console.error(` Run this command from within a Foundry project directory.`);
311
+ process.exit(1);
312
+ }
313
+ runSourcemap(projectDir).catch(err => {
314
+ console.error('Error:', err.message || err);
315
+ process.exit(1);
316
+ });
317
+ }
package/dist/types.d.ts CHANGED
@@ -64,3 +64,8 @@ export type CallTree = {
64
64
  isRecursive?: boolean;
65
65
  recursiveTargetNodeId?: number;
66
66
  };
67
+ export type CallTreeData = {
68
+ contract: string;
69
+ function: FunctionDefinition;
70
+ callTree: CallTree;
71
+ };
@@ -390,7 +390,7 @@ class WakeGenerator {
390
390
  }
391
391
  const hasCreationCode = typeof c.creation_code === 'string' && c.creation_code.trim().length > 0;
392
392
  if (!hasCreationCode) {
393
- this.logDebug('Skipping contract without creation code (likely abstract)', { contract: c.name, module: c.module });
393
+ this.logDebug('Skipping contract without creation code (abstract)', { contract: c.name, module: c.module });
394
394
  continue;
395
395
  }
396
396
  if (!this.isContractAllowed(c.name)) {
@@ -481,6 +481,7 @@ class WakeGenerator {
481
481
  if (!explicitlyIncluded && isInterface) {
482
482
  return false;
483
483
  }
484
+ // Always filter abstract contracts (no creation code), even if explicitly included
484
485
  const hasCreationCode = typeof c.creation_code === 'string' && c.creation_code.trim().length > 0;
485
486
  if (!hasCreationCode) {
486
487
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "abi-to-mock": "^1.0.4",
24
24
  "case": "^1.6.3",
25
25
  "commander": "^14.0.2",
26
+ "ethers": "^6.16.0",
26
27
  "handlebars": "^4.7.8",
27
28
  "solc-typed-ast": "^19.1.0",
28
29
  "src-location": "^1.1.0",