feat(sdk): portable Argus CLI — pip install, Docker execution, MCP server, CI preflight #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |