88
99import argparse
1010import copy
11+ import logging
1112import os
1213import subprocess
1314import sys
2223from wasmParser import parser
2324from strategies import structural_perturbation as sp
2425from strategies import code_perturbation as cp
26+ from strategies import state as strategy_state
27+
28+ logger = logging .getLogger ("swamped" )
2529
2630# ── Stack Operation Insertion sub-variants ────────────────────────────────
2731
@@ -94,15 +98,15 @@ def wasm_to_wast(wasm_path, wast_path):
9498 capture_output = True , text = True ,
9599 )
96100 if result .returncode != 0 :
97- print ( f"[error] wasm2wat failed: { result .stderr .strip ()} " , file = sys . stderr )
101+ logger . error ( " wasm2wat failed: %s" , result .stderr .strip ())
98102 sys .exit (1 )
99103
100104
101105def resolve_input (input_path , tmp_dir ):
102106 """Return the path to a .wast file, converting from .wasm if necessary."""
103107 if input_path .endswith (".wasm" ):
104108 wast_path = os .path .join (tmp_dir , os .path .basename (input_path ).replace (".wasm" , ".wast" ))
105- print ( f "[*] Converting { input_path } -> WAT ..." )
109+ logger . info ( "[*] Converting %s -> WAT ..." , input_path )
106110 wasm_to_wast (input_path , wast_path )
107111 return wast_path
108112 return input_path
@@ -127,6 +131,39 @@ def list_strategies():
127131 print ()
128132
129133
134+ def _count_body_instructions (parsed ):
135+ """Count total body instructions across all functions."""
136+ total = 0
137+ for f in parsed .get ("Function" , {}).values ():
138+ if f .body :
139+ total += len (f .body )
140+ return total
141+
142+
143+ def _section_counts (parsed ):
144+ """Return a dict of section_name -> entry count."""
145+ return {sec : len (entries ) for sec , entries in parsed .items ()}
146+
147+
148+ def _print_diff (before_counts , before_instructions , after_counts , after_instructions ):
149+ """Print a summary of what changed."""
150+ print ("\n Diff summary:\n " )
151+ print (f" { 'Section' :<20s} { 'Before' :>8s} { 'After' :>8s} { 'Delta' :>8s} " )
152+ print (f" { '─' * 20 } { '─' * 8 } { '─' * 8 } { '─' * 8 } " )
153+ for sec in before_counts :
154+ b = before_counts [sec ]
155+ a = after_counts .get (sec , 0 )
156+ delta = a - b
157+ marker = f"+{ delta } " if delta > 0 else str (delta ) if delta < 0 else "0"
158+ if delta != 0 :
159+ print (f" { sec :<20s} { b :>8d} { a :>8d} { marker :>8s} " )
160+ instr_delta = after_instructions - before_instructions
161+ marker = f"+{ instr_delta } " if instr_delta > 0 else str (instr_delta ) if instr_delta < 0 else "0"
162+ if instr_delta != 0 :
163+ print (f" { 'instructions' :<20s} { before_instructions :>8d} { after_instructions :>8d} { marker :>8s} " )
164+ print ()
165+
166+
130167# ── CLI entry point ───────────────────────────────────────────────────────
131168
132169def build_parser ():
@@ -150,6 +187,7 @@ def build_parser():
150187 swamped obfuscate input.wasm -o out.wasm -s code -e direct_to_indirect
151188 swamped obfuscate input.wasm -o out.wasm -s structural --alpha 2 --beta 5
152189 swamped obfuscate input.wasm -o out.wasm -s stack_op_insertion --stackop m b
190+ swamped obfuscate input.wasm -o out.wasm -s all --seed 42 --diff
153191 """ ),
154192 )
155193 sub = p .add_subparsers (dest = "command" , required = True )
@@ -207,6 +245,11 @@ def build_parser():
207245 obf .add_argument ("--ratio" , type = float , default = 1.0 , help = "Perturbation ratio 0.0-1.0 (default: 1.0)" )
208246 obf .add_argument ("--validate" , action = "store_true" , help = "Run wasm-validate on the output" )
209247 obf .add_argument ("-t" , "--timing" , action = "store_true" , help = "Show execution time for each strategy" )
248+ obf .add_argument ("--seed" , type = int , default = None , help = "RNG seed for reproducible runs" )
249+ obf .add_argument ("--diff" , action = "store_true" , help = "Show a summary of sections/instructions changed" )
250+ obf .add_argument ("--strict" , action = "store_true" , help = "Abort on the first strategy failure" )
251+ obf .add_argument ("-v" , "--verbose" , action = "store_true" , help = "Enable debug-level logging" )
252+ obf .add_argument ("-q" , "--quiet" , action = "store_true" , help = "Suppress all output except errors" )
210253
211254 return p
212255
@@ -224,14 +267,14 @@ def resolve_strategy_names(requested, excluded):
224267 elif r in STRATEGIES :
225268 names .append (r )
226269 else :
227- print ( f"[error] Unknown strategy: '{ r } '" , file = sys . stderr )
228- print ( f" Run 'swamped list' to see available strategies.", file = sys . stderr )
270+ logger . error ( " Unknown strategy: '%s '" , r )
271+ logger . error ( " Run 'swamped list' to see available strategies." )
229272 sys .exit (1 )
230273
231274 # Validate exclusions
232275 for e in excluded :
233276 if e not in STRATEGIES :
234- print ( f"[error] Unknown strategy to exclude: '{ e } '" , file = sys . stderr )
277+ logger . error ( " Unknown strategy to exclude: '%s '" , e )
235278 sys .exit (1 )
236279 exclude_set = set (excluded )
237280
@@ -244,19 +287,19 @@ def resolve_strategy_names(requested, excluded):
244287 unique .append (n )
245288
246289 if not unique :
247- print ( "[error] No strategies left after exclusions.", file = sys . stderr )
290+ logger . error ( " No strategies left after exclusions." )
248291 sys .exit (1 )
249292
250293 # Skip not-yet-implemented strategies with a warning
251294 final = []
252295 for n in unique :
253296 if STRATEGIES [n ]["fn" ] is None :
254- print ( f"[warning] { n } is not yet implemented, skipping." , file = sys . stderr )
297+ logger . warning ( "%s is not yet implemented, skipping." , n )
255298 else :
256299 final .append (n )
257300
258301 if not final :
259- print ( "[error] No implemented strategies left.", file = sys . stderr )
302+ logger . error ( " No implemented strategies left." )
260303 sys .exit (1 )
261304
262305 return final
@@ -267,30 +310,41 @@ def cmd_obfuscate(args):
267310 output_path = os .path .abspath (args .output )
268311
269312 if not os .path .isfile (input_path ):
270- print ( f"[error] Input file not found: { input_path } " , file = sys . stderr )
313+ logger . error ( " Input file not found: %s " , input_path )
271314 sys .exit (1 )
272315
273316 output_dir = os .path .dirname (output_path )
274317 os .makedirs (output_dir , exist_ok = True )
275318
319+ # Seed RNG if requested
320+ if args .seed is not None :
321+ strategy_state .set_seed (args .seed )
322+ logger .info ("[*] RNG seed set to %d" , args .seed )
323+
276324 # Convert .wasm to .wast if needed
277325 wast_path = resolve_input (input_path , output_dir )
278326
279327 # Parse
280- print ( f "[*] Parsing { wast_path } ..." )
328+ logger . info ( "[*] Parsing %s ..." , wast_path )
281329 with open (wast_path ) as f :
282330 origin_wast = f .readlines ()
283331 parsed = parser .parseWast (origin_wast )
284332
333+ # Snapshot before-counts for --diff
334+ if args .diff :
335+ before_counts = _section_counts (parsed )
336+ before_instructions = _count_body_instructions (parsed )
337+
285338 # Apply strategies sequentially
286339 strategy_names = resolve_strategy_names (args .strategies , args .exclude )
287340 section = copy .deepcopy (parsed )
288341
289- print (f"[*] Applying { len (strategy_names )} perturbation(s) (alpha={ args .alpha } , beta={ args .beta } , ratio={ args .ratio } ):\n " )
342+ logger .info ("[*] Applying %d perturbation(s) (alpha=%s, beta=%s, ratio=%s):\n " ,
343+ len (strategy_names ), args .alpha , args .beta , args .ratio )
290344 timings = []
291345 for name in strategy_names :
292346 entry = STRATEGIES [name ]
293- print ( f " -> { name } " , end = "" , flush = True )
347+ logger . info ( " -> %s " , name )
294348 t0 = time .monotonic ()
295349 try :
296350 if name == "stack_op_insertion" :
@@ -300,58 +354,83 @@ def cmd_obfuscate(args):
300354 elapsed = time .monotonic () - t0
301355 timings .append ((name , elapsed , True ))
302356 if args .timing :
303- print (f" ({ elapsed :.2f} s)" )
304- else :
305- print ()
357+ logger .info (" (%0.2fs)" , elapsed )
306358 except Exception as e :
307359 elapsed = time .monotonic () - t0
308360 timings .append ((name , elapsed , False ))
309- print ()
310- print (f" [warning] { name } failed: { e } " , file = sys .stderr )
361+ if args .strict :
362+ logger .error (" [error] %s failed: %s" , name , e )
363+ sys .exit (1 )
364+ else :
365+ logger .warning (" [warning] %s failed: %s" , name , e )
311366
312367 if args .timing and len (timings ) > 1 :
313368 total = sum (t for _ , t , _ in timings )
314369 slowest = max (timings , key = lambda x : x [1 ])
315- print (f"\n total: { total :.2f} s | slowest: { slowest [0 ]} ({ slowest [1 ]:.2f} s)" )
370+ logger .info ("\n total: %.2fs | slowest: %s (%.2fs)" , total , slowest [0 ], slowest [1 ])
371+
372+ # --diff summary
373+ if args .diff :
374+ after_counts = _section_counts (section )
375+ after_instructions = _count_body_instructions (section )
376+ _print_diff (before_counts , before_instructions , after_counts , after_instructions )
316377
317378 # Save output
318379 output_name = os .path .basename (output_path )
319380 if output_path .endswith (".wasm" ):
320381 # savePertWasm writes .wast and converts to .wasm
321382 wast_out = output_path .replace (".wasm" , ".wast" )
322- print ( f" \n [*] Writing { wast_out } ..." )
383+ logger . info ( " [*] Writing %s ...", wast_out )
323384 parser .savePertWasm (output_dir + "/" , os .path .basename (wast_out ), section )
324385 if os .path .isfile (output_path ):
325- print ( f "[+] Done! Output: { output_path } " )
386+ logger . info ( "[+] Done! Output: %s" , output_path )
326387 else :
327- print ( f "[!] WAT written but wat2wasm conversion may have failed.", file = sys . stderr )
328- print ( f " WAT output: { wast_out } " , file = sys . stderr )
388+ logger . error ( "[!] WAT written but wat2wasm conversion may have failed." )
389+ logger . error ( " WAT output: %s " , wast_out )
329390 sys .exit (1 )
330391 else :
331392 # .wast output
332- print ( f" \n [*] Writing { output_path } ..." )
393+ logger . info ( " [*] Writing %s ...", output_path )
333394 parser .savePertWasm (output_dir + "/" , output_name , section )
334- print ( f "[+] Done! Output: { output_path } " )
395+ logger . info ( "[+] Done! Output: %s" , output_path )
335396
336397 # Validate
337398 if args .validate :
338399 wasm_file = output_path if output_path .endswith (".wasm" ) else output_path .replace (".wast" , ".wasm" )
339400 if not os .path .isfile (wasm_file ):
340- print ( f "[!] No .wasm file to validate.", file = sys . stderr )
401+ logger . error ( "[!] No .wasm file to validate." )
341402 sys .exit (1 )
342- print ( f "[*] Validating { wasm_file } ..." )
403+ logger . info ( "[*] Validating %s ..." , wasm_file )
343404 result = subprocess .run (["wasm-validate" , wasm_file ], capture_output = True , text = True )
344405 if result .returncode == 0 :
345- print ( f "[+] Validation passed." )
406+ logger . info ( "[+] Validation passed." )
346407 else :
347- print ( f "[!] Validation failed: { result .stderr .strip ()} " , file = sys . stderr )
408+ logger . error ( "[!] Validation failed: %s" , result .stderr .strip ())
348409 sys .exit (1 )
349410
350411
412+ def _setup_logging (verbose = False , quiet = False ):
413+ """Configure logging. Default output matches the original print() behaviour."""
414+ if quiet :
415+ level = logging .ERROR
416+ elif verbose :
417+ level = logging .DEBUG
418+ else :
419+ level = logging .INFO
420+ handler = logging .StreamHandler (sys .stdout )
421+ handler .setFormatter (logging .Formatter ("%(message)s" ))
422+ logger .addHandler (handler )
423+ logger .setLevel (level )
424+
425+
351426def main ():
352427 p = build_parser ()
353428 args = p .parse_args ()
354429
430+ # Logging setup — only relevant for obfuscate (list always uses print)
431+ if args .command == "obfuscate" :
432+ _setup_logging (verbose = args .verbose , quiet = args .quiet )
433+
355434 if args .command == "list" :
356435 list_strategies ()
357436 elif args .command == "obfuscate" :
0 commit comments