# Container Security Scanning Workflow # Sprint: CI/CD Enhancement - Security Scanning # # Purpose: Scan container images for vulnerabilities beyond SBOM generation # Triggers: Dockerfile changes, scheduled daily, manual dispatch # # Tool: PLACEHOLDER - Choose one: Trivy, Grype, or Snyk name: Container Security Scan on: push: paths: - '**/Dockerfile' - '**/Dockerfile.*' - 'devops/docker/**' pull_request: paths: - '**/Dockerfile' - '**/Dockerfile.*' - 'devops/docker/**' schedule: # Run daily at 4 AM UTC - cron: '0 4 * * *' workflow_dispatch: inputs: severity_threshold: description: 'Minimum severity to fail' required: false type: choice options: - CRITICAL - HIGH - MEDIUM - LOW default: HIGH image: description: 'Specific image to scan (optional)' required: false type: string env: SEVERITY_THRESHOLD: ${{ github.event.inputs.severity_threshold || 'HIGH' }} jobs: discover-images: name: Discover Container Images runs-on: ubuntu-latest outputs: images: ${{ steps.discover.outputs.images }} count: ${{ steps.discover.outputs.count }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Discover Dockerfiles id: discover run: | # Find all Dockerfiles DOCKERFILES=$(find . -name "Dockerfile" -o -name "Dockerfile.*" | grep -v node_modules | grep -v bin | grep -v obj || true) # Build image list IMAGES='[]' COUNT=0 while IFS= read -r dockerfile; do if [[ -n "$dockerfile" ]]; then DIR=$(dirname "$dockerfile") NAME=$(basename "$DIR" | tr '[:upper:]' '[:lower:]' | tr '.' '-') # Get image name from directory structure if [[ "$DIR" == *"devops/docker"* ]]; then NAME=$(echo "$dockerfile" | sed 's|.*devops/docker/||' | sed 's|/Dockerfile.*||' | tr '/' '-') fi IMAGES=$(echo "$IMAGES" | jq --arg name "$NAME" --arg path "$dockerfile" '. + [{"name": $name, "dockerfile": $path}]') COUNT=$((COUNT + 1)) fi done <<< "$DOCKERFILES" echo "Found $COUNT Dockerfile(s)" echo "images=$(echo "$IMAGES" | jq -c .)" >> $GITHUB_OUTPUT echo "count=$COUNT" >> $GITHUB_OUTPUT scan-images: name: Scan ${{ matrix.image.name }} runs-on: ubuntu-latest needs: [discover-images] if: needs.discover-images.outputs.count != '0' strategy: fail-fast: false matrix: image: ${{ fromJson(needs.discover-images.outputs.images) }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build image for scanning id: build run: | IMAGE_TAG="scan-${{ matrix.image.name }}:${{ github.sha }}" DOCKERFILE="${{ matrix.image.dockerfile }}" CONTEXT=$(dirname "$DOCKERFILE") echo "Building $IMAGE_TAG from $DOCKERFILE..." docker build -t "$IMAGE_TAG" -f "$DOCKERFILE" "$CONTEXT" || { echo "::warning::Failed to build $IMAGE_TAG - skipping scan" echo "skip=true" >> $GITHUB_OUTPUT exit 0 } echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "skip=false" >> $GITHUB_OUTPUT # PLACEHOLDER: Choose your container scanner # Option 1: Trivy (recommended - comprehensive, free) # Option 2: Grype (Anchore - good integration with Syft SBOMs) # Option 3: Snyk (commercial, comprehensive) - name: Trivy Vulnerability Scan if: steps.build.outputs.skip != 'true' id: trivy # Uncomment when ready to use Trivy: # uses: aquasecurity/trivy-action@master # with: # image-ref: ${{ steps.build.outputs.image_tag }} # format: 'sarif' # output: 'trivy-${{ matrix.image.name }}.sarif' # severity: ${{ env.SEVERITY_THRESHOLD }},CRITICAL # exit-code: '1' run: | echo "::notice::Container scanning placeholder - configure scanner below" echo "" echo "Image: ${{ steps.build.outputs.image_tag }}" echo "Severity threshold: ${{ env.SEVERITY_THRESHOLD }}" echo "" echo "Available scanners:" echo " 1. Trivy: aquasecurity/trivy-action@master" echo " 2. Grype: anchore/scan-action@v3" echo " 3. Snyk: snyk/actions/docker@master" # Create placeholder report mkdir -p scan-results echo '{"placeholder": true, "image": "${{ matrix.image.name }}"}' > scan-results/scan-${{ matrix.image.name }}.json # Alternative: Grype (works well with existing Syft SBOM workflow) # - name: Grype Vulnerability Scan # if: steps.build.outputs.skip != 'true' # uses: anchore/scan-action@v3 # with: # image: ${{ steps.build.outputs.image_tag }} # severity-cutoff: ${{ env.SEVERITY_THRESHOLD }} # fail-build: true # Alternative: Snyk Container # - name: Snyk Container Scan # if: steps.build.outputs.skip != 'true' # uses: snyk/actions/docker@master # env: # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} # with: # image: ${{ steps.build.outputs.image_tag }} # args: --severity-threshold=${{ env.SEVERITY_THRESHOLD }} - name: Upload scan results if: always() && steps.build.outputs.skip != 'true' uses: actions/upload-artifact@v4 with: name: container-scan-${{ matrix.image.name }} path: | scan-results/ *.sarif *.json retention-days: 30 if-no-files-found: ignore - name: Cleanup if: always() run: | docker rmi "${{ steps.build.outputs.image_tag }}" 2>/dev/null || true summary: name: Scan Summary runs-on: ubuntu-latest needs: [discover-images, scan-images] if: always() steps: - name: Download all scan results uses: actions/download-artifact@v4 with: pattern: container-scan-* path: all-results/ merge-multiple: true continue-on-error: true - name: Generate summary run: | echo "## Container Security Scan Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Image | Status |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY IMAGES='${{ needs.discover-images.outputs.images }}' SCAN_RESULT="${{ needs.scan-images.result }}" echo "$IMAGES" | jq -r '.[] | .name' | while read -r name; do if [[ "$SCAN_RESULT" == "success" ]]; then echo "| $name | No vulnerabilities found |" >> $GITHUB_STEP_SUMMARY elif [[ "$SCAN_RESULT" == "failure" ]]; then echo "| $name | Vulnerabilities detected |" >> $GITHUB_STEP_SUMMARY else echo "| $name | $SCAN_RESULT |" >> $GITHUB_STEP_SUMMARY fi done echo "" >> $GITHUB_STEP_SUMMARY echo "### Configuration" >> $GITHUB_STEP_SUMMARY echo "- **Scanner:** Placeholder (configure in workflow)" >> $GITHUB_STEP_SUMMARY echo "- **Severity Threshold:** ${{ env.SEVERITY_THRESHOLD }}" >> $GITHUB_STEP_SUMMARY echo "- **Images Scanned:** ${{ needs.discover-images.outputs.count }}" >> $GITHUB_STEP_SUMMARY echo "- **Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY