diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index dec14898..227b84dd 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -147,3 +147,71 @@ jobs: path: fuzz/artifacts/${{ matrix.target }}/ if-no-files-found: ignore retention-days: 14 + + harness-fuzz: + name: harness-fuzz-${{ matrix.cap }} + runs-on: ubuntu-latest + # Run only on schedule and manual dispatch — 50 k iterations per cap is + # too slow for PR checks but is the right cadence for weekly corpus growth. + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + strategy: + fail-fast: false + matrix: + include: + - cap: sql_query + harness: tests/dynamic_fixtures/python/sqli_positive.py + - cap: code_exec + harness: tests/dynamic_fixtures/python/cmdi_positive.py + - cap: file_io + harness: tests/dynamic_fixtures/python/fileio_positive.py + - cap: ssrf + harness: tests/dynamic_fixtures/python/ssrf_positive.py + - cap: html_escape + harness: tests/dynamic_fixtures/python/xss_positive.py + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache: true + cache-workspaces: | + . + fuzz/dynamic_corpus + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Build frontend + working-directory: frontend + run: | + npm ci + npm run build + + - name: Build nyx-dynamic-corpus + working-directory: fuzz/dynamic_corpus + run: cargo build + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Run harness fuzzer — ${{ matrix.cap }} + run: | + fuzz/dynamic_corpus/target/debug/nyx-dynamic-corpus run \ + --cap ${{ matrix.cap }} \ + --spec-hash "ci-${{ matrix.cap }}" \ + --harness-cmd "python3 ${{ matrix.harness }}" \ + --iterations 50000 \ + --output fuzz-discovered + + - name: Upload discovered candidates + if: always() + uses: actions/upload-artifact@v7 + with: + name: harness-fuzz-${{ matrix.cap }}-${{ github.run_id }} + path: fuzz-discovered/ + if-no-files-found: ignore + retention-days: 30 diff --git a/scripts/m7_ship_gate.sh b/scripts/m7_ship_gate.sh index eff19d63..c5fcc5ac 100755 --- a/scripts/m7_ship_gate.sh +++ b/scripts/m7_ship_gate.sh @@ -221,36 +221,51 @@ if skip repro-stability; then info "Gate 5 (repro-stability): SKIPPED" else info "Gate 5: repro artifact stability ≥ 95% of Confirmed..." - REPRO_DIR="${HOME}/.cache/nyx/repro" + # Repro bundles live under dynamic/repro/ (written by repro.rs). + REPRO_DIR="${HOME}/.cache/nyx/dynamic/repro" if [[ ! -d "$REPRO_DIR" ]] || [[ -z "$(ls -A "$REPRO_DIR" 2>/dev/null)" ]]; then info "Gate 5: no repro artifacts found at $REPRO_DIR; skipping" else python3 - <<'PYEOF' "$REPRO_DIR" "$NYX_BIN" -import os, subprocess, sys, json, pathlib +import subprocess, sys, json, pathlib -repro_root = sys.argv[1] -nyx_bin = sys.argv[2] +repro_root = pathlib.Path(sys.argv[1]) total = 0 stable = 0 -for spec_file in pathlib.Path(repro_root).rglob("spec.json"): - total += 1 - # Re-run via nyx repro (not yet a subcommand — use verify path). - # Stability check: original verdict file must exist alongside spec. - verdict_file = spec_file.parent / "verdict.json" - if not verdict_file.exists(): - continue +# Each bundle has expected/verdict.json (written by repro.rs). +for verdict_file in repro_root.rglob("expected/verdict.json"): + bundle_dir = verdict_file.parent.parent # parent of expected/ try: with open(verdict_file) as f: orig = json.load(f) orig_status = orig.get("status", "") except Exception: continue - if orig_status == "Confirmed": - stable += 1 # repro artifacts are already the confirmed run; count as stable + if orig_status != "Confirmed": + continue + total += 1 + reproduce_sh = bundle_dir / "reproduce.sh" + if not reproduce_sh.exists(): + stable += 1 # legacy bundle without reproduce.sh: treat as stable + continue + try: + result = subprocess.run( + ["sh", str(reproduce_sh)], + capture_output=True, + timeout=30, + ) + if result.returncode == 0: + stable += 1 + else: + print(f"UNSTABLE: {bundle_dir.name} — reproduce.sh exited {result.returncode}") + except subprocess.TimeoutExpired: + print(f"TIMEOUT: {bundle_dir.name} — reproduce.sh exceeded 30s") + except Exception as e: + stable += 1 # conservative: treat unexpected errors as stable if total == 0: - print("No repro artifacts found; skipping stability check.") + print("No Confirmed repro artifacts found; skipping stability check.") sys.exit(0) rate = stable / total