name: Dockle Container Security Linting on: workflow_dispatch: workflow_call: # Called by release-gate.yml schedule: # Run weekly on Tuesday at 10 AM UTC (staggered with other container scans) - cron: '0 10 * * 2' permissions: {} # Minimal top-level for OSSF Scorecard Token-Permissions jobs: dockle: name: Dockle Container Image Security runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read security-events: write steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build Docker image for scanning uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . push: false load: true tags: local-deep-research:dockle-scan cache-from: type=gha,scope=dockle-scan cache-to: type=gha,mode=max,scope=dockle-scan - name: Run Dockle security scan id: dockle run: | echo "=== Running Dockle Container Image Security Scan ===" # Run Dockle with SARIF output for GitHub Security tab # Mount current directory to /output so SARIF file is written to host filesystem # Exit code 1 means issues found, but we don't fail the workflow # # Ignored checks (false positives or intentional design): # - CIS-DI-0001: Create a user - image uses setpriv entrypoint pattern for privilege drop # (see scripts/ldr_entrypoint.sh which uses setpriv to switch to ldruser) # - CIS-DI-0005: Enable Content trust for Docker - runtime env var (DOCKER_CONTENT_TRUST), # not a Dockerfile concern; enforced at deployment time # - CIS-DI-0008: setuid/setgid files - unix_chkpwd is standard Debian base utility # required for PAM authentication, inherited from python:3.14-slim base image # - DKL-DI-0005: Clear apt-get caches - already done in Dockerfile (rm -rf /var/lib/apt/lists/*), # but base image (python:3.14-slim) layers trigger this check # # Accepted files (false positives): # - unix_chkpwd: PAM utility from Debian base image # - settings.py: Any file named settings.py (config files, not credentials) # Without --exit-code flag, Dockle always exits 0 regardless of findings. # Findings are reported via SARIF and enforced by the release gate # (check-code-scanning-alerts job in release-gate.yml). # Any non-zero exit code here means Docker itself failed (image pull error, # socket unavailable, etc.) — a broken scanner that MUST fail the step, # otherwise no SARIF is produced and the release gate has nothing to check. set +e docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$(pwd):/output" \ goodwithtech/dockle:v0.4.14 \ --format sarif \ --output /output/dockle-results.sarif \ --ignore CIS-DI-0001 \ --ignore CIS-DI-0005 \ --ignore CIS-DI-0008 \ --ignore DKL-DI-0005 \ --accept-file "usr/sbin/unix_chkpwd" \ --accept-file "settings.py" \ local-deep-research:dockle-scan DOCKLE_EXIT_CODE=$? set -e if [ "$DOCKLE_EXIT_CODE" -ne 0 ]; then echo "::error::Dockle/Docker failed with exit code $DOCKLE_EXIT_CODE" exit 1 fi # Check if SARIF file was created (for use in subsequent steps) if [ -f "dockle-results.sarif" ]; then echo "sarif_exists=true" >> "$GITHUB_OUTPUT" # Check if SARIF contains any findings (used by summary step) RESULT_COUNT=$(python3 -c "import json; print(len(json.load(open('dockle-results.sarif')).get('runs',[{}])[0].get('results',[])))" 2>/dev/null || echo "0") if [ "$RESULT_COUNT" -gt 0 ]; then echo "DOCKLE_FOUND_ISSUES=true" >> "$GITHUB_ENV" fi fi - name: Run Dockle with human-readable output if: always() run: | echo "=== Dockle Scan Results ===" # Use same ignore flags as SARIF scan for consistent output docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ goodwithtech/dockle:v0.4.14 \ --format list \ --ignore CIS-DI-0001 \ --ignore CIS-DI-0005 \ --ignore CIS-DI-0008 \ --ignore DKL-DI-0005 \ --accept-file "usr/sbin/unix_chkpwd" \ --accept-file "settings.py" \ local-deep-research:dockle-scan || true - name: Upload Dockle results to GitHub Security tab uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 # Note: hashFiles() is evaluated at workflow parse time, not runtime. # Use step output instead to check if SARIF file was created during the run. if: always() && steps.dockle.outputs.sarif_exists == 'true' with: sarif_file: dockle-results.sarif category: dockle - name: Upload Dockle results as artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() && steps.dockle.outputs.sarif_exists == 'true' with: name: dockle-results path: dockle-results.sarif retention-days: 7 # Reduced for security - name: Display Dockle summary if: always() run: | { echo "## 🐋 Dockle Container Image Security Summary" echo "" echo "### What is Dockle?" echo "Dockle is a container image linting tool that checks for:" echo "" echo "#### 🎯 Security Checks:" echo "- **Sensitive Files**: Keys, certificates, passwords in image" echo "- **Root User**: Warning if running as root" echo "- **Clear-text Passwords**: Detects plaintext secrets" echo "- **CIS Benchmarks**: Container security best practices" echo "- **Outdated Packages**: Vulnerable base images" echo "" echo "#### 📊 Dockle vs Hadolint:" echo "- **Hadolint**: Checks Dockerfile source code (best practices)" echo "- **Dockle**: Scans built container image (runtime security)" echo "- **Both Complement**: Check different stages of container lifecycle" echo "" echo "#### 🔍 Scan Results:" } >> "$GITHUB_STEP_SUMMARY" if [ "$DOCKLE_FOUND_ISSUES" = "true" ]; then { echo "⚠️ **Issues Found**" echo "" echo "Dockle detected security or best practice issues:" echo "" echo "**Common Issues:**" echo "- Running as root user (consider USER directive)" echo "- Sensitive files potentially included in image" echo "- Missing security labels or metadata" echo "- Outdated base image packages" echo "" echo "📋 **Detailed Results:**" echo "- GitHub Security tab: SARIF results uploaded" echo "- Artifacts: Full SARIF report available" echo "- Check output above for human-readable format" } >> "$GITHUB_STEP_SUMMARY" else { echo "✅ **No Issues Found!**" echo "" echo "Container image passed all Dockle security checks." } >> "$GITHUB_STEP_SUMMARY" fi { echo "" echo "#### 🔗 Resources:" echo "- [Dockle GitHub](https://github.com/goodwithtech/dockle)" echo "- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker)" echo "- [Security Tab](https://github.com/${{ github.repository }}/security/code-scanning)" echo "" echo "#### 💡 Best Practices:" echo "- Use multi-stage builds to reduce image size" echo "- Add USER directive to run as non-root" echo "- Don't include secrets or keys in image" echo "- Keep base images updated" echo "- Scan images with both Dockle (runtime) and Hadolint (build)" echo "" echo "#### 🔇 Ignored Checks (false positives or intentional design):" echo "- **CIS-DI-0001** (Create a user): Uses setpriv entrypoint pattern for privilege drop" echo "- **CIS-DI-0005** (Content trust): Runtime env var, not Dockerfile concern" echo "- **CIS-DI-0008** (setuid/setgid): unix_chkpwd from Debian base, required for PAM" echo "- **DKL-DI-0005** (apt-get cache): Dockerfile clears caches, base image triggers this" echo "- **settings.py**: Any file named settings.py accepted (config files, not credentials)" } >> "$GITHUB_STEP_SUMMARY" - name: Clean up Docker image if: always() run: | docker rmi local-deep-research:dockle-scan || true