Skip to content
Open
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
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Self-test CI for the dev-rig.
#
# The rig's other workflows are all reusable (workflow_call) and were never
# exercised end-to-end. This workflow invokes each of them against the rig
# itself — using LOCAL refs (./.github/workflows/*.yml) so a PR validates its
# own versions of the workflows, not the ones already on main. It both proves
# the reusable workflows work and guards them against silent regressions.

name: CI (self-test)

on:
push:
branches: [main]
pull_request: # all PRs, regardless of base — so stacked branches self-test too

jobs:
lint:
uses: ./.github/workflows/lint.yml
with:
source-dirs: "src/legionforge_dev_rig"
extra-mypy-deps: "types-PyYAML httpx respx pytest"

test:
uses: ./.github/workflows/test.yml
with:
coverage-source: "legionforge_dev_rig"
coverage-threshold: 80

sast:
uses: ./.github/workflows/sast.yml
with:
source-dirs: "src/legionforge_dev_rig"
semgrep-configs: "p/python"
permissions:
security-events: write

audit:
uses: ./.github/workflows/audit.yml

supply-chain:
uses: ./.github/workflows/supply-chain.yml
with:
# Pull the risky-exec ruleset from THIS branch so self-CI tests the PR's
# rules, not whatever is on main.
rig-ref: ${{ github.head_ref || github.ref_name }}
secrets: inherit

secrets:
uses: ./.github/workflows/secrets.yml

sbom:
uses: ./.github/workflows/sbom.yml
100 changes: 100 additions & 0 deletions .github/workflows/supply-chain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Reusable workflow: supply-chain hardening.
#
# osv-scanner — multi-ecosystem dependency vuln + malicious-package scan
# (Python / npm / Cargo / Go / … from lockfiles)
# risky-exec — LegionForge custom Semgrep rules: curl|bash installers,
# PowerShell download-cradles, decode-and-exec, TLS bypass
# socket — OPTIONAL behavioral npm analysis; runs only when the caller
# provides a SOCKET_SECURITY_API_KEY secret
#
# Caller syntax:
#
# jobs:
# supply-chain:
# uses: LegionForge/dev-rig/.github/workflows/supply-chain.yml@main
# secrets: inherit # only needed to enable the Socket job
#
# Complements audit.yml (pip-audit/licenses). osv-scanner adds the malicious-
# package feed and the npm/Cargo ecosystems that pip-audit doesn't cover.

name: Supply Chain

on:
workflow_call:
inputs:
osv-version:
description: "osv-scanner image tag to pin"
required: false
type: string
default: "v2.3.8"
rig-ref:
description: "dev-rig ref to pull the custom risky-exec ruleset from"
required: false
type: string
default: "main"
secrets:
SOCKET_SECURITY_API_KEY:
description: "Optional Socket.dev API key — enables the npm behavioral scan"
required: false

jobs:
# ── Dependency vulnerabilities + known-malicious packages ────────────────────
osv-scanner:
name: osv-scanner (deps + malicious packages)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Pinned container image (matches the rig's docker-based tooling pattern).
# --allow-no-lockfiles: a repo with nothing to scan is a pass, not a fail.
- name: osv-scanner scan
run: |
docker run --rm -v "${{ github.workspace }}:/src" \
"ghcr.io/google/osv-scanner:${{ inputs.osv-version }}" \
scan source --recursive --allow-no-lockfiles /src

# ── Custom risky-exec / supply-chain pattern rules ───────────────────────────
risky-exec:
name: risky-exec (custom Semgrep rules)
runs-on: ubuntu-latest
container: semgrep/semgrep
steps:
- name: Checkout caller repo
uses: actions/checkout@v4

# The ruleset lives in dev-rig, not the consuming repo — pull it alongside.
- name: Checkout dev-rig ruleset
uses: actions/checkout@v4
with:
repository: LegionForge/dev-rig
ref: ${{ inputs.rig-ref }}
path: .dev-rig

- name: semgrep risky-exec
run: |
semgrep --config .dev-rig/semgrep/legionforge-risky-exec.yml \
--error --metrics=off \
--exclude .dev-rig --exclude .claude --exclude node_modules --exclude semgrep .

# ── Optional Socket.dev behavioral npm scan ──────────────────────────────────
# Self-skips when no SOCKET_SECURITY_API_KEY secret is provided, so callers
# without a Socket account incur no failure and leak no dependency data.
socket:
name: socket (optional npm behavioral)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Socket scan (skips without token)
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }}
run: |
if [ -z "${SOCKET_SECURITY_API_KEY:-}" ]; then
echo "SOCKET_SECURITY_API_KEY not set — skipping optional Socket scan."
exit 0
fi
if [ ! -f package.json ]; then
echo "No package.json — nothing for Socket to scan."
exit 0
fi
npx -y @socketsecurity/cli@latest scan create --report .
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ repos:
- psutil
- uvicorn

# ── shellcheck — shell script correctness + footguns ──────────────────────
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheck

# ── osv-scanner — dependency vuln + malicious-package scan (multi-ecosystem)─
# Local hook (uses the installed osv-scanner binary). Triggers only when a
# lockfile changes; scans the whole tree so cross-ecosystem repos are covered.
- repo: local
hooks:
- id: osv-scanner
name: osv-scanner (deps + malicious packages)
entry: osv-scanner scan source --recursive --allow-no-lockfiles .
language: system
pass_filenames: false
files: '(requirements.*\.txt|poetry\.lock|uv\.lock|Pipfile\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum)$'

# ── gitleaks — secret scanning (OWASP SAMM — Implementation/Secure Build) ──
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ Shared CI pipeline, pre-commit hooks, and pytest fixtures for LegionForge projec
| `.github/workflows/test.yml` | Reusable CI job: pytest + coverage enforcement |
| `.github/workflows/sast.yml` | Reusable CI job: semgrep (p/python + p/fastapi) + CodeQL |
| `.github/workflows/audit.yml` | Reusable CI job: pip-audit CVE scan + pip-licenses compliance |
| `.github/workflows/supply-chain.yml` | Reusable CI job: osv-scanner (multi-ecosystem deps + malicious packages) + risky-exec custom rules + optional Socket.dev |
| `.github/workflows/secrets.yml` | Reusable CI job: gitleaks secret scanning |
| `.github/workflows/sbom.yml` | Reusable CI job: CycloneDX SBOM generation |
| `semgrep/legionforge-risky-exec.yml` | Custom Semgrep ruleset: curl\|bash installers, PowerShell download-cradles, decode-and-exec, TLS bypass |
| `scripts/audit.sh` / `scripts/audit.ps1` | Local audit harness — Python + osv-scanner + shellcheck + semgrep + risky-exec |
| `.pre-commit-hooks.yaml` | Hook definitions consumed via pre-commit |
| `.pre-commit-config.yaml` | Default config to copy into new projects (includes gitleaks) |
| `.pre-commit-config.yaml` | Default config to copy into new projects (includes gitleaks, shellcheck, osv-scanner) |
| `SECURITY.md` | Vulnerability disclosure policy template — copy and adjust |
| `src/legionforge_dev_rig/fixtures/` | Shared pytest fixtures (httpx mocking, etc.) |
| `examples/` | Template conftest.py and example tests |
Expand Down Expand Up @@ -94,8 +97,19 @@ jobs:

audit:
uses: LegionForge/dev-rig/.github/workflows/audit.yml@main

supply-chain:
uses: LegionForge/dev-rig/.github/workflows/supply-chain.yml@main
secrets: inherit # ← only needed to enable the optional Socket.dev scan
```

> **Multi-language note.** `supply-chain.yml` and the local `audit.sh`/`audit.ps1`
> harness cover Python, JS/TS, and Rust (via osv-scanner lockfile scanning) plus
> shell (shellcheck) and risky-exec patterns in shell/PowerShell/CI YAML. Each
> section self-skips when its files or tools aren't present, so the rig is safe
> to wire into any repo regardless of language mix. cargo-deny (Rust policy) and
> PSScriptAnalyzer (PowerShell SAST) are the planned next additions.

### 4 — Add shared fixtures to tests/conftest.py

```python
Expand Down Expand Up @@ -133,6 +147,8 @@ pre-commit autoupdate
| bandit | 1.7 | `pyproject.toml [tool.bandit]` |
| mypy | 1.10 | `pyproject.toml [tool.mypy]` |
| pip-audit | 2.7 | no config — runs against installed packages |
| semgrep | 1.70 | rulesets passed as CLI args |
| osv-scanner | 2.3 | no config — scans lockfiles recursively (`brew install osv-scanner`) |
| shellcheck | 0.10 | inline directives / `.shellcheckrc` (`brew install shellcheck`) |
| semgrep | 1.70 | rulesets passed as CLI args + `semgrep/legionforge-risky-exec.yml` |
| pytest-cov | 5 | `pyproject.toml [tool.pytest.ini_options]` |
| pre-commit | 3.7 | `.pre-commit-config.yaml` |
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ analysis = [
"pre-commit>=3.7",
"types-PyYAML",
]
# The `dev` extra is the contract the reusable lint.yml / test.yml workflows
# install (`pip install -e .[dev]`). Consuming projects provide their own; the
# rig defines one so it can self-test those workflows (see ci.yml).
dev = [
"ruff>=0.4",
"bandit[toml]>=1.7",
"mypy>=1.10",
"pytest>=8",
"pytest-asyncio>=0.23",
"pytest-cov>=5",
"types-PyYAML",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand All @@ -43,6 +55,11 @@ ignore = [
"S607", # partial executable path — acceptable for system binaries
]

[tool.ruff.lint.per-file-ignores]
# Tests and examples legitimately use `assert` (S101) for verification.
"tests/**" = ["S101"]
"examples/**" = ["S101"]

[tool.bandit]
exclude_dirs = [".venv", "venv", ".git", "tests"]
skips = [
Expand Down
62 changes: 59 additions & 3 deletions scripts/audit.ps1
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<#
.SYNOPSIS
LegionForge audit harness — ruff, bandit, mypy, pip-audit, semgrep.
LegionForge audit harness — ruff, bandit, mypy, pip-audit, osv-scanner,
shellcheck, semgrep, and the custom risky-exec supply-chain ruleset.

.DESCRIPTION
Runs all five static-analysis and security tools against a project directory.
Runs the static-analysis and security tools against a project directory.
Native tools (ruff/bandit/mypy/pip-audit) run directly via py -3.13.
Semgrep runs via Docker to avoid Windows/Python 3.13+ build failures.
osv-scanner (multi-ecosystem dependency + malicious-package scan) and
shellcheck run as native binaries; both self-skip when absent.
Semgrep and the custom risky-exec ruleset run via Docker to avoid
Windows/Python 3.13+ build failures.

Per-project configuration lives in two small files at the project root:
.audit-dirs — space-separated source dirs (e.g. "llm_valet svcmgr")
Expand All @@ -32,6 +36,10 @@ param(

$ProjectPath = (Resolve-Path $ProjectPath).Path

# Dev-rig root (this script lives in <rig>/scripts/) — locates the bundled
# custom Semgrep ruleset regardless of the consuming project's CWD.
$RigRoot = Split-Path $PSScriptRoot -Parent

# ── Read per-project config ───────────────────────────────────────────────────

# Which source directories to scan (ruff / bandit / mypy / semgrep).
Expand Down Expand Up @@ -120,6 +128,36 @@ Invoke-Tool "pip-audit" {
py -3.13 -m pip_audit .
}

# ── osv-scanner — multi-ecosystem dependency + malicious-package scan ─────────
# One pass over every lockfile (Python / npm / Cargo / …), checked against OSV
# (CVEs + malicious-packages feed). --allow-no-lockfiles: nothing to scan is a
# pass, not a failure. Self-skips when the binary isn't installed.

if (Get-Command osv-scanner -ErrorAction SilentlyContinue) {
Invoke-Tool "osv-scanner" {
osv-scanner scan source --recursive --allow-no-lockfiles .
}
} else {
Write-Section "osv-scanner"
Write-Host " osv-scanner not installed — skipping (winget install Google.osv-scanner)" -ForegroundColor DarkGray
}

# ── shellcheck — shell script correctness + footguns ─────────────────────────
# Runs only when the repo contains shell scripts.

$shellFiles = Get-ChildItem -Path $ProjectPath -Recurse -File -Include *.sh, *.bash -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch '[\\/](\.git|\.venv|node_modules|\.claude)[\\/]' }
if ($shellFiles) {
if (Get-Command shellcheck -ErrorAction SilentlyContinue) {
Invoke-Tool "shellcheck" {
shellcheck @($shellFiles.FullName)
}
} else {
Write-Section "shellcheck"
Write-Host " $($shellFiles.Count) shell script(s) found but shellcheck not installed — skipping" -ForegroundColor DarkGray
}
}

# ── semgrep — OWASP / framework-specific vulnerability patterns ───────────────
# Runs inside the official Docker image to avoid Windows/Python 3.13 build issues.
# First run pulls the image (~200 MB); subsequent runs use the local cache.
Expand All @@ -142,6 +180,24 @@ Invoke-Tool "semgrep" {
& docker @dockerArgs
}

# ── risky-exec — LegionForge custom supply-chain / RCE pattern rules ──────────
# Flags curl|bash installers, PowerShell download-cradles, decode-and-exec, and
# TLS-bypass patterns ecosystem scanners can't see. Same Docker semgrep image,
# mounting the rig's bundled ruleset read-only.

$riskyRules = Join-Path $RigRoot "semgrep/legionforge-risky-exec.yml"
if (Test-Path $riskyRules) {
Invoke-Tool "risky-exec" {
$rigSemgrep = Join-Path $RigRoot "semgrep"
& docker run --rm `
-v "${ProjectPath}:/src" `
-v "${rigSemgrep}:/rules:ro" `
semgrep/semgrep `
semgrep --config /rules/legionforge-risky-exec.yml /src --error `
--exclude .claude --exclude .git --exclude node_modules --exclude semgrep
}
}

# ── Summary ───────────────────────────────────────────────────────────────────

Write-Section "Summary"
Expand Down
Loading