diff --git a/.github/workflows/ci-main-pull-request.yml b/.github/workflows/ci-main-pull-request.yml index 3b99931..fa7c647 100644 --- a/.github/workflows/ci-main-pull-request.yml +++ b/.github/workflows/ci-main-pull-request.yml @@ -96,12 +96,56 @@ on: required: false type: boolean default: true + fail-trufflehog-on-secrets-found: + description: 'Fail the pipeline if Trufflehog finds verified secrets' + required: false + type: boolean + default: true perform-trivy-scan: description: 'Perform Trivy scan' required: false type: boolean default: true - + trivy-fail-on-high: + description: 'Fail pipeline if Trivy finds HIGH vulnerabilities' + required: false + type: boolean + default: false + trivy-fail-on-critical: + description: 'Fail pipeline if Trivy finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false + perform-grype-scan: + description: 'Perform Grype scan on source code' + required: false + type: boolean + default: false + grype-fail-on-high: + description: 'Fail pipeline if Grype finds HIGH vulnerabilities' + required: false + type: boolean + default: false + grype-fail-on-critical: + description: 'Fail pipeline if Grype finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false + perform-grype-image-scan: + description: 'Perform Grype scan on Docker image' + required: false + type: boolean + default: false + grype-image-fail-on-high: + description: 'Fail pipeline if Grype image scan finds HIGH vulnerabilities' + required: false + type: boolean + default: false + grype-image-fail-on-critical: + description: 'Fail pipeline if Grype image scan finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false build: description: 'CI Build (language-specific)' required: false @@ -164,15 +208,15 @@ on: type: string polaris-coverity-clean-command: # NEW IN 1.0.7 - description: 'Coverity clean command, typically done before build stage by language or here as param 1-liner like "mvn clean"' + description: 'Coverity clean command, typically done before build stage by language or here as param 1-liner like "mvn clean". Leave empty for buildless analysis (Ruby, Python, etc.)' required: false - default: 'go clean' + default: '' type: string polaris-coverity-build-command: # NEW IN 1.0.7 - description: 'Coverity build command, typically done in build stage by language or here as param 1-liner like "mvn clean install"' + description: 'Coverity build command, typically done in build stage by language or here as param 1-liner like "mvn clean install". Leave empty for buildless analysis (Ruby, Python, etc.)' required: false - default: 'go build' + default: '' type: string polaris-coverity-args: # NEW IN 1.0.7 @@ -204,6 +248,16 @@ on: required: false default: true type: boolean + polaris-fail-on-high: + description: 'Fail the pipeline if Polaris SAST scan finds HIGH vulnerabilities' + required: false + type: boolean + default: false + polaris-fail-on-critical: + description: 'Fail the pipeline if Polaris SAST scan finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false perform-sonarqube-scan: description: 'Perform basic SonarQube scan' @@ -369,6 +423,26 @@ on: required: false type: boolean default: false + ruby-app-directory: + description: 'Subdirectory containing Ruby Gemfile (e.g., "src/supermarket" for repos with non-root Gemfile location). Leave empty if Gemfile is in root.' + required: false + type: string + default: '' + blackduck-fail-on-blocker: + description: 'Fail the pipeline if BlackDuck SCA scan finds BLOCKER vulnerabilities' + required: false + type: boolean + default: false + blackduck-fail-on-critical: + description: 'Fail the pipeline if BlackDuck SCA scan finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false + blackduck-fail-on-major: + description: 'Fail the pipeline if BlackDuck SCA scan finds MAJOR vulnerabilities' + required: false + type: boolean + default: false udf1: description: 'User defined flag 1' @@ -705,16 +779,117 @@ jobs: run-trufflehog: name: 'Trufflehog scan' if: ${{ inputs.perform-trufflehog-scan }} - uses: chef/common-github-actions/.github/workflows/trufflehog.yml@main + uses: chef/common-github-actions/.github/workflows/trufflehog.yml@sandhi/fix-blackduc-sca needs: checkout + with: + fail-trufflehog-on-secrets-found: ${{ inputs.fail-trufflehog-on-secrets-found }} run-trivy: name: 'Trivy scan' if: ${{ inputs.perform-trivy-scan }} - uses: chef/common-github-actions/.github/workflows/trivy.yml@main + uses: chef/common-github-actions/.github/workflows/trivy.yml@sandhi/fix-blackduc-sca needs: checkout with: version: ${{ inputs.version }} + trivy-fail-on-high: ${{ inputs.trivy-fail-on-high }} + trivy-fail-on-critical: ${{ inputs.trivy-fail-on-critical }} + + run-grype: + name: 'Grype scan' + if: ${{ inputs.perform-grype-scan }} + runs-on: ubuntu-latest + needs: checkout # TODO: fix set-application-version + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Determine severity threshold + id: severity + run: | + if [ "${{ inputs.grype-fail-on-high }}" == "true" ]; then + echo "level=high" >> $GITHUB_OUTPUT + elif [ "${{ inputs.grype-fail-on-critical }}" == "true" ]; then + echo "level=critical" >> $GITHUB_OUTPUT + else + echo "level=none" >> $GITHUB_OUTPUT + fi + + - name: Run Grype scan on repo + id: scan + uses: anchore/scan-action@v3 + with: + path: . + fail-build: true + severity-cutoff: ${{ steps.severity.outputs.level }} + output-format: json + + + - name: Check Grype results and fail if vulnerabilities found + if: always() + run: | + JSON_FILE="./results.json" + + if [ ! -f "$JSON_FILE" ] || [ -z "$JSON_FILE" ]; then + echo "⚠️ Grype JSON output not found" + exit 0 + fi + + # Extract vulnerability counts using jq or grep fallback + if command -v jq &> /dev/null; then + CRITICAL_COUNT=$(jq '[.matches[]? | select(.vulnerability.severity == "Critical")] | length' "$JSON_FILE" 2>/dev/null || echo "0") + HIGH_COUNT=$(jq '[.matches[]? | select(.vulnerability.severity == "High")] | length' "$JSON_FILE" 2>/dev/null || echo "0") + else + CRITICAL_COUNT=$(grep -o '"severity":"Critical"' "$JSON_FILE" | wc -l | tr -d ' ' || echo "0") + HIGH_COUNT=$(grep -o '"severity":"High"' "$JSON_FILE" | wc -l | tr -d ' ' || echo "0") + fi + + echo "" + echo "============================================" + echo "Grype Security Scan Summary" + echo "============================================" + echo "CRITICAL vulnerabilities: $CRITICAL_COUNT" + echo "HIGH vulnerabilities: $HIGH_COUNT" + echo "============================================" + + VIOLATIONS="" + [ "${{ inputs.grype-fail-on-critical }}" == "true" ] && [ "$CRITICAL_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$CRITICAL_COUNT CRITICAL, " + [ "${{ inputs.grype-fail-on-high }}" == "true" ] && [ "$HIGH_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$HIGH_COUNT HIGH, " + + if [ -n "$VIOLATIONS" ]; then + echo "" + echo "❌ BUILD FAILED: Found ${VIOLATIONS%, }" + exit 1 + fi + + echo "" + echo "✅ No policy violations found" + + - name: Upload Grype scan results + if: always() + uses: actions/upload-artifact@v4 + with: + name: grype-results + path: ./results.json + retention-days: 30 + + # - name: Run Grype scan on repo + # uses: anchore/scan-action@v3 + # with: + # path: . + # fail-build: true + # severity-cutoff: high + + run-grype-image: + name: 'Grype Docker image scan' + if: ${{ inputs.perform-grype-image-scan }} + uses: chef/common-github-actions/.github/workflows/grype.yml@sandhi/fix-blackduc-sca + needs: checkout + secrets: inherit + with: + fail-grype-on-high: ${{ inputs.grype-image-fail-on-high }} + fail-grype-on-critical: ${{ inputs.grype-image-fail-on-critical }} # run-srcclr: # if: ${{ inputs.perform-srcclr-scan == true }} @@ -743,6 +918,11 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Configure git for private Go modules + if: inputs.language == 'go' + env: + GOPRIVATE: ${{ inputs.go-private-modules }} + run: git config --global url."https://${{ secrets.GH_TOKEN }}@github.com/".insteadOf "https://github.com/" - name: 'Go build' if: ${{ inputs.language == 'go' && env.GA_BUILD_PROFILE == 'cli' }} continue-on-error: true @@ -923,73 +1103,73 @@ jobs: # # A file, directory or wildcard pattern that describes what to upload # path: test/unittest/coverage.out - Sonar-public-repo: - name: 'PUBLIC Sonar SAST scan' - needs: ci-build - if: ${{ inputs.perform-sonarqube-scan == true && success() && inputs.visibility == 'public'}} - uses: chef/common-github-actions/.github/workflows/sonarqube-public-repo.yml@main - secrets: inherit - permissions: - id-token: write - contents: read - with: - perform-build: ${{ inputs.build }} # was ${{ inputs.perform-sonar-build }} - build-profile: ${{ inputs.build-profile }} - language: ${{ inputs.language }} - report-unit-test-coverage: ${{ inputs.report-unit-test-coverage }} - report-to-atlassian-dashboard: ${{ inputs.report-to-atlassian-dashboard }} - quality-product-name: ${{ inputs.quality-product-name }} - quality-sonar-app-name: ${{ inputs.quality-sonar-app-name }} - quality-testing-type: ${{ inputs.quality-testing-type }} - quality-service-name: ${{ inputs.quality-service-name }} - quality-junit-report: ${{ inputs.quality-junit-report }} - visibility: ${{ inputs.visibility }} - go-private-modules: ${{ inputs.go-private-modules }} - udf1: ${{ inputs.udf1 }} - udf2: ${{ inputs.udf2 }} - udf3: ${{ inputs.udf3 }} + # Sonar-public-repo: + # name: 'PUBLIC Sonar SAST scan' + # needs: ci-build + # if: ${{ inputs.perform-sonarqube-scan == true && success() && inputs.visibility == 'public'}} + # uses: chef/common-github-actions/.github/workflows/sonarqube-public-repo.yml@main + # secrets: inherit + # permissions: + # id-token: write + # contents: read + # with: + # perform-build: ${{ inputs.build }} # was ${{ inputs.perform-sonar-build }} + # build-profile: ${{ inputs.build-profile }} + # language: ${{ inputs.language }} + # report-unit-test-coverage: ${{ inputs.report-unit-test-coverage }} + # report-to-atlassian-dashboard: ${{ inputs.report-to-atlassian-dashboard }} + # quality-product-name: ${{ inputs.quality-product-name }} + # quality-sonar-app-name: ${{ inputs.quality-sonar-app-name }} + # quality-testing-type: ${{ inputs.quality-testing-type }} + # quality-service-name: ${{ inputs.quality-service-name }} + # quality-junit-report: ${{ inputs.quality-junit-report }} + # visibility: ${{ inputs.visibility }} + # go-private-modules: ${{ inputs.go-private-modules }} + # udf1: ${{ inputs.udf1 }} + # udf2: ${{ inputs.udf2 }} + # udf3: ${{ inputs.udf3 }} - Sonar-private-repo: - name: 'PRIVATE Sonar scan (inline)' - if: ${{ inputs.perform-sonarqube-scan == true && success() && inputs.visibility == 'private'}} - needs: ci-build - # was uses: chef/common-github-actions/.github/workflows/sonarqube-private-repo.yml@main - runs-on: ubuntu-latest - steps: - - name: SonarQube Scan - if: ${{ inputs.visibility == 'private' }} - uses: sonarsource/sonarqube-scan-action@v5.3.1 - continue-on-error: true - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + # Sonar-private-repo: + # name: 'PRIVATE Sonar scan (inline)' + # if: ${{ inputs.perform-sonarqube-scan == true && success() && inputs.visibility == 'private'}} + # needs: ci-build + # # was uses: chef/common-github-actions/.github/workflows/sonarqube-private-repo.yml@main + # runs-on: ubuntu-latest + # steps: + # - name: SonarQube Scan + # if: ${{ inputs.visibility == 'private' }} + # uses: sonarsource/sonarqube-scan-action@v5.3.1 + # continue-on-error: true + # env: + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - Sonar-internal-repo: - name: 'INTERNAL Sonar scan' - if: ${{ inputs.perform-sonarqube-scan == true && inputs.visibility == 'internal'}} - # was if: ${{ inputs.perform-sonarqube-scan == true && success() && inputs.visibility == 'internal'}} - needs: ci-build - uses: chef/common-github-actions/.github/workflows/sonarqube-internal-repo.yml@main - secrets: inherit - permissions: - id-token: write - contents: read - with: - perform-build: ${{ inputs.build }} # was ${{ inputs.perform-sonar-build }} - build-profile: ${{ inputs.build-profile }} - language: ${{ inputs.language }} - report-unit-test-coverage: ${{ inputs.report-unit-test-coverage }} - report-to-atlassian-dashboard: ${{ inputs.report-to-atlassian-dashboard }} - quality-product-name: ${{ inputs.quality-product-name }} - quality-sonar-app-name: ${{ inputs.quality-sonar-app-name }} - quality-testing-type: ${{ inputs.quality-testing-type }} - quality-service-name: ${{ inputs.quality-service-name }} - quality-junit-report: ${{ inputs.quality-junit-report }} - visibility: ${{ inputs.visibility }} - go-private-modules: ${{ inputs.go-private-modules }} - udf1: ${{ inputs.udf1 }} - udf2: ${{ inputs.udf2 }} - udf3: ${{ inputs.udf3 }} + # Sonar-internal-repo: + # name: 'INTERNAL Sonar scan' + # if: ${{ inputs.perform-sonarqube-scan == true && inputs.visibility == 'internal'}} + # # was if: ${{ inputs.perform-sonarqube-scan == true && success() && inputs.visibility == 'internal'}} + # needs: ci-build + # uses: chef/common-github-actions/.github/workflows/sonarqube-internal-repo.yml@main + # secrets: inherit + # permissions: + # id-token: write + # contents: read + # with: + # perform-build: ${{ inputs.build }} # was ${{ inputs.perform-sonar-build }} + # build-profile: ${{ inputs.build-profile }} + # language: ${{ inputs.language }} + # report-unit-test-coverage: ${{ inputs.report-unit-test-coverage }} + # report-to-atlassian-dashboard: ${{ inputs.report-to-atlassian-dashboard }} + # quality-product-name: ${{ inputs.quality-product-name }} + # quality-sonar-app-name: ${{ inputs.quality-sonar-app-name }} + # quality-testing-type: ${{ inputs.quality-testing-type }} + # quality-service-name: ${{ inputs.quality-service-name }} + # quality-junit-report: ${{ inputs.quality-junit-report }} + # visibility: ${{ inputs.visibility }} + # go-private-modules: ${{ inputs.go-private-modules }} + # udf1: ${{ inputs.udf1 }} + # udf2: ${{ inputs.udf2 }} + # udf3: ${{ inputs.udf3 }} BlackDuck-Polaris-SAST: # branding: applied at action.yml level, not workflow, see https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax#branding @@ -1008,6 +1188,58 @@ jobs: runs-on: ubuntu-latest needs: checkout # TODO: fix set-application-version steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Configure git for private + run: git config --global url."https://${{ secrets.GH_TOKEN }}@github.com/".insteadOf "https://github.com/" + + - name: Install build tools for Erlang + if: inputs.language == 'erlang' + run: | + sudo apt-get update + sudo apt-get install -y build-essential + + - name: Set up Erlang/OTP and rebar3 + if: inputs.language == 'erlang' + uses: erlef/setup-beam@v1 + with: + otp-version: '25.3.2.16' + rebar3-version: '3.22.0' + + - name: Set up Ruby + if: inputs.language == 'ruby' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: false + + - name: Create bundle stub for Erlang SAST scan + if: inputs.language == 'erlang' + working-directory: ${{ github.workspace }} + run: | + # Polaris scans Erlang source code for SAST - Ruby gems not needed + # System ruby-dev from apt provides Ruby runtime (already installed) + # Create bundle stub to skip gem installation during Polaris scan + echo "Creating bundle stub to bypass Ruby gem installation" + + # Create no-op bundle script + mkdir -p "$HOME/.polaris-stubs" + cat > "$HOME/.polaris-stubs/bundle" << 'EOF' + #!/bin/bash + # Stub: skips gem installation during SAST scan + echo "[STUB] Skipping bundle $@ - not needed for Erlang SAST" + exit 0 + EOF + chmod +x "$HOME/.polaris-stubs/bundle" + + # Prepend to PATH so stub is found before any system bundler + echo "$HOME/.polaris-stubs" >> $GITHUB_PATH + + echo "Bundle stub created and added to PATH" + - name: Starting Black Duck Polaris scan run: | echo "Starting Polaris SAST scan" @@ -1028,6 +1260,7 @@ jobs: fi - name: BlackDuck Polaris scan + id: polaris-scan uses: blackduck-inc/black-duck-security-scan@v2 # copied from uses: prgs-community/githubactions-securityscans/polaris@v0.5 in https://github.com/prgs-community/githubactions-securityscans/blob/main/polaris/README.md # uses: blackduck-inc/black-duck-security-scan@805cbd09e806b01907bbea0f990723c2bb85abe9 # 2.0.0 - Jan's version @@ -1069,7 +1302,46 @@ jobs: # polaris_upload_sarif_report: true # Mark build status if policy violating issues are found # mark_build_status: 'success' - continue-on-error: true + continue-on-error: false + + - name: Check Polaris scan results and fail on HIGH or CRITICAL vulnerabilities + if: ${{ inputs.polaris-fail-on-high == true || inputs.polaris-fail-on-critical == true }} + run: | + echo "Checking Polaris SAST scan results..." + echo "Enforcement policy: HIGH=${{ inputs.polaris-fail-on-high }}, CRITICAL=${{ inputs.polaris-fail-on-critical }}" + + # Parse bridge.log for vulnerability counts + BRIDGE_LOG=".bridge/bridge.log" + + if [ ! -f "$BRIDGE_LOG" ]; then + echo "⚠️ Bridge log not found - failing as precaution" + exit 1 + fi + + # Extract vulnerability counts from log + HIGH_COUNT=$(grep -oP '"high":\s*\K\d+' "$BRIDGE_LOG" | tail -1 || echo 0) + CRITICAL_COUNT=$(grep -oP '"critical":\s*\K\d+' "$BRIDGE_LOG" | tail -1 || echo 0) + + echo "Found HIGH: $HIGH_COUNT, CRITICAL: $CRITICAL_COUNT" + + # Check for policy violations + SHOULD_FAIL=false + + if [ "${{ inputs.polaris-fail-on-critical }}" == "true" ] && [ "$CRITICAL_COUNT" -gt 0 ]; then + echo "❌ Found $CRITICAL_COUNT CRITICAL vulnerabilities (policy violation)" + SHOULD_FAIL=true + fi + + if [ "${{ inputs.polaris-fail-on-high }}" == "true" ] && [ "$HIGH_COUNT" -gt 0 ]; then + echo "❌ Found $HIGH_COUNT HIGH vulnerabilities (policy violation)" + SHOULD_FAIL=true + fi + + if [ "$SHOULD_FAIL" == "true" ]; then + exit 1 + else + echo "✅ No policy-violating vulnerabilities found" + fi package-binary: name: 'Creating packaged binaries' @@ -1316,8 +1588,8 @@ jobs: name: 'Generating SBOM' # Create software bill-of-materials (SBOM) using SPDX format if: ${{ inputs.generate-sbom == true }} - uses: chef/common-github-actions/.github/workflows/sbom.yml@main - needs: ci-build + uses: chef/common-github-actions/.github/workflows/sbom.yml@sandhi/fix-blackduc-sca + needs: checkout # TODO: fix set-application-version secrets: inherit with: version: ${{ inputs.version }} @@ -1332,6 +1604,10 @@ jobs: blackduck-force-low-accuracy-mode: ${{ inputs.blackduck-force-low-accuracy-mode }} run-bundle-install: ${{ inputs.run-bundle-install }} # Passed to sbom.yml to generate Gemfile.lock at runtime language: ${{ inputs.language }} + ruby-app-directory: ${{ inputs.ruby-app-directory }} + blackduck-fail-on-blocker: ${{ inputs.blackduck-fail-on-blocker }} + blackduck-fail-on-critical: ${{ inputs.blackduck-fail-on-critical }} + blackduck-fail-on-major: ${{ inputs.blackduck-fail-on-major }} quality-dashboard: name: 'Reporting to quality dashboard' diff --git a/.github/workflows/grype.yml b/.github/workflows/grype.yml new file mode 100644 index 0000000..97c5f23 --- /dev/null +++ b/.github/workflows/grype.yml @@ -0,0 +1,168 @@ +# grype.yml +# Grype security scan for source code vulnerabilities +# https://github.com/anchore/grype + +name: Grype security scan + +on: + workflow_call: + inputs: + fail-grype-on-high: + description: 'Fail the pipeline if Grype finds HIGH vulnerabilities' + required: false + type: boolean + default: false + fail-grype-on-critical: + description: 'Fail the pipeline if Grype finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false + +jobs: + grype-scan: + name: Grype vulnerability scan + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Configure git for private + env: + GOPRIVATE: ${{ inputs.go-private-modules }} + run: git config --global url."https://${{ secrets.GH_TOKEN }}@github.com/".insteadOf "https://github.com/" + + - name: Install Grype + run: | + curl -sSfL https://get.anchore.io/grype | sh -s -- -b /usr/local/bin + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} + aws-region: us-east-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Scan with Grype + id: grype-scan + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + SCAN_NAME="${{ github.repository }}" + + if [ ! -f "Dockerfile" ]; then + echo "❌ No Dockerfile found - this workflow requires a Dockerfile to scan Docker image" + exit 1 + fi + + echo "Building Docker image..." + REPO_NAME=$(basename $(pwd)) + + # Strategy 1: Check for build-docker.sh script (e.g., dsm-erchef) + if [ -f "build-docker.sh" ]; then + echo "Found build-docker.sh script - using it to build images" + chmod +x build-docker.sh + GITHUB_TOKEN="${{ secrets.GH_TOKEN }}" ./build-docker.sh + + # Detect all images built (typically repo name or repo-name-init) + IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -E "^${REPO_NAME}" | grep -v "^") + + if [ -z "$IMAGES" ]; then + echo "⚠️ No images found with prefix ${REPO_NAME} after build-docker.sh" + echo "Checking for any recently built images..." + IMAGES=$(docker images --format "{{.CreatedAt}}\t{{.Repository}}:{{.Tag}}" | sort -r | head -5 | cut -f2 | grep -v "^") + fi + # Strategy 2: Check for Makefile with compose-build target (e.g., chef-platform-user-accounts-service) + elif [ -f "Makefile" ] && grep -q "^compose-build:" Makefile; then + echo "Using Makefile compose-build target with GITHUB_TOKEN" + export GITHUB_TOKEN="${{ secrets.GH_TOKEN }}" + make compose-build + + echo "Detecting built images..." + docker compose images + + IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${REPO_NAME}" | grep -v "^") + + if [ -z "$IMAGES" ]; then + echo "No images found with prefix ${REPO_NAME}, scanning all recent images" + IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "^" | head -5) + fi + # Strategy 3: Fallback to standard docker build + else + echo "Using standard docker build with GITHUB_TOKEN build arg" + docker build --build-arg GITHUB_TOKEN="${{ secrets.GH_TOKEN }}" -t "${REPO_NAME}:latest" . + IMAGES="${REPO_NAME}:latest" + fi + + if [ -z "$IMAGES" ]; then + echo "❌ No Docker images found after build" + exit 1 + fi + + echo "Found images to scan:" + echo "$IMAGES" + + # Scan all images and combine results into single files + > grype-scan.json # Initialize empty JSON file + > grype-scan.log # Initialize empty log file + + for IMAGE_NAME in $IMAGES; do + echo "" + echo "Scanning Docker image: $IMAGE_NAME" + grype "$IMAGE_NAME" --name "$SCAN_NAME-$(basename $IMAGE_NAME)" --output json >> grype-scan.json + grype "$IMAGE_NAME" --name "$SCAN_NAME-$(basename $IMAGE_NAME)" --output table >> grype-scan.log || true + done + + - name: Check Grype results and fail if vulnerabilities found + if: ${{ always() && (inputs.fail-grype-on-high == true || inputs.fail-grype-on-critical == true) }} + run: | + JSON_FILE="grype-scan.json" + + if [ ! -f "$JSON_FILE" ]; then + echo "⚠️ Grype JSON output not found" + exit 0 + fi + + # Extract vulnerability counts by severity from multiple JSON documents + # Use jq -s to slurp all JSON objects and combine matches + CRITICAL_COUNT=$(jq -s '[.[] | .matches[]? | select(.vulnerability.severity == "Critical")] | length' "$JSON_FILE" 2>/dev/null || echo "0") + HIGH_COUNT=$(jq -s '[.[] | .matches[]? | select(.vulnerability.severity == "High")] | length' "$JSON_FILE" 2>/dev/null || echo "0") + + echo "" + echo "============================================" + echo "Grype Security Scan Summary" + echo "============================================" + echo "CRITICAL vulnerabilities: $CRITICAL_COUNT" + echo "HIGH vulnerabilities: $HIGH_COUNT" + echo "============================================" + + VIOLATIONS="" + [ "${{ inputs.fail-grype-on-critical }}" == "true" ] && [ "$CRITICAL_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$CRITICAL_COUNT CRITICAL, " + [ "${{ inputs.fail-grype-on-high }}" == "true" ] && [ "$HIGH_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$HIGH_COUNT HIGH, " + + if [ -n "$VIOLATIONS" ]; then + echo "" + echo "❌ BUILD FAILED: Found ${VIOLATIONS%, }" + exit 1 + else + echo "" + echo "✅ No policy-violating vulnerabilities found" + fi + + - name: Upload Grype scan results + if: always() + uses: actions/upload-artifact@v4 + with: + name: grype-scan-results + path: | + grype-scan.json + grype-scan.log diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index c5cab7f..432be9f 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -77,6 +77,26 @@ on: required: false type: string default: 'ruby' + ruby-app-directory: + description: 'Subdirectory containing Ruby Gemfile (e.g., "src/supermarket" for repos with non-root Gemfile location). Leave empty if Gemfile is in root.' + required: false + type: string + default: '' + blackduck-fail-on-blocker: + description: 'Fail the pipeline if BlackDuck SCA scan finds BLOCKER vulnerabilities' + required: false + type: boolean + default: false + blackduck-fail-on-critical: + description: 'Fail the pipeline if BlackDuck SCA scan finds CRITICAL vulnerabilities' + required: false + type: boolean + default: false + blackduck-fail-on-major: + description: 'Fail the pipeline if BlackDuck SCA scan finds MAJOR vulnerabilities' + required: false + type: boolean + default: false env: # Set the default SBOM filename prefix @@ -203,11 +223,19 @@ jobs: uses: actions/checkout@v6 - name: Set up Ruby and run bundle install - if: ${{ inputs.language == 'ruby' }} # only run for Ruby projects where we need to generate Gemfile.lock at runtime, inputs.run-bundle-install == true + if: inputs.language == 'ruby' uses: ruby/setup-ruby@v1 with: - ruby-version: '3.4' - bundler-cache: true + ruby-version: '3.4.2' + bundler-cache: false + working-directory: ${{ inputs.ruby-app-directory != '' && inputs.ruby-app-directory || '.' }} + + - name: Set up Erlang/OTP and rebar3 + if: inputs.language == 'erlang' + uses: erlef/setup-beam@v1 + with: + otp-version: '25.3.2.16' + rebar3-version: '3.22.0' - name: Configure git for private Go modules if : ${{ inputs.go-private-modules != '' }} @@ -218,6 +246,7 @@ jobs: - name: generate Gemfile.lock if needed for Ruby projects if: ${{ inputs.run-bundle-install == true && inputs.language == 'ruby' }} continue-on-error: true + working-directory: ${{ inputs.ruby-app-directory != '' && inputs.ruby-app-directory || '.' }} run: | if [ ! -f Gemfile.lock ]; then bundle install @@ -228,12 +257,60 @@ jobs: uses: actions/upload-artifact@v4 continue-on-error: true with: - path: Gemfile.lock + path: ${{ inputs.ruby-app-directory != '' && format('{0}/Gemfile.lock', inputs.ruby-app-directory) || 'Gemfile.lock' }} name: ${{ github.event.repository.name }}-Gemfile-lock.txt + - name: Construct BlackDuck detect arguments + id: detect-args + run: | + # Start with base arguments (always exclude PIP detector) + DETECT_ARGS="--detect.excluded.detector.types=PIP" + + # Add low accuracy mode if requested + if [[ "${{ inputs.blackduck-force-low-accuracy-mode }}" == "true" ]]; then + DETECT_ARGS="${DETECT_ARGS} --detect.accuracy.required=NONE" + fi + + # Add source path if ruby-app-directory is specified + if [[ -n "${{ inputs.ruby-app-directory }}" ]]; then + DETECT_ARGS="${DETECT_ARGS} --detect.source.path=${{ inputs.ruby-app-directory }}" + fi + + echo "DETECT_ARGS=${DETECT_ARGS}" >> $GITHUB_ENV + echo "Constructed detect_args: ${DETECT_ARGS}" + + - name: Construct BlackDuck failure severities + id: failure-severities + run: | + SEVERITIES="" + + if [[ "${{ inputs.blackduck-fail-on-blocker }}" == "true" ]]; then + SEVERITIES="BLOCKER" + fi + + if [[ "${{ inputs.blackduck-fail-on-critical }}" == "true" ]]; then + if [[ -n "$SEVERITIES" ]]; then + SEVERITIES="${SEVERITIES},CRITICAL" + else + SEVERITIES="CRITICAL" + fi + fi + + if [[ "${{ inputs.blackduck-fail-on-major }}" == "true" ]]; then + if [[ -n "$SEVERITIES" ]]; then + SEVERITIES="${SEVERITIES},MAJOR" + else + SEVERITIES="MAJOR" + fi + fi + + echo "FAILURE_SEVERITIES=${SEVERITIES}" >> $GITHUB_ENV + echo "Enforcement policy: ${SEVERITIES}" + - name: BlackDuck SCA scan + id: blackduck-scan uses: blackduck-inc/black-duck-security-scan@v2.1.1 - continue-on-error: true # Allow pipeline to continue even with policy violations + continue-on-error: false # Allow pipeline to continue even with policy violations env: GOPRIVATE: ${{ inputs.go-private-modules }} DETECT_PROJECT_GROUP_NAME: ${{ inputs.blackduck-project-group-name}} #'Chef-Agents' # , Chef, Chef-Agents, Chef-Automate, Chef-Chef360, Chef-Habitat, Chef-Infrastructure-Server, Chef-Shared-Services @@ -242,10 +319,55 @@ jobs: with: blackducksca_url: ${{ secrets.BLACKDUCK_SBOM_URL }} # BLACKDUCK_URL, should be https://progresssoftware.app.blackduck.com/ blackducksca_token: ${{ secrets.BLACKDUCK_SCA_TOKEN }} # was BLACKDUCK_API_KEY - detect_args: ${{ inputs.blackduck-force-low-accuracy-mode == true && '--detect.excluded.detector.types=PIP --detect.accuracy.required=NONE' || '--detect.excluded.detector.types=PIP' }} - # blackducksca_scan_failure_severities: 'BLOCKER,CRITICAL' + blackducksca_scan_failure_severities: ${{ env.FAILURE_SEVERITIES }} + blackducksca_scan_full: true # Force INTELLIGENT scan mode for all branches (uploads results to server) + detect_args: ${{ env.DETECT_ARGS }} # ignore python per https://documentation.blackduck.com/bundle/detect/page/packagemgrs/python.html + - name: Check BlackDuck SCA results and report violations + if: ${{ always() && (inputs.blackduck-fail-on-blocker == true || inputs.blackduck-fail-on-critical == true || inputs.blackduck-fail-on-major == true) }} + run: | + BRIDGE_LOG=".bridge/bridge.log" + + if [ ! -f "$BRIDGE_LOG" ]; then + echo "⚠️ Bridge log not found" + exit 0 + fi + + SEVERITY_LINE=$(grep "Policy Severity counts:" "$BRIDGE_LOG" || true) + + if [ -z "$SEVERITY_LINE" ]; then + echo "⚠️ Policy Severity counts line not found in bridge.log" + exit 0 + fi + + BLOCKER_COUNT=$(echo "$SEVERITY_LINE" | grep -oE '[0-9]+ match(es)? ha(s|ve) a severity level of BLOCKER' | grep -oE '^[0-9]+' || echo "0") + CRITICAL_COUNT=$(echo "$SEVERITY_LINE" | grep -oE '[0-9]+ match(es)? ha(s|ve) a severity level of CRITICAL' | grep -oE '^[0-9]+' || echo "0") + MAJOR_COUNT=$(echo "$SEVERITY_LINE" | grep -oE '[0-9]+ match(es)? ha(s|ve) a severity level of MAJOR' | grep -oE '^[0-9]+' || echo "0") + + echo "" + echo "============================================" + echo "BlackDuck SCA Policy Violation Summary" + echo "============================================" + echo "BLOCKER violations: $BLOCKER_COUNT" + echo "CRITICAL violations: $CRITICAL_COUNT" + echo "MAJOR violations: $MAJOR_COUNT" + echo "============================================" + + VIOLATIONS="" + [ "${{ inputs.blackduck-fail-on-blocker }}" == "true" ] && [ "$BLOCKER_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$BLOCKER_COUNT BLOCKER, " + [ "${{ inputs.blackduck-fail-on-critical }}" == "true" ] && [ "$CRITICAL_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$CRITICAL_COUNT CRITICAL, " + [ "${{ inputs.blackduck-fail-on-major }}" == "true" ] && [ "$MAJOR_COUNT" -gt 0 ] && VIOLATIONS="${VIOLATIONS}$MAJOR_COUNT MAJOR, " + + if [ -n "$VIOLATIONS" ]; then + echo "" + echo "❌ Vulnerabilities Found: ${VIOLATIONS%, }" + exit 1 + else + echo "" + echo "✅ No policy-violating vulnerabilities found" + fi + # original from https://github.com/progress-platform-services/common-github-actions/blob/main/.github/workflows/examples/ci-all-sbom-main.yml generate-msft-sbom: name: Generate MSFT SBOM diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 8749e2f..ef96ade 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -15,6 +15,16 @@ on: required: false type: string default: '1.0.0' + trivy-fail-on-high: + description: 'Fail the build if HIGH vulnerabilities are found' + required: false + type: boolean + default: false + trivy-fail-on-critical: + description: 'Fail the build if CRITICAL vulnerabilities are found' + required: false + type: boolean + default: false jobs: trivy: @@ -67,14 +77,32 @@ jobs: # name: trivy-report-${{ github.event.repository.name }}-${{ github.ref_name }}-${{ inputs.version }}-$(date +'%Y%m%d')-text path: trivy-report.txt retention-days: 30 - # - name: Fail build on High/Criticial Vulnerabilities - # uses: aquasecurity/trivy-action@master - # with: - # scan-type: "fs" - # format: table - # scan-ref: . - # severity: HIGH,CRITICAL - # ignore-unfixed: true - # exit-code: 1 - # # On a subsequent call to the action we know trivy is already installed so can skip this - # skip-setup-trivy: true \ No newline at end of file + + - name: Construct Trivy failure severities + id: failure-severities + run: | + SEVERITIES="" + + if [[ "${{ inputs.trivy-fail-on-high }}" == "true" ]]; then + SEVERITIES="HIGH" + fi + + if [[ "${{ inputs.trivy-fail-on-critical }}" == "true" ]]; then + SEVERITIES="${SEVERITIES},CRITICAL" + fi + + echo "FAILURE_SEVERITIES=${SEVERITIES}" >> $GITHUB_ENV + echo "Enforcement policy: ${SEVERITIES}" + + - name: Fail build on High/Critical Vulnerabilities + if: ${{ inputs.trivy-fail-on-high || inputs.trivy-fail-on-critical }} + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + format: table + scan-ref: . + severity: ${{ env.FAILURE_SEVERITIES }} + ignore-unfixed: true + exit-code: 1 + # On a subsequent call to the action we know trivy is already installed so can skip this + skip-setup-trivy: true \ No newline at end of file diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index c816344..520d3c1 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -4,6 +4,12 @@ name: Trufflehog secret scan on: workflow_call: + inputs: + fail-trufflehog-on-secrets-found: + description: 'Fail the pipeline if Trufflehog finds verified secrets' + required: false + type: boolean + default: true jobs: Trufflehog: @@ -15,11 +21,33 @@ jobs: fetch-depth: 0 - name: TruffleHog secret scan + id: trufflehog-scan uses: trufflesecurity/trufflehog@main with: path: ./ extra_args: --only-verified continue-on-error: false + + - name: Check results and fail if secrets found + if: ${{ always() && inputs.fail-trufflehog-on-secrets-found == true }} + run: | + # Parse the log output from the trufflehog action + LOG_OUTPUT="${{ steps.trufflehog-scan.outputs.stdout }}" + + # Extract verified_secrets count from JSON output + VERIFIED_COUNT=$(echo "$LOG_OUTPUT" | grep -oE '"verified_secrets":\s*[0-9]+' | grep -oE '[0-9]+' | tail -1 || echo "0") + + if [ "$VERIFIED_COUNT" -gt 0 ]; then + echo "" + echo "============================================" + echo "❌ Trufflehog Secret Scan Failed" + echo "============================================" + echo "Found $VERIFIED_COUNT verified secret(s)" + echo "============================================" + exit 1 + else + echo "✅ No verified secrets found" + fi # --only-verified --fail --github-actions --results=verified,unknown --branch dev # TODO: use the GH_TOKEN --org=progress --token=ghp_xxxxx