Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,81 @@ jobs:
echo "FAIL: a tree with leaks should exit non-zero under --fail-on-finding"; exit 1
fi
echo "OK: --fail-on-finding surfaced the leaks as a non-zero exit"
- name: SARIF surface is a valid 2.1.0 log (the structure code scanning enforces)
run: |
# The contract GitHub's code-scanning ingest enforces, checked locally so
# the upload (own-check-codescan job) is never the first place a drift is
# found: a single-run 2.1.0 log, the Own.NET driver, and every result
# carrying a catalogue ruleId + a located file. No upload, no permissions.
out="$RUNNER_TEMP/own.sarif"
scripts/own-check.sh --format sarif --severity warning -- frontend/roslyn/samples > "$out"
echo "wrote $(wc -c < "$out") bytes"
jq -e '.version == "2.1.0" and ((.runs | length) == 1)' "$out" >/dev/null \
|| { echo "FAIL: not a single-run SARIF 2.1.0 log"; exit 1; }
jq -e '.runs[0].tool.driver.name == "Own.NET"' "$out" >/dev/null \
|| { echo "FAIL: tool.driver.name is not Own.NET"; exit 1; }
# A dangling ruleId or an unlocated result is the #1 reason GitHub rejects
# a SARIF; startLine is optional (a file-level finding omits it -> // 1).
jq -e '
(.runs[0].tool.driver.rules | map(.id)) as $ids
| .runs[0].results
| (length > 0)
and all(.[];
(.ruleId | type == "string")
and ((([.ruleId] - $ids) | length) == 0)
and (.locations[0].physicalLocation.artifactLocation.uri | type == "string")
and ((.locations[0].physicalLocation.region.startLine // 1) | type == "number"))
' "$out" >/dev/null \
|| { echo "FAIL: a result is unlocated or references an undeclared rule"; exit 1; }
echo "OK: SARIF 2.1.0 — Own.NET driver, every result rule-backed + located"
- name: The composite action runs end-to-end (non-failing)
uses: ./
with:
path: frontend/roslyn/samples
format: github
fail-on-finding: "false"

# Dog-food the code-scanning surface end-to-end (P-013): run the composite action
# with format: sarif over the sample tree, then upload the log to GitHub code
# scanning. The repo is public, so code scanning is free — this is the live proof
# that GitHub *accepts* our SARIF (upload-sarif waits for processing and fails the
# job if the log is rejected), not just that it is schema-valid (the surface job
# above). It also lights up the Security tab + inline PR annotations — the
# consumer-facing payoff the exporter was built for. The samples are intentional
# leak fixtures, so the alerts are real-if-intentional; a dedicated
# `own-net-samples` category keeps them from colliding with anything else.
own-check-codescan:
name: own-check SARIF -> GitHub code scanning (dog-food)
runs-on: ubuntu-latest
# Skip on fork PRs: GitHub downgrades GITHUB_TOKEN to read-only for a
# pull_request from a fork, so security-events:write is never granted and the
# upload would fail — red CI for an external contributor through no fault of
# their own. Same-repo pushes and PRs (where the token keeps write) still run.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
# The one job that writes: scoped to security-events so it can upload to code
# scanning. Every other job stays contents:read (the workflow-level default).
permissions:
contents: read
security-events: write
Comment thread
coderabbitai[bot] marked this conversation as resolved.
steps:
- uses: actions/checkout@v4
- name: Own.NET leak check (SARIF surface)
id: own
uses: ./
with:
path: frontend/roslyn/samples
format: sarif
severity: warning # include the injected-source (warning-tier) leaks
fail-on-finding: "false" # let code scanning be the gate, not the step
- name: The action exposes the SARIF path
run: |
f="${{ steps.own.outputs.sarif-file }}"
test -n "$f" || { echo "FAIL: action did not set the sarif-file output"; exit 1; }
test -s "$f" || { echo "FAIL: sarif-file '$f' is missing or empty"; exit 1; }
echo "OK: action wrote $(wc -c < "$f") bytes to $f"
- name: Upload to GitHub code scanning
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.own.outputs.sarif-file }}
category: own-net-samples

60 changes: 58 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ inputs:
required: false
default: "."
format:
description: "Finding surface: github (PR annotations), msbuild, or human."
description: >-
Finding surface: github (PR annotations), msbuild, human, or sarif — a SARIF
2.1.0 log for GitHub code scanning, written to a file and exposed as the
sarif-file output (feed it to github/codeql-action/upload-sarif).
required: false
default: "github"
severity:
Expand All @@ -33,6 +36,20 @@ inputs:
description: "The .NET SDK version for the Roslyn extractor."
required: false
default: "8.0.x"
sarif-file:
description: >-
Where to write the SARIF log when format: sarif (default:
$RUNNER_TEMP/own-net.sarif). The chosen path is echoed back as the
sarif-file output regardless.
required: false
default: ""

outputs:
sarif-file:
description: >-
Path to the SARIF 2.1.0 log — set only when format: sarif. Hand it to
github/codeql-action/upload-sarif to publish to code scanning.
value: ${{ steps.own.outputs.sarif-file }}

runs:
using: "composite"
Expand All @@ -48,6 +65,7 @@ runs:
dotnet-version: ${{ inputs.dotnet-version }}

- name: Own.NET leak check
id: own
shell: bash
env:
# Pass user-controlled inputs through the environment (data), not by
Expand All @@ -58,9 +76,47 @@ runs:
OWN_FORMAT: ${{ inputs.format }}
OWN_SEVERITY: ${{ inputs.severity }}
OWN_FAIL_ON_FINDING: ${{ inputs.fail-on-finding }}
OWN_SARIF_FILE: ${{ inputs.sarif-file }}
run: |
check="${{ github.action_path }}/scripts/own-check.sh"
if [ "$OWN_FORMAT" = "sarif" ]; then
# The code-scanning surface: write a SARIF 2.1.0 log to a file and expose
# its path as an output, so the caller hands it to
# github/codeql-action/upload-sarif. Code scanning renders inline PR
# annotations *and* the Security tab — it subsumes the `github` format,
# so this stays a single own-check run, not two. --fail-on-finding is
# passed so the true exit code (0 clean / 1 findings / >=2 hard error) is
# captured *after* the file is written; whether a finding fails the step
# is the action's own fail-on-finding (default: let code scanning gate).
sarif="${OWN_SARIF_FILE:-$RUNNER_TEMP/own-net.sarif}"
set +e
"$check" --root "${{ github.action_path }}" --format sarif \
--severity "$OWN_SEVERITY" --fail-on-finding -- "$OWN_PATH" > "$sarif"
rc=$?
set -e
echo "sarif-file=$sarif" >> "$GITHUB_OUTPUT"
echo "Own.NET wrote SARIF to $sarif ($(wc -c < "$sarif" 2>/dev/null || echo 0) bytes; own-check rc=$rc)"
if [ "$rc" -ge 2 ]; then
echo "::error::Own.NET hard error (bad facts / drifted contract)"
cat "$sarif" >&2 || true
exit "$rc"
fi
# Below the hard-error tier the log must exist and be non-empty. A
# missing/empty file with rc<2 means the *write* failed, not that there
# were findings — e.g. a custom sarif-file whose parent dir is absent
# fails the redirection (exit 1, outside own-check's 0/1/>=2 contract).
# Surface it; never let it pass as a gated-off finding with no SARIF.
if [ ! -s "$sarif" ]; then
echo "::error::Own.NET produced no SARIF at $sarif (own-check rc=$rc) — the run or its redirection failed"
exit 1
fi
if [ "$OWN_FAIL_ON_FINDING" = "true" ] && [ "$rc" -eq 1 ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve non-check failures in SARIF mode

When format: sarif is used with the recommended fail-on-finding: false, this wrapper treats every exit status 1 as “findings only” and exits successfully. Failures outside own-check.sh's exit-code contract can also produce 1 here—for example, if the configured sarif-file parent directory does not exist, the shell redirection fails before own-check.sh runs—so consumers can get a successful action with an empty or missing SARIF output, and any later upload either fails elsewhere or is skipped. Check that the SARIF file was actually written, or fail redirection/setup errors separately, before applying the finding gate.

Useful? React with 👍 / 👎.

exit 1
fi
exit 0
fi
args=(--root "${{ github.action_path }}" --format "$OWN_FORMAT" --severity "$OWN_SEVERITY")
if [ "$OWN_FAIL_ON_FINDING" = "true" ]; then
args+=(--fail-on-finding)
fi
"${{ github.action_path }}/scripts/own-check.sh" "${args[@]}" -- "$OWN_PATH"
"$check" "${args[@]}" -- "$OWN_PATH"
50 changes: 46 additions & 4 deletions docs/notes/sarif-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,53 @@ The two internal follow-ups are done (a second slice):
the **live** eval paths, not just in capability. (Filenames kept to keep the CI
edit minimal; the content, not the extension, is what the consumers read.)

## Shipped — code-scanning upload (the consumer surface)

The composite action now has a **fourth surface, `format: sarif`**: it writes a
SARIF 2.1.0 log to a file and exposes the path as the **`sarif-file` output**, so a
consumer hands it straight to code scanning. Code scanning *subsumes* the `github`
PR-annotation format — it renders inline PR annotations **and** the Security tab —
so this stays a single own-check run, not two. A consumer wires it in three steps:

```yaml
permissions:
contents: read
security-events: write # to upload to code scanning
steps:
- uses: actions/checkout@v4
- uses: PhysShell/Own.NET@main # pin to a tag or SHA in production
id: ownnet
with:
path: src
format: sarif
fail-on-finding: "false" # let code scanning be the gate, not the step
- uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.ownnet.outputs.sarif-file }}
category: own-net
```

### Why it is dog-fooded here (the deferral reversed)

The earlier draft deferred this — *"this repo's C# is test fixtures, so uploading
to its own Security tab is low-value."* That weighed the *findings* (intentional
fixture leaks — yes, low news value). It mis-weighed the *integration*: the repo is
**public**, so code scanning is free, and a live upload is the one thing a local
schema check cannot give — **proof that GitHub itself accepts the SARIF**
(`upload-sarif` waits for processing and fails the job if the log is rejected). So
CI validates the exporter on two levels:

- **structure** (`own-check-surface`): a `jq` contract over a freshly-emitted log —
`version 2.1.0`, the `Own.NET` driver, and *every* result carrying a catalogue
ruleId and a located file (the shape GitHub's ingest enforces). No upload, no
permissions — runs everywhere.
- **end-to-end** (`own-check-codescan`): the composite action `format: sarif` over
the sample tree, then `upload-sarif` under a *scoped* `security-events: write`
(the only non-`contents:read` job in CI). The sample alerts ride a dedicated
`own-net-samples` category and are real-if-intentional — they double as the live
demo of the Security-tab surface this exporter was built for.

Still open:

- **GitHub code-scanning upload.** Deferred deliberately: this repo's C# is test
fixtures, so uploading own-check SARIF to *its own* Security tab is low-value.
Code scanning belongs in the consumer-facing distribution surface (P-013), run
on a real target — not here.
- **Per-rule `helpUri`.** Once a stable per-code docs anchor exists, point each
`rules[]` entry at it (intentionally omitted now rather than link a 404).
Loading