This commit is contained in:
Eli Peter 2026-06-05 10:16:30 -05:00 committed by GitHub
parent 55247b7fcd
commit 991c84a1eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1464 changed files with 225448 additions and 1985 deletions

View file

@ -8,6 +8,7 @@ on:
branches: ["master"]
pull_request:
branches: ["master"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -197,8 +198,8 @@ jobs:
- name: Compile check at MSRV
run: cargo check --all-features --tests
rust-stable-test:
name: rust-stable-test
rust-stable-test-linux-without-docker:
name: rust-stable-test / linux-without-docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@ -210,8 +211,59 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust tests (stable)
run: cargo nextest run --all-features
- name: Rust tests (stable, no docker)
run: cargo nextest run --no-fail-fast --all-features
rust-stable-test-linux-with-docker:
name: rust-stable-test / linux-with-docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
- name: Pull language images for sandbox tests
run: |
docker pull python:3-slim
docker pull node:20-slim
docker pull eclipse-temurin:21-jre-jammy
docker pull php:8-cli
- name: Smoke-test interpreter availability
run: |
docker run --rm python:3-slim python3 --version
docker run --rm node:20-slim node --version
docker run --rm eclipse-temurin:21-jre-jammy java -version
docker run --rm php:8-cli php --version
- name: Rust tests with docker (sandbox escape gate)
run: cargo nextest run --no-fail-fast --all-features --test dynamic_sandbox_escape --test dynamic_parity
escape-positive-control:
name: escape-positive-control
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
- name: Pull python image
run: docker pull python:3-slim
- name: Escape positive control (gate wiring check)
run: |
cargo nextest run --no-fail-fast --all-features --test dynamic_sandbox_escape \
-- --include-ignored positive_control_cap_sys_admin
cross-platform-smoke:
name: cross-platform-smoke
@ -234,7 +286,7 @@ jobs:
run: cargo build --release --all-features
- name: Smoke tests
run: cargo nextest run --all-features --test integration_tests --test pattern_tests --test cli_validation_tests
run: cargo nextest run --no-fail-fast --all-features --test integration_tests --test pattern_tests --test cli_validation_tests
rust-beta-test:
name: rust-beta-test
@ -250,7 +302,7 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust tests (beta)
run: cargo nextest run --all-features
run: cargo nextest run --no-fail-fast --all-features
cargo-package:
name: cargo-package
@ -299,16 +351,18 @@ jobs:
cache: true
cache-key: benchmark-gate-release
- uses: taiki-e/install-action@nextest
- name: Build benchmark + perf test binaries
run: cargo test --release --all-features --test benchmark_test --test perf_tests --no-run
run: cargo nextest run --release --all-features --test benchmark_test --test perf_tests --no-run
- name: Accuracy regression gate (P/R/F1)
run: cargo test --release --all-features --test benchmark_test -- --ignored --nocapture benchmark_evaluation
run: cargo nextest run --no-fail-fast --release --all-features --test benchmark_test --run-ignored only --no-capture benchmark_evaluation
- name: Performance regression gate
env:
NYX_CI_BENCH: "1"
run: cargo test --release --all-features --test perf_tests -- --nocapture
run: cargo nextest run --no-fail-fast --release --all-features --test perf_tests --no-capture
- name: Upload benchmark results
if: always()
@ -317,3 +371,34 @@ jobs:
name: benchmark-results
path: tests/benchmark/results/latest.json
if-no-files-found: warn
corpus-marker-audit:
name: corpus-marker-audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Marker collision audit (§16.3)
run: python3 scripts/corpus_dashboard.py
# Exits non-zero if any oracle marker from one cap appears in another
# cap's payload bytes. This catches cross-cap oracle collisions that
# would cause false-positive confirmed verdicts.
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
- name: Corpus unit tests (no_marker_collisions, all_payloads_have_fixture_paths)
run: cargo nextest run --no-fail-fast --lib -p nyx-scanner dynamic::corpus
env:
RUST_LOG: error
- name: Corpus dashboard sync check (Python/Rust payload table parity)
run: python3 scripts/check_corpus_sync.py

167
.github/workflows/corpus_promote.yml vendored Normal file
View file

@ -0,0 +1,167 @@
name: Corpus Promote
# Weekly automated promotion-PR template.
#
# Scans fuzz-discovered/ for candidates not yet in src/dynamic/corpus.rs
# and opens a PR proposing them for human review (§16.4 — no auto-merge).
#
# Also runs the marker-collision audit as a hard gate: if any collision is
# found the workflow fails rather than proposing the promotion.
on:
schedule:
# Sundays at 09:00 UTC — offset from the fuzz run (06:00 UTC) so
# discovered candidates are ready before the promotion job runs.
- cron: "0 9 * * 0"
workflow_dispatch:
inputs:
dry_run:
description: "Dry run (print PR body but do not open)"
required: false
default: "false"
permissions:
contents: write
pull-requests: write
concurrency:
group: corpus-promote
cancel-in-progress: true
jobs:
promote:
name: Propose corpus promotions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- 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
# ── Marker collision audit ──────────────────────────────────────────────
- name: Marker collision audit
run: |
set -euo pipefail
cargo build --features dynamic -p nyx-scanner 2>/dev/null || true
cd fuzz/dynamic_corpus
cargo run -- audit-markers
env:
RUST_LOG: error
# ── Discover candidates ─────────────────────────────────────────────────
- name: Find promotion candidates
id: candidates
run: |
set -euo pipefail
count=0
files=""
if [ -d fuzz-discovered ]; then
while IFS= read -r f; do
# Skip .gitkeep, sidecar JSONs, and files already listed in corpus.rs.
[[ "$f" == *".gitkeep" ]] && continue
[[ "$f" == *".json" ]] && continue
bytes=$(xxd -p "$f" | tr -d '\n')
if ! grep -q "$bytes" src/dynamic/corpus.rs 2>/dev/null; then
count=$((count + 1))
files="$files $f"
fi
done < <(find fuzz-discovered -type f | sort)
fi
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "files=$files" >> "$GITHUB_OUTPUT"
- name: Skip if no new candidates
if: steps.candidates.outputs.count == '0'
run: |
echo "No new candidates found in fuzz-discovered/. Nothing to promote."
# ── Open promotion PR ───────────────────────────────────────────────────
- name: Open promotion PR
if: >
steps.candidates.outputs.count != '0' &&
github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE_COUNT: ${{ steps.candidates.outputs.count }}
CANDIDATE_FILES: ${{ steps.candidates.outputs.files }}
run: |
set -euo pipefail
branch="corpus-promote-$(date +%Y%m%d)"
git checkout -b "$branch"
# Stage candidate files into fuzz-discovered (already there).
# The PR body provides the reviewer with everything they need.
# Build PR body into a temp file to avoid shell re-interpolation of
# sidecar JSON content (which may contain backticks or $(...) sequences).
body_file=$(mktemp)
cat > "$body_file" <<'PREAMBLE'
## Corpus Promotion Proposal
This PR was generated automatically by the weekly corpus-promote workflow.
It does **not** auto-merge — a human reviewer must approve each candidate
before it can land in `src/dynamic/corpus.rs` (§16.4).
### Candidates
The following payloads were discovered by the internal mutation fuzzer and
confirmed via `sink_hit && oracle_fired` against instrumented fixtures:
PREAMBLE
for f in $CANDIDATE_FILES; do
sidecar="${f}.json"
printf -- '- `%s`\n' "$f" >> "$body_file"
if [ -f "$sidecar" ]; then
printf ' ```json\n' >> "$body_file"
cat "$sidecar" >> "$body_file"
printf '\n ```\n' >> "$body_file"
fi
done
cat >> "$body_file" <<'CHECKLIST'
### Review checklist
- [ ] Bytes are a genuine attack vector, not a fixture artifact
- [ ] Oracle marker is unique (no collision with other caps)
- [ ] `fixture_paths` updated in `src/dynamic/corpus.rs`
- [ ] `since_corpus_version` set to next version
- [ ] `CORPUS_VERSION` bumped and bump history updated
_Generated by corpus_promote.yml — do not auto-merge._
CHECKLIST
git add fuzz-discovered/ || true
git diff --cached --quiet || git commit -m "chore: add ${CANDIDATE_COUNT} fuzzer-discovered corpus candidates"
git push origin "$branch"
gh pr create \
--title "chore(corpus): promote ${CANDIDATE_COUNT} fuzzer-discovered payload(s)" \
--body "$(cat "$body_file")" \
--base master \
--label "corpus-promotion" || true
rm -f "$body_file"
- name: Dry run summary
if: github.event.inputs.dry_run == 'true'
run: |
echo "Dry run: would promote ${{ steps.candidates.outputs.count }} candidate(s)."
echo "Files: ${{ steps.candidates.outputs.files }}"

View file

@ -25,6 +25,11 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- name: Cache mdbook
id: cache-mdbook
uses: actions/cache@v5

146
.github/workflows/dynamic.yml vendored Normal file
View file

@ -0,0 +1,146 @@
# Phase 29 (Track I): dedicated dynamic-verification matrix.
#
# Three rows exercise the dynamic harness pipeline (`cargo nextest run
# --features dynamic`) under the host configurations the Phase 1728
# tracks documented as supported:
#
# linux-process-only — Ubuntu host, no docker daemon. Forces the
# process backend and exercises the Phase 17
# Linux hardening primitives (chroot, seccomp,
# unshare, no_new_privs). `libc6-dev` is
# installed so the hardening probe + escape
# suite can `cc -static`; without it the
# chroot-leg of the escape suite skips silently
# (Phase 20 follow-up #4 in deferred.md).
#
# linux-with-docker — Ubuntu host with the runner Docker daemon. Exercises
# the docker backend (Phase 19) and the
# differential-confirmation parity tests.
#
# macos — macOS-latest, no docker. Exercises the
# Phase-18 `sandbox-exec` primitives plus the
# process backend on Darwin. Track-I acceptance
# literal: "cargo nextest run --features dynamic
# is green on macOS without docker."
name: dynamic
permissions:
contents: read
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
linux-process-only:
name: dynamic / linux-process-only
runs-on: ubuntu-latest
env:
# Force the process backend even when callers default to Auto so
# docker-unavailable paths cannot accidentally hide a regression.
NYX_SANDBOX_BACKEND: process
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
# Phase 17 / Phase 20 follow-up: the hardening probe + escape
# suite chroot leg need static glibc. Without these packages the
# `cc -static probe.c` step in tests/sandbox_hardening_linux.rs +
# tests/sandbox_escape_suite.rs falls back to dynamic linking and
# the chroot leg silently skips.
- name: Install fixture prerequisites (static libc)
run: |
sudo apt-get update -y
sudo apt-get install -y --no-install-recommends libc6-dev libc-dev-bin
- name: Smoke-test interpreter availability
run: |
python3 --version
node --version || sudo apt-get install -y --no-install-recommends nodejs
ruby --version || true
php --version || true
- name: Dynamic suite (process backend only)
run: cargo nextest run --no-fail-fast --features dynamic
linux-with-docker:
name: dynamic / linux-with-docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
- name: Install fixture prerequisites (static libc)
run: |
sudo apt-get update -y
sudo apt-get install -y --no-install-recommends libc6-dev libc-dev-bin
- name: Pull language images for sandbox tests
run: |
docker pull python:3-slim
docker pull node:20-slim
docker pull eclipse-temurin:21-jre-jammy
docker pull php:8-cli
- name: Smoke-test docker interpreter availability
run: |
docker run --rm python:3-slim python3 --version
docker run --rm node:20-slim node --version
docker run --rm eclipse-temurin:21-jre-jammy java -version
docker run --rm php:8-cli php --version
- name: Dynamic suite (process + docker backends)
run: cargo nextest run --no-fail-fast --features dynamic
macos:
name: dynamic / macos
runs-on: macos-latest
env:
# macOS runners ship without docker; force process backend so the
# `Auto` resolver in src/dynamic/sandbox.rs cannot accidentally
# pick up a stray Lima/Colima daemon and confuse the matrix.
NYX_SANDBOX_BACKEND: process
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
- name: Smoke-test sandbox-exec availability
run: |
/usr/bin/sandbox-exec -p '(version 1)(allow default)' /bin/echo ok
- name: Smoke-test interpreter availability
run: |
python3 --version
node --version
ruby --version
# Phase 29 acceptance literal: "cargo nextest run --features
# dynamic is green on macOS without docker (process-only row)."
- name: Dynamic suite (macOS, process backend)
run: cargo nextest run --no-fail-fast --features dynamic

348
.github/workflows/eval.yml vendored Normal file
View file

@ -0,0 +1,348 @@
# Real-corpus acceptance (Track R).
#
# * owasp (Phase 27 / Track R.0): Gate 6 vs a real OWASP BenchmarkJava
# checkout (Java).
# * jsts (Phase 28 / Track R.1): Gate 7 vs OWASP NodeGoat (Express, .js)
# and OWASP Juice Shop (TypeScript, .ts), one matrix row per corpus.
# * polyglot (Phase 29 / Track R.2): Gate 8 vs OWASP RailsGoat (Rails, .rb),
# DVWA (PHP), DVPWA (aiohttp, .py), gosec (Go) and the RustSec advisory-db
# (Rust negative control), one matrix row per corpus.
#
# Runs on every PR that touches the dynamic verifier (src/dynamic/), the
# eval-corpus harness (tests/eval_corpus/), or the gate script itself.
#
# Each gate enforces, against the committed ground truth:
# * verify wall-clock <= 15 min (CI budget; the dev reference is 10 min),
# * the per-(cap,lang) budget in tests/eval_corpus/budget.toml,
# * per-cap confirmed-rate / precision / recall — hard-gated only for caps
# in NYX_*_FLOOR_CAPS (empty by default → published report-only until a
# cap Confirms end to end), with destinations >= 40% / >= 0.85 / >= 0.40.
#
# No corpus is vendored. Each is cloned at a pinned ref and cached so reruns
# skip the clone. Before the gate runs, the committed ground truth is
# regenerated from its source against the fresh clone and asserted in sync,
# and the converter hard-errors on any labelled path missing from the corpus,
# so a corpus bump that drifts the labels fails the job loudly.
name: eval
permissions:
contents: read
on:
push:
branches: ["master"]
paths:
- "src/dynamic/**"
- "tests/eval_corpus/**"
- "scripts/m7_ship_gate.sh"
- ".github/workflows/eval.yml"
pull_request:
branches: ["master"]
paths:
- "src/dynamic/**"
- "tests/eval_corpus/**"
- "scripts/m7_ship_gate.sh"
- ".github/workflows/eval.yml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
owasp:
name: eval / owasp-benchmark-v1.2
runs-on: ubuntu-latest
env:
# Gate 6 self-skips unless this points at a real checkout.
NYX_OWASP_CORPUS: ${{ github.workspace }}/.eval-corpus/owasp_benchmark_v1.2
# CI wall-clock budget: 20 min. The 2740-file OWASP scan+verify lands
# right at the old 15-min ceiling on the hosted runners (observed 900.2s),
# so the gate tripped on CI variance alone; 1200s restores headroom. The
# dev reference stays 10 min — override locally to tighten.
NYX_OWASP_WALLCLOCK_BUDGET_SECONDS: "1200"
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
# The Phase 22 Java compile pool drives `com.sun.tools.javac` out of a
# warm JDK; temurin 21 ships the compiler module the pool loads.
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Cache OWASP BenchmarkJava (1.2beta)
id: cache-owasp
uses: actions/cache@v4
with:
path: .eval-corpus/owasp_benchmark_v1.2
key: owasp-benchmark-1.2beta
- name: Clone OWASP BenchmarkJava (1.2beta tag)
if: steps.cache-owasp.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch 1.2beta \
https://github.com/OWASP-Benchmark/BenchmarkJava \
.eval-corpus/owasp_benchmark_v1.2
# No-compromise guard: the committed ground truth must be exactly what a
# fresh conversion of the pinned CSV produces. Catches GT drift (a
# corpus bump, a hand-edit) before the gate runs on stale labels.
- name: Verify ground truth is in sync with the pinned corpus
run: |
python3 tests/eval_corpus/owasp_gt_convert.py \
--corpus-dir .eval-corpus/owasp_benchmark_v1.2 \
--output /tmp/owasp_gt_regen.json
python3 - <<'PY'
import json, sys
committed = json.load(open("tests/eval_corpus/ground_truth/owasp_benchmark_v1.2.json"))
regen = json.load(open("/tmp/owasp_gt_regen.json"))
if committed != regen:
sys.exit("committed ground truth diverges from a fresh conversion of "
"the 1.2beta CSV; regenerate with owasp_gt_convert.py")
print(f"ground truth in sync: {len(committed)} records")
PY
- name: eval-corpus harness regression tests
run: |
python3 tests/eval_corpus/test_tabulate_regression.py
python3 tests/eval_corpus/test_manifest_gt_convert.py
- name: Gate 6 — OWASP Benchmark v1.2 acceptance
run: scripts/m7_ship_gate.sh --sets owasp
jsts:
name: eval / ${{ matrix.corpus.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
corpus:
- name: nodegoat
repo: https://github.com/OWASP/NodeGoat
# NodeGoat ships no release tags; pin the default branch and let
# the cache key hold it stable. The manifest's path layout
# (app/, config/) has been constant for years.
ref: master
env: NYX_NODEGOAT_CORPUS
manifest: nodegoat.manifest.toml
ground_truth: nodegoat.json
- name: juiceshop
repo: https://github.com/juice-shop/juice-shop
ref: v15.0.0
env: NYX_JUICESHOP_CORPUS
manifest: juiceshop.manifest.toml
ground_truth: juiceshop.json
env:
# CI wall-clock budget: 15 min. Override locally to tighten.
NYX_JSTS_WALLCLOCK_BUDGET_SECONDS: "900"
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
# The dynamic verifier's Node build pool (Phase 23) compiles its
# harnesses with a real node/npm toolchain.
- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Cache ${{ matrix.corpus.name }}
id: cache-corpus
uses: actions/cache@v4
with:
path: .eval-corpus/${{ matrix.corpus.name }}
key: jsts-${{ matrix.corpus.name }}-${{ matrix.corpus.ref }}
- name: Clone ${{ matrix.corpus.name }} (${{ matrix.corpus.ref }})
if: steps.cache-corpus.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch ${{ matrix.corpus.ref }} \
${{ matrix.corpus.repo }} \
.eval-corpus/${{ matrix.corpus.name }}
# No-compromise guard: the committed ground truth must be exactly what a
# fresh conversion of the curated manifest produces *against this
# corpus*. manifest_gt_convert.py hard-errors on any labelled path that
# no longer exists in the clone (corpus drift / typo), and the diff
# below catches a stale committed JSON.
- name: Verify ground truth is in sync with the pinned corpus
run: |
python3 tests/eval_corpus/manifest_gt_convert.py \
--manifest tests/eval_corpus/ground_truth/${{ matrix.corpus.manifest }} \
--corpus-dir .eval-corpus/${{ matrix.corpus.name }} \
--output /tmp/${{ matrix.corpus.name }}_gt_regen.json
python3 - <<'PY'
import json, sys
name = "${{ matrix.corpus.ground_truth }}"
committed = json.load(open(f"tests/eval_corpus/ground_truth/{name}"))
regen = json.load(open("/tmp/${{ matrix.corpus.name }}_gt_regen.json"))
if committed != regen:
sys.exit("committed ground truth diverges from a fresh conversion of "
"the manifest against the pinned corpus; regenerate with "
"manifest_gt_convert.py")
print(f"ground truth in sync: {len(committed)} records")
PY
- name: eval-corpus harness regression tests
run: |
python3 tests/eval_corpus/test_tabulate_regression.py
python3 tests/eval_corpus/test_manifest_gt_convert.py
- name: Gate 7 — ${{ matrix.corpus.name }} acceptance
run: |
export ${{ matrix.corpus.env }}="${{ github.workspace }}/.eval-corpus/${{ matrix.corpus.name }}"
scripts/m7_ship_gate.sh --sets ${{ matrix.corpus.name }}
polyglot:
name: eval / ${{ matrix.corpus.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
corpus:
- name: railsgoat
repo: https://github.com/OWASP/railsgoat
ref: rails.5.0.0
lang: ruby
env: NYX_RAILSGOAT_CORPUS
manifest: railsgoat.manifest.toml
ground_truth: railsgoat.json
- name: dvwa
repo: https://github.com/digininja/DVWA
ref: "2.5"
lang: php
env: NYX_DVWA_CORPUS
manifest: dvwa.manifest.toml
ground_truth: dvwa.json
- name: dvpwa
repo: https://github.com/anxolerd/dvpwa
# DVPWA ships no release tags; pin the default branch and let the
# cache key hold it stable.
ref: master
lang: python
env: NYX_DVPWA_CORPUS
manifest: dvpwa.manifest.toml
ground_truth: dvpwa.json
- name: gosec
repo: https://github.com/securego/gosec
ref: v2.26.1
lang: go
env: NYX_GOSEC_CORPUS
manifest: gosec.manifest.toml
ground_truth: gosec.json
- name: rustsec
repo: https://github.com/rustsec/advisory-db
# advisory-db ships no release tags; pin the default branch. This
# is the Rust NEGATIVE CONTROL (advisory metadata, no scannable
# source) — its committed ground truth is empty by construction.
ref: main
lang: rust
env: NYX_RUSTSEC_CORPUS
manifest: rustsec.manifest.toml
ground_truth: rustsec.json
env:
# CI wall-clock budget: 15 min. Override locally to tighten.
NYX_POLYGLOT_WALLCLOCK_BUDGET_SECONDS: "900"
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- uses: taiki-e/install-action@nextest
# The dynamic verifier's per-language build pool (Phase 22/23) compiles
# its harnesses with a real toolchain. Each matrix row sets up only the
# toolchain for its corpus's target language; the Rust row needs no extra
# step (the rust toolchain above covers it, and advisory-db has no
# buildable source anyway).
- name: Set up Ruby
if: matrix.corpus.lang == 'ruby'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
- name: Set up PHP
if: matrix.corpus.lang == 'php'
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- name: Set up Python
if: matrix.corpus.lang == 'python'
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Set up Go
if: matrix.corpus.lang == 'go'
uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Cache ${{ matrix.corpus.name }}
id: cache-corpus
uses: actions/cache@v4
with:
path: .eval-corpus/${{ matrix.corpus.name }}
key: polyglot-${{ matrix.corpus.name }}-${{ matrix.corpus.ref }}
- name: Clone ${{ matrix.corpus.name }} (${{ matrix.corpus.ref }})
if: steps.cache-corpus.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch ${{ matrix.corpus.ref }} \
${{ matrix.corpus.repo }} \
.eval-corpus/${{ matrix.corpus.name }}
# No-compromise guard: the committed ground truth must be exactly what a
# fresh conversion of the curated manifest produces *against this corpus*.
# manifest_gt_convert.py hard-errors on any labelled path that no longer
# exists in the clone (corpus drift / typo); the diff below catches a
# stale committed JSON. For the RustSec negative control the manifest
# carries `negative_control = true` and zero entries, so the converter
# emits an empty `[]` — still validated against the real clone.
- name: Verify ground truth is in sync with the pinned corpus
run: |
python3 tests/eval_corpus/manifest_gt_convert.py \
--manifest tests/eval_corpus/ground_truth/${{ matrix.corpus.manifest }} \
--corpus-dir .eval-corpus/${{ matrix.corpus.name }} \
--output /tmp/${{ matrix.corpus.name }}_gt_regen.json
python3 - <<'PY'
import json, sys
name = "${{ matrix.corpus.ground_truth }}"
committed = json.load(open(f"tests/eval_corpus/ground_truth/{name}"))
regen = json.load(open("/tmp/${{ matrix.corpus.name }}_gt_regen.json"))
if committed != regen:
sys.exit("committed ground truth diverges from a fresh conversion of "
"the manifest against the pinned corpus; regenerate with "
"manifest_gt_convert.py")
print(f"ground truth in sync: {len(committed)} records")
PY
- name: eval-corpus harness regression tests
run: |
python3 tests/eval_corpus/test_tabulate_regression.py
python3 tests/eval_corpus/test_manifest_gt_convert.py
- name: Gate 8 — ${{ matrix.corpus.name }} acceptance
run: |
export ${{ matrix.corpus.env }}="${{ github.workspace }}/.eval-corpus/${{ matrix.corpus.name }}"
scripts/m7_ship_gate.sh --sets ${{ matrix.corpus.name }}

View file

@ -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

68
.github/workflows/image-builder.yml vendored Normal file
View file

@ -0,0 +1,68 @@
name: image-builder
# Phase 19 (Track E.3): daily drift PR.
#
# Runs `nyx-image-builder build --all` on a Linux runner that has docker
# available, captures the rewritten `tools/image-builder/images.toml`, and
# opens a PR when any pinned digest changed. The PR is reviewed manually
# before merge so a hostile upstream image cannot silently land in
# `IMAGE_DIGESTS`.
permissions:
contents: write
pull-requests: write
on:
schedule:
# 04:23 UTC daily — off-peak for the major upstream registries so
# transient pull errors are rare.
- cron: "23 4 * * *"
workflow_dispatch:
concurrency:
group: image-builder
cancel-in-progress: false
jobs:
refresh-digests:
name: refresh image digests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- name: Verify docker is reachable
run: docker info
- name: Build pinned-digest catalogue
run: |
cargo run -F image-builder --bin nyx-image-builder -- build --all
- name: Verify catalogue against local pulls
run: |
cargo run -F image-builder --bin nyx-image-builder -- verify
- name: Open PR on drift
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "image-builder: refresh pinned digests"
title: "image-builder: refresh pinned digests"
body: |
Automated digest refresh by `nyx-image-builder build --all`.
The CI job pulled every base image in
`tools/image-builder/images.toml`, captured the resolved
`sha256:` digest, and wrote it back into the file. Review
the diff before merging — a hostile upstream image would
show up here as an unexpected digest change.
branch: image-builder/refresh-digests
base: master
delete-branch: true
labels: |
image-builder
automation

View file

@ -110,7 +110,12 @@ jobs:
BIN_PATH=target/$TARGET/release/$BIN$EXT
mkdir -p dist
ARCHIVE=$BIN-$TARGET.zip
zip -9 "dist/$ARCHIVE" "$BIN_PATH" THIRDPARTY-LICENSES.html LICENSE* COPYING*
files=("$BIN_PATH" THIRDPARTY-LICENSES.html)
shopt -s nullglob
license_files=(LICENSE* COPYING*)
shopt -u nullglob
files+=("${license_files[@]}")
zip -9 "dist/$ARCHIVE" "${files[@]}"
echo "ASSET=$ARCHIVE" >> "$GITHUB_ENV"
- name: Package (Windows)
@ -123,9 +128,11 @@ jobs:
$BinPath = "target/$Target/release/$Bin$Ext"
New-Item -ItemType Directory -Path dist -Force | Out-Null
$Archive = "$Bin-$Target.zip"
$LicenseFiles = @(Get-ChildItem -Path 'LICENSE*', 'COPYING*' -File -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })
$Files = @($BinPath, 'THIRDPARTY-LICENSES.html') + $LicenseFiles
Compress-Archive `
-Path $BinPath, 'THIRDPARTY-LICENSES.html', 'LICENSE*', 'COPYING*' `
-Path $Files `
-DestinationPath "dist/$Archive" `
-CompressionLevel Optimal

104
.github/workflows/repro-bare.yml vendored Normal file
View file

@ -0,0 +1,104 @@
# Replay every tree-committed dynamic repro bundle with host language
# toolchains blocked so we catch regressions where a bundle silently
# depends on an interpreter the operator does not have.
#
# The setup step prepends deny-list wrappers for python3, node, ruby,
# php, and Java so the only toolchain the bundle can use is the docker
# daemon. reproduce.sh in --docker mode pulls the pinned base image
# (via docker_pull.sh) and runs the harness inside the container; if the
# bundle accidentally relied on a host interpreter the run falls over
# before the sentinel check.
#
# Adding a new fixture: extend the `matrix.fixture` list with the new
# `tests/repro_fixtures/<toolchain_id>/<spec_hash>` path. The bundle
# must already exist on disk, see tests/repro_fixture_bundles.rs for
# the regeneration recipe.
name: repro-bare
permissions:
contents: read
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
bare-image-replay:
name: repro-bare / ${{ matrix.fixture }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fixture:
- tests/repro_fixtures/python-3.11/repro
steps:
- uses: actions/checkout@v6
- name: Block host language toolchains
run: |
set -euo pipefail
# Do not mutate the hosted runner image. ubuntu-latest carries
# preinstalled and cached language runtimes, and apt package
# relationships can shift underneath us as the image is updated.
# A PATH-level deny layer gives this job the bare-host semantics it
# needs without depending on apt being able to uninstall core bits.
deny_dir="${RUNNER_TEMP}/nyx-deny-toolchains"
mkdir -p "$deny_dir"
for exe in \
python python3 python3.10 python3.11 python3.12 python3.13 python3.14 \
node npm npx corepack \
ruby gem bundle \
php \
java javac jar
do
{
printf '%s\n' '#!/bin/sh'
printf '%s\n' 'echo "error: host language toolchain is disabled in repro-bare; use the Docker replay path" >&2'
printf '%s\n' 'exit 127'
} > "${deny_dir}/${exe}"
chmod +x "${deny_dir}/${exe}"
done
export PATH="${deny_dir}:${PATH}"
echo "${deny_dir}" >> "${GITHUB_PATH}"
hash -r 2>/dev/null || true
# Confirm the deny layer is active — surface the failure here
# rather than inside reproduce.sh where it would look like a
# bundle bug.
for exe in python3 node ruby php java; do
resolved="$(command -v "${exe}" || true)"
if [ "${resolved}" != "${deny_dir}/${exe}" ]; then
echo "error: ${exe} deny wrapper is not first on PATH (got ${resolved:-not found})" >&2
exit 1
fi
if "${exe}" --version >/dev/null 2>&1; then
echo "error: ${exe} still runs after host-toolchain block" >&2
exit 1
fi
done
if ! command -v docker >/dev/null 2>&1; then
echo "error: docker is no longer reachable after host-toolchain block" >&2
exit 1
fi
- name: Verify docker is reachable
run: docker info
- name: Pre-pull pinned image
working-directory: ${{ matrix.fixture }}
run: ./docker_pull.sh
- name: Replay bundle via docker
working-directory: ${{ matrix.fixture }}
run: ./reproduce.sh --docker