From cdd89f5df1f0c5d9f5a99675ff579ca89725ffaa Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Wed, 8 Jul 2026 00:25:23 +0200 Subject: [PATCH] Add fossa-scan actions Signed-off-by: Jakub Stejskal --- .../security/fossa-container-scan/action.yml | 114 +++++++++++ .../security/fossa-maven-scan/.fossa.yml | 6 + .../security/fossa-maven-scan/action.yml | 100 ++++++++++ .github/workflows/test-fossa.yml | 181 ++++++++++++++++++ .github/workflows/test-integrations.yml | 3 +- .github/workflows/test-snyk.yml | 2 +- README.md | 11 ++ 7 files changed, 414 insertions(+), 3 deletions(-) create mode 100644 .github/actions/security/fossa-container-scan/action.yml create mode 100644 .github/actions/security/fossa-maven-scan/.fossa.yml create mode 100644 .github/actions/security/fossa-maven-scan/action.yml create mode 100644 .github/workflows/test-fossa.yml diff --git a/.github/actions/security/fossa-container-scan/action.yml b/.github/actions/security/fossa-container-scan/action.yml new file mode 100644 index 0000000..38562df --- /dev/null +++ b/.github/actions/security/fossa-container-scan/action.yml @@ -0,0 +1,114 @@ +name: "FOSSA Container Scan" +description: "Scan a single container image with FOSSA for license compliance and vulnerability analysis" + +inputs: + imageFile: + description: "Path to container image file for docker load" + required: true + image: + description: "Image name for artifact naming and categorization" + required: true + scanName: + description: "Name used as FOSSA project name. Defaults to the image name if not set." + required: false + default: "" + fossaTest: + description: "Whether to also run 'fossa container test' for policy compliance checking" + required: false + default: "false" + fossaTeam: + description: "Team name that is used for uploading results." + required: false + default: "Strimzi" + branch: + description: "Branch name to associate with this scan in FOSSA (e.g., branch name or image tag)" + required: false + default: "" + +runs: + using: "composite" + steps: + - name: Install FOSSA CLI + shell: bash + run: | + curl -H 'Cache-Control: no-cache' \ + https://raw.githubusercontent.com/fossas/fossa-cli/f9cd35ff5d917672d5433c46603de4ed92c53a0d/install-latest.sh | bash # v3.17.12 + + - name: Load container image + shell: bash + env: + IMAGE_FILE: ${{ inputs.imageFile }} + IMAGE_NAME: ${{ inputs.image }} + run: | + if [ ! -f "$IMAGE_FILE" ]; then + echo "::error::Image file not found: $IMAGE_FILE" + exit 1 + fi + + LOAD_OUTPUT=$(docker load < "$IMAGE_FILE") + echo "$LOAD_OUTPUT" + LOADED_IMAGE=$(echo "$LOAD_OUTPUT" | grep "Loaded image" | sed 's/Loaded image: //') + + if [ -z "$LOADED_IMAGE" ]; then + echo "::error::Could not determine loaded image tag for $IMAGE_NAME" + exit 1 + fi + + echo "LOADED_IMAGE=$LOADED_IMAGE" >> "$GITHUB_ENV" + + - name: Run FOSSA Container Analyze + shell: bash + continue-on-error: true + run: | + ANALYZE_LOG=$(mktemp) + set +e + SCAN_NAME="${{ inputs.scanName }}" + if [ -z "$SCAN_NAME" ]; then + SCAN_NAME="${{ inputs.image }}" + fi + fossa container analyze \ + --project "$SCAN_NAME" \ + --team "${{ inputs.fossaTeam }}" \ + --branch "${{ inputs.branch }}" \ + "$LOADED_IMAGE" > "$ANALYZE_LOG" 2>&1 + set -e + cat "$ANALYZE_LOG" + FOSSA_URL=$(grep -o 'https://app.fossa.com[^ ]*' "$ANALYZE_LOG" | head -1 || true) + echo "FOSSA_URL=$FOSSA_URL" >> "$GITHUB_ENV" + rm -f "$ANALYZE_LOG" + + - name: Run FOSSA Test and Write Summary + if: always() + shell: bash + run: | + set +e + SCAN_NAME="${{ inputs.scanName }}" + if [ -z "$SCAN_NAME" ]; then + SCAN_NAME="${{ inputs.image }}" + fi + TEST_OUTPUT=$(fossa container test --project "$SCAN_NAME" "$LOADED_IMAGE" 2>&1) + TEST_EXIT_CODE=$? + set -e + echo "$TEST_OUTPUT" + + if [ "${{ inputs.fossaTest }}" == "true" ]; then + if [ "$TEST_EXIT_CODE" -eq 0 ]; then + TEST_RESULT="✅ passed" + else + TEST_RESULT="❌ failed" + fi + else + TEST_RESULT="⏭️ skipped" + fi + + echo "## FOSSA Scan Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Type | Image | Report | Test |" >> "$GITHUB_STEP_SUMMARY" + echo "|------|-------|--------|------|" >> "$GITHUB_STEP_SUMMARY" + if [ -n "$FOSSA_URL" ]; then + echo "| Container | ${{ inputs.image }} | [View in FOSSA]($FOSSA_URL) | $TEST_RESULT |" >> "$GITHUB_STEP_SUMMARY" + else + echo "| Container | ${{ inputs.image }} | ❌ upload failed | $TEST_RESULT |" >> "$GITHUB_STEP_SUMMARY" + echo "::error::FOSSA container analyze failed — no report URL found in output" + exit 1 + fi diff --git a/.github/actions/security/fossa-maven-scan/.fossa.yml b/.github/actions/security/fossa-maven-scan/.fossa.yml new file mode 100644 index 0000000..8e12f61 --- /dev/null +++ b/.github/actions/security/fossa-maven-scan/.fossa.yml @@ -0,0 +1,6 @@ +version: 3 + +maven: + scope-exclude: + - test + - provided diff --git a/.github/actions/security/fossa-maven-scan/action.yml b/.github/actions/security/fossa-maven-scan/action.yml new file mode 100644 index 0000000..e3c8424 --- /dev/null +++ b/.github/actions/security/fossa-maven-scan/action.yml @@ -0,0 +1,100 @@ +name: "FOSSA Maven Scan" +description: "Run FOSSA scan on Maven dependencies with license compliance and vulnerability analysis" + +inputs: + fossaTest: + description: "Whether to also run 'fossa test' for policy compliance checking" + required: false + default: "false" + scanName: + description: "Name used for artifact naming (e.g., 'strimzi')" + required: true + exclude: + description: "Comma-separated list of directories to exclude from scanning (e.g., 'mockkube,test')" + required: false + default: "" + fossaTeam: + description: "Team name that is used for uploading results." + required: false + default: "Strimzi" + branch: + description: "Branch name to associate with this scan in FOSSA (e.g., branch name or image tag)" + required: false + default: "" + +runs: + using: "composite" + steps: + - name: Install FOSSA CLI + shell: bash + run: | + curl -H 'Cache-Control: no-cache' \ + https://raw.githubusercontent.com/fossas/fossa-cli/f9cd35ff5d917672d5433c46603de4ed92c53a0d/install-latest.sh | bash # v3.17.12 + + - name: Configure FOSSA + shell: bash + run: | + if [ -f .fossa.yml ]; then + echo "::warning::Existing .fossa.yml found in workspace — using project configuration instead of action defaults" + else + cp "${{ github.action_path }}/.fossa.yml" .fossa.yml + fi + + if [ -n "${{ inputs.exclude }}" ]; then + echo "" >> .fossa.yml + echo "paths:" >> .fossa.yml + echo " exclude:" >> .fossa.yml + IFS=',' read -ra DIRS <<< "${{ inputs.exclude }}" + for dir in "${DIRS[@]}"; do + dir=$(echo "$dir" | xargs) + echo " - $dir/" >> .fossa.yml + done + fi + + - name: Run FOSSA Analyze + shell: bash + continue-on-error: true + run: | + ANALYZE_LOG=$(mktemp) + set +e + fossa analyze \ + --project "${{ inputs.scanName }}" \ + --team "${{ inputs.fossaTeam }}" \ + --branch "${{ inputs.branch }}" > "$ANALYZE_LOG" 2>&1 + set -e + cat "$ANALYZE_LOG" + FOSSA_URL=$(grep -o 'https://app.fossa.com[^ ]*' "$ANALYZE_LOG" | head -1 || true) + echo "FOSSA_URL=$FOSSA_URL" >> "$GITHUB_ENV" + rm -f "$ANALYZE_LOG" + + - name: Run FOSSA Test and Write Summary + if: always() + shell: bash + run: | + set +e + TEST_OUTPUT=$(fossa test --project "${{ inputs.scanName }}" 2>&1) + TEST_EXIT_CODE=$? + set -e + echo "$TEST_OUTPUT" + + if [ "${{ inputs.fossaTest }}" == "true" ]; then + if [ "$TEST_EXIT_CODE" -eq 0 ]; then + TEST_RESULT="✅ passed" + else + TEST_RESULT="❌ failed" + fi + else + TEST_RESULT="⏭️ skipped" + fi + + echo "## FOSSA Scan Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Type | Project | Report | Test |" >> "$GITHUB_STEP_SUMMARY" + echo "|------|---------|--------|------|" >> "$GITHUB_STEP_SUMMARY" + if [ -n "$FOSSA_URL" ]; then + echo "| Maven | ${{ inputs.scanName }} | [View in FOSSA]($FOSSA_URL) | $TEST_RESULT |" >> "$GITHUB_STEP_SUMMARY" + else + echo "| Maven | ${{ inputs.scanName }} | ❌ upload failed | $TEST_RESULT |" >> "$GITHUB_STEP_SUMMARY" + echo "::error::FOSSA analyze failed — no report URL found in output" + exit 1 + fi diff --git a/.github/workflows/test-fossa.yml b/.github/workflows/test-fossa.yml new file mode 100644 index 0000000..3f66fda --- /dev/null +++ b/.github/workflows/test-fossa.yml @@ -0,0 +1,181 @@ +name: FOSSA Scan Tests + +on: + # Due to security constraints we cannot run the workflow on PRs due to missing secrets on PRs from forks + push: + branches: + - "main" + - "release-*" + +permissions: + contents: read + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-fossa-maven-scan: + name: Test FOSSA Maven Scan + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout github-actions + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + + - name: Checkout strimzi/drain-cleaner + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: strimzi/drain-cleaner + ref: 1.6.1 + path: drain-cleaner + + - name: Copy drain-cleaner project to workspace root + run: rsync -a --exclude='.git' --exclude='.github' drain-cleaner/ ./ + + - name: Setup Java and Maven + uses: ./.github/actions/dependencies/setup-java + + - name: Install yq + uses: ./.github/actions/dependencies/install-yq + + - name: Restore Maven cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.m2/repository + key: maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + maven- + + - name: Build Maven project + shell: bash + run: mvn -B -DskipTests -Dmaven.javadoc.skip=true clean install + + - name: Run FOSSA Maven scan + uses: ./.github/actions/security/fossa-maven-scan + with: + # Keep false to avoid running policy tests during testing + fossaTest: "true" + scanName: test-maven-drain-cleaner + branch: "1.6.1" + env: + FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} + + # Test workflow loads image artifacts from test-integrations workflow. + # To avoid additional image build, the scan check will wait until integration workflow store the artifacts + wait-for-container-artifact: + name: Wait for Container Artifact + runs-on: ubuntu-latest + timeout-minutes: 90 + outputs: + run-id: ${{ steps.find-build.outputs.run_id }} + steps: + - name: Wait for container artifact + id: find-build + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_SHA: ${{ github.sha }} + ARTIFACT_NAME: "containers-drain-cleaner-amd64.tar" + MAX_WAIT_MINUTES: "20" + with: + script: | + const {owner, repo} = context.repo; + const workflowName = 'test-integrations.yml'; + const sha = process.env.INPUT_SHA; + const artifactName = process.env.ARTIFACT_NAME; + const maxWaitMinutes = parseInt(process.env.MAX_WAIT_MINUTES); + const maxWaitSeconds = maxWaitMinutes * 60; + const startTime = Date.now(); + + core.info(`Waiting for artifact '${artifactName}' from commit ${sha}`); + + async function findArtifact() { + const runs = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: workflowName, + head_sha: sha, + per_page: 1 + }); + + const run = runs.data.workflow_runs[0]; + if (!run) return null; + + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: run.id + }); + + const artifact = artifacts.data.artifacts.find(a => a.name === artifactName); + if (artifact) { + return { runId: run.id, artifactId: artifact.id }; + } + + if (run.status === 'completed') { + core.setFailed(`Integration tests completed (${run.conclusion}) but artifact '${artifactName}' not found`); + core.setFailed(`Run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${run.id}`); + return 'failed'; + } + + return null; + } + + while (true) { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + + if (elapsed >= maxWaitSeconds) { + core.setFailed(`Timeout: Artifact '${artifactName}' not found after ${maxWaitMinutes} minutes`); + return; + } + + const result = await findArtifact(); + + if (result === 'failed') return; + + if (result) { + core.setOutput('run_id', result.runId.toString()); + core.info(`Artifact '${artifactName}' found in run #${result.runId}`); + return; + } + + core.info(`Artifact not available yet... (${elapsed}s elapsed, max: ${maxWaitSeconds}s)`); + await new Promise(resolve => setTimeout(resolve, 30000)); + } + + test-fossa-container-scan-drain-cleaner: + name: Test FOSSA Container Scan (drain-cleaner) + needs: wait-for-container-artifact + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout github-actions + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install Docker + uses: ./.github/actions/dependencies/install-docker + + - name: Download container archive + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: containers-drain-cleaner-amd64.tar + run-id: ${{ needs.wait-for-container-artifact.outputs.run-id }} + github-token: ${{ github.token }} + + - name: Untar container archive + run: tar -xvf containers-drain-cleaner-amd64.tar + + - name: Run FOSSA container scan + uses: ./.github/actions/security/fossa-container-scan + with: + imageFile: drain-cleaner-container-amd64.tar.gz + image: drain-cleaner-amd64 + scanName: test-container-drain-cleaner-amd64 + fossaTest: "true" + branch: "1.6.1" + env: + FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} diff --git a/.github/workflows/test-integrations.yml b/.github/workflows/test-integrations.yml index 12293d4..6bc71e1 100644 --- a/.github/workflows/test-integrations.yml +++ b/.github/workflows/test-integrations.yml @@ -89,8 +89,7 @@ jobs: # Drain Cleaner - Full pipeline with containers and Helm - repo: "strimzi/drain-cleaner" - # Use 1.7.x once it will be prepared - ref: "main" + ref: "1.6.1" artifactSuffix: "drain-cleaner" architecture: "amd64" buildContainers: true diff --git a/.github/workflows/test-snyk.yml b/.github/workflows/test-snyk.yml index 865a24c..89e9a8a 100644 --- a/.github/workflows/test-snyk.yml +++ b/.github/workflows/test-snyk.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: strimzi/drain-cleaner - ref: 1.6.0 + ref: 1.6.1 path: drain-cleaner - name: Copy drain-cleaner project to workspace root diff --git a/README.md b/README.md index 0fe0ebe..c325c90 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,17 @@ Actions for building, testing, and releasing Strimzi components. > The `build-binaries` action supports an `clusterOperatorBuild` input (default `false`) that enables Strimzi Kafka Operator specific build steps — Helm chart generation, CRD distribution, dashboard setup, documentation checks, and uncommitted changes verification. > Other repositories should leave this disabled. +### Security Actions + +Actions for security scanning of dependencies and container images. + +| Action | Description | Key Inputs | +|-------------------------------------|----------------------------------------------------------------------|---------------------------------------------------------| +| `security/snyk-maven-scan` | Run Snyk scan on Maven dependencies with SARIF upload | `scanName` (required), `snykMonitor`, `exclude` | +| `security/snyk-container-scan` | Scan a container image with Snyk, upload results to Code Scanning | `imageFile` (required), `image` (required), `snykMonitor` | +| `security/fossa-maven-scan` | Run FOSSA scan on Maven dependencies for license and vulnerability analysis | `scanName` (required), `fossaTest`, `exclude` | +| `security/fossa-container-scan` | Scan a container image with FOSSA for license and vulnerability analysis | `imageFile` (required), `image` (required), `fossaTest` | + ### Utils Actions Actions used as utils mostly in operators repository.