diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 11f0029f..368cdf15 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -4,11 +4,17 @@ CI has a fast per-PR host lane, the tiered M33MU emulator matrix, and static analysis. M33MU runs a per-port smoke set on every pull request and the full matrix on labels, main pushes, and nightly. -`codeql.yml` runs C security queries on pull requests, pushes to `main` and -release branches, and weekly. `coverity.yml` scans on pushes to `main`, daily, -or on manual dispatch from `wolfSSL/wolfTrust` only. Both trace host suite -builds and secure-image builds for STM32H563 and MIMXRT700 with both crypto -engines via `tools/ci-static-analysis-build.sh`. +`codeql.yml` runs C security queries. `coverity.yml` runs on Sundays at +00:00 UTC or by manual dispatch. Both build the host suites and secure images +for STM32H563 and MIMXRT700 with both crypto engines. + +Coverity scans wolfTrust runtime, ports, tests, and generated policy code. +`tools/ci-coverity-filter.py` removes dependency source units before upload +and rejects an invalid capture. Keep submodules under `lib/` and enable +**Ignore component in analysis** for the `Third party dependencies` component +(`.*/lib/.*`) in Coverity Scan to exclude dependency headers too. + +An accepted upload queues analysis; results appear after Coverity processes it. ## At a glance diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index a3a20768..a7a5fa14 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -1,10 +1,8 @@ name: Coverity Scan on: - push: - branches: [main] schedule: - - cron: '0 0 * * *' + - cron: '0 0 * * 0' # Sundays at 00:00 UTC workflow_dispatch: concurrency: @@ -43,10 +41,41 @@ jobs: echo 'Register wolfTrust on Coverity Scan and configure COVERITY_SCAN_TOKEN and COVERITY_SCAN_EMAIL.' >&2 exit 1 fi - - name: Run Coverity Scan + - name: Capture Coverity build uses: vapier/coverity-scan-action@1b6fd4eaba6651aa354c9ea7f48caa1710b4e12e # v1 with: project: wolfTrust token: ${{ secrets.COVERITY_SCAN_TOKEN }} email: ${{ secrets.COVERITY_SCAN_EMAIL }} command: sh tools/ci-static-analysis-build.sh + dry_run: 'true' + - name: Restrict capture to wolfTrust sources + shell: bash + run: | + export PATH="$PWD/cov-analysis/bin:$PATH" + python3 tools/ci-coverity-filter.py + tar -czf cov-int.tgz cov-int + - name: Submit build to Coverity Scan + shell: bash + env: + COVERITY_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} + COVERITY_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }} + run: | + response_file="$RUNNER_TEMP/coverity-upload-response.txt" + http_status=$(curl \ + --silent --show-error \ + --output "$response_file" \ + --write-out '%{http_code}' \ + --form-string "token=$COVERITY_TOKEN" \ + --form-string "email=$COVERITY_EMAIL" \ + --form 'file=@cov-int.tgz' \ + --form-string "version=$GITHUB_SHA" \ + --form-string "description=wolfTrust $GITHUB_REF" \ + 'https://scan.coverity.com/builds?project=wolfTrust') + cat "$response_file" + # Coverity's official upload script requires 201 for a new submission. + if [ "$http_status" != '201' ]; then + echo "::error::Coverity did not accept the build submission (HTTP $http_status)." + exit 1 + fi + printf '%s\n' 'Coverity accepted the build for analysis. Results are published after server-side processing.' >> "$GITHUB_STEP_SUMMARY" diff --git a/tools/ci-coverity-filter.py b/tools/ci-coverity-filter.py new file mode 100644 index 00000000..14006962 --- /dev/null +++ b/tools/ci-coverity-filter.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Remove dependency translation units before a Coverity Scan upload.""" + +import re +import subprocess +from pathlib import Path + + +def filter_capture(): + root = Path.cwd().resolve() + dependencies = {root / "lib"} + index = subprocess.check_output(["git", "ls-files", "--stage", "-z"]) + for entry in index.split(b"\0"): + if entry.startswith(b"160000 "): + dependencies.add(root / entry.split(b"\t", 1)[1].decode()) + # Coverity Scan's ignored component also covers included headers in lib/. + # Refuse a new submodule elsewhere rather than upload unexcluded headers. + if any(path != root / "lib" and root / "lib" not in path.parents + for path in dependencies): + raise ValueError("Coverity dependencies must reside under lib/ to " + "match the project's ignored component") + + command = ["cov-manage-emit", "--dir", "cov-int"] + + def owned(source): + path = Path(source).resolve() + return root in path.parents and not any( + dependency in path.parents for dependency in dependencies + ) + + def list_units(): + output = subprocess.check_output(command + ["list"], text=True) + print(output, end="") + units = [] + for line in output.splitlines(): + if "->" not in line: + continue + match = re.fullmatch( + r"\s*([0-9]+)\s+->\s+(/.+?)(?: \(recoverable errors\))?", line + ) + if match is None: + raise ValueError("Unrecognized Coverity translation unit listing") + units.append((match[1], match[2])) + if not units: + raise ValueError("Coverity capture is empty") + return units + + units = list_units() + excluded = [unit for unit, source in units if not owned(source)] + if excluded: + subprocess.run(command + ["--tu", ",".join(excluded), "delete"], + check=True) + retained = list_units() + if any(not owned(source) for _, source in retained): + raise ValueError("Coverity capture still contains dependency sources") + print(f"Coverity scope: excluded {len(excluded)} dependency units; " + f"retained {len(retained)} wolfTrust units") + + +if __name__ == "__main__": + try: + filter_capture() + except (OSError, ValueError, subprocess.CalledProcessError) as error: + raise SystemExit(f"::error::{error}") from error