Container Security Scan #25
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: Container Security Scan | |
| on: | |
| workflow_run: | |
| workflows: ["Build and Push Docker Image"] | |
| types: | |
| - completed | |
| branches: | |
| - main | |
| push: | |
| tags: | |
| - 'v*' | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - 'Dockerfile' | |
| - 'entrypoint.sh' | |
| - '**/*.sh' | |
| workflow_dispatch: | |
| inputs: | |
| image_ref: | |
| description: 'Image reference to scan (e.g., ghcr.io/azdolinski/devcoder:v0.6.12)' | |
| required: false | |
| type: string | |
| severity: | |
| description: 'Minimum severity level to report' | |
| required: false | |
| default: 'medium' | |
| type: choice | |
| options: | |
| - critical | |
| - high | |
| - medium | |
| - low | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| extract-version: | |
| runs-on: ubuntu-latest | |
| if: | | |
| (github.event_name == 'workflow_run' && | |
| github.event.workflow_run.conclusion == 'success' && | |
| (github.event.workflow_run.outputs.run_security_scan != 'false' || github.event.workflow_run.outputs.run_security_scan == '')) || | |
| github.event_name == 'push' || | |
| github.event_name == 'pull_request' || | |
| github.event_name == 'workflow_dispatch' | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| image-ref: ${{ steps.version.outputs.image-ref }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Get tag from workflow_run or current event | |
| id: get_tag | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_run" ]; then | |
| # Get the tag that was created in the build workflow | |
| echo "Triggered by workflow_run, fetching tag..." | |
| TAG=$(git -c 'versionsort.suffix=-alpha' -c 'versionsort.suffix=-beta' \ | |
| ls-remote --tags --sort=-v:refname origin \ | |
| | grep 'refs/tags/v' \ | |
| | head -1 \ | |
| | awk '{print $2}' \ | |
| | sed 's/refs\/tags\///' \ | |
| | tr -d '{}^') | |
| echo "TAG=$TAG" >> $GITHUB_OUTPUT | |
| elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == refs/tags/* ]]; then | |
| # Normal tag push event | |
| TAG="${{ github.ref_name }}" | |
| echo "TAG=$TAG" >> $GITHUB_OUTPUT | |
| elif [[ "${{ github.event.inputs.image_ref }}" != "" ]]; then | |
| # Manual workflow dispatch with image reference | |
| TAG="" | |
| echo "TAG=" >> $GITHUB_OUTPUT | |
| else | |
| # Pull request or other event | |
| VERSION=$(node -e "console.log(require('./version.json').version)" 2>/dev/null || echo "latest") | |
| TAG="$VERSION" | |
| echo "TAG=$TAG" >> $GITHUB_OUTPUT | |
| fi | |
| echo "Using tag: $TAG" | |
| - name: Extract version and image reference | |
| id: version | |
| run: | | |
| TAG="${{ steps.get_tag.outputs.TAG }}" | |
| if [ -n "$TAG" ]; then | |
| # Strip 'v' prefix from Git tag for Docker image reference | |
| # Git: v0.6.13 → Docker: 0.6.13 | |
| VERSION=${TAG#v} | |
| IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}" | |
| elif [[ "${{ github.event.inputs.image_ref }}" != "" ]]; then | |
| IMAGE_REF=${{ github.event.inputs.image_ref }} | |
| VERSION="manual" | |
| else | |
| VERSION=$(node -e "console.log(require('./version.json').version)" 2>/dev/null || echo "latest") | |
| IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "image-ref=$IMAGE_REF" >> $GITHUB_OUTPUT | |
| echo "Using image reference: $IMAGE_REF" | |
| trivy-scan: | |
| runs-on: ubuntu-latest | |
| needs: extract-version | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Build test image (for PRs) | |
| if: ${{ github.event_name == 'pull_request' }} | |
| run: | | |
| docker build -t devcoder-test . | |
| echo "IMAGE_REF=devcoder-test" >> $GITHUB_ENV | |
| - name: Build image for scanning (for tags) | |
| if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.extract-version.outputs.image-ref != '' }} | |
| run: | | |
| docker build -t ${{ needs.extract-version.outputs.image-ref }} . | |
| echo "IMAGE_REF=${{ needs.extract-version.outputs.image-ref }}" >> $GITHUB_ENV | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| continue-on-error: true | |
| with: | |
| image-ref: ${{ env.IMAGE_REF || needs.extract-version.outputs.image-ref }} | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| exit-code: '0' | |
| ignore-unfixed: true | |
| severity: ${{ github.event.inputs.severity || 'CRITICAL,HIGH,MEDIUM' }} | |
| vuln-type: 'os,library' | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| if: hashFiles('trivy-results.sarif') != '' | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| snyk-container: | |
| runs-on: ubuntu-latest | |
| needs: extract-version | |
| name: Snyk Container - Docker Image Scanning | |
| environment: prod | |
| permissions: | |
| contents: read | |
| security-events: write | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Build Docker image | |
| if: ${{ env.SNYK_TOKEN != '' }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| IMAGE_REF=devcoder-test | |
| else | |
| IMAGE_REF="${{ needs.extract-version.outputs.image-ref }}" | |
| fi | |
| if [ -z "$IMAGE_REF" ]; then | |
| echo "Missing image reference" >&2 | |
| exit 1 | |
| fi | |
| echo "IMAGE_REF=$IMAGE_REF" >> $GITHUB_ENV | |
| docker build -t "$IMAGE_REF" . | |
| - name: Run Snyk Container scan | |
| if: ${{ env.SNYK_TOKEN != '' }} | |
| continue-on-error: true | |
| uses: snyk/actions/docker@master | |
| env: | |
| SNYK_TOKEN: ${{ env.SNYK_TOKEN }} | |
| with: | |
| image: ${{ env.IMAGE_REF }} | |
| args: --severity-threshold=${{ github.event.inputs.severity || 'medium' }} --file=Dockerfile | |
| sarif: true | |
| - name: Debug SARIF structure | |
| if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk.sarif') != '' }} | |
| run: | | |
| echo "📊 SARIF file structure:" | |
| echo " Runs count: $(jq '.runs | length' snyk.sarif)" | |
| echo " Tools: $(jq -r '.runs[].tool.driver.name' snyk.sarif | paste -sd ',' -)" | |
| echo " Results per run: $(jq '.runs[].results | length' snyk.sarif | paste -sd ',' -)" | |
| - name: Install SARIF SDK | |
| if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk.sarif') != '' }} | |
| run: dotnet tool install --global Sarif.Multitool | |
| - name: Merge SARIF runs using official SARIF SDK | |
| if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk.sarif') != '' }} | |
| run: | | |
| sarif merge snyk.sarif \ | |
| --merge-runs \ | |
| --output-file snyk-container.sarif \ | |
| --log "Optimize;Minify" | |
| echo "✅ Merged SARIF runs → snyk-container.sarif" | |
| echo " Final runs count: $(jq '.runs | length' snyk-container.sarif)" | |
| echo " Final results count: $(jq '.runs[0].results | length' snyk-container.sarif)" | |
| - name: Upload Snyk Container results to GitHub Security tab | |
| if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk-container.sarif') != '' }} | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: snyk-container.sarif | |
| category: snyk-container | |
| dockle-scan: | |
| runs-on: ubuntu-latest | |
| needs: extract-version | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Build test image (for PRs) | |
| if: ${{ github.event_name == 'pull_request' }} | |
| run: | | |
| docker build -t devcoder-test . | |
| - name: Build image for Dockle scanning (for tags) | |
| if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.extract-version.outputs.image-ref != '' }} | |
| run: | | |
| docker build -t ${{ needs.extract-version.outputs.image-ref }} . | |
| - name: Install Dockle | |
| run: | | |
| curl -sL https://github.com/goodwithtech/dockle/releases/download/v0.4.14/dockle_0.4.14_Linux-64bit.tar.gz | tar -xz | |
| sudo mv dockle /usr/local/bin/ | |
| - name: Run Dockle container image linter | |
| run: | | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| dockle --exit-code 1 --exit-level WARN devcoder-test || echo "Dockle scan completed with warnings" | |
| else | |
| dockle --exit-code 1 --exit-level WARN ${{ needs.extract-version.outputs.image-ref }} || echo "Dockle scan completed with warnings" | |
| fi | |
| generate-report: | |
| runs-on: ubuntu-latest | |
| needs: [extract-version, trivy-scan, snyk-container, dockle-scan] | |
| if: ${{ always() && needs.trivy-scan.result == 'success' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Generate security report | |
| run: | | |
| echo "# Security Scan Report for ${{ needs.extract-version.outputs.image-ref }}" > security-report.md | |
| echo "" >> security-report.md | |
| echo "Generated on: $(date)" >> security-report.md | |
| echo "Version: ${{ needs.extract-version.outputs.version }}" >> security-report.md | |
| echo "" >> security-report.md | |
| if [ -f "trivy-results.sarif" ]; then | |
| echo "## Trivy Vulnerability Scan Results" >> security-report.md | |
| echo "Trivy scan completed. Check the 'Security' tab for detailed results." >> security-report.md | |
| echo "" >> security-report.md | |
| fi | |
| if [ -f "snyk-container.sarif" ]; then | |
| echo "## Snyk Container Security Scan Results" >> security-report.md | |
| echo "Snyk container scan completed. Check the 'Security' tab for detailed results." >> security-report.md | |
| echo "" >> security-report.md | |
| fi | |
| echo "## Dockle Container Linting Results" >> security-report.md | |
| echo "Container image linting completed." >> security-report.md | |
| echo "" >> security-report.md | |
| echo "## Summary" >> security-report.md | |
| echo "- All security scans have been completed" >> security-report.md | |
| echo "- Review the Security tab in this repository for detailed findings" >> security-report.md | |
| echo "- Address any CRITICAL or HIGH severity vulnerabilities found" >> security-report.md | |
| - name: Upload security report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: security-report-${{ needs.extract-version.outputs.version }} | |
| path: security-report.md |