Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ on:
permissions:
contents: read

concurrency:
group: wardnet-ci-${{ github.repository }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
rust:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
Expand Down
46 changes: 18 additions & 28 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,15 @@ permissions:
contents: read

concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
group: wardnet-fuzz-${{ github.repository }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'schedule' && format('schedule-{0}', github.ref) || format('run-{0}', github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
fuzz:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- fuzz_score_request
- fuzz_appdata_json
- fuzz_parse_admin_tokens
- fuzz_dnsbl_zone
# One runner covers every bounded target; a matrix would consume four org slots.
runs-on: ubuntu-24.04
env:
FUZZ_SECONDS: ${{ github.event_name == 'pull_request' && '60' || '300' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

Expand All @@ -46,30 +41,25 @@ jobs:
path: |
~/.cargo/bin/cargo-fuzz
fuzz/target
key: cargo-fuzz-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('fuzz/Cargo.toml') }}
key: cargo-fuzz-0.13.2-${{ runner.os }}-${{ hashFiles('fuzz/Cargo.toml') }}

- name: Install cargo-fuzz
run: cargo +nightly install cargo-fuzz --version 0.13.2 --locked || true
run: command -v cargo-fuzz >/dev/null || cargo +nightly install cargo-fuzz --version 0.13.2 --locked

# PRs: 60s per target. Nightly/manual: 300s per target.
- name: Set fuzz duration
- name: Fuzz bounded targets
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "FUZZ_SECONDS=60" >> "$GITHUB_ENV"
else
echo "FUZZ_SECONDS=300" >> "$GITHUB_ENV"
fi

- name: Fuzz ${{ matrix.target }}
run: |
cargo +nightly fuzz run ${{ matrix.target }} -- \
-max_total_time="$FUZZ_SECONDS" \
-rss_limit_mb=2048
exit_code=0
for target in fuzz_score_request fuzz_appdata_json fuzz_parse_admin_tokens fuzz_dnsbl_zone; do
cargo +nightly fuzz run "$target" -- \
-max_total_time="$FUZZ_SECONDS" \
-rss_limit_mb=2048 || exit_code=$?
done
exit "$exit_code"

- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: fuzz-artifacts-${{ matrix.target }}
path: fuzz/artifacts/${{ matrix.target }}/
name: fuzz-artifacts
path: fuzz/artifacts/
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion .github/workflows/scorecard-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ permissions:
jobs:
analysis:
name: Scorecard Analysis
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ Cross-agent conventions for any agent (Claude, Codex, Cursor, opencode, …) wor
- Do **not** weaken or disable the gate. A local scan with a stale DB misses findings: run `trivy --download-db-only` first, then scan the **merge ref**, not just the PR head (e.g. `trivy fs --scanners vuln,misconfig --severity CRITICAL,HIGH --ignore-unfixed .`).
- Gating is by the Security Scan **job result**, not the `code_scanning` rule. That org ruleset is intentionally **CodeQL-only** (multiple code-scanning tools can't converge on one PR ref) — do **not** add tools to it.

### GitHub Actions ownership

- Organization ruleset `18156473` owns Wardnet PR review, governance, security, and CodeQL through the seven required workflows in `ContextualWisdomLab/.github@769691526f8c73cf714de8fe8ba51ae6cfa2901a`. Do not add local PR Governance, Dependency Review, Close Empty PR, CodeQL, Security Scan, SAST, Strix, OpenCode, or Noema copies.
- Keep repository-local `CI` and path-filtered `Fuzz` because they validate Wardnet's Rust code. Their PR concurrency keys must include a fixed workflow name, `github.repository`, and the PR number; only superseded heads of that PR may be cancelled.
- Keep the local Scorecard default-branch, weekly, and branch-protection workflow until the central reusable owner matches Wardnet's pinned Scorecard version. The compared central revision uses Scorecard v2.4.3 while Wardnet uses v2.4.4, so delegating now would weaken deployed security evidence.

### Code exploration

- There is no `.codegraph/` index in this repo, so use normal search (grep/ripgrep, `cargo` tooling, editor navigation). If a `.codegraph/` index is added later, prefer CodeGraph (`codegraph explore "<query>"` or the code-review-graph MCP tools) before grep/find — it surfaces callers/callees/impact that text search misses.
Expand Down
67 changes: 67 additions & 0 deletions tests/workflow_queue_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::{fs, path::PathBuf};

fn workflow_text(name: &str) -> String {
fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".github/workflows")
.join(name),
)
.unwrap()
}

#[test]
fn local_pr_workflows_cancel_only_superseded_heads_of_the_same_pull_request() {
for (name, fixed_group) in [("ci.yml", "wardnet-ci"), ("fuzz.yml", "wardnet-fuzz")] {
let workflow = workflow_text(name);
assert!(workflow.contains(&format!(
"group: {fixed_group}-${{{{ github.repository }}}}-${{{{ github.event_name == 'pull_request'"
)));
assert!(workflow.contains("format('pr-{0}', github.event.pull_request.number)"));
assert!(
workflow.contains("cancel-in-progress: ${{ github.event_name == 'pull_request' }}")
);
}
}

#[test]
fn fuzz_uses_one_runner_for_all_bounded_targets() {
let workflow = workflow_text("fuzz.yml");
assert!(!workflow.contains("\n strategy:\n"));
assert!(!workflow.contains("Set fuzz duration"));
for target in [
"fuzz_score_request",
"fuzz_appdata_json",
"fuzz_parse_admin_tokens",
"fuzz_dnsbl_zone",
] {
assert!(workflow.contains(target));
}
}

#[test]
fn central_required_workflows_are_not_copied_locally() {
let workflow_directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".github/workflows");
for name in [
"pr-governance.yml",
"dependency-review.yml",
"close-empty-pr.yml",
"codeql-pr.yml",
"security-scan.yml",
"sast-semgrep.yml",
"strix.yml",
"opencode-review.yml",
"noema-review.yml",
] {
assert!(!workflow_directory.join(name).exists());
}
}

#[test]
fn local_scorecard_preserves_non_pr_security_evidence() {
let workflow = workflow_text("scorecard-analysis.yml");
assert!(workflow.contains("branch_protection_rule:"));
assert!(workflow.contains("schedule:"));
assert!(workflow.contains("push:"));
assert!(!workflow.contains("pull_request:"));
assert!(workflow.contains("# v2.4.4"));
}
49 changes: 49 additions & 0 deletions tests/workflow_runner_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Repository contract for deterministic GitHub-hosted runner selection.
//!
//! Wardnet's required pull-request workflows must not depend on GitHub's floating
//! `ubuntu-latest` alias. A floating image can change independently of the
//! repository and, during hosted-runner transitions, can leave exact-head jobs
//! queued before checkout. Pinning the Ubuntu image makes runner acquisition a
//! reviewed repository change while preserving GitHub-hosted execution.

use std::fs;
use std::path::Path;

const PINNED_UBUNTU_RUNNER: &str = "ubuntu-24.04";
const FLOATING_UBUNTU_RUNNER: &str = "ubuntu-latest";

const RUNNER_BACKED_WORKFLOWS: &[&str] = &[
".github/workflows/ci.yml",
".github/workflows/fuzz.yml",
".github/workflows/scorecard-analysis.yml",
];

#[test]
fn runner_backed_workflows_pin_the_hosted_ubuntu_image() {
let repository = Path::new(env!("CARGO_MANIFEST_DIR"));

for relative in RUNNER_BACKED_WORKFLOWS {
let path = repository.join(relative);
let workflow = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));

assert!(
!workflow.contains(FLOATING_UBUNTU_RUNNER),
"{relative} must not use the floating {FLOATING_UBUNTU_RUNNER} runner alias"
);

let runners = workflow
.lines()
.filter_map(|line| line.trim().strip_prefix("runs-on:"))
.map(str::trim)
.collect::<Vec<_>>();
assert!(
!runners.is_empty(),
"{relative} must define at least one runs-on value"
);
assert!(
runners.iter().all(|runner| *runner == PINNED_UBUNTU_RUNNER),
"{relative} must use {PINNED_UBUNTU_RUNNER} for every runs-on value; found {runners:?}"
);
}
}
Loading