Skip to content

Commit e8cd5d8

Browse files
committed
feat(cli): add --seed, --diff, --strict, -v/-q flags
1 parent 9e14fbf commit e8cd5d8

5 files changed

Lines changed: 147 additions & 55 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ __pycache__/
55
dist/
66
build/
77
.eggs/
8+
meta/*

cli/README.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,11 @@ swamped obfuscate input.wasm -o output.wasm -s nop_insertion shift_transformatio
2525
# Apply all strategies at once
2626
swamped obfuscate input.wasm -o output.wasm -s all
2727

28-
# Only structural or only code-level
29-
swamped obfuscate input.wasm -o output.wasm -s structural
30-
swamped obfuscate input.wasm -o output.wasm -s code
28+
# Abort on first failure (useful in CI)
29+
swamped obfuscate input.wasm -o output.wasm -s all --strict
3130

32-
# Exclude specific strategies with -e
33-
swamped obfuscate input.wasm -o output.wasm -s all -e data_encryption direct_to_indirect
34-
swamped obfuscate input.wasm -o output.wasm -s code -e nop_insertion
35-
36-
# Control intensity (0.0 = none, 1.0 = max)
37-
swamped obfuscate input.wasm -o output.wasm -s all --ratio 0.3
38-
39-
# Validate the output after obfuscation
40-
swamped obfuscate input.wasm -o output.wasm -s nop_insertion --validate
41-
42-
# Stack operation insertion: all 6 variants (default)
43-
swamped obfuscate input.wasm -o output.wasm -s stack_op_insertion
44-
45-
# Stack operation insertion: only specific variants
46-
swamped obfuscate input.wasm -o output.wasm -s stack_op_insertion --stackop m b f
31+
# Combine seed + diff for reproducible experiments
32+
swamped obfuscate input.wasm -o output.wasm -s all --seed 42 --diff -t
4733
```
4834

4935
Accepts both `.wasm` and `.wast` as input/output — conversion is automatic.
@@ -109,6 +95,11 @@ Example: `swamped obfuscate input.wasm -o out.wasm -s stack_op_insertion --stack
10995
| `--alpha` | `1.0` | Beta-distribution alpha (target selection bias) |
11096
| `--beta` | `1.0` | Beta-distribution beta (target selection bias) |
11197
| `--validate` | off | Run `wasm-validate` on the output |
98+
| `--seed` || RNG seed for reproducible runs (see below) |
99+
| `--diff` | off | Print a before/after summary of sections and instructions |
100+
| `--strict` | off | Abort immediately on the first strategy failure |
101+
| `-v` | off | Verbose (debug-level) logging |
102+
| `-q` | off | Quiet mode — only print errors |
112103

113104
## Parameters
114105

@@ -127,6 +118,19 @@ Defaults (`alpha=1, beta=1, ratio=1`) match the original project.
127118

128119
If you just want to control how much gets changed, `--ratio` is enough. Use `--alpha`/`--beta` when you need finer control over the spatial distribution of perturbations.
129120

121+
**`--seed`** enables **reproducible runs**. Given the same input file, seed, and parameters, SWAMPED will produce the exact same output. This is useful for:
122+
123+
- **Research** — reproduce an experiment exactly and report the seed alongside results
124+
- **Debugging** — if a run produces an invalid module, re-run with the same seed to investigate
125+
- **Comparison** — test different strategy sets against the same input with identical perturbation distributions
126+
127+
```bash
128+
swamped obfuscate input.wasm -o out.wasm -s all --seed 42
129+
swamped obfuscate input.wasm -o out.wasm -s all --seed 42 # same output
130+
```
131+
132+
Without `--seed`, every run uses a fresh unseeded RNG (non-deterministic).
133+
130134
## Preliminary tests
131135

132136
**Setup:** the `function_insertion` strategy requires a symlink `strategies/data/function_body.pkl` pointing to one of the precomputed pool files. Create it before running obfuscation:

cli/swamped_cli.py

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import argparse
1010
import copy
11+
import logging
1112
import os
1213
import subprocess
1314
import sys
@@ -22,6 +23,9 @@
2223
from wasmParser import parser
2324
from strategies import structural_perturbation as sp
2425
from 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

101105
def 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

132169
def 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+
351426
def 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":

strategies/data/function_body.pkl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
function_body_tmp10000.pkl

0 commit comments

Comments
 (0)