name: dependency-vulnerability on: pull_request: push: branches: ["main"] schedule: - cron: "0 6 * * *" workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # The compare API exists only on GitHub.com. Trivy remains the full-snapshot backstop elsewhere. dependency-review: if: github.event_name == 'pull_request' && github.server_url == 'https://github.com' runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - name: Review newly introduced dependencies uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # actions/dependency-review-action@v4.8.0 with: config-file: ./.github/dependency-review-config.yml # The submission API is also GitHub.com-only and is not required for the platform-neutral scan. dependency-submission: if: github.event_name == 'push' && github.server_url == 'https://github.com' runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1 with: distribution: temurin java-version: "21.0.11+10" cache: gradle cache-dependency-path: | src/**/*.gradle src/**/gradle-wrapper.properties src/**/gradle.lockfile - name: Submit the resolved Gradle dependency graph uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4 with: build-root-directory: src trivy-fs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - name: Install pinned Trivy under RUNNER_TEMP env: TRIVY_DOWNLOAD_BASE_URL: ${{ vars.TRIVY_DOWNLOAD_BASE_URL }} run: | set -euo pipefail readonly TRIVY_VERSION='0.71.2' readonly TRIVY_SHA256_AMD64='0510e71e2fd39bf863856d499c8dc19feb4e7336546394c502a8f5cc7ab27460' readonly TRIVY_SHA256_ARM64='fe1c7106e15a5365d485b098a8c338f91e3b7ba71cb0e4963b98a3a098763cfc' readonly DOWNLOAD_BASE_URL="${TRIVY_DOWNLOAD_BASE_URL:-https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}}" case "${RUNNER_ARCH:-X64}" in X64) asset_arch='64bit' expected_sha256="${TRIVY_SHA256_AMD64}" ;; ARM64) asset_arch='ARM64' expected_sha256="${TRIVY_SHA256_ARM64}" ;; *) echo "::error::unsupported runner architecture: ${RUNNER_ARCH:-unknown}" exit 1 ;; esac install_dir="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}" archive="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}.tar.gz" mkdir -p "${install_dir}" curl --fail --show-error --silent --location --retry 3 \ --proto '=https' --tlsv1.2 \ "${DOWNLOAD_BASE_URL}/trivy_${TRIVY_VERSION}_Linux-${asset_arch}.tar.gz" \ --output "${archive}" printf '%s %s\n' "${expected_sha256}" "${archive}" | sha256sum -c - tar -xzf "${archive}" -C "${install_dir}" trivy chmod 0755 "${install_dir}/trivy" printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" - name: Install checksum-pinned jq env: JQ_DOWNLOAD_BASE_URL: ${{ vars.JQ_DOWNLOAD_BASE_URL }} run: bash .github/scripts/install-jq.sh - name: Block High and Critical vulnerabilities run: | trivy fs \ --scanners vuln,license \ --severity CRITICAL,HIGH \ --exit-code 1 \ --ignorefile .trivyignore.yaml \ . - name: Report Medium and Low vulnerabilities run: | trivy fs \ --scanners vuln,license \ --severity MEDIUM,LOW \ --exit-code 0 \ --ignorefile .trivyignore.yaml \ . - name: Produce the governed all-severity KEV input run: | trivy fs \ --scanners vuln \ --severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN \ --exit-code 0 \ --ignorefile .trivyignore.yaml \ --format json \ --output trivy-kev.json \ . - name: Fail closed on any CISA KEV match env: CONFIGURED_KEV_FEED_URL: ${{ vars.KEV_FEED_URL }} run: | set -euo pipefail readonly DEFAULT_KEV_FEED_URL='https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json' readonly KEV_FEED_URL="${CONFIGURED_KEV_FEED_URL:-${DEFAULT_KEV_FEED_URL}}" if ! curl --fail --show-error --silent --location --retry 3 \ --proto '=https' --tlsv1.2 "${KEV_FEED_URL}" --output kev.json; then echo "::error::KEV feed unavailable; configure KEV_FEED_URL to an approved internal mirror" exit 1 fi if ! jq -e ' (.catalogVersion | type == "string" and test("\\S")) and (.dateReleased | type == "string" and test("\\S")) and (.count | type == "number") and (.count > 0) and (.count == (.count | floor)) and (.vulnerabilities | type == "array") and ((.vulnerabilities | length) > 0) and (.count == (.vulnerabilities | length)) and (all( .vulnerabilities[]; (.cveID | type == "string" and test("^CVE-[0-9]{4}-[0-9]{4,}$")) )) and (([.vulnerabilities[].cveID] | unique | length) == .count) ' kev.json >/dev/null; then echo "::error::KEV feed is malformed, empty, count-inconsistent, or contains invalid/duplicate cveID values" exit 1 fi if ! jq -e ' (type == "object") and (.Results | type == "array") and ((.Results | length) > 0) and (all(.Results[]; type == "object")) and (all( .Results[]; (.Vulnerabilities == null) or (.Vulnerabilities | type == "array") )) and (all( .Results[]; all( .Vulnerabilities[]?; (type == "object") and (.VulnerabilityID | type == "string" and test("\\S")) ) )) ' trivy-kev.json >/dev/null; then echo "::error::Trivy KEV input is malformed, empty, or contains an invalid VulnerabilityID" exit 1 fi jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID | select(type == "string")] | unique[]?' \ trivy-kev.json | sort -u > found-cves.txt jq -r '.vulnerabilities[]?.cveID | select(type == "string")' \ kev.json | sort -u > kev-cves.txt hits="$(comm -12 found-cves.txt kev-cves.txt || true)" if [[ -n "${hits}" ]]; then echo "::error::CISA KEV-listed vulnerability found regardless of CVSS:" printf '%s\n' "${hits}" exit 1 fi echo "KEV cross-check: no catalog match."