augure 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +136 -0
  3. data/LICENSE +21 -0
  4. data/README.md +217 -0
  5. data/exe/augure +181 -0
  6. data/exe/augure-benchmark +67 -0
  7. data/home/runner/work/augure/augure/docs/agent-snippet.md +31 -0
  8. data/home/runner/work/augure/augure/docs/auditability.md +160 -0
  9. data/home/runner/work/augure/augure/docs/build-format.md +119 -0
  10. data/home/runner/work/augure/augure/docs/explanation-architecture.md +74 -0
  11. data/home/runner/work/augure/augure/docs/how-to-swap-solver.md +73 -0
  12. data/home/runner/work/augure/augure/docs/how-to-write-rules.md +71 -0
  13. data/home/runner/work/augure/augure/docs/pack-format.md +137 -0
  14. data/home/runner/work/augure/augure/docs/reference.md +293 -0
  15. data/home/runner/work/augure/augure/docs/tutorial.md +108 -0
  16. data/home/runner/work/augure/augure/examples/ret2win.facts +11 -0
  17. data/home/runner/work/augure/augure/examples/split.facts +12 -0
  18. data/home/runner/work/augure/augure/exe/augure +181 -0
  19. data/home/runner/work/augure/augure/exe/augure-benchmark +67 -0
  20. data/home/runner/work/augure/augure/lib/augure/bandit.rb +118 -0
  21. data/home/runner/work/augure/augure/lib/augure/coverage.rb +56 -0
  22. data/home/runner/work/augure/augure/lib/augure/ctf_corpus.json +773 -0
  23. data/home/runner/work/augure/augure/lib/augure/engine.rb +128 -0
  24. data/home/runner/work/augure/augure/lib/augure/facts.rb +194 -0
  25. data/home/runner/work/augure/augure/lib/augure/kb_corpus.json +345 -0
  26. data/home/runner/work/augure/augure/lib/augure/knowledge_base.rb +109 -0
  27. data/home/runner/work/augure/augure/lib/augure/mcts.rb +178 -0
  28. data/home/runner/work/augure/augure/lib/augure/pack.rb +244 -0
  29. data/home/runner/work/augure/augure/lib/augure/pack_loader.rb +93 -0
  30. data/home/runner/work/augure/augure/lib/augure/parity.rb +69 -0
  31. data/home/runner/work/augure/augure/lib/augure/pipeline.rb +107 -0
  32. data/home/runner/work/augure/augure/lib/augure/priors.rb +32 -0
  33. data/home/runner/work/augure/augure/lib/augure/rules.rb +300 -0
  34. data/home/runner/work/augure/augure/lib/augure/smt_process.rb +48 -0
  35. data/home/runner/work/augure/augure/lib/augure/verifier.rb +40 -0
  36. data/home/runner/work/augure/augure/lib/augure/version.rb +5 -0
  37. data/home/runner/work/augure/augure/lib/augure.rb +25 -0
  38. data/home/runner/work/augure/augure/packs/banner_leak.yml +46 -0
  39. data/home/runner/work/augure/augure/packs/brop.yml +46 -0
  40. data/home/runner/work/augure/augure/packs/fastbin_hook.yml +45 -0
  41. data/home/runner/work/augure/augure/packs/got_partial_overwrite.yml +45 -0
  42. data/home/runner/work/augure/augure/packs/got_partial_overwrite_deref.yml +39 -0
  43. data/home/runner/work/augure/augure/packs/house_botcake.yml +38 -0
  44. data/home/runner/work/augure/augure/packs/house_force.yml +37 -0
  45. data/home/runner/work/augure/augure/packs/house_of_apple2.yml +45 -0
  46. data/home/runner/work/augure/augure/packs/house_orange.yml +38 -0
  47. data/home/runner/work/augure/augure/packs/jop.yml +39 -0
  48. data/home/runner/work/augure/augure/packs/large_bin_attack.yml +39 -0
  49. data/home/runner/work/augure/augure/packs/one_gadget.yml +38 -0
  50. data/home/runner/work/augure/augure/packs/orw.yml +55 -0
  51. data/home/runner/work/augure/augure/packs/ret2csu_v2.yml +55 -0
  52. data/home/runner/work/augure/augure/packs/ret2dlresolve_x86.yml +39 -0
  53. data/home/runner/work/augure/augure/packs/ret2partial_overwrite.yml +38 -0
  54. data/home/runner/work/augure/augure/packs/ret2vdso.yml +39 -0
  55. data/home/runner/work/augure/augure/packs/setcontext_srop.yml +46 -0
  56. data/home/runner/work/augure/augure/packs/uaf_tcache.yml +45 -0
  57. data/home/runner/work/augure/augure/packs/unsorted_bin_attack.yml +38 -0
  58. metadata +103 -0
@@ -0,0 +1,293 @@
1
+ # Reference: fact format, rules table, API
2
+
3
+ Version: augure 0.1.x.
4
+
5
+ ## Fact format
6
+
7
+ One fact per line: `predicate("string-atom").` or `predicate("atom", 42).`
8
+ Blank lines and `// comments` ignored. Rules (`:-`) rejected. Unknown
9
+ predicate, wrong arity or wrong argument type raises an explicit error with
10
+ the line number. So does:
11
+
12
+ - a **value outside a closed domain** (18 predicates are validated: the
13
+ booleans take `"true"`/`"false"` only, `relro` takes `none`/`partial`/`full`,
14
+ `verified` takes `"remote"` - a typo'd atom parses under a string-only
15
+ schema, disables the closed-world default and silently kills every rule
16
+ of the predicate; the validation makes it loud instead);
17
+ - an **atom carrying quotes, backslashes, parens or newlines** (the
18
+ grammar keeps the fact stream a stream).
19
+
20
+ Free-form predicates (`vuln`, `vuln_hint`, `allocator`, `gadget`,
21
+ `win_symbol`, `plt`...) take any atom.
22
+
23
+ ### A complete example
24
+
25
+ This is the shape of a `target.facts` file (see the
26
+ [tutorial](tutorial.md) for the full walkthrough):
27
+
28
+ ```
29
+ // the target
30
+ nx("true").
31
+ pie("false").
32
+ canary("false").
33
+ relro("partial").
34
+
35
+ // what it imports
36
+ plt("system").
37
+ plt("puts").
38
+
39
+ // what gadgets exist inside it
40
+ gadget("pop_rdi_ret", 4198400).
41
+ gadget("ret_align", 4198404).
42
+
43
+ // what the profiler noticed
44
+ vuln_hint("unsafe_func:gets").
45
+ ```
46
+
47
+ ### Schema
48
+
49
+ | Predicate | Arity | Arg types | Meaning |
50
+ |---|---|---|---|
51
+ | `nx` | 1 | string | data execution prevention (`"true"`/`"false"`) |
52
+ | `win_symbol` | 2 | string, integer | the winning function's name and address (emitted by the profiler) |
53
+ | `crash_site` | 1 | string | the crash's identification: `ret` when the control is confirmed by the planted pattern |
54
+ | `leaked_address` | 2 | string, integer | an address the service's own banner leaked (name + value, emitted by lictor's harvest) |
55
+ | `plt_addr` | 2 | string, integer | a JMP_SLOT import's PLT stub address (name + value, emitted by the profiler from the section layout) |
56
+ | `got_addr` | 2 | string, integer | a JMP_SLOT import's GOT entry address (the relocation offset) |
57
+ | `vuln_function` | 1 | string | the vulnerable function the static frame read named |
58
+ | `sink` | 1 | string | the unsafe sink (strcpy, gets, ...) the static read identified |
59
+ | `pie` | 1 | string | position-independent executable |
60
+ | `canary` | 1 | string | stack canary present |
61
+ | `relro` | 1 | string | `"none"`, `"partial"`, `"full"` |
62
+ | `plt` | 1 | string | imported function name |
63
+ | `gadget` | 2 | string, integer | gadget semantic type + address |
64
+ | `vuln_hint` | 1 | string | profiler hint, e.g. `"unsafe_func:gets"` |
65
+ | `vuln` | 1 | string | explicit vuln class: `sof`, `fmtstr`, `heap` |
66
+ | `target_function` | 1 | string | win-style function present |
67
+ | `libc_present` | 1 | string | libc known/version pinned |
68
+ | `allocator` | 1 | string | `tcache`, `ptmalloc2`, `dlmalloc` |
69
+ | `glibc_minor` | 1 | integer | glibc minor version |
70
+ | `return_addr_filtered` | 1 | string | return addresses constrained (stack6-style) |
71
+ | `got_overwrite_target` | 1 | string | GOT overwrite viable |
72
+ | `function_pointer_on_heap` | 1 | string | heap function pointer |
73
+ | `seccomp` | 1 | string | seccomp filter context |
74
+ | `shellcode_input` | 1 | string | input reachable and executable |
75
+ | `sigreturn_frame` | 1 | string | sigreturn frame constructible |
76
+ | `fmtstr_read` | 1 | string | format string has a read primitive |
77
+ | `reloc_writable` | 1 | string | relocations writable (ret2dlresolve) |
78
+ | `dt_lazy` | 1 | string | lazy binding in effect |
79
+ | `limited_stack` | 1 | string | stack space constrains the chain |
80
+ | `service` | 1 | string | remote service name (nmap: `ssh`, `http`...) |
81
+ | `software_version` | 1 | string | remote product + version (from the scan) |
82
+ | `remote` | 1 | string | the facts describe a remote service, not a local ELF |
83
+ | `verified` | 1 | string | `"remote"` = validated by a differential probe: the remote faults like the local model |
84
+
85
+ Defaults (closed world): absent `nx` = `"true"`, absent `pie` = `"true"`,
86
+ absent `canary` = `"false"`.
87
+
88
+ ## Rules table (applicability)
89
+
90
+ | Rule ID | Head | Fires when |
91
+ |---|---|---|
92
+ | `app_shellcode` | `shellcode` | sof, NX off, no canary, return not filtered, NOT seccomp |
93
+ | `app_ret2func` | `ret2func` | sof, PIE off, target function present |
94
+ | `app_ret2plt` | `ret2plt` | sof, NX on, PIE off, `system` imported |
95
+ | `app_ret2libc_plt` | `ret2libc` | sof, `__libc_start_main` imported, NOT seccomp |
96
+ | `app_ret2libc_libc` | `ret2libc` | sof, libc present, NOT seccomp |
97
+ | `app_rop` | `rop` | sof, NX on, gadget supply (pop or mov-store), NOT seccomp |
98
+ | `app_srop` | `srop` | sof, sigreturn frame, syscall gadget, NOT seccomp |
99
+ | `app_ret2plt_leak` | `ret2plt_leak` | sof, NX on, PIE on, `puts`, reg control |
100
+ | `app_fmtstr_write` | `fmtstr_write` | format-string vuln (NX-independent) |
101
+ | `app_fmtstr_leak_read` | `fmtstr_leak` | fmtstr + read primitive |
102
+ | `app_fmtstr_leak_pie` | `fmtstr_leak` | fmtstr + PIE on |
103
+ | `app_fmtstr_leak_canary` | `fmtstr_leak` | fmtstr + canary on |
104
+ | `app_ret2dlresolve` | `ret2dlresolve` | sof, NX on, PIE off, lazy binding, writable reloc |
105
+ | `app_ret2csu` | `ret2csu` | sof, NX on, csu gadget family |
106
+ | `app_stack_pivot` | `stack_pivot` | sof, NX on, limited stack, pivot gadget |
107
+ | `app_got_overwrite_sof` | `got_overwrite` | sof, NX on, GOT target, not full RELRO |
108
+ | `app_got_overwrite_fmtstr` | `got_overwrite` | fmtstr, GOT target, not full RELRO |
109
+ | `app_heap_tcache_alloc` | `heap_tcache` | heap, allocator tcache |
110
+ | `app_heap_tcache_glibc` | `heap_tcache` | heap, glibc 2.26..2.33 |
111
+ | `app_heap_fastbin_glibc` | `heap_fastbin` | heap, glibc < 2.26 |
112
+ | `app_heap_fastbin_alloc` | `heap_fastbin` | heap, allocator ptmalloc2 |
113
+ | `app_heap_overwrite_dlmalloc` | `heap_overwrite` | heap, dlmalloc |
114
+ | `app_heap_overwrite_fp` | `heap_overwrite` | heap, function pointer on heap |
115
+
116
+ ("NOT seccomp" = the rule carries `[not_fact, seccomp, "true"]`: the
117
+ technique ends in execve, dead under any filter that kills execve; the
118
+ condition passes when the fact is absent.)
119
+
120
+ Derived relations: `vuln` (from hints), `has_reg_control`,
121
+ `has_write_primitive`, `has_syscall_gadget`, `has_csu_gadget`,
122
+ `has_pivot_gadget`, `enough_gadgets`.
123
+
124
+ ## CLI
125
+
126
+ ```
127
+ augure analyze <facts-file | -> [--json] [--seed N] [--packs DIR]
128
+ augure coverage [--flip NAME:VALUE ...] [--facts FILE] [--json]
129
+ augure rules [--mitre] [--packs DIR] [--json]
130
+ augure explain <technique>
131
+ augure doctor
132
+ ```
133
+
134
+ | Flag | Effect |
135
+ |---|---|
136
+ | `--json`, `-j` | machine-readable decision (analyze) / the coverage scorecard (coverage) / the Navigator layer (rules --mitre) |
137
+ | `--seed N`, `-s N` | deterministic bandit/MCTS draws |
138
+ | `--packs DIR` | merge technique packs (analyze, explain, rules --mitre's source) |
139
+ | `--flip NAME:VALUE` | coverage: a hardening override (`nx:true`, `canary:true`, `relro:full`; `NAME:nil` removes) |
140
+ | `--facts FILE` | coverage: one entry instead of the whole corpus |
141
+ | (stdin) | `-` reads facts from stdin |
142
+
143
+ Exit codes: `0` decision emitted, non-zero on malformed facts (message on
144
+ stderr names the line). `coverage` always exits 0 (an analysis, not a gate).
145
+
146
+ ## Ruby API
147
+
148
+ ### `Augure::Facts`
149
+
150
+ - `.parse(text) -> Facts` - strict parse, raises `MalformedFact` /
151
+ `UnknownPredicate` with line numbers.
152
+ - `.from_file(path) -> Facts`
153
+ - `.new` + `#add(name, *args)` - the builder API (the profiler's
154
+ contract; the same domain + grammar validation as `parse`).
155
+ - `#rel(name) -> Array<[arg, ...]> | nil` - tuples by relation.
156
+ - `#each`, `#size`, `#[]` - iteration, count, direct lookup.
157
+ - `#merge(other) -> Facts` - combined copy.
158
+ - `#to_s -> String` - deterministic emission; round-trips.
159
+
160
+ ### `Augure::Engine`
161
+
162
+ - `.new(facts, rules: Rules.all)`
163
+ - `#run -> Result`
164
+ - `Result#applicable -> [String]` - rule-table order.
165
+ - `Result#derived(rel) -> [tuple]`
166
+ - `Result#provenance(technique) -> [{rule:, origin:, evidence:}]` - every sibling firing, each attributed
167
+
168
+ ### `Augure::Bandit`
169
+
170
+ - `.new(priors: {"tech" => [alpha, beta]}, rng: Random.new)`
171
+ - `#arm(tech) -> Arm` (auto-arms unknown with (1, 1))
172
+ - `#select(verified) -> String | nil` - Thompson sample argmax.
173
+ - `#feedback(tech, success)` - updates posterior, logs history.
174
+ - `#ranking_of(techniques) -> [[tech, mean], ...]` - prior-mean pairs,
175
+ deterministic (the method the corpus freezes; the pipeline maps
176
+ `&:first` over it).
177
+ - `#rankings -> [[tech, mean], ...]` - ALL arms sorted by prior mean
178
+ (deterministic, not a draw).
179
+ - `#mean(tech) -> Float` - the prior mean (alpha / (alpha + beta)).
180
+ - `#select(verified)` - the Thompson sampler (the draw).
181
+
182
+ ### `Augure::Mcts`
183
+
184
+ - `.plan(allowed, iterations: 2000, seed:) -> [first_move, path]`
185
+ (`model:` overrides the transition graph; technique packs merge into
186
+ it via `merged_model(packs)`)
187
+ - `.available(caps, allowed)`, `.transition(caps, tech)`,
188
+ `.terminal?(caps, model:)` - terminal = any capability provided by a
189
+ `terminal: true` stage of the model (packs own their semantics; the
190
+ built-in model's terminals all provide `shell`).
191
+ - `.stage(tech)` - one stage's `[requires, provides, terminal, success]`.
192
+ - `STAGE_MODEL` - the technique transition graph.
193
+
194
+ ### `Augure::Pack` / `PackLoader`
195
+
196
+ - `Pack.load_file(path)` - validates a technique pack (YAML) through
197
+ the **10 armor checks** (see [pack-format.md](pack-format.md)): shape,
198
+ stratification, new heads, provenance, kb/mcts/priors shapes, match
199
+ regexes + cmp types (7), the optional `build:` (8), `detection:` (9),
200
+ `mitre:` (10).
201
+ - `PackLoader.load_dir(dir) -> [Pack]`, `.priors_with(packs, base)`,
202
+ `.mcts_model(packs)`.
203
+ - `.corpus_guard(packs)` - refuses any pack that reorders an existing
204
+ technique or takes a documented target's top-1; honors a documented
205
+ `pack_verdict` entry (the pack answer must ARRIVE) and tolerates
206
+ empty frozen verdicts.
207
+ - The gem SHIPS all 20 packs (`packs/` in the gem); `packs/ret2csu_v2.yml`
208
+ is the canonical authoring example.
209
+
210
+ ### `Augure::Verifier`
211
+
212
+ - `.payload_fits(buffer_size:, payload_min:) -> :sat | :unsat`
213
+ - `.bad_bytes?(payload:, bad_bytes:) -> bool`
214
+ - `.rop_chain_feasible?(...) -> bool`
215
+ - `.payload_fits_smtlib(buffer_size, payload_min) -> String`
216
+
217
+ ### `Augure::SmtProcess`
218
+
219
+ - `.solve(smt_text, solver:, timeout: 10) -> :sat | :unsat | :unknown`
220
+
221
+ ### `Augure::Pipeline`
222
+
223
+ - `.analyze(facts:, seed: nil, priors: nil, buffer_size: 256,
224
+ packs: nil, plan: true) -> {applicable:, verified:, ranking:, selected:,
225
+ plan:, explain:}` - `packs:` merges technique packs (rules, priors,
226
+ MCTS transitions); `plan: false` skips the MCTS search (the corpus
227
+ guard uses it).
228
+
229
+ ### Machine payload contract
230
+
231
+ `augure analyze --json` emits the decision hash stamped with a stable
232
+ schema tag:
233
+
234
+ ```json
235
+ { "schema": "augure/decision@1", "applicable": [...], ... }
236
+ ```
237
+
238
+ Consumers (CI, agents, the MCP server) pin `augure/decision@1`; a
239
+ breaking change bumps the tag and is documented in the CHANGELOG.
240
+
241
+ ### Taught technique packs
242
+
243
+ The shipped packs extend the rule table as data (`packs/`, loaded with
244
+ `augure analyze --packs DIR` or taught to lictor with
245
+ `lictor rule install`): `house_force`, `house_orange`, `house_botcake`,
246
+ `unsorted_bin_attack`, `fastbin_hook`, `ret2dlresolve_x86`,
247
+ `got_partial_overwrite`, `brop` (the first rule that consumes
248
+ `verified("remote")` - an observation anchors it), `ret2partial_overwrite`,
249
+ `one_gadget`, `banner_leak`, `got_partial_overwrite_deref`,
250
+ `house_of_apple2`, `jop`, `large_bin_attack`, `orw`, `ret2vdso`,
251
+ `setcontext_srop`, `uaf_tcache` - plus the canonical `ret2csu_v2`
252
+ example: **20 packs** ship. Each carries its rules, knowledge-base
253
+ entries, MCTS transition and Beta priors, the author and source as
254
+ provenance, and (optionally) `build:` (the assembly order),
255
+ `detection:` (the defender side) and `mitre:` (the ATT&CK mapping).
256
+ Pack priors fill the gaps of the prior table; a consumer-provided or
257
+ outcome-adapted entry always wins.
258
+
259
+ ### `Augure::KnowledgeBase`
260
+
261
+ - `.corpus -> [entry]` (22 documented patterns)
262
+ - `.corpus_with(extra) -> [entry]` (seed corpus + technique-pack knowledge)
263
+ - `.priors -> {"tech" => [alpha, beta]}` (N=10 pseudo-counts)
264
+ - `.transitions -> {"tech" => [unlock, ...]}`
265
+ - `#query(text, technique: nil, n_results: 3) -> [{doc:, score:}]`
266
+
267
+ ## MCTS stage model
268
+
269
+ | Technique | Requires | Provides | Terminal | Base success |
270
+ |---|---|---|---|---|
271
+ | `fmtstr_leak` | - | libc_base | no | 0.75 |
272
+ | `fmtstr_canary` | - | canary | no | 0.70 |
273
+ | `ret2plt_leak` | - | libc_base | no | 0.68 |
274
+ | `ret2libc` | libc_base | shell | yes | 0.85 |
275
+ | `rop` | libc_base | shell | yes | 0.75 |
276
+ | `rop_nocontext` | - | shell | yes | 0.25 |
277
+ | `ret2plt` | - | shell | yes | 0.75 |
278
+ | `shellcode` | - | shell | yes | 0.90 |
279
+ | `stack_pivot` | - | pivot | no | 0.60 |
280
+ | `ret2plt_leak_big` | pivot | libc_base | no | 0.68 |
281
+ | `ret2libc_big` | pivot+libc_base | shell | yes | 0.85 |
282
+ | `ret2csu` | - | libc_base | no | 0.70 |
283
+ | `dlresolve` | - | shell | yes | 0.55 |
284
+ | `got_overwrite` | - | shell | yes | 0.65 |
285
+ | `io_uring_register` | - | iouring | no | 0.90 |
286
+ | `rds_pin_steal` | iouring | pin_underflow | no | 0.70 |
287
+ | `page_free` | pin_underflow | freed_page | no | 0.75 |
288
+ | `pagecache_reclaim` | freed_page | pagecache_ctrl | no | 0.65 |
289
+ | `io_uring_write` | pagecache_ctrl+iouring | suid_overwrite | no | 0.70 |
290
+ | `suid_exec` | suid_overwrite | shell | yes | 0.90 |
291
+
292
+ The last six are the PinTheft chain (V12 Security), an externally documented
293
+ ordering - augure only tests whether the planner reproduces it.
@@ -0,0 +1,108 @@
1
+ # Get your first auditable decision in 10 minutes
2
+
3
+ *Tutorial. By the end you will have run Augure on a real fact file, read its
4
+ proof trail, and made it disagree with you on purpose.*
5
+
6
+ Prerequisites: Ruby 3.4+.
7
+
8
+ ## 1. Install
9
+
10
+ ```sh
11
+ gem install augure
12
+ augure --help
13
+ ```
14
+
15
+ You should see the usage banner. **Check:** `Usage: augure analyze`.
16
+
17
+ ## 2. Write a fact file
18
+
19
+ > **Where do facts come from?** From the profiler, not your keyboard:
20
+ > `augure-profile app.elf -o target.facts` generates exactly this file from
21
+ > a binary (checksec, imports, unsafe symbols, gadgets) via metasm - gem
22
+ > `augure-profiler`. Hand-writing facts is still useful to learn the
23
+ > format the profiler speaks.
24
+
25
+ Facts are one target's properties, one per line. Create `target.facts`:
26
+
27
+ ```
28
+ // the target
29
+ nx("true").
30
+ pie("false").
31
+ canary("false").
32
+ relro("partial").
33
+
34
+ // what it imports
35
+ plt("system").
36
+ plt("puts").
37
+
38
+ // what gadgets exist inside it
39
+ gadget("pop_rdi_ret", 4198400).
40
+ gadget("ret_align", 4198404).
41
+
42
+ // what the profiler noticed
43
+ vuln_hint("unsafe_func:gets").
44
+ ```
45
+
46
+ **Check:** the file has no trailing `:-` and every line ends with `.`.
47
+
48
+ ## 3. Decide
49
+
50
+ ```sh
51
+ augure analyze target.facts
52
+ ```
53
+
54
+ You should see:
55
+
56
+ ```
57
+ applicable: ret2plt, rop
58
+ selected: ret2plt
59
+ plan: ret2plt
60
+ # (selected is a seeded Thompson draw: same facts + same --seed = same
61
+ # selection, always. Unseeded, the draw explores the ranking.)
62
+ ret2plt <- app_ret2plt (vuln=sof, nx=true, pie=false, plt=system)
63
+ rop <- app_rop (vuln=sof, nx=true, enough_gadgets=true)
64
+ ```
65
+
66
+ Augure picked `ret2plt` - the technique human operators routinely skip in
67
+ favor of a ROP chain. The rules, not the reflex, made the call.
68
+
69
+ ## 4. Read the proof
70
+
71
+ ```sh
72
+ augure analyze target.facts --json | head -30
73
+ ```
74
+
75
+ Find the `explain.provenance` block (the JSON is long: `grep -A 20 provenance`
76
+ after the pipe) - the rule ID, the evidence pairs, and the origin that
77
+ fired it. That block is the whole product: hand it to a colleague, they can
78
+ verify the decision without rerunning anything.
79
+
80
+ ## 5. Reproduce it
81
+
82
+ ```sh
83
+ augure analyze target.facts --json --seed 42 > run1.json
84
+ augure analyze target.facts --json --seed 42 > run2.json
85
+ diff run1.json run2.json && echo "identical"
86
+ ```
87
+
88
+ Same input, same seed, same decision. **Check:** the diff is empty.
89
+
90
+ ## 6. Make it disagree with you
91
+
92
+ Remove the `plt("system")` line and run again:
93
+
94
+ ```sh
95
+ grep -v 'plt("system")' target.facts > no-system.facts
96
+ augure analyze no-system.facts
97
+ ```
98
+
99
+ `ret2plt` vanished - and the proof trail explains why: the rule
100
+ `app_ret2plt` requires the fact you just removed. This is the loop the
101
+ whole tool is built on: change the facts, watch the decision move, know
102
+ exactly which rule moved it.
103
+
104
+ ## Next steps
105
+
106
+ - [Write your own technique rules](how-to-write-rules.md)
107
+ - [The full reference](reference.md) - fact schema and API
108
+ - [What the proof trail buys each team](auditability.md)
@@ -0,0 +1,11 @@
1
+ nx("true").
2
+ pie("false").
3
+ canary("false").
4
+ relro("partial").
5
+ plt("puts").
6
+ plt("memset").
7
+ plt("read").
8
+ plt("printf").
9
+ gadget("ret_align", 4198400).
10
+ vuln_hint("unsafe_func:gets").
11
+ target_function("true").
@@ -0,0 +1,12 @@
1
+ nx("true").
2
+ pie("false").
3
+ canary("false").
4
+ relro("partial").
5
+ plt("system").
6
+ plt("puts").
7
+ plt("memset").
8
+ plt("read").
9
+ plt("printf").
10
+ gadget("pop_rdi_ret", 4198400).
11
+ gadget("ret_align", 4198400).
12
+ vuln_hint("unsafe_func:gets").
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "optparse"
6
+ require_relative "../lib/augure"
7
+
8
+ VERSION_LINE = "augure #{Augure::VERSION}"
9
+
10
+ options = {json: false, seed: nil}
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = <<~USAGE
13
+
14
+ #{VERSION_LINE} - the auditable exploitation decision layer
15
+
16
+ Usage:
17
+ augure analyze <facts-file | -> [options] decide on facts
18
+ augure coverage [options] the corpus read backwards (defender side)
19
+ augure doctor verify the environment
20
+ augure rules print the rules-as-data table
21
+ augure explain <technique> rules + knowledge for one technique
22
+ augure --version print the version
23
+
24
+ analyze options:
25
+ USAGE
26
+ opts.on("-j", "--json", "Emit the decision as JSON (schema: augure/decision@1)") { options[:json] = true }
27
+ opts.on("-s", "--seed N", Integer, "Seed for deterministic bandit/MCTS") { |n| options[:seed] = n }
28
+ opts.on("--packs DIR", "Load technique packs (armor + corpus guard) for analyze/explain") { |v| options[:packs] = v }
29
+ opts.on("--flip NAME:VALUE", "Coverage: a hardening flip (nx:true, canary:true, relro:full; NAME:nil removes)") do |v|
30
+ name, value = v.split(":", 2)
31
+ (options[:flips] ||= {})[name] = ((value == "nil") ? nil : value)
32
+ end
33
+ opts.on("--facts FILE", "Coverage: one facts entry instead of the corpus") { |v| options[:facts_entry] = v }
34
+ opts.on("--mitre", "rules: export the ATT&CK navigator layer (from the packs' mitre: fields)") { options[:mitre] = true }
35
+ opts.on("--version", "Print the version") do
36
+ puts VERSION_LINE
37
+ exit 0
38
+ end
39
+ opts.on("-h", "--help", "Show this help") do
40
+ puts opts
41
+ exit 0
42
+ end
43
+ end
44
+ parser.parse!
45
+
46
+ command = ARGV.shift
47
+
48
+ case command
49
+ when "--version", "-v"
50
+ puts VERSION_LINE
51
+ when "doctor"
52
+ checks = []
53
+ checks << ["ruby >= 3.4", (RUBY_VERSION >= "3.4") ? nil : "upgrade ruby (mise install ruby@3.4)"]
54
+ checks << [VERSION_LINE, nil]
55
+
56
+ # The corpus self-verifies: parse a shipped example, decide, compare.
57
+ begin
58
+ example = File.join(__dir__, "../examples/ret2win.facts")
59
+ result = Augure::Pipeline.analyze(facts: File.read(example))
60
+ checks << if result[:ranking].first == "ret2func"
61
+ ["corpus self-test (selected: ret2func)", nil]
62
+ else
63
+ ["corpus self-test", "corpus example decided differently - report a bug"]
64
+ end
65
+ rescue => e
66
+ checks << ["corpus self-test", e.message]
67
+ end
68
+
69
+ solvers = %w[z3 bitwuzla cvc5].select do |s|
70
+ system("sh", "-c", "command -v '#{s}' >/dev/null 2>&1")
71
+ end
72
+ checks << ["SMT solvers",
73
+ solvers.empty? ? "optional - none found; the arithmetic path works without (apt install z3 for the SMT path)" : "optional, found: #{solvers.join(", ")}"]
74
+
75
+ failed = false
76
+ checks.each do |name, problem|
77
+ if problem&.start_with?("optional")
78
+ puts " opt #{name} - #{problem}"
79
+ elsif problem
80
+ failed = true
81
+ puts " FAIL #{name} - fix: #{problem}"
82
+ else
83
+ puts " ok #{name}"
84
+ end
85
+ end
86
+ if failed
87
+ n = checks.count { |_, p| p && !p.start_with?("optional") }
88
+ abort("augure: doctor found #{n} issue(s) - fixes above")
89
+ end
90
+ when "rules"
91
+ if options[:mitre]
92
+ mitre_by_pack = {}
93
+ packs_dir = options[:packs] || "packs"
94
+ Dir.glob(File.expand_path(File.join(packs_dir, "*.yml"))).each do |path|
95
+ pack = Augure::Pack.load_file(path)
96
+ (mitre_by_pack[pack.technique] = pack.mitre) if pack.mitre
97
+ rescue
98
+ nil
99
+ end
100
+ if options[:json]
101
+ puts JSON.pretty_generate("schema" => "augure/mitre-navigator@1", "layers" => [{
102
+ "name" => "augure techniques", "versions" => {"attack" => "14", "navigator" => "4.9"},
103
+ "techniques" => mitre_by_pack.map { |tech, codes| codes.map { |c| {"techniqueID" => c, "score" => 1, "comment" => tech} } }.flatten
104
+ }])
105
+ exit 0
106
+ end
107
+ mitre_by_pack.each { |tech, codes| puts "#{tech}\t#{codes.join(", ")}" }
108
+ exit 0
109
+ end
110
+ Augure::Rules.all.each do |rule|
111
+ puts rule.id
112
+ puts " head: #{rule.head.join("=")}"
113
+ puts " why: #{rule.source}"
114
+ end
115
+ when "explain"
116
+ technique = ARGV.shift or abort(parser.to_s)
117
+ packs = options[:packs] && Augure::PackLoader.load_dir(options[:packs]).tap { |ps| Augure::PackLoader.corpus_guard(ps) }
118
+ rules = Augure::Rules.all + packs.to_a.flat_map(&:rules)
119
+ rules = rules.select { |r| r.head == [Augure::Rules::APPLICABLE, technique] }
120
+ entries = Augure::KnowledgeBase.corpus_with(packs.to_a.flat_map(&:kb))
121
+ .select { |e| e["technique"] == technique }
122
+ if rules.empty? && entries.empty?
123
+ known = Augure::Rules.applicable_rules.map { |r| r.head[1] }.uniq.sort
124
+ abort("augure: unknown technique '#{technique}' - known: #{known.join(", ")}")
125
+ end
126
+ puts "technique: #{technique}"
127
+ rules.each do |rule|
128
+ puts " rule: #{rule.id} - #{rule.source}"
129
+ end
130
+ entries.each do |e|
131
+ puts " kb: #{e["id"]} (success_rate #{e["success_rate"]})"
132
+ puts " #{e["description"]}"
133
+ end
134
+ when "coverage"
135
+ entries = if options[:facts_entry]
136
+ [{"suite" => "facts", "name" => File.basename(options[:facts_entry]),
137
+ "facts" => File.read(options[:facts_entry])}]
138
+ else
139
+ corpus = JSON.parse(File.read(File.expand_path("../lib/augure/ctf_corpus.json", __dir__)))
140
+ corpus["suites"].values.flatten
141
+ end
142
+ unless options[:flips]
143
+ abort("augure: coverage needs at least one --flip NAME:VALUE (nx:true, canary:true, relro:full)")
144
+ end
145
+ report = Augure::Coverage.report(entries, flips: options[:flips])
146
+ if options[:json]
147
+ puts JSON.pretty_generate("schema" => "augure/coverage@1", **report)
148
+ exit 0
149
+ end
150
+ entries.size
151
+ report[:entries].count { |e| e[:lost].empty? && e[:gained].empty? }
152
+ puts "the corpus read backwards: #{entries.size} targets, flips #{options[:flips].inspect}"
153
+ puts "targets that change their applicable set: #{report[:entries].count { |e| e[:lost] || e[:gained] } - report[:entries].count { |e| e[:lost].empty? && e[:gained].empty? }}"
154
+ report[:died].each { |tech, n| puts " died: #{tech} (#{n} target(s))" }
155
+ report[:gained].each { |tech, n| puts " gained: #{tech} (#{n} target(s))" }
156
+ puts "untouched targets: #{report[:survived_count]}"
157
+ puts "the remaining applicable techniques are the attacker's next moves - harden accordingly."
158
+ when "analyze"
159
+ path = ARGV.shift or abort(parser.to_s)
160
+ input = (path == "-") ? $stdin.read : File.read(path)
161
+ begin
162
+ packs = options[:packs] && Augure::PackLoader.load_dir(options[:packs]).tap { |ps| Augure::PackLoader.corpus_guard(ps) }
163
+ result = Augure::Pipeline.analyze(facts: input, seed: options[:seed], packs: packs)
164
+ if options[:json]
165
+ puts JSON.pretty_generate({schema: "augure/decision@1"}.merge(result))
166
+ else
167
+ puts "applicable: #{result[:applicable].join(", ")}"
168
+ puts "selected: #{result[:selected]}"
169
+ puts "plan: #{result[:plan].join(" -> ")}"
170
+ result[:explain][:provenance].each do |tech, provs|
171
+ provs.each do |p|
172
+ puts " #{tech} <- #{p[:rule]} (#{p[:evidence].map { |e| e.join("=") }.join(", ")})"
173
+ end
174
+ end
175
+ end
176
+ rescue Augure::MalformedFact, Augure::UnknownPredicate => e
177
+ abort("augure: #{e.message}")
178
+ end
179
+ else
180
+ abort(parser.to_s)
181
+ end
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "optparse"
6
+ require_relative "../lib/augure"
7
+
8
+ # Concordance benchmark against the frozen CTF corpus, in the spirit of the
9
+ # Python prototype's benchmark harness: predictions vs documented truth,
10
+ # per suite, with an explicit exit code gate.
11
+
12
+ threshold = 1.00
13
+ OptionParser.new do |opts|
14
+ opts.banner = "Usage: augure-benchmark <corpus.json> [--min-top1 N]"
15
+ opts.on("--min-top1 N", Float, "Fail below this top-1 concordance (default 1.00)") { |v| threshold = v }
16
+ opts.on("--version", "Print the version") do
17
+ puts "augure #{Augure::VERSION} (benchmark)"
18
+ exit 0
19
+ end
20
+ end.parse!(ARGV)
21
+
22
+ path = ARGV.shift or abort("Usage: augure-benchmark <corpus.json> [--min-top1 N]")
23
+ corpus = JSON.parse(File.read(path))
24
+ meta_priors = corpus.dig("meta", "priors")
25
+
26
+ puts " augure-benchmark: prediction vs documented ground truth"
27
+ puts
28
+
29
+ correct_top1 = 0
30
+ correct_top3 = 0
31
+ n = 0
32
+
33
+ corpus["suites"].each do |suite, entries|
34
+ suite_ok1 = 0
35
+ suite_ok3 = 0
36
+ entries.each do |entry|
37
+ result = Augure::Pipeline.analyze(facts: entry["facts"], priors: meta_priors)
38
+ top3 = result[:ranking].first(3)
39
+ # An empty frozen verdict is a verdict: concordance = the engine
40
+ # reproduces it exactly (the pack layer owns the technique; the
41
+ # built-in table intentionally says nothing).
42
+ if entry["applicable"].empty?
43
+ ok = result[:applicable].empty?
44
+ suite_ok1 += 1 if ok
45
+ suite_ok3 += 1 if ok
46
+ next
47
+ end
48
+ suite_ok1 += 1 if result[:ranking].first == entry["truth"]
49
+ suite_ok3 += 1 if top3.include?(entry["truth"])
50
+ end
51
+ count = entries.size
52
+ n += count
53
+ correct_top1 += suite_ok1
54
+ correct_top3 += suite_ok3
55
+ puts format(" %-12s %d/%d top-1 %d/%d top-3",
56
+ suite, suite_ok1, count, suite_ok3, count)
57
+ end
58
+
59
+ puts
60
+ pct = correct_top1.to_f / n
61
+ puts format(" Top-1 concordance: %d/%d (%.0f%%)", correct_top1, n, pct * 100)
62
+ puts format(" Top-3 concordance: %d/%d (%.0f%%)", correct_top3, n, (correct_top3.to_f / n) * 100)
63
+
64
+ if pct < threshold
65
+ puts format(" BELOW THRESHOLD (%.0f%% < %.0f%%)", pct * 100, threshold * 100)
66
+ exit 1
67
+ end