Skip to content

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

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 #5

name: Build & Scan Containers
on:
pull_request:
paths:
- 'docker/**'
- 'argus/**'
- 'argus.yml'
- '.github/workflows/build-containers.yml'
workflow_dispatch:
permissions:
contents: read
security-events: write
pull-requests: write
# Cancel in-progress runs when a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-scan:
name: Build & Scan ${{ matrix.image }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
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 repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Build image
run: |
docker build \
--tag "${IMAGE_REGISTRY}/${IMAGE_NAME}:pr-${PR_NUMBER}" \
--file "${DOCKERFILE}" \
--label "org.opencontainers.image.revision=${COMMIT_SHA}" \
.
env:
IMAGE_REGISTRY: ghcr.io/huntridge-labs/argus
IMAGE_NAME: ${{ matrix.image }}
DOCKERFILE: ${{ matrix.dockerfile }}
PR_NUMBER: ${{ github.event.pull_request.number || 'local' }}
COMMIT_SHA: ${{ github.sha }}
# Scan with Trivy (container vuln scanner)
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: "ghcr.io/huntridge-labs/argus/${{ matrix.image }}:pr-${{ github.event.pull_request.number || 'local' }}"
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- 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
# Also scan with Grype for cross-validation
- name: Scan image with Grype
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7
with:
image: "ghcr.io/huntridge-labs/argus/${{ matrix.image }}:pr-${{ github.event.pull_request.number || 'local' }}"
fail-build: false
severity-cutoff: critical
# Generate scan report for aggregation
- name: Generate scan report
if: always()
run: |
mkdir -p scan-reports
python3 -c "
import json
sarif_file = 'trivy-results.sarif'
image = '${IMAGE_NAME}'
try:
sarif = json.load(open(sarif_file))
results = sarif.get('runs', [{}])[0].get('results', [])
report = {
'image': image,
'finding_count': len(results),
'findings': [
{
'id': r.get('ruleId', 'unknown'),
'message': r.get('message', {}).get('text', ''),
'level': r.get('level', 'warning'),
}
for r in results[:20] # Top 20 findings
],
}
except Exception:
report = {'image': image, 'finding_count': -1, 'findings': []}
json.dump(report, open(f'scan-reports/{image}.json', 'w'))
"
env:
IMAGE_NAME: ${{ matrix.image }}
- name: Upload scan artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: container-scan-${{ matrix.image }}
path: |
trivy-results.sarif
scan-reports/
retention-days: 30
- name: Post scan summary
if: always()
run: |
{
echo "## Container Scan: ${IMAGE_NAME}"
echo ""
echo "Image: \`${IMAGE_REGISTRY}/${IMAGE_NAME}:pr-${PR_NUMBER}\`"
echo ""
echo "### Trivy Results (CRITICAL + HIGH)"
echo ""
if [ -f trivy-results.sarif ]; then
FINDING_COUNT=$(python3 -c "
import json
sarif = json.load(open('trivy-results.sarif'))
print(len(sarif.get('runs', [{}])[0].get('results', [])))
" 2>/dev/null || echo "unknown")
echo "Total findings: **${FINDING_COUNT}**"
else
echo "No SARIF results found."
fi
} >> "$GITHUB_STEP_SUMMARY"
env:
IMAGE_REGISTRY: ghcr.io/huntridge-labs/argus
IMAGE_NAME: ${{ matrix.image }}
PR_NUMBER: ${{ github.event.pull_request.number || 'local' }}
# Post aggregated container scan results as a PR comment
comment-pr:
name: Container Scan Summary
if: always() && github.event_name == 'pull_request'
needs: [build-and-scan]
runs-on: ubuntu-latest
steps:
- name: Download all scan reports
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
pattern: container-scan-*
merge-multiple: true
path: scan-reports
- name: Generate PR comment
id: comment
run: |
python3 -c "
import json, glob, os
reports = sorted(glob.glob('scan-reports/*.json'))
lines = ['## Container Image Scan Results', '']
lines.append('| Image | Findings (CRITICAL+HIGH) | Status |')
lines.append('|-------|-------------------------|--------|')
total = 0
for path in reports:
data = json.load(open(path))
name = data['image']
count = data['finding_count']
if count < 0:
status = 'Scan failed'
elif count == 0:
status = 'Clean'
else:
status = f'{count} finding(s)'
total += count
icon = 'pass' if count == 0 else ('fail' if count < 0 else 'warning')
emoji = {'pass': ':white_check_mark:', 'fail': ':x:', 'warning': ':warning:'}[icon]
lines.append(f'| \`{name}\` | {count if count >= 0 else \"N/A\"} | {emoji} {status} |')
lines.append('')
if total > 0:
lines.append(f'**Total: {total} finding(s) across all images.** See the Security tab for details.')
else:
lines.append('All images clean.')
lines.append('')
lines.append('---')
lines.append('*Scanned with Trivy + Grype. SARIF uploaded to Security tab.*')
body = '\n'.join(lines)
# Write to file for gh pr comment
with open('comment-body.md', 'w') as f:
f.write(body)
print(body)
"
- name: Post PR comment
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Update or create the comment using a marker
MARKER="<!-- argus-container-scan -->"
BODY=$(cat comment-body.md)
FULL_BODY="${MARKER}
${BODY}"
# Find existing comment
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" 2>/dev/null | head -1)
if [ -n "$COMMENT_ID" ]; then
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
-X PATCH -f body="$FULL_BODY"
else
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="$FULL_BODY"
fi