diff --git a/.github/workflows/action-smoke-test.yml b/.github/workflows/action-smoke-test.yml new file mode 100644 index 0000000..52b83b0 --- /dev/null +++ b/.github/workflows/action-smoke-test.yml @@ -0,0 +1,46 @@ +name: Action Smoke Test + +# issue #213: action.yml has no C code for ctest to exercise, so this is +# its regression test -- runs the composite action, at the exact commit +# under test (`uses: ./`), against this repo itself, and checks it +# actually produced a SARIF file. Separate from the "SARIF Upload" job in +# ci.yml, which builds cfusa from source; this job installs the latest +# *released* binary, since that's what a real adopting project's workflow +# does via `uses: SoundMatt/c-FuSa@`. +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + security-events: write + +jobs: + smoke-test: + name: Action Smoke Test + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Run c-FuSa via this action + id: cfusa + uses: ./ + with: + dir: '.' + sarif-file: 'action-smoke-results.sarif' + # This repo's own findings are all WARNING/INFO (see the + # "Self-qualification"/"SARIF Upload" jobs in ci.yml) -- don't + # gate this smoke test on that pre-existing backlog, only on + # the action actually running end-to-end. + fail-on-error: 'false' + + - name: Verify SARIF was produced + run: | + set -euo pipefail + f="${{ steps.cfusa.outputs.sarif-file }}" + test -s "$f" || { echo "::error::expected a non-empty SARIF file at $f"; exit 1; } + grep -q '"version"' "$f" || { echo "::error::$f doesn't look like SARIF"; exit 1; } + echo "exit-code output: ${{ steps.cfusa.outputs.exit-code }}" + echo "OK: $f ($(wc -c < "$f") bytes)" diff --git a/README.md b/README.md index efed774..2a941f7 100644 --- a/README.md +++ b/README.md @@ -415,12 +415,38 @@ Copy `.cfusa.json.template` and `.cfusa-reqs.json.template` to start a new proje ## GitHub Actions Integration +A first-party composite action (`action.yml`, this repo) installs a pinned +`cfusa` release binary, runs `cfusa check`, and uploads results to the +GitHub Security tab as SARIF — no manual build step needed in the calling +repo: + +```yaml +- uses: actions/checkout@v4 + +- name: c-FuSa safety & security check + uses: SoundMatt/c-FuSa@v0.6.1 # pin a release tag, or use "main" + with: + dir: src/ # default: "." + strict: 'false' # gate on WARNING too, not just ERROR + changed-since: ${{ github.event_name == 'pull_request' && 'origin/main' || '' }} + fail-on-error: 'true' # default: fail the step on gated findings +``` + +SARIF is always uploaded (via `github/codeql-action/upload-sarif`) even +when `fail-on-error: false`, so findings still surface as code-scanning +annotations without blocking the build. See +[`action.yml`](action.yml) for every input/output, and +[`.github/workflows/action-smoke-test.yml`](.github/workflows/action-smoke-test.yml) +for a working end-to-end example (this repo dogfoods its own action). + +Prefer to build from source instead of installing a release binary (e.g. +to check an unreleased fix)? Do it directly: + ```yaml - name: cfusa safety check run: | cmake -B build && cmake --build build ./build/cfusa check --dir src/ --format sarif --output results.sarif - ./build/cfusa trace --req-coverage 80 --sec-tested 70 - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..87039fc --- /dev/null +++ b/action.yml @@ -0,0 +1,146 @@ +name: 'c-FuSa Safety & Security Check' +description: >- + Runs c-FuSa (MISRA-C/CERT-C/CWE static analysis for functional-safety + and cybersecurity C projects), uploads results to the GitHub code + scanning tab as SARIF, and gates the workflow on findings. +author: 'SoundMatt' +branding: + icon: 'shield' + color: 'blue' + +inputs: + dir: + description: 'Directory to analyze (passed to `cfusa check --dir`).' + required: false + default: '.' + version: + description: >- + c-FuSa release tag to install (e.g. "v0.6.1"). "latest" resolves the + most recent GitHub release at run time. + required: false + default: 'latest' + strict: + description: 'Exit non-zero on WARNING findings too, not just ERROR.' + required: false + default: 'false' + changed-since: + description: >- + Only report findings on lines changed since this git ref (`cfusa + check --changed-since`) — e.g. "origin/main" on a pull request, to + gate only on newly-introduced findings rather than a project's full + existing backlog. Leave empty to check the whole tree. + required: false + default: '' + sarif-file: + description: 'Path to write the SARIF report to.' + required: false + default: 'cfusa-results.sarif' + fail-on-error: + description: >- + Fail this workflow step when `cfusa check`'s exit-code gate trips + (any ERROR finding, or any WARNING finding too under `strict`). + SARIF is uploaded either way, so code-scanning annotations still + appear even when this is "false". + required: false + default: 'true' + +outputs: + sarif-file: + description: 'Path to the generated SARIF report.' + value: ${{ steps.run-cfusa.outputs.sarif-file }} + exit-code: + description: "cfusa check's raw exit code (0 = clean, 1 = gated findings)." + value: ${{ steps.run-cfusa.outputs.exit-code }} + +runs: + using: 'composite' + steps: + - name: Install cfusa + id: install + shell: bash + run: | + set -euo pipefail + version="${{ inputs.version }}" + if [ "$version" = "latest" ]; then + # Capture the full response into a variable before parsing it, + # rather than piping curl straight into `grep -m1`: grep -m1 + # exits (closing its stdin pipe) right after its first match, + # and if curl is still writing later bytes of a large response + # body when that happens it can get SIGPIPE'd -- observed in CI + # as an intermittent "curl: (23) Failure writing output to + # destination" that, under `set -eo pipefail`, aborted this + # entire step before the actual binary download ever ran. Once + # curl has fully finished into a variable there's no reader to + # race. + resp=$(curl -fsSL https://api.github.com/repos/SoundMatt/c-FuSa/releases/latest) || { + echo "::error::Failed to query the GitHub releases API for the latest c-FuSa release." + exit 1 + } + version=$(printf '%s' "$resp" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/') + if [ -z "$version" ]; then + echo "::error::Could not resolve the latest c-FuSa release tag from the GitHub API." + exit 1 + fi + fi + + case "$(uname -s)-$(uname -m)" in + Linux-x86_64) asset='cfusa-linux-amd64' ;; + Darwin-arm64) asset='cfusa-darwin-arm64' ;; + *) + echo "::error::Unsupported runner platform $(uname -s)-$(uname -m)."\ + " c-FuSa currently ships prebuilt cfusa-linux-amd64 and"\ + " cfusa-darwin-arm64 release binaries only — see issue #211"\ + " for native-Windows status." + exit 1 + ;; + esac + + url="https://github.com/SoundMatt/c-FuSa/releases/download/${version}/${asset}" + echo "Installing cfusa ${version} (${asset})" + curl -fsSL -o cfusa-bin "$url" + chmod +x cfusa-bin + echo "bin=$(pwd)/cfusa-bin" >> "$GITHUB_OUTPUT" + ./cfusa-bin version + + - name: Run cfusa check + id: run-cfusa + shell: bash + run: | + set -uo pipefail + args=(check --dir "${{ inputs.dir }}" --format sarif --output "${{ inputs.sarif-file }}") + if [ "${{ inputs.strict }}" = "true" ]; then + args+=(--strict) + fi + if [ -n "${{ inputs.changed-since }}" ]; then + args+=(--changed-since "${{ inputs.changed-since }}") + fi + + "${{ steps.install.outputs.bin }}" "${args[@]}" + rc=$? + + echo "exit-code=$rc" >> "$GITHUB_OUTPUT" + echo "sarif-file=${{ inputs.sarif-file }}" >> "$GITHUB_OUTPUT" + + # rc 0 (clean) and rc 1 (gated findings, handled by the separate + # "Enforce findings gate" step below so SARIF still gets uploaded + # first) are both normal outcomes here. rc 2/3 (bad --changed-since + # ref, or a failed SARIF write) are tool/usage errors -- always + # fatal, independent of fail-on-error, since there is no SARIF + # file to upload in either case. + if [ "$rc" -gt 1 ]; then + echo "::error::cfusa check exited $rc (usage error, not a findings gate) — see the log above." + exit "$rc" + fi + exit 0 + + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ inputs.sarif-file }} + + - name: Enforce findings gate + if: ${{ inputs.fail-on-error == 'true' && steps.run-cfusa.outputs.exit-code == '1' }} + shell: bash + run: | + echo "::error::cfusa check reported gating findings (ERROR${{ inputs.strict == 'true' && ', or WARNING under strict mode,' || '' }}). See the Security tab (code scanning) for details." + exit 1