Reads the signs in a project's own commit history. Two independent sections:
python3 -m augur radar <path-to-local-clone> [--limit N]
python3 -m augur harness --old OLD.c --new NEW.c --function NAME --seed "..." [--param name=value ...]
Scans a local git clone's recent commits and flags the ones that look like an undisclosed ("silent") security fix: a commit that touches a sensitive path or has a defensive-shaped diff (adds a validation call, removes a dangerous sink), but whose own message never says so.
$ python3 -m augur radar ./some-clone --limit 300
### a1b2c3d4e5 — small cleanup
- Author: ... · Confidence: high (score 6.0)
- sensitive_path (+1.5): 1 file(s) touch sensitive paths (matched: auth)
- defensive_diff_shape (+1.5): added line(s) call validate
- vague_message_defensive_diff (+3.0): diff looks defensive but message
uses a downplaying phrase
Works fully offline against any local clone — no GitHub API, no token,
no rate limit, no dependency on the repo being hosted on GitHub at all.
This is a deliberate difference from this portfolio's earlier
silent-patch-finder, which needed the GitHub API for PR-association
metadata; Augur drops that one signal in exchange for working on any
repo, anywhere, offline.
The detection logic (keyword lists, signal weights, confidence
thresholds) is ported unchanged from silent-patch-finder, not
re-derived — see METHODOLOGY.md for why, and for the real
false-positive classes that logic was already debugged against before
Augur existed.
Confidence is a reading order, not a verdict. "High" means "read this one first." A commit can legitimately show up at "low" confidence just for touching a path with a sensitive-sounding name — the radar is broad-recall by design; judgment stays with the human reading the report.
For a narrow, specific class of C/C++ fixes, turns a heuristic flag into an executable proof instead of a guess.
$ python3 -m augur harness \
--old vulnerable_version.c --new fixed_version.c \
--function getCrashAddress --param index=0
[*] no --seed given, attempting automatic prefix derivation...
[*] verdict: confirmed_regression_fix
[*] [auto-derived prefix '#0 '] old crashes at truncation length 3 (heap-buffer-overflow); new runs cleanly at the same length
No --seed was supplied above — Augur read the function's own snprintf(...)/strstr(...) logic and derived the exact matching prefix ("#0 ") mechanically, then swept truncation lengths against it. --seed still exists for functions whose matching logic doesn't fit that shape (see below).
What it does, mechanically:
- Extracts the named function's full body from both source files (brace matching, not a real C parser).
- Checks the function's signature is "simple" — every parameter is
either a primitive scalar (
int,size_t, ...) or achar*/const char*string. Anything else (structs, function pointers, multiple string parameters) is refused, not guessed at. - Runs a narrow pattern detector: does a fixed-size
memcpycopy into a local buffer from a pointer that traces back to the string parameter through simple, single-step pointer arithmetic, with no length check in between? This is a single-pass "taint-lite" tracker over exactly that statement shape — not general dataflow analysis. - If the pattern is found, generates one C file containing both
function versions (renamed to avoid a symbol clash) and a
main()that truncates a caller-supplied seed string to every length from 0 up to the seed's own length, heap-allocating each truncated candidate with zero slack past its real end so AddressSanitizer can catch a read past it. - Compiles with
-fsanitize=addressand runs every (version, length) combination as its own subprocess (ASan aborts the whole process on the first detected error, so one process per candidate is the only reliable way to test many candidates). - Reports one of four honest verdicts — see below.
Augur first tries to mechanically read what a "matching" input looks
like straight out of the function's own source: if it finds a
snprintf(var, len, "format", args...) call whose result is later
searched for via strstr(param, var), it renders that format string
using the concrete parameter values you passed with --param, giving
the exact literal prefix bytes the function's own logic requires — not
a guess, not a reimplementation of the search logic, just the format
string it already contains. It then automatically sweeps every
truncation length after that prefix, the same way a manually-supplied
seed would be swept.
This covers a real, specific shape (FormatStringPrefixDeriver,
augur/pattern/format_string_prefix.py) — not every function's
matching logic looks like this. When it doesn't (a hand-rolled parsing
loop, a match against a hardcoded byte value, anything without a
snprintf-then-strstr pair), derivation fails honestly
(seed_derivation_failed) and you fall back to --seed: supply one
realistic, full-length example value yourself, and Augur sweeps every
truncation of it the same way.
What was deliberately not built: a general symbolic-execution-based
input solver (via angr, installed and evaluated during this feature's
development) that would remove even the --seed fallback for arbitrary
matching logic, not just the snprintf/strstr shape. angr symbolic
execution of a real compiled binary carries a real, known risk class of
its own — state explosion, subtle setup errors, or an incorrect model
of a hooked libc function silently producing a wrong satisfying
input — and this project's standing rule is not to ship anything into
the path that produces a confirmed_regression_fix verdict without
being confident it can't be silently wrong. The mechanical derivation
above already closes the practical gap (no human input needed) for a
real, common pattern; a symbolic fallback for the general case is
documented here as a real next step, not attempted under time pressure
just to use a fashionable technique.
| Verdict | Meaning |
|---|---|
confirmed_regression_fix |
Old crashed at some length under ASan; new ran cleanly at that same length. The strongest thing this tool can say. |
no_difference_found |
Old never crashed across the whole sweep — either the seed never reaches the bug, or there isn't one at this shape. |
inconclusive |
New also crashed at the same length old did — the fix may be incomplete, or (more likely) the harness's assumptions don't hold for this function. Never reported as a silent pass. |
needs_manual_review |
The signature isn't simple, no matching parameter, or the narrow memcpy pattern wasn't found. This is the expected, common outcome for most real functions — Augur is honest that its scope is narrow, not that most bugs fit it. |
seed_derivation_failed |
(analyze_auto / no --seed given only) The function's matching logic doesn't fit the snprintf-then-strstr shape automatic derivation needs. Supply --seed manually. |
For the same narrow bug shape, answers a different question: given the fix commit, when was the vulnerable pattern actually introduced, and which released versions contain it?
$ python3 -m augur provenance ./rtcon-clone \
--file skel/crash.c --function getCrashAddress \
--fix-commit e8b4127 --param report
[*] found: True
{
"introduction_commit": "3f49a23...",
"fix_commit": "e8b4127",
"vulnerable_tags": [],
"first_fixed_tag": null
}
This targets a real, published research gap: a 2025 paper found that
existing "which versions are affected" tools (SZZ-based and ML-based
alike) have "low precision and recall" and don't generalize across
projects. provenance doesn't solve that in general -- it answers it
precisely for the one bug shape it already understands, by re-running
the same structural detector against every historical revision of the
function instead of trusting whichever commit last touched the line
(classic SZZ's well-known weakness: a pure reformatting or renaming
commit gets blamed instead of the real one).
Validated against a real external repository, not just a synthetic
one -- see METHODOLOGY.md for running this against
kaist-hacking/RTCON's actual merged fix and independently confirming
the answer by hand.
An empty vulnerable_tags list means either the repository has no
tags, or the introduction/fix commits fall outside all of them --
provenance never guesses at version numbers it can't verify against
real tags.
No third-party runtime dependencies. Needs git (for radar) and a C
compiler with -fsanitize=address support, typically gcc or clang
(for harness) already on the system.
pip install -r requirements.txt # pytest, for running the test suite
python3 -m pytest tests/radar's signals are heuristics, not verdicts — see the confidence note above and METHODOLOGY.md's account of the false positives this logic was built to survive.harnesscovers one specific bug shape: a fixed-sizememcpyfrom a string parameter, reached through at most one intermediate pointer variable. A bug reached through a loop, a helper function call, or multiple reassignment steps will not be detected — reported asneeds_manual_review, never silently missed as a false "no bug here."- One string parameter, one taint hop. Functions with two or more string-like parameters, or where the tainted pointer passes through more than one intermediate variable, are out of scope for the same reason: correctly generalizing either would require real dataflow analysis, which this tool deliberately does not attempt.
- This does not fuzz. The truncation sweep is exhaustive over one dimension (string length) for one seed, not a search over arbitrary byte content. It will find "too short" bugs; it will not find bugs that need specific byte values, not just length.