mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
Dynamic (#77)
This commit is contained in:
parent
55247b7fcd
commit
991c84a1eb
1464 changed files with 225448 additions and 1985 deletions
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
|
|
@ -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
167
.github/workflows/corpus_promote.yml
vendored
Normal 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 }}"
|
||||
5
.github/workflows/docs.yml
vendored
5
.github/workflows/docs.yml
vendored
|
|
@ -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
146
.github/workflows/dynamic.yml
vendored
Normal 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 17–28
|
||||
# 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
348
.github/workflows/eval.yml
vendored
Normal 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 }}
|
||||
68
.github/workflows/fuzz.yml
vendored
68
.github/workflows/fuzz.yml
vendored
|
|
@ -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
68
.github/workflows/image-builder.yml
vendored
Normal 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
|
||||
11
.github/workflows/release-build.yml
vendored
11
.github/workflows/release-build.yml
vendored
|
|
@ -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
104
.github/workflows/repro-bare.yml
vendored
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue