Container Security Scanning #7
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: PR Container Security Scan | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| paths: | |
| - 'recipes/**' | |
| - 'builder/**' | |
| - 'Dockerfile*' | |
| - '.github/workflows/pr-container-security-scan.yml' | |
| env: | |
| SCAN_OUTPUT_DIR: scan_results | |
| TEMP_DIR: temp_scan | |
| # Security scan configuration | |
| ENABLE_GRYPE: true | |
| ENABLE_TRIVY: true | |
| ENABLE_SEMGREP: true | |
| SEVERITY_THRESHOLD: medium # Options: low, medium, high, critical | |
| MAX_SCAN_TIMEOUT: 1800 # 30 minutes | |
| SEMGREP_TIMEOUT: 900 # 15 minutes | |
| FAIL_ON_CRITICAL: false # Set to true to fail PR on critical vulnerabilities | |
| jobs: | |
| container-security-scan: | |
| name: Automated Container Security Scan | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: read | |
| security-events: write | |
| actions: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y jq curl wget git | |
| docker --version | |
| - name: Install Python dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| if [ -f requirements.txt ]; then | |
| pip install -r requirements.txt | |
| fi | |
| pip install flask jinja2 requests pyyaml jsonschema | |
| - name: Install vulnerability scanners | |
| run: | | |
| # Install Grype | |
| if [[ "${{ env.ENABLE_GRYPE }}" == "true" ]]; then | |
| echo "Installing Grype..." | |
| curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin | |
| grype version | |
| fi | |
| # Install Trivy | |
| if [[ "${{ env.ENABLE_TRIVY }}" == "true" ]]; then | |
| echo "Installing Trivy..." | |
| sudo apt-get install -y wget apt-transport-https gnupg lsb-release | |
| wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg | |
| echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list | |
| sudo apt-get update | |
| sudo apt-get install -y trivy | |
| trivy --version | |
| fi | |
| # Pull Semgrep Docker image | |
| if [[ "${{ env.ENABLE_SEMGREP }}" == "true" ]]; then | |
| echo "Pulling Semgrep Docker image..." | |
| docker pull returntocorp/semgrep:latest | |
| fi | |
| - name: Create output directories | |
| run: | | |
| mkdir -p ${{ env.SCAN_OUTPUT_DIR }} | |
| mkdir -p ${{ env.TEMP_DIR }} | |
| - name: Detect changed containers | |
| id: detect-containers | |
| run: | | |
| echo "Detecting changed container recipes..." | |
| # Get changed files in this PR | |
| git fetch origin ${{ github.base_ref }} --depth=1 | |
| changed_files=$(git diff --name-only origin/${{ github.base_ref }}..HEAD || git diff --name-only HEAD~1..HEAD) | |
| echo "Changed files:" | |
| echo "$changed_files" | |
| # Extract recipe directories that changed | |
| changed_recipes=$(echo "$changed_files" | grep '^recipes/' | cut -d'/' -f2 | sort -u) | |
| echo "Changed recipes:" | |
| echo "$changed_recipes" | |
| # Store for next step | |
| echo "$changed_recipes" > ${{ env.TEMP_DIR }}/changed_recipes.txt | |
| # Count changed recipes | |
| recipe_count=$(echo "$changed_recipes" | grep -v '^$' | wc -l) | |
| echo "recipe_count=$recipe_count" >> $GITHUB_OUTPUT | |
| if [ "$recipe_count" -eq 0 ]; then | |
| echo "No container recipes changed in this PR" | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Found $recipe_count changed recipe(s)" | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build changed containers | |
| if: steps.detect-containers.outputs.has_changes == 'true' | |
| id: build-containers | |
| run: | | |
| echo "Building changed containers for scanning..." | |
| built_images="" | |
| while IFS= read -r recipe; do | |
| if [ -z "$recipe" ]; then | |
| continue | |
| fi | |
| echo "Processing recipe: $recipe" | |
| # Check if recipe has build.yaml | |
| if [ -f "recipes/$recipe/build.yaml" ]; then | |
| echo "Building container for $recipe using builder/build.py..." | |
| # Use the neurocontainers build system | |
| if python builder/build.py generate "$recipe" --build; then | |
| echo "Successfully built $recipe" | |
| # Try to detect built image name (neurocontainers format) | |
| image_name=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "$recipe" | head -1) | |
| if [ -n "$image_name" ]; then | |
| echo "Built image: $image_name" | |
| built_images="${built_images}${image_name}," | |
| fi | |
| else | |
| echo "Warning: Failed to build $recipe" | |
| fi | |
| else | |
| echo "No build.yaml found for $recipe, skipping" | |
| fi | |
| done < ${{ env.TEMP_DIR }}/changed_recipes.txt | |
| # Remove trailing comma | |
| built_images="${built_images%,}" | |
| echo "Built images: $built_images" | |
| echo "built_images=$built_images" >> $GITHUB_OUTPUT | |
| if [ -z "$built_images" ]; then | |
| echo "No images were successfully built" | |
| echo "has_built_images=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_built_images=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Scan containers with Grype | |
| if: steps.build-containers.outputs.has_built_images == 'true' && env.ENABLE_GRYPE == 'true' | |
| id: scan-grype | |
| run: | | |
| echo "Scanning built containers with Grype..." | |
| IFS=',' read -ra IMAGES <<< "${{ steps.build-containers.outputs.built_images }}" | |
| all_critical=0 | |
| all_high=0 | |
| all_medium=0 | |
| all_low=0 | |
| for image in "${IMAGES[@]}"; do | |
| if [ -z "$image" ]; then | |
| continue | |
| fi | |
| echo "Scanning image: $image" | |
| # Sanitize image name for filename | |
| safe_name=$(echo "$image" | tr '/:' '_') | |
| # Get image info | |
| docker inspect "$image" > "${{ env.SCAN_OUTPUT_DIR }}/image_info_${safe_name}.json" || true | |
| # Run Grype scan with JSON output | |
| grype "$image" \ | |
| --output json \ | |
| --file "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" || true | |
| # Generate human-readable report | |
| grype "$image" \ | |
| --output table \ | |
| --file "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.txt" || true | |
| # Count vulnerabilities by severity | |
| if [ -f "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" ]; then | |
| critical=$(jq -r '[.matches[] | select(.vulnerability.severity == "Critical")] | length' "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" 2>/dev/null || echo "0") | |
| high=$(jq -r '[.matches[] | select(.vulnerability.severity == "High")] | length' "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" 2>/dev/null || echo "0") | |
| medium=$(jq -r '[.matches[] | select(.vulnerability.severity == "Medium")] | length' "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" 2>/dev/null || echo "0") | |
| low=$(jq -r '[.matches[] | select(.vulnerability.severity == "Low")] | length' "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" 2>/dev/null || echo "0") | |
| echo "Image $image:" | |
| echo " Critical: $critical" | |
| echo " High: $high" | |
| echo " Medium: $medium" | |
| echo " Low: $low" | |
| all_critical=$((all_critical + critical)) | |
| all_high=$((all_high + high)) | |
| all_medium=$((all_medium + medium)) | |
| all_low=$((all_low + low)) | |
| fi | |
| done | |
| echo "total_critical=$all_critical" >> $GITHUB_OUTPUT | |
| echo "total_high=$all_high" >> $GITHUB_OUTPUT | |
| echo "total_medium=$all_medium" >> $GITHUB_OUTPUT | |
| echo "total_low=$all_low" >> $GITHUB_OUTPUT | |
| - name: Scan containers with Trivy | |
| if: steps.build-containers.outputs.has_built_images == 'true' && env.ENABLE_TRIVY == 'true' | |
| id: scan-trivy | |
| run: | | |
| echo "Scanning built containers with Trivy..." | |
| IFS=',' read -ra IMAGES <<< "${{ steps.build-containers.outputs.built_images }}" | |
| trivy_critical=0 | |
| trivy_high=0 | |
| trivy_medium=0 | |
| trivy_low=0 | |
| for image in "${IMAGES[@]}"; do | |
| if [ -z "$image" ]; then | |
| continue | |
| fi | |
| echo "Scanning image: $image" | |
| # Sanitize image name for filename | |
| safe_name=$(echo "$image" | tr '/:' '_') | |
| # Run Trivy scan with JSON output | |
| trivy image \ | |
| --format json \ | |
| --output "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.json" \ | |
| --timeout 10m \ | |
| --severity "LOW,MEDIUM,HIGH,CRITICAL" \ | |
| "$image" || true | |
| # Generate human-readable report | |
| trivy image \ | |
| --format table \ | |
| --output "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.txt" \ | |
| --severity "LOW,MEDIUM,HIGH,CRITICAL" \ | |
| "$image" || true | |
| # Count vulnerabilities by severity | |
| if [ -f "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.json" ]; then | |
| critical=$(jq -r '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.json" 2>/dev/null || echo "0") | |
| high=$(jq -r '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.json" 2>/dev/null || echo "0") | |
| medium=$(jq -r '[.Results[]?.Vulnerabilities[]? | select(.Severity == "MEDIUM")] | length' "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.json" 2>/dev/null || echo "0") | |
| low=$(jq -r '[.Results[]?.Vulnerabilities[]? | select(.Severity == "LOW")] | length' "${{ env.SCAN_OUTPUT_DIR }}/trivy_${safe_name}.json" 2>/dev/null || echo "0") | |
| echo "Image $image (Trivy):" | |
| echo " Critical: $critical" | |
| echo " High: $high" | |
| echo " Medium: $medium" | |
| echo " Low: $low" | |
| trivy_critical=$((trivy_critical + critical)) | |
| trivy_high=$((trivy_high + high)) | |
| trivy_medium=$((trivy_medium + medium)) | |
| trivy_low=$((trivy_low + low)) | |
| fi | |
| done | |
| echo "trivy_critical=$trivy_critical" >> $GITHUB_OUTPUT | |
| echo "trivy_high=$trivy_high" >> $GITHUB_OUTPUT | |
| echo "trivy_medium=$trivy_medium" >> $GITHUB_OUTPUT | |
| echo "trivy_low=$trivy_low" >> $GITHUB_OUTPUT | |
| - name: Run Semgrep static analysis | |
| if: steps.build-containers.outputs.has_built_images == 'true' && env.ENABLE_SEMGREP == 'true' | |
| id: scan-semgrep | |
| run: | | |
| echo "Running Semgrep static analysis..." | |
| IFS=',' read -ra IMAGES <<< "${{ steps.build-containers.outputs.built_images }}" | |
| semgrep_total=0 | |
| semgrep_high=0 | |
| for image in "${IMAGES[@]}"; do | |
| if [ -z "$image" ]; then | |
| continue | |
| fi | |
| echo "Analyzing image: $image" | |
| # Sanitize image name for filename | |
| safe_name=$(echo "$image" | tr '/:' '_') | |
| # Create container to extract filesystem | |
| container_id=$(docker create "$image") | |
| # Extract container filesystem | |
| temp_extract_dir="${{ env.TEMP_DIR }}/container_fs_${safe_name}" | |
| mkdir -p "$temp_extract_dir" | |
| docker export "$container_id" | tar -xf - -C "$temp_extract_dir" 2>/dev/null || true | |
| # Run Semgrep analysis | |
| timeout 15m docker run --rm \ | |
| -v "$PWD/$temp_extract_dir":/src \ | |
| -v "$PWD/${{ env.SCAN_OUTPUT_DIR }}":/output \ | |
| returntocorp/semgrep:latest \ | |
| --config=auto \ | |
| --config=p/security-audit \ | |
| --config=p/secrets \ | |
| --json \ | |
| --output=/output/semgrep_${safe_name}.json \ | |
| /src 2>/dev/null || echo "Semgrep completed with warnings" | |
| # Count issues | |
| if [ -f "${{ env.SCAN_OUTPUT_DIR }}/semgrep_${safe_name}.json" ]; then | |
| total=$(jq -r '.results | length' "${{ env.SCAN_OUTPUT_DIR }}/semgrep_${safe_name}.json" 2>/dev/null || echo "0") | |
| high=$(jq -r '[.results[] | select(.extra.severity == "ERROR")] | length' "${{ env.SCAN_OUTPUT_DIR }}/semgrep_${safe_name}.json" 2>/dev/null || echo "0") | |
| echo "Image $image (Semgrep):" | |
| echo " Total issues: $total" | |
| echo " High severity: $high" | |
| semgrep_total=$((semgrep_total + total)) | |
| semgrep_high=$((semgrep_high + high)) | |
| fi | |
| # Cleanup | |
| docker rm "$container_id" || true | |
| rm -rf "$temp_extract_dir" | |
| done | |
| echo "semgrep_total=$semgrep_total" >> $GITHUB_OUTPUT | |
| echo "semgrep_high=$semgrep_high" >> $GITHUB_OUTPUT | |
| - name: Generate comprehensive security reports | |
| if: steps.build-containers.outputs.has_built_images == 'true' | |
| run: | | |
| echo "Generating comprehensive security reports..." | |
| # Check if the report generator script exists | |
| if [ ! -f "generate_comprehensive_security_report.py" ]; then | |
| echo "Downloading comprehensive report generator..." | |
| # Use the same script from the manual workflow | |
| curl -sSL https://raw.githubusercontent.com/neurodesk/neurocontainers/main/generate_comprehensive_security_report.py -o generate_comprehensive_security_report.py 2>/dev/null || true | |
| fi | |
| # Generate comprehensive reports for each scanned image | |
| IFS=',' read -ra IMAGES <<< "${{ steps.build-containers.outputs.built_images }}" | |
| for image in "${IMAGES[@]}"; do | |
| if [ -z "$image" ]; then | |
| continue | |
| fi | |
| safe_name=$(echo "$image" | tr '/:' '_') | |
| if [ -f "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" ] && [ -f "generate_comprehensive_security_report.py" ]; then | |
| echo "Generating comprehensive report for $image..." | |
| python3 generate_comprehensive_security_report.py \ | |
| "${{ env.SCAN_OUTPUT_DIR }}/grype_${safe_name}.json" \ | |
| --image-info "${{ env.SCAN_OUTPUT_DIR }}/image_info_${safe_name}.json" \ | |
| --output "${{ env.SCAN_OUTPUT_DIR }}/comprehensive_${safe_name}.md" \ | |
| --quick || echo "Failed to generate comprehensive report for $image" | |
| fi | |
| done | |
| - name: Generate PR summary report | |
| if: steps.build-containers.outputs.has_built_images == 'true' | |
| run: | | |
| echo "Generating summary report for PR..." | |
| cat > "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" << 'EOF' | |
| # Container Security Scan Results | |
| **PR:** #${{ github.event.pull_request.number }} | |
| **Scan Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC') | |
| **Scanners:** Grype, Trivy, Semgrep | |
| ## Summary | |
| This PR modifies container recipes. Comprehensive security scanning has been performed on all changed containers. | |
| ### Vulnerability Summary by Scanner | |
| #### Grype Results | |
| | Severity | Count | | |
| |----------|-------| | |
| | Critical | ${{ steps.scan-grype.outputs.total_critical || 0 }} | | |
| | High | ${{ steps.scan-grype.outputs.total_high || 0 }} | | |
| | Medium | ${{ steps.scan-grype.outputs.total_medium || 0 }} | | |
| | Low | ${{ steps.scan-grype.outputs.total_low || 0 }} | | |
| #### Trivy Results | |
| | Severity | Count | | |
| |----------|-------| | |
| | Critical | ${{ steps.scan-trivy.outputs.trivy_critical || 0 }} | | |
| | High | ${{ steps.scan-trivy.outputs.trivy_high || 0 }} | | |
| | Medium | ${{ steps.scan-trivy.outputs.trivy_medium || 0 }} | | |
| | Low | ${{ steps.scan-trivy.outputs.trivy_low || 0 }} | | |
| #### Semgrep Static Analysis | |
| | Metric | Count | | |
| |--------|-------| | |
| | Total Issues | ${{ steps.scan-semgrep.outputs.semgrep_total || 0 }} | | |
| | High Severity | ${{ steps.scan-semgrep.outputs.semgrep_high || 0 }} | | |
| ### Scanned Images | |
| EOF | |
| IFS=',' read -ra IMAGES <<< "${{ steps.build-containers.outputs.built_images }}" | |
| for image in "${IMAGES[@]}"; do | |
| if [ -n "$image" ]; then | |
| echo "- \`$image\`" >> "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" | |
| fi | |
| done | |
| cat >> "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" << 'EOF' | |
| ### Risk Assessment | |
| EOF | |
| grype_critical=${{ steps.scan-grype.outputs.total_critical || 0 }} | |
| trivy_critical=${{ steps.scan-trivy.outputs.trivy_critical || 0 }} | |
| grype_high=${{ steps.scan-grype.outputs.total_high || 0 }} | |
| trivy_high=${{ steps.scan-trivy.outputs.trivy_high || 0 }} | |
| semgrep_high=${{ steps.scan-semgrep.outputs.semgrep_high || 0 }} | |
| max_critical=$((grype_critical > trivy_critical ? grype_critical : trivy_critical)) | |
| max_high=$((grype_high > trivy_high ? grype_high : trivy_high)) | |
| if [ "$max_critical" -gt 0 ]; then | |
| echo "**CRITICAL**: This PR introduces containers with **$max_critical critical vulnerabilities**. Please review and address before merging." >> "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" | |
| elif [ "$max_high" -gt 10 ] || [ "$semgrep_high" -gt 5 ]; then | |
| echo "**WARNING**: This PR introduces containers with multiple high severity issues. Review recommended." >> "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" | |
| else | |
| echo "**OK**: No critical vulnerabilities detected. Standard security practices apply." >> "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" | |
| fi | |
| cat >> "${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md" << 'EOF' | |
| ### Detailed Reports | |
| Comprehensive security reports with EPSS scores and KEV data are available in the workflow artifacts: | |
| - Individual scanner reports (JSON and text formats) | |
| - Comprehensive security assessment reports | |
| - Static analysis results | |
| --- | |
| **Artifacts:** Download detailed scan results from the workflow artifacts section. | |
| **Manual Scan:** For even more detailed analysis, use the [Manual Container Security Scan workflow](../../actions/workflows/manual-container-security-scan.yml). | |
| **Security Tools Used:** | |
| - **Grype** - Vulnerability scanner for container images | |
| - **Trivy** - Comprehensive security scanner | |
| - **Semgrep** - Static analysis for code security patterns | |
| EOF | |
| - name: Comment on PR | |
| if: steps.build-containers.outputs.has_built_images == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const summary = fs.readFileSync('${{ env.SCAN_OUTPUT_DIR }}/pr_security_summary.md', 'utf8'); | |
| // Find existing comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('Container Security Scan Results') | |
| ); | |
| if (botComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: summary | |
| }); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: summary | |
| }); | |
| } | |
| - name: Upload scan results | |
| if: steps.build-containers.outputs.has_built_images == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pr-security-scan-results-${{ github.event.pull_request.number }} | |
| path: | | |
| ${{ env.SCAN_OUTPUT_DIR }}/ | |
| retention-days: 30 | |
| - name: Check for critical vulnerabilities | |
| if: steps.build-containers.outputs.has_built_images == 'true' | |
| run: | | |
| grype_critical=${{ steps.scan-grype.outputs.total_critical || 0 }} | |
| trivy_critical=${{ steps.scan-trivy.outputs.trivy_critical || 0 }} | |
| semgrep_high=${{ steps.scan-semgrep.outputs.semgrep_high || 0 }} | |
| max_critical=$((grype_critical > trivy_critical ? grype_critical : trivy_critical)) | |
| echo "=== Security Scan Summary ===" | |
| echo "Grype Critical: $grype_critical" | |
| echo "Trivy Critical: $trivy_critical" | |
| echo "Semgrep High: $semgrep_high" | |
| echo "Max Critical: $max_critical" | |
| if [ "$max_critical" -gt 0 ]; then | |
| echo "" | |
| echo "[CRITICAL] Found $max_critical critical vulnerabilities!" | |
| echo "Please review the scan results and address critical issues before merging." | |
| echo "" | |
| echo "TIP: Check the workflow artifacts for detailed reports." | |
| # Fail workflow if configured to do so | |
| if [[ "${{ env.FAIL_ON_CRITICAL }}" == "true" ]]; then | |
| echo "" | |
| echo "[FAIL] FAIL_ON_CRITICAL is enabled. Failing the workflow." | |
| exit 1 | |
| fi | |
| elif [ "$semgrep_high" -gt 5 ]; then | |
| echo "" | |
| echo "[WARNING] Found $semgrep_high high-severity static analysis issues." | |
| echo "Review recommended before merging." | |
| else | |
| echo "" | |
| echo "[OK] No critical vulnerabilities found" | |
| echo "Standard security practices apply." | |
| fi | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| echo "Cleaning up temporary files..." | |
| rm -rf ${{ env.TEMP_DIR }} | |
| docker system prune -f || true |