-
Notifications
You must be signed in to change notification settings - Fork 0
294 lines (281 loc) · 14.2 KB
/
Copy pathoracle.yml
File metadata and controls
294 lines (281 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
name: oracle (cross-tool)
# On-demand cross-tool validation: run Own.NET's leak check, Infer#, and CodeQL
# over the SAME public C# repo and diff their leak-class findings into an
# agreement report (scripts/oracle_compare.py). Evaluation tooling — the mature
# detectors are both a recall bar and an oracle. See docs/notes/oracle.md.
#
# Trigger from the Actions tab ("Run workflow") or the API. Inputs reach the
# shell via env (never interpolated into a `run:` script) to avoid injection.
#
# Honest notes:
# * Own.NET needs no build (error-tolerant SemanticModel). The two oracles do:
# CodeQL builds a database (build-mode: none, from source); Infer# analyses
# compiled .dll+.pdb, so the target must `dotnet build`. Each oracle step is
# continue-on-error, so a build failure still yields a partial report.
# * The diff core (oracle_compare.py) is unit-tested (--selftest, run first);
# this orchestration is validated on dispatch, like mine.yml.
on:
workflow_dispatch:
inputs:
repo:
description: "Target: owner/repo (e.g. DapperLib/Dapper) or a git URL"
required: true
ref:
description: "Branch / tag / sha to analyse (optional, default: repo HEAD)"
required: false
default: ""
paths:
description: "Subdir to scan with own-check (optional, default: whole repo)"
required: false
default: ""
build:
description: "Project/solution under the target to `dotnet build` for Infer# (optional, default: repo root)"
required: false
default: ""
include_tests:
description: "Also analyse test/benchmark/sample code (default: product code only)"
required: false
type: boolean
default: false
# Dev-loop fallback (the automation token can't `workflow_dispatch`): bump the
# sentinel corpus/oracle-target.txt to run the oracle push-triggered, reading the
# target from that file. Dev-branch only — remove before merging to main.
push:
branches:
- claude/zen-pasteur-76hfs1
paths:
- corpus/oracle-target.txt
permissions:
contents: read
security-events: write # required by the CodeQL action internals (upload is off)
jobs:
oracle:
name: oracle ${{ inputs.repo }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
# Resolve the target: workflow_dispatch inputs win; on push, read the
# sentinel corpus/oracle-target.txt (same format/allowlist as mine-on-push).
# Everything downstream reads steps.t.outputs.* so both triggers share steps.
- name: Resolve target
id: t
env:
IN_REPO: ${{ inputs.repo }}
IN_REF: ${{ inputs.ref }}
IN_PATHS: ${{ inputs.paths }}
IN_BUILD: ${{ inputs.build }}
IN_TESTS: ${{ inputs.include_tests }}
run: |
file=corpus/oracle-target.txt
if [[ -n "$IN_REPO" ]]; then
repo="$IN_REPO"; ref="$IN_REF"; paths="$IN_PATHS"; build="$IN_BUILD"; tests="$IN_TESTS"
else
repo=$(grep -vE '^[[:space:]]*(#|$)' "$file" | head -1 | tr -d '[:space:]')
ref=$(grep -E '^ref=' "$file" | head -1 | sed 's/^ref=//' | tr -d '[:space:]')
paths=$(grep -E '^paths=' "$file" | head -1 | sed 's/^paths=//' | tr -d '[:space:]')
build=$(grep -E '^build=' "$file" | head -1 | sed 's/^build=//' | tr -d '[:space:]')
tests=$(grep -E '^include_tests=' "$file"| head -1 | sed 's/^include_tests=//' | tr -d '[:space:]')
fi
# Allowlist: GitHub owner/repo, an https URL, or local:<in-repo path>
# (a fixture copied into target/ instead of cloned) — never an odd scheme.
if ! [[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ || "$repo" =~ ^https://[A-Za-z0-9./_-]+$ || "$repo" =~ ^local:[A-Za-z0-9._/-]+$ ]]; then
echo "oracle: invalid target '$repo'" >&2; exit 2
fi
{ echo "repo=$repo"; echo "ref=$ref"; echo "paths=$paths";
echo "build=$build"; echo "tests=${tests:-false}"; } >> "$GITHUB_OUTPUT"
echo "oracle target: $repo (ref='${ref:-HEAD}' paths='${paths:-*}' build='${build:-auto}' tests='${tests:-false}')"
# Fast fail: the diff logic is unit-tested before we clone/build anything.
- name: Comparator selftest
run: python scripts/oracle_compare.py --selftest
- name: Clone the target
env:
REPO: ${{ steps.t.outputs.repo }}
REF: ${{ steps.t.outputs.ref }}
run: |
if [[ "$REPO" == local:* ]]; then
# local:<in-repo path> — copy an in-repo fixture into target/ instead of
# cloning, so a tiny buildable repro (where ScreenToGif can't build on
# Linux) lets every oracle — including Infer#, which needs binaries — run.
src="${REPO#local:}"
[[ "$src" != *..* && -d "$src" ]] || { echo "oracle: bad local fixture '$src'" >&2; exit 2; }
cp -r "$src" target
echo "COMMIT=local@$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
echo "using in-repo fixture: $src"
else
case "$REPO" in
http://*|https://*|git@*) url="$REPO" ;;
*) url="https://github.com/${REPO}.git" ;;
esac
if [[ -n "$REF" ]]; then
# A blobless clone keeps the full commit/ref graph (blobs fetched on
# demand), so ANY ref — branch, tag, or an abbreviated SHA — resolves
# locally at checkout. A shallow `fetch origin <short-sha>` does NOT:
# servers reject abbreviated / unadvertised SHAs in a want request.
git clone --filter=blob:none --quiet "$url" target
git -C target checkout --quiet --detach "$REF"
else
git clone --depth 1 --quiet "$url" target
fi
echo "COMMIT=$(git -C target rev-parse HEAD)" >> "$GITHUB_ENV"
fi
# Framework-type references for own-check. The Roslyn extractor resolves a
# `+=` only when the event's declaring type is on its reference set — else the
# subscription is an OWN050 "unchecked" note, not a leak. Materialize the
# WindowsDesktop ref pack (WPF / WinForms / Microsoft.Win32.SystemEvents) and
# export OWN_EXTRA_REF_DIRS so own-check resolves framework events instead of
# dropping them — putting it on equal footing with CodeQL, which resolves
# types from source. Harmless for non-Windows targets (deduped vs the runtime).
- name: Materialize framework reference assemblies
continue-on-error: true
run: |
tmp=$(mktemp -d)
printf '%s\n' \
'<Project Sdk="Microsoft.NET.Sdk">' \
' <PropertyGroup>' \
' <TargetFramework>net8.0-windows</TargetFramework>' \
' <UseWPF>true</UseWPF>' \
' <UseWindowsForms>true</UseWindowsForms>' \
' <EnableWindowsTargeting>true</EnableWindowsTargeting>' \
' </PropertyGroup>' \
'</Project>' > "$tmp/ref.csproj"
dotnet restore "$tmp/ref.csproj" >/dev/null 2>&1 || echo "ref restore failed (continuing)"
d=$(find "$HOME/.nuget/packages/microsoft.windowsdesktop.app.ref" -type d -name 'net8.0' 2>/dev/null | sort | tail -1 || true)
if [ -n "$d" ]; then
echo "OWN_EXTRA_REF_DIRS=$d" >> "$GITHUB_ENV"
echo "framework refs: $d ($(find "$d" -name '*.dll' | wc -l) dlls)"
else
echo "framework refs not found — own-check resolves runtime types only"
fi
# Own.NET — no build needed; scans .cs directly. --format sarif so the diff
# reads our findings through the SAME parser as the Infer#/CodeQL SARIF (no
# bespoke text parser, no parser drift; own.txt holds a SARIF log — the
# comparator sniffs the format). --severity warning so the warning-tier leaks
# (injected-source subscriptions, e.g. VideoSource) are included, not just the
# provable static-source errors. OWN_EXTRA_REF_DIRS (above) is inherited by the
# extractor process.
- name: Own.NET own-check
env:
PATHS: ${{ steps.t.outputs.paths }}
run: |
scan="target"; [[ -n "$PATHS" ]] && scan="target/$PATHS"
set +e
scripts/own-check.sh --format sarif --severity warning -- "$scan" > own.txt 2> own-extract.log
echo "own-check rc=$? ; own.txt is a SARIF log ($(wc -c < own.txt) bytes)"
# CodeQL — database from source (no build). The dispose/leak queries
# (cs/local-not-disposed & friends) are *quality* queries, absent from the
# default code-scanning (security) suite — so request security-and-quality,
# else CodeQL silently contributes zero. Comparator filters to the leak family.
- name: CodeQL init
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
continue-on-error: true
with:
languages: csharp
build-mode: none
source-root: target
queries: security-and-quality
- name: CodeQL analyze
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
continue-on-error: true
with:
category: ownnet-oracle
output: codeql-out
upload: false
# Infer# — needs compiled binaries; build the target into one output dir.
# Choose what to build: explicit `build` input wins; else the product
# library (`<repo>.csproj` outside tests/benchmarks — a leak scan wants the
# library, and building the whole solution often drags in unbuildable test
# projects); else a lone solution (.sln/.slnx); else the dir (-> partial).
- name: Build the target (for Infer#)
env:
BUILD: ${{ steps.t.outputs.build }}
REPO: ${{ steps.t.outputs.repo }}
continue-on-error: true
run: |
if [[ -n "$BUILD" ]]; then
tgt="target/$BUILD"
else
# The main library is almost always named after the repo; prefer a
# unique <repo>.csproj outside the test/benchmark/sample/example trees
# (-ipath: case-insensitive, so Tests/ Benchmarks/ etc. are excluded too).
name="${REPO##*/}"; name="${name%.git}"
mapfile -t named < <(find target -type f -name "$name.csproj" \
-not -ipath '*/test/*' -not -ipath '*/tests/*' -not -ipath '*/benchmark*' \
-not -ipath '*/sample*' -not -ipath '*/example*' | sort)
mapfile -t root_slns < <(find target -maxdepth 1 \( -name '*.sln' -o -name '*.slnx' \) | sort)
mapfile -t all_slns < <(find target \( -name '*.sln' -o -name '*.slnx' \) | sort)
if [[ ${#named[@]} -eq 1 ]]; then
tgt="${named[0]}"
elif [[ ${#root_slns[@]} -eq 1 ]]; then
tgt="${root_slns[0]}"
elif [[ ${#root_slns[@]} -eq 0 && ${#all_slns[@]} -eq 1 ]]; then
tgt="${all_slns[0]}"
else
tgt="target"
echo "note: no unique '$name.csproj' and ${#root_slns[@]} root / ${#all_slns[@]} total solution(s); pass the 'build' input to disambiguate"
fi
fi
# Some repos (e.g. those using Nerdbank.GitVersioning) need git history to
# compute a version at build time; the clone is shallow, so deepen it.
# Harmless (|| true) when the repo is already complete or doesn't need it.
git -C target fetch --unshallow --tags --quiet 2>/dev/null || true
echo "Infer# build target: $tgt"
if dotnet build "$tgt" -c Release -o _bin -v quiet; then
echo "BUILD_OK=1" >> "$GITHUB_ENV"
else
echo "target build failed — Infer# will be skipped, report stays partial"
fi
- name: Run Infer#
if: env.BUILD_OK == '1'
uses: microsoft/infersharpaction@b749060de518f410f92c87d37d2366e5e9d7c5fc # v1.5
continue-on-error: true
with:
binary-path: _bin
- name: Diff Own.NET vs the oracles
if: always()
env:
REPO: ${{ steps.t.outputs.repo }}
INCLUDE_TESTS: ${{ steps.t.outputs.tests }}
run: |
args=(--own own.txt --target "$REPO" --commit "${COMMIT:-}"
--strip "$GITHUB_WORKSPACE/target" --strip target
--json report.json)
# Compare on product code by default: tests/benchmarks pollute the diff
# (and Infer# only built the product project). include_tests keeps them.
[[ "${INCLUDE_TESTS,,}" == "true" ]] || args+=(--exclude-tests)
# Suppress verified false positives (triaged against real source) so the
# triage queue surfaces only NEW own-only findings. Matched by name, not
# line, so it survives the target drifting at HEAD. See the file header and
# docs/notes/oracle-known-fps.md.
[[ -f corpus/oracle-fp-baseline.txt ]] && args+=(--baseline corpus/oracle-fp-baseline.txt)
[[ -f infer-out/report.sarif ]] && args+=(--infersharp infer-out/report.sarif)
cq=$(find codeql-out -name '*.sarif' -type f 2>/dev/null | head -1 || true)
[[ -n "$cq" ]] && args+=(--codeql "$cq")
python scripts/oracle_compare.py "${args[@]}" > report.md
cat report.md
- name: Publish the report to the run summary
if: always()
run: |
if [[ -s report.md ]]; then
cat report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "no report produced (see the Diff step log)" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload the report and raw outputs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: oracle-report
path: |
report.md
report.json
own.txt
own-extract.log
infer-out/report.sarif
codeql-out/*.sarif
if-no-files-found: warn