diff --git a/.github/workflows/ci-main-pull-request.yml b/.github/workflows/ci-main-pull-request.yml index 2e0f7c5..fa09278 100644 --- a/.github/workflows/ci-main-pull-request.yml +++ b/.github/workflows/ci-main-pull-request.yml @@ -161,6 +161,21 @@ on: required: false type: boolean default: false + perform-wiz-scan: + description: 'Perform Wiz CLI scan on Docker image' + required: false + type: boolean + default: false + wiz-fail-build: + description: 'Fail the build on Wiz policy violations' + required: false + type: boolean + default: true + wiz-image-skip-aws: + description: 'Skip AWS ECR login for Wiz image scan' + required: false + type: boolean + default: false build: description: 'CI Build (language-specific)' required: false @@ -908,6 +923,16 @@ jobs: fail-grype-on-high: ${{ inputs.grype-image-fail-on-high }} fail-grype-on-critical: ${{ inputs.grype-image-fail-on-critical }} grype-image-skip-aws: ${{ inputs.grype-image-skip-aws }} + + run-wiz-image: + name: 'Wiz CLI Docker image scan' + if: ${{ inputs.perform-wiz-scan }} + uses: chef/common-github-actions/.github/workflows/wiz.yml@test-pipeline + needs: checkout + secrets: inherit + with: + fail-build: ${{ inputs.wiz-fail-build }} + wiz-image-skip-aws: ${{ inputs.wiz-image-skip-aws }} # run-srcclr: # if: ${{ inputs.perform-srcclr-scan == true }} @@ -1460,10 +1485,12 @@ 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 + uses: chef/common-github-actions/.github/workflows/sbom.yml@test-pipeline needs: checkout # TODO: fix set-application-version secrets: inherit with: + github-event-name: ${{ inputs.github-event-name }} + github-branch-name: ${{ inputs.github-branch-name }} version: ${{ inputs.version }} export-github-sbom: ${{ inputs.export-github-sbom }} perform-blackduck-sca-scan: ${{ inputs.perform-blackduck-sca-scan }} diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 432be9f..b9aae3a 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -29,6 +29,16 @@ name: Download SBOM from Insights and Convert to CSV on: workflow_call: inputs: + github-event-name: + description: 'GitHub event name (pass github.event_name from calling workflow for PR comment detection)' + required: false + type: string + default: '' + github-branch-name: + description: 'GitHub branch name (pass github.ref_name from calling workflow for branch-specific logic)' + required: false + type: string + default: '' version: description: 'Version of the project' required: true @@ -208,7 +218,7 @@ jobs: generate-blackduck-sbom: name: Blackduck SCA Scan (PURPLE) runs-on: ubuntu-latest - if: ${{ inputs.perform-blackduck-sca-scan == true }} + if: ${{ inputs.perform-blackduck-sca-scan == true }} steps: - name: 'Echo inputs' run: | @@ -218,6 +228,7 @@ jobs: echo "DETECT_PROJECT_GROUP_NAME: ${{ inputs.blackduck-project-group-name }}" echo "DETECT_PROJECT_NAME: ${{ inputs.blackduck-project-name }}" echo "DETECT_PROJECT_VERSION_NAME: ${{ inputs.version }}" + echo "GitHub branch: ${{ inputs.github-branch-name }}" - name: Checkout source uses: actions/checkout@v6 @@ -266,6 +277,9 @@ jobs: # Start with base arguments (always exclude PIP detector) DETECT_ARGS="--detect.excluded.detector.types=PIP" + # Add timeout configurations to prevent FAILURE_TIMEOUT errors + DETECT_ARGS="${DETECT_ARGS} --detect.timeout=1800" + # Add low accuracy mode if requested if [[ "${{ inputs.blackduck-force-low-accuracy-mode }}" == "true" ]]; then DETECT_ARGS="${DETECT_ARGS} --detect.accuracy.required=NONE" @@ -275,6 +289,11 @@ jobs: if [[ -n "${{ inputs.ruby-app-directory }}" ]]; then DETECT_ARGS="${DETECT_ARGS} --detect.source.path=${{ inputs.ruby-app-directory }}" fi + + if [[ "${{ inputs.github-event-name }}" == "pull_request" ]]; then + # RAPID scan for PRs - automatically compares against baseline from target branch + DETECT_ARGS="${DETECT_ARGS} --detect.blackduck.scan.mode=RAPID" + fi echo "DETECT_ARGS=${DETECT_ARGS}" >> $GITHUB_ENV echo "Constructed detect_args: ${DETECT_ARGS}" @@ -307,58 +326,110 @@ jobs: echo "FAILURE_SEVERITIES=${SEVERITIES}" >> $GITHUB_ENV echo "Enforcement policy: ${SEVERITIES}" - - name: BlackDuck SCA scan - id: blackduck-scan + # Full / INTELLIGENT scan — push to main, develop, release branches. + # Persists a baseline to the BlackDuck server so PR scans can diff against it. + - name: BlackDuck SCA Full Scan + id: blackduck-full-scan + if: ${{ inputs.github-event-name != 'pull_request' }} uses: blackduck-inc/black-duck-security-scan@v2.1.1 - continue-on-error: false # Allow pipeline to continue even with policy violations - env: + continue-on-error: false + 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 + DETECT_PROJECT_GROUP_NAME: ${{ inputs.blackduck-project-group-name }} DETECT_PROJECT_NAME: ${{ inputs.blackduck-project-name }} - DETECT_PROJECT_VERSION_NAME: ${{ inputs.version }} # + DETECT_PROJECT_VERSION_NAME: ${{ inputs.version }} with: - blackducksca_url: ${{ secrets.BLACKDUCK_SBOM_URL }} # BLACKDUCK_URL, should be https://progresssoftware.app.blackduck.com/ + blackducksca_url: ${{ secrets.BLACKDUCK_SBOM_URL }} + blackducksca_token: ${{ secrets.BLACKDUCK_SCA_TOKEN }} + blackducksca_scan_failure_severities: ${{ env.FAILURE_SEVERITIES }} + blackducksca_scan_full: true + detect_args: ${{ env.DETECT_ARGS }} + + # RAPID scan — pull request events only. + # Diffs against the persisted baseline so only violations *new to this PR* are reported. + - name: BlackDuck SCA PR Scan + id: blackduck-pr-scan + if: ${{ inputs.github-event-name == 'pull_request' }} + uses: blackduck-inc/black-duck-security-scan@v2.1.1 + continue-on-error: false + env: + GOPRIVATE: ${{ inputs.go-private-modules }} + DETECT_PROJECT_GROUP_NAME: ${{ inputs.blackduck-project-group-name }} + DETECT_PROJECT_NAME: ${{ inputs.blackduck-project-name }} + DETECT_PROJECT_VERSION_NAME: ${{ inputs.version }} + DETECT_BLACKDUCK_RAPID_COMPARE_MODE: BOM_COMPARE + 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 blackducksca_scan_failure_severities: ${{ env.FAILURE_SEVERITIES }} - blackducksca_scan_full: true # Force INTELLIGENT scan mode for all branches (uploads results to server) + blackducksca_scan_full: false detect_args: ${{ env.DETECT_ARGS }} - # ignore python per https://documentation.blackduck.com/bundle/detect/page/packagemgrs/python.html + github_token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} + blackducksca_prComment_enabled: true + + - name: Debug - dump bridge.log + if: always() + run: | + BRIDGE_LOG=".bridge/bridge.log" + if [ -f "$BRIDGE_LOG" ]; then + echo "========== bridge.log START ==========" + cat "$BRIDGE_LOG" + echo "========== bridge.log END ==========" + else + echo "bridge.log not found" + echo "Contents of .bridge/:" + ls -la .bridge/ || true + fi - 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" - + BLOCKER_COUNT=0 + CRITICAL_COUNT=0 + MAJOR_COUNT=0 + if [ ! -f "$BRIDGE_LOG" ]; then - echo "⚠️ Bridge log not found" + echo "⚠️ Bridge log not found - skipping violation count check" 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" + + # Works for both RAPID and Full scans: + # - RAPID log has: "Critical and blocking policy violations" → "* Components: N", "* Security: N", etc. + # - Full log has: "Policy Severity counts:" → "N matches have a severity level of BLOCKER", etc. + extract() { grep "$1" "$BRIDGE_LOG" | awk '{print $NF}' | head -1; } + + if grep -q "Critical and blocking policy violations" "$BRIDGE_LOG"; then + # RAPID scan — map categories to severity levels + BLOCKER_COUNT=$(extract "\* Components:" || echo 0) + CRITICAL_COUNT=$(extract "\* Security:" || echo 0) + MAJOR_COUNT=$(extract "\* License:" || echo 0) + OTHER_COUNT=$(extract "\* Other:" || echo 0) + [ "${OTHER_COUNT:-0}" -gt 0 ] && CRITICAL_COUNT=$((CRITICAL_COUNT + OTHER_COUNT)) + elif grep -q "Policy Severity counts:" "$BRIDGE_LOG"; then + # Full scan + BLOCKER_COUNT=$(grep "severity level of BLOCKER" "$BRIDGE_LOG" | grep -oE '^[0-9]+' || echo 0) + CRITICAL_COUNT=$(grep "severity level of CRITICAL" "$BRIDGE_LOG" | grep -oE '^[0-9]+' || echo 0) + MAJOR_COUNT=$(grep "severity level of MAJOR" "$BRIDGE_LOG" | grep -oE '^[0-9]+' || echo 0) + else + echo "⚠️ No policy violation summary 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 "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, " - + [ "${{ 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%, }" diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index 59547b5..6e9901d 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -38,6 +38,12 @@ jobs: git fetch origin main git branch main origin/main + - name: Setup git refs for PR scan + if: ${{ inputs.github-event-name == 'pull_request' }} + run: | + git fetch origin main + git branch main origin/main + - name: TruffleHog Full secret scan id: trufflehog-full-scan if: ${{ inputs.github-event-name != 'pull_request' }} diff --git a/.github/workflows/wiz.yml b/.github/workflows/wiz.yml new file mode 100644 index 0000000..d3cbd92 --- /dev/null +++ b/.github/workflows/wiz.yml @@ -0,0 +1,116 @@ +# wiz.yml +# Wiz CLI security scan for Docker image vulnerabilities and policy violations +# Uses the prgs-community/githubactions-reusableworkflows/actions/wizcli composite action +# which handles Wiz CLI install, AKeyless auth, scanning, and job summary automatically. +# https://docs.wiz.io/wiz-docs/docs/wiz-cli-overview + +name: Wiz CLI security scan + +on: + workflow_call: + inputs: + fail-build: + description: 'Fail the build on Wiz policy violations' + required: false + type: boolean + default: true + wiz-image-skip-aws: + description: 'Skip AWS ECR login (assumes images are scanned elsewhere)' + required: false + type: boolean + default: false + +jobs: + wiz-scan: + name: Wiz CLI container image 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 repos + run: git config --global url."https://${{ secrets.GH_TOKEN }}@github.com/".insteadOf "https://github.com/" + + # - name: Configure AWS credentials + # uses: aws-actions/configure-aws-credentials@v4 + # if: ${{ !inputs.wiz-image-skip-aws }} + # 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 + # if: ${{ !inputs.wiz-image-skip-aws }} + # uses: aws-actions/amazon-ecr-login@v2 + + - name: Build Docker image + id: build-image + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + REPO_NAME=$(basename $(pwd)) + + if [ ! -f "Dockerfile" ]; then + echo "❌ No Dockerfile found - this workflow requires a Dockerfile to scan Docker image" + exit 1 + fi + + echo "Building Docker image..." + + # 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 + + IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -E "^${REPO_NAME}" | grep -v "^" | head -1) + + if [ -z "$IMAGE" ]; then + echo "⚠️ No image found with prefix ${REPO_NAME} after build-docker.sh" + echo "Checking for any recently built images..." + IMAGE=$(docker images --format "{{.CreatedAt}}\t{{.Repository}}:{{.Tag}}" | sort -r | head -1 | cut -f2) + fi + # Strategy 2: Check for Makefile with compose-build target + 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 image..." + docker compose images + + IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${REPO_NAME}" | grep -v "^" | head -1) + + if [ -z "$IMAGE" ]; then + echo "No image found with prefix ${REPO_NAME}, using most recent image" + IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "^" | head -1) + 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" . + IMAGE="${REPO_NAME}:latest" + fi + + if [ -z "$IMAGE" ]; then + echo "❌ No Docker image found after build" + exit 1 + fi + + echo "Image to scan: $IMAGE" + echo "IMAGE=$IMAGE" >> "$GITHUB_OUTPUT" + + - name: Wiz CLI container image scan + id: wiz-scan + uses: prgs-community/githubactions-reusableworkflows/actions/wizcli@latest + with: + scan-type: 'container-image' + scan-target: ${{ steps.build-image.outputs.IMAGE }} + fail-build: ${{ inputs.fail-build }}