foxguard 0.9.0

A security scanner as fast as a linter, written in Rust. 170+ built-in rules across 11 languages.
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  NODE_VERSION: "22.12.0"

jobs:
  fmt:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - run: cargo fmt --check

  clippy:
    name: Clippy
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-clippy-
      - run: cargo clippy --all-targets --all-features -- -D warnings

  test:
    name: Test
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-test-
      - run: cargo test --all-features

  rule-inventory-check:
    name: Rule Inventory
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-gen-rules-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-gen-rules-
      - name: Regenerate rules.ts
        run: cargo run --quiet --bin gen_rules_ts > /tmp/rules.ts.generated
      - name: Diff against committed file
        run: |
          if ! diff -u www/src/data/rules.ts /tmp/rules.ts.generated; then
            echo "::error::www/src/data/rules.ts is out of sync with the Rust rule registry."
            echo "Regenerate with: cargo run --bin gen_rules_ts > www/src/data/rules.ts"
            exit 1
          fi

  semgrep-parity:
    name: Semgrep Parity
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ubuntu-cargo-semgrep-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ubuntu-cargo-semgrep-
      - name: Install semgrep
        # Pin Semgrep to a known version so parity-test drift comes from foxguard changes, not upstream behavior changes. Bump deliberately via PR + parity-test run.
        run: pip install semgrep==1.163.0
      - name: Run Semgrep parity suite (Python)
        run: cargo test --test semgrep_parity
      - name: Run Semgrep parity suite (JavaScript)
        run: cargo test --test semgrep_parity_javascript
      - name: Run Semgrep parity suite (Go)
        run: cargo test --test semgrep_parity_go
      - name: Run Semgrep parity suite (Java)
        run: cargo test --test semgrep_parity_java
      - name: Run Semgrep parity suite (C)
        run: cargo test --test semgrep_parity_c
      - name: Run Semgrep parity suite (Kotlin)
        run: cargo test --test semgrep_parity_kotlin
      - name: Run Semgrep parity suite (inverse / unsupported-skip)
        run: cargo test --test semgrep_parity_inverse

  website:
    name: Website Build
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: www
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
          cache-dependency-path: www/package-lock.json
      - run: npm ci
      - run: npm run build

  vscode-extension:
    name: VS Code Extension
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: vscode-extension
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
          cache-dependency-path: vscode-extension/package-lock.json
      - run: npm ci
      - run: npm run compile

  npm-package:
    name: npm Package
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: packages/npm
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm pack --dry-run

  scripts:
    name: Script Syntax
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: bash -n action/entrypoint.sh
      - run: bash -n benchmarks/precision/run.sh
      - run: bash -n benchmarks/run.sh
      - run: bash -n scripts/linux-codeql-dirty-frag.sh
      - run: bash -n scripts/release.sh

  precision-corpus-metadata:
    name: Precision Corpus Metadata
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
        with:
          python-version: "3.12"
      - run: python3 benchmarks/precision/precision.py validate

  readme-version-refs:
    name: README Version Refs
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Check README install refs match Cargo.toml
        run: |
          cargo_v=$(grep '^version' Cargo.toml | head -1 | sed -E 's/version = "(.+)"/\1/')
          action_v=$(grep -oE '0sec-labs/foxguard/action@v[0-9]+\.[0-9]+\.[0-9]+' README.md | head -1 | sed -E 's/.*v(.+)/\1/')
          rev_v=$(grep -oE 'rev:\s+v[0-9]+\.[0-9]+\.[0-9]+' README.md | head -1 | sed -E 's/.*v(.+)/\1/')
          fail=0
          if [ "$action_v" != "$cargo_v" ]; then
            echo "::error::action ref v$action_v != Cargo v$cargo_v"
            fail=1
          fi
          if [ "$rev_v" != "$cargo_v" ]; then
            echo "::error::pre-commit rev v$rev_v != Cargo v$cargo_v"
            fail=1
          fi
          exit $fail

  build:
    name: Build Release
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-build-
      - run: cargo build --release --all-features

  linux-compat:
    name: Linux glibc compat
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-unknown-linux-musl
      - name: Install musl tools
        run: sudo apt-get update && sudo apt-get install -y musl-tools
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-musl-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-musl-cargo-
      - name: Build static musl binary
        run: cargo build --release --target x86_64-unknown-linux-musl
      - name: Verify binary is static
        run: |
          file target/x86_64-unknown-linux-musl/release/foxguard
          ldd target/x86_64-unknown-linux-musl/release/foxguard 2>&1 | grep -q "not a dynamic executable" || ldd target/x86_64-unknown-linux-musl/release/foxguard 2>&1 | grep -q "statically linked"
      - name: Test binary across Linux distros
        run: |
          binary="target/x86_64-unknown-linux-musl/release/foxguard"
          for image in ubuntu:20.04 ubuntu:22.04 debian:11 debian:12 fedora:38 alpine:3.18 amazonlinux:2023; do
            echo "--- Testing on $image ---"
            docker run --rm -v "$PWD:/work:ro" "$image" /work/$binary --version
          done

  dogfood:
    name: Dogfood (foxguard scans itself)
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-build-

      - name: Build foxguard
        run: cargo build --release

      - name: Scan own source (must be clean)
        run: ./target/release/foxguard --severity high src/

      - name: Scan own source for secrets (must be clean)
        run: ./target/release/foxguard secrets src/

      - name: Verify fixture detection (must find issues)
        run: |
          # Bypass .foxguard.yml so the repo baseline does not suppress fixture findings.
          if ./target/release/foxguard --config /dev/null tests/fixtures/ 2>&1; then
            echo "::error::Expected foxguard to find issues in test fixtures but it exited clean"
            exit 1
          else
            echo "Fixtures correctly detected — foxguard found issues as expected"
          fi