From 398a4249b8615ea3ee026ed63c9e836be4375d25 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 14:51:50 -0700 Subject: [PATCH 1/4] Limit Coverity captures to wolfTrust sources --- .github/workflows/README.md | 19 +++++++++++--- .github/workflows/coverity.yml | 45 +++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 11f0029f..c717c5e9 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -5,10 +5,21 @@ 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`. +release branches, and weekly. `coverity.yml` submits builds weekly on Mondays +at 00:00 UTC, 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`. + +Before upload, Coverity removes compilation units whose primary source is +under `lib/`, then recreates the archive. The scan covers wolfTrust runtime, +ports, tests, and generated policy code. Dependency headers remain available +to parse those sources. The job lists the retained units and refuses to upload +an empty capture or one that still contains dependency source units. + +Coverity uploads must return HTTP 201 to pass. A refusal because an earlier +build is still queued fails the submission step and shows the server response. +An accepted upload is queued for server-side analysis; results appear on +Coverity Scan after processing, with notifications sent to `COVERITY_SCAN_EMAIL`. ## At a glance diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index a3a20768..e5188c6b 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 * * 1' # Mondays at 00:00 UTC workflow_dispatch: concurrency: @@ -43,10 +41,49 @@ 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" + cov-manage-emit --dir cov-int --tu-pattern "file('/lib/')" delete + units_file="$RUNNER_TEMP/coverity-units.txt" + cov-manage-emit --dir cov-int list > "$units_file" + cat "$units_file" + if grep -Eq '^[[:space:]]*[0-9]+[[:space:]]+->[[:space:]]+.*/lib/' "$units_file" || + ! grep -Eq '^[[:space:]]*[0-9]+[[:space:]]+->[[:space:]]+/' "$units_file"; then + echo '::error::Coverity capture is empty or still contains dependency sources.' + exit 1 + fi + 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" From ef59334d9a62d0851852cd1d904a3927d6059566 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 15:08:37 -0700 Subject: [PATCH 2/4] Exclude every submodule from Coverity captures --- .github/workflows/README.md | 17 +++++++--- .github/workflows/coverity.yml | 10 +----- tools/ci-coverity-filter.py | 58 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 tools/ci-coverity-filter.py diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c717c5e9..e4d08c11 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -10,11 +10,18 @@ at 00:00 UTC, 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`. -Before upload, Coverity removes compilation units whose primary source is -under `lib/`, then recreates the archive. The scan covers wolfTrust runtime, -ports, tests, and generated policy code. Dependency headers remain available -to parse those sources. The job lists the retained units and refuses to upload -an empty capture or one that still contains dependency source units. +Before upload, `tools/ci-coverity-filter.py` removes compilation units whose +primary source is under `lib/`, in any Git submodule, or outside the checkout, +then the workflow recreates the archive. Submodule paths come from Git's index, +so a new submodule outside `lib/` is excluded automatically. The scan covers +wolfTrust runtime, ports, tests, and generated policy code. The job lists the +retained units and refuses to upload an empty capture, an unrecognized listing, +or one that still contains dependency source units. + +Dependency headers remain available to parse wolfTrust sources. Source-unit +filtering does not suppress findings in included dependency headers. A project +component exclusion in Coverity Scan is needed to ignore those header findings; +keep that exclusion aligned with `lib/` and any other submodule paths. Coverity uploads must return HTTP 201 to pass. A refusal because an earlier build is still queued fails the submission step and shows the server response. diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index e5188c6b..41c18113 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -53,15 +53,7 @@ jobs: shell: bash run: | export PATH="$PWD/cov-analysis/bin:$PATH" - cov-manage-emit --dir cov-int --tu-pattern "file('/lib/')" delete - units_file="$RUNNER_TEMP/coverity-units.txt" - cov-manage-emit --dir cov-int list > "$units_file" - cat "$units_file" - if grep -Eq '^[[:space:]]*[0-9]+[[:space:]]+->[[:space:]]+.*/lib/' "$units_file" || - ! grep -Eq '^[[:space:]]*[0-9]+[[:space:]]+->[[:space:]]+/' "$units_file"; then - echo '::error::Coverity capture is empty or still contains dependency sources.' - exit 1 - fi + python3 tools/ci-coverity-filter.py tar -czf cov-int.tgz cov-int - name: Submit build to Coverity Scan shell: bash diff --git a/tools/ci-coverity-filter.py b/tools/ci-coverity-filter.py new file mode 100644 index 00000000..3bc6c20b --- /dev/null +++ b/tools/ci-coverity-filter.py @@ -0,0 +1,58 @@ +#!/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()) + + 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 From 37ee813d5d946a0f8f49c7c95f906bbd4ec4db20 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 15:54:12 -0700 Subject: [PATCH 3/4] Keep dependency headers inside Coverity exclusion scope --- .github/workflows/README.md | 13 ++++++++----- tools/ci-coverity-filter.py | 6 ++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e4d08c11..e61f6ae0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -12,16 +12,19 @@ both crypto engines via `tools/ci-static-analysis-build.sh`. Before upload, `tools/ci-coverity-filter.py` removes compilation units whose primary source is under `lib/`, in any Git submodule, or outside the checkout, -then the workflow recreates the archive. Submodule paths come from Git's index, -so a new submodule outside `lib/` is excluded automatically. The scan covers +then the workflow recreates the archive. Submodule paths come from Git's index. +Dependencies must reside under `lib/`; a new submodule elsewhere fails the job +before upload so its headers cannot bypass the project exclusion. The scan covers wolfTrust runtime, ports, tests, and generated policy code. The job lists the retained units and refuses to upload an empty capture, an unrecognized listing, or one that still contains dependency source units. Dependency headers remain available to parse wolfTrust sources. Source-unit -filtering does not suppress findings in included dependency headers. A project -component exclusion in Coverity Scan is needed to ignore those header findings; -keep that exclusion aligned with `lib/` and any other submodule paths. +filtering does not suppress findings in included dependency headers. In wolfTrust's +Coverity Scan Analysis Settings, keep the `Third party dependencies` component +with pattern `.*/lib/.*` and **Ignore component in analysis** enabled. This excludes +dependency headers as well as sources; the CI guard keeps new submodules inside +that scope. Component changes can take up to 15 minutes to appear in Scan. Coverity uploads must return HTTP 201 to pass. A refusal because an earlier build is still queued fails the submission step and shows the server response. diff --git a/tools/ci-coverity-filter.py b/tools/ci-coverity-filter.py index 3bc6c20b..14006962 100644 --- a/tools/ci-coverity-filter.py +++ b/tools/ci-coverity-filter.py @@ -13,6 +13,12 @@ def filter_capture(): 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"] From 943f62b5be746320719a837ea596935005676810 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 16:57:17 -0700 Subject: [PATCH 4/4] Run Coverity on Sundays and simplify CI documentation --- .github/workflows/README.md | 37 ++++++++++------------------------ .github/workflows/coverity.yml | 2 +- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e61f6ae0..368cdf15 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -4,32 +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` submits builds weekly on Mondays -at 00:00 UTC, 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`. - -Before upload, `tools/ci-coverity-filter.py` removes compilation units whose -primary source is under `lib/`, in any Git submodule, or outside the checkout, -then the workflow recreates the archive. Submodule paths come from Git's index. -Dependencies must reside under `lib/`; a new submodule elsewhere fails the job -before upload so its headers cannot bypass the project exclusion. The scan covers -wolfTrust runtime, ports, tests, and generated policy code. The job lists the -retained units and refuses to upload an empty capture, an unrecognized listing, -or one that still contains dependency source units. - -Dependency headers remain available to parse wolfTrust sources. Source-unit -filtering does not suppress findings in included dependency headers. In wolfTrust's -Coverity Scan Analysis Settings, keep the `Third party dependencies` component -with pattern `.*/lib/.*` and **Ignore component in analysis** enabled. This excludes -dependency headers as well as sources; the CI guard keeps new submodules inside -that scope. Component changes can take up to 15 minutes to appear in Scan. - -Coverity uploads must return HTTP 201 to pass. A refusal because an earlier -build is still queued fails the submission step and shows the server response. -An accepted upload is queued for server-side analysis; results appear on -Coverity Scan after processing, with notifications sent to `COVERITY_SCAN_EMAIL`. +`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 41c18113..a7a5fa14 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -2,7 +2,7 @@ name: Coverity Scan on: schedule: - - cron: '0 0 * * 1' # Mondays at 00:00 UTC + - cron: '0 0 * * 0' # Sundays at 00:00 UTC workflow_dispatch: concurrency: