Skip to content

feat(sdk): portable Argus CLI — pip install, Docker execution, MCP server, CI preflight #11

feat(sdk): portable Argus CLI — pip install, Docker execution, MCP server, CI preflight

feat(sdk): portable Argus CLI — pip install, Docker execution, MCP server, CI preflight #11

name: Build, Scan & Test Containers
# Single workflow for container lifecycle on PRs:
# 1. Build custom images from the branch (once)
# 2. Scan images with Trivy + Grype
# 3. Test argus CLI using the built images
# 4. Post PR comment with aggregated results
#
# Images are built ONCE and shared via docker save/load artifacts.
# No triple-build. No masked failures.
on:
pull_request:
paths:
- 'docker/**'
- 'argus/**'
- 'argus.yml'
- '.github/workflows/build-containers.yml'
push:
branches: [main]
paths:
- 'docker/**'
- 'argus/**'
workflow_dispatch:
permissions:
contents: read
security-events: write
pull-requests: write
concurrency:
group: containers-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Step 1: Build all custom images ──────────────────────────────────
build:
name: Build Images
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
include:
- image: scanner-bandit
dockerfile: docker/Dockerfile.bandit
- image: scanner-opengrep
dockerfile: docker/Dockerfile.opengrep
- image: scanner-supply-chain
dockerfile: docker/Dockerfile.supply-chain
- image: cli
dockerfile: docker/Dockerfile.cli
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Build image
run: |
docker build \
--tag "${IMAGE_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" \
--file "${DOCKERFILE}" \
--label "org.opencontainers.image.revision=${COMMIT_SHA}" \
.
env:
IMAGE_REGISTRY: ghcr.io/huntridge-labs/argus
IMAGE_NAME: ${{ matrix.image }}
IMAGE_TAG: ${{ github.sha }}
DOCKERFILE: ${{ matrix.dockerfile }}
COMMIT_SHA: ${{ github.sha }}
- name: Save image to artifact
run: |
docker save "${IMAGE_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" \
| gzip > "/tmp/${IMAGE_NAME}.tar.gz"
env:
IMAGE_REGISTRY: ghcr.io/huntridge-labs/argus
IMAGE_NAME: ${{ matrix.image }}
IMAGE_TAG: ${{ github.sha }}
- name: Upload image artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: image-${{ matrix.image }}
path: /tmp/${{ matrix.image }}.tar.gz
retention-days: 1
# ── Step 2: Scan each image with Trivy + Grype ──────────────────────
scan:
name: Scan ${{ matrix.image }}
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- image: scanner-bandit
- image: scanner-opengrep
- image: scanner-supply-chain
- image: cli
steps:
- name: Download image artifact
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: image-${{ matrix.image }}
path: /tmp
- name: Load image
run: |
gunzip -c "/tmp/${IMAGE_NAME}.tar.gz" | docker load
env:
IMAGE_NAME: ${{ matrix.image }}
- name: Checkout (for argus SDK)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.13'
- name: Install Argus SDK
run: |
pip install --quiet pyyaml
echo "PYTHONPATH=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
# Scan with ALL severities — perception is protection
- name: Scan with Trivy (SARIF)
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: "ghcr.io/huntridge-labs/argus/${{ matrix.image }}:${{ github.sha }}"
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy SARIF
if: always()
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4
with:
sarif_file: trivy-results.sarif
category: "container-${{ matrix.image }}"
continue-on-error: true
- name: Scan with Grype
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7
with:
image: "ghcr.io/huntridge-labs/argus/${{ matrix.image }}:${{ github.sha }}"
fail-build: false
severity-cutoff: critical
- name: Scan with Trivy (JSON)
if: always()
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: "ghcr.io/huntridge-labs/argus/${{ matrix.image }}:${{ github.sha }}"
format: 'json'
output: 'trivy-results.json'
# Use argus container scanner parser + container markdown reporter
- name: Generate report with Argus
if: always()
run: |
mkdir -p scanner-summaries
python3 << 'PYEOF'
import sys, os
sys.path.insert(0, os.environ.get("PYTHONPATH", "."))
from pathlib import Path
from argus.scanners.container import ContainerScanner
from argus.container.scanner import ContainerScanResult, ContainerScanSummary
from argus.reporters.container_markdown import ContainerMarkdownReporter
image_name = os.environ["IMAGE_NAME"]
image_tag = os.environ.get("IMAGE_TAG", "latest")
image_ref = f"ghcr.io/huntridge-labs/argus/{image_name}:{image_tag}"
trivy_json = Path("trivy-results.json")
scanner = ContainerScanner()
trivy_findings = scanner.parse_trivy_results(trivy_json) if trivy_json.exists() else []
result = ContainerScanResult(
name=image_name,
image_ref=image_ref,
trivy_findings=trivy_findings,
combined_findings=trivy_findings,
)
summary = ContainerScanSummary(results=[result])
reporter = ContainerMarkdownReporter()
reporter.report(summary, "scanner-summaries")
# Rename for per-image aggregation
src = Path("scanner-summaries/container-scan.md")
dst = Path(f"scanner-summaries/{image_name}.md")
if src.exists():
dst.write_text(src.read_text())
PYEOF
env:
IMAGE_NAME: ${{ matrix.image }}
IMAGE_TAG: ${{ github.sha }}
- name: Upload scan artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: scanner-summary-container-${{ matrix.image }}
path: scanner-summaries/
retention-days: 7
# ── Step 3: Test argus CLI using built images ───────────────────────
test-cli:
name: Test Argus CLI
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.13'
- name: Install Argus
run: |
pip install --quiet pyyaml
echo "PYTHONPATH=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
# Load all custom images built in step 1
- name: Download all image artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: image-*
merge-multiple: true
path: /tmp/images
- name: Load and retag images
run: |
for tarball in /tmp/images/*.tar.gz; do
gunzip -c "$tarball" | docker load
done
# Retag from SHA to version tag that containers.py expects
for image in scanner-bandit scanner-opengrep scanner-supply-chain cli; do
SHA_TAG="ghcr.io/huntridge-labs/argus/${image}:${GITHUB_SHA}"
VERSION_TAG="ghcr.io/huntridge-labs/argus/${image}:1.0.0"
if docker image inspect "$SHA_TAG" > /dev/null 2>&1; then
docker tag "$SHA_TAG" "$VERSION_TAG"
fi
done
env:
GITHUB_SHA: ${{ github.sha }}
- name: Test scanner listing
run: |
OUTPUT=$(python -m argus scan --list)
echo "$OUTPUT"
for scanner in bandit clamav trivy-iac gitleaks osv checkov opengrep supply-chain zap container; do
echo "$OUTPUT" | grep -q "$scanner" || { echo "FAIL: $scanner not listed"; exit 1; }
done
- name: Run argus scan
run: |
set +e
python -m argus scan \
--config argus.yml \
--format terminal \
--format sarif \
--format json \
--output-dir ./cli-test-results \
--verbose
EXIT_CODE=$?
set -e
if [ "$EXIT_CODE" -eq 2 ]; then
echo "::error::Argus scan failed with error (exit code 2)"
exit 2
fi
echo "Scan completed with exit code $EXIT_CODE"
- name: Verify outputs
run: |
for f in cli-test-results/argus-results.json cli-test-results/argus-results.sarif; do
test -f "$f" || { echo "FAIL: $f not produced"; exit 1; }
done
- name: Validate SARIF
run: |
python3 -c "
import json
data = json.load(open('cli-test-results/argus-results.sarif'))
assert data.get('version') == '2.1.0', f'Bad SARIF version: {data.get(\"version\")}'
assert 'runs' in data, 'Missing runs'
print(f'SARIF valid: {len(data[\"runs\"])} run(s)')
"
- name: Validate JSON results
run: |
python3 -c "
import json
data = json.load(open('cli-test-results/argus-results.json'))
results = data.get('results', [])
assert len(results) > 0, 'No scanner results'
scanners = [r['scanner'] for r in results]
total = sum(len(r.get('findings', [])) for r in results)
print(f'Scanners: {scanners}')
print(f'Total findings: {total}')
"
- name: Upload test results
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: argus-cli-test-results
path: cli-test-results/
retention-days: 7
# ── Step 4: Post PR comment using argus markdown + comment-pr action ─
comment-pr:
name: Container Scan Summary
if: always() && github.event_name == 'pull_request'
needs: [scan, test-cli]
runs-on: ubuntu-latest
steps:
- name: Checkout (for comment-pr action)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Download the argus-generated markdown from each scan matrix job
- name: Download scanner summaries
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: scanner-summary-container-*
merge-multiple: true
path: scanner-summaries
- name: Download CLI test results
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: argus-cli-test-results
path: cli-results
continue-on-error: true
# Combine per-image argus markdown reports into one file
- name: Combine scanner summaries
run: |
mkdir -p scanner-summaries
{
for md in scanner-summaries/*.md; do
[ -f "$md" ] && cat "$md" && echo ""
done
if [ -f "cli-results/argus-summary.md" ]; then
echo "---"
echo ""
cat cli-results/argus-summary.md
fi
} > scanner-summaries/combined-container-scan.md
# Post using the existing comment-pr composite action
- name: Comment PR with scan results
uses: ./.github/actions/comment-pr
with:
summary_file: scanner-summaries/combined-container-scan.md
comment_marker: argus-container-scan
title: '🔒 Argus Container Security Scan'
fallback_message: 'No container scan results available'