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,160 @@
1
+ # Auditability: what it means, and what it costs each team
2
+
3
+ *Explanation. This page is the "why" behind the `explain` payload.*
4
+
5
+ ## The tools (the narrative runs)
6
+
7
+ The pattern has a name now. Kambhampati et al. call it the
8
+ LLM-Modulo framework ("LLMs Can't Plan, But Can Help Planning", ICML
9
+ 2024): the LLM proposes, external model-based verifiers dispose.
10
+ Wang et al.'s CHECKMATE (arXiv 2512.11143, 2025) measured it: Claude
11
+ Code + an external classical planner improves penetration success by
12
+ over 20%. augure-lictor IS that shape, with the receipts: the
13
+ verifier is Datalog + a frozen corpus, not prose.
14
+
15
+ > The narrative below is now a TOOL: `augure coverage --flip
16
+ > canary:true` replays the corpus under a hardening override and
17
+ > prints what died (the blue scorecard); `augure rules --mitre
18
+ > --json` exports the ATT&CK Navigator layer; the packs' `detection:`
19
+ > sections give lictor's `report --defender` its per-technique
20
+ > observables. The prose describes the design; the commands run it.
21
+
22
+ ## What auditability means here
23
+
24
+ Every decision Augure produces is a set of readable proofs, not a score:
25
+
26
+ ```json
27
+ {
28
+ "schema": "augure/decision@1",
29
+ "selected": "ret2plt",
30
+ "explain": {
31
+ "provenance": {
32
+ "ret2plt": [{
33
+ "rule": "app_ret2plt",
34
+ "origin": null,
35
+ "evidence": [["vuln", "sof"], ["nx", "true"], ["pie", "false"], ["plt", "system"]]
36
+ }]
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ When sibling rules share a head (the documented way to express OR),
43
+ EVERY firing appears - each attributed to its own rule id and origin.
44
+
45
+ Three properties fall out of that shape:
46
+
47
+ 1. **Traceability.** Every technique claim points to a rule ID and the exact
48
+ facts that satisfied it. Nothing is a vibe.
49
+ 2. **Reproducibility.** The bandit's draw is seeded. Same facts, same seed,
50
+ same decision - re-runnable years later, in front of a client, in front
51
+ of an auditor.
52
+ 3. **Disprovability.** A wrong rule is a *findable* wrong rule. When Augure
53
+ disagrees with your intuition, you can locate the disagreement precisely:
54
+ which fact was missing, which condition is too narrow. That disagreement
55
+ is a bug report or a research result - never a shrug.
56
+
57
+ The original system's motto still applies: *one is a clock, the other is a
58
+ cat*. An LLM agent improvises; a clock ticks the same way every time. You
59
+ can open a clock.
60
+
61
+ ## For red team: the defensible choice
62
+
63
+ An engagement report that says *"we chose ret2plt because the model said
64
+ so"* does not survive client scrutiny. One that says *"ret2plt was selected
65
+ by rule `app_ret2plt` because the binary imports `system@plt`, PIE is off,
66
+ and the pop-rdi gadget is present; the chain passed the payload-fit check"* -
67
+ does.
68
+
69
+ Concretely, red teams get:
70
+
71
+ - **Methodology you can defend.** The explain block drops into the report as
72
+ evidence, per target.
73
+ - **After-action replay.** Same facts + same seed = same decision. Re-run
74
+ last quarter's engagement against the current rules and see exactly what
75
+ the rule updates changed.
76
+ - **Benchmarked intuition.** The prior table encodes measured weight
77
+ inversions: human operators overweighted shellcode and `ret2libc` and
78
+ underweighted `ret2plt`. Your team can measure its own priors the same
79
+ way - and the disagreement with Augure's corpus is itself signal.
80
+
81
+ The honest boundary: the priors are **authored, not measured on your
82
+ targets**. Treat Augure's recommendation as a falsifiable hypothesis that
83
+ happens to carry its own proof trail - not as an oracle.
84
+
85
+ ## For blue team: the model read backwards
86
+
87
+ The same fact/rule table, read in reverse, is a hardening map:
88
+
89
+ - **Coverage mapping.** "Which techniques does our estate make
90
+ *impossible*?" A rule fires only if its facts hold. `app_shellcode`
91
+ requires NX off - flip the fact, the rule dies. Enumerate which
92
+ protections kill which rules, per asset class.
93
+ - **Hardening prioritization.** Each control eliminates a set of rules. The
94
+ control that kills the most rules on your asset mix is your best next
95
+ spend - computable, because the model is explicit.
96
+ - **Anti-reflex prediction.** The corpus's discriminating cases
97
+ (`stack6`/`stack7`-style: filtered return addresses) show what an
98
+ *adaptive* attacker does when the obvious technique dies: it plans the
99
+ boring alternative. Blue teams can run the same planner on their own
100
+ fact base to see the second-order moves.
101
+ - **Reconnaissance signatures.** Every fact a rule needs is something an
102
+ attacker must *learn* first: `plt("system")`, `gadget("pop_rdi_ret")`,
103
+ `glibc_minor`. The fact list is a detection surface: what your monitoring
104
+ should catch someone enumerating.
105
+
106
+ ## For purple team: the shared artifact
107
+
108
+ Red and blue arguing about "likely attack paths" argue from different
109
+ imaginations. Both teams arguing about a *rule table* argue from the same
110
+ object:
111
+
112
+ ```mermaid
113
+ graph LR
114
+ RB["blue adds a control"] --> C["fact set changes<br/>(NX on, RELRO full...)"]
115
+ C --> R["corpus re-run:<br/>rules stop firing"]
116
+ R --> P["planner predicts<br/>the next move"]
117
+ P --> D["blue validates<br/>detection for THAT move"]
118
+ D --> RB
119
+ ```
120
+
121
+ - **One vocabulary.** A technique rule is written once; red side proves it
122
+ fires, blue side proves it hurts, purple side keeps both proofs in one
123
+ repo.
124
+ - **The corpus is double-sided.** The 33 frozen CTF targets are offensive
125
+ regression tests *and* detection validation targets: when blue adds a
126
+ control, re-run the corpus, and the rules that fell are the measured
127
+ progress.
128
+ - **The loop.** Blue adds a control → fact set changes → rules stop firing →
129
+ planner predicts the next move → blue validates detection for *that*
130
+ move. That is a purple-team program with numbers on it.
131
+
132
+ ## For compliance and assurance
133
+
134
+ - **Reproducible evidence.** Decision + seed + rules version = a run you
135
+ can re-execute during an audit. Penetration-test reports can cite the
136
+ engine output the way they cite a tool version.
137
+ - **Predictions vs field results.** The benchmark numbers are model
138
+ predictions against frozen corpora. The distinction is in the README and
139
+ it stays there: an auditor cares that you know the difference.
140
+ - **Full trail (Lictor).** When the companion orchestrator executes a
141
+ decision, the persisted chain is decision → rule → module → outcome.
142
+ Every operational action traces back to a proof.
143
+
144
+ ## What auditability does NOT cover
145
+
146
+ Say it plainly:
147
+
148
+ 1. **Completeness of rules.** What is not encoded is not audited. A
149
+ technique absent from the table is invisible to the decision layer -
150
+ auditability guarantees you can inspect what was considered, not that
151
+ everything was.
152
+ 2. **Prior accuracy.** The Beta priors are authored constants calibrated
153
+ from a corpus of write-ups. They are a starting point to be fed back,
154
+ not a measurement of your targets.
155
+ 3. **The execution side.** Augure decides; Lictor executes. An audit of the
156
+ decision layer says nothing about what the executor did beyond what its
157
+ own trail records.
158
+
159
+ Auditability is a property of the *decision*, and that is exactly as far as
160
+ it reaches. Everything else still requires a human who reads.
@@ -0,0 +1,119 @@
1
+ # The build format: from a decision to bytes
2
+
3
+ augure decides; the pack says how the decision becomes a payload.
4
+ This page documents the BUILD format: the slots vocabulary, how a
5
+ technique becomes buildable, and where every number comes from.
6
+
7
+ ## The lineage (all published)
8
+
9
+ The build layer descends from a line of published work:
10
+
11
+ - **metasm** - Y. Guillot, *"Metasm: un framework de manipulation de
12
+ code en Ruby"*, SSTIC 2007 (also hack.lu 2007).
13
+ - **Déprotection semi-automatique de binaire** - Guillot & Gazet,
14
+ SSTIC 2008: static unpacking driven by metasm's disassembler.
15
+ - **Exploitation automatique avec metasm** - esec-lab (Sogeti),
16
+ June 2010: the automatic-exploitation loop this project re-derives,
17
+ auditable this time.
18
+ - **miasm** - C. Desclaux, SSTIC 2012: metasm's Python successor. The
19
+ strategy-sim prototype carries a miasm backend in its profiler - the
20
+ docstring says "Python equivalent of Metasm", literally.
21
+ - **Ghost writing** - screwnomore, 2015: shellcode as preprocessor
22
+ templates that mutate per build. Our shellcode stubs are assembly
23
+ SOURCE with equivalent variants picked by a seeded rng.
24
+
25
+ ## Where the numbers come from
26
+
27
+ | Number | Source | Provenance in the trail |
28
+ |---|---|---|
29
+ | the offset | the frame read statically (`lea reg, [rbp-N]` before the unsafe call) or the cyclic pattern crashed under the debugger | `offset_source: static-frame` / `gdb-cyclic` |
30
+ | the return address | the facts: `win_symbol("win", addr)` or a gadget address | `ret_source: win_symbol:win@0x...` |
31
+ | the gadget | the profiler's byte patterns, then RE-DECODED at the claimed address (a lying gadget is refused) | the gadget fact's address |
32
+ | the shellcode | assembly SOURCE in `augure-profiler/shellcode.rb`, assembled by metasm | the stub name |
33
+
34
+ Nothing is guessed. A number the facts cannot supply is a loud
35
+ `BuildError`, never a magic constant.
36
+
37
+ ## The slots vocabulary
38
+
39
+ A build layout is an ordered list of slots (data, in
40
+ `lictor/lib/config/build_layouts.yml` or a pack's `build:` section):
41
+
42
+ | Slot | Arguments | Emits |
43
+ |---|---|---|
44
+ | `padding` | `measured_offset`, optional `sled` | the measured offset of `A` (or `\x90`) |
45
+ | `qword` | `win` or `gadget:TYPE` | the fact's address, 8 bytes |
46
+ | `dword` | same | 4 bytes (x86), whatever the arch |
47
+ | `shellcode` | `sh`, `bash`, `orw` (+ the flag path as the third element), or `file` with `--payload-file` | the assembled stub |
48
+ | `chain` | one or more refs | one arch-width word per ref, in order |
49
+
50
+ The `chain` refs resolve against the facts, loud `BuildError` on any
51
+ missing piece: `gadget:TYPE`, `plt_addr:NAME`, `got_addr:NAME` (the
52
+ profiler's address facts), `win`, `leak:NAME` (a
53
+ `leaked_address(name)` fact - the harvest, the run's leak capture),
54
+ `const:N` (a literal - a syscall number, an offset). The built-in
55
+ layouts cover `ret2plt` (the puts@got leak chain) and `ret2libc`
56
+ (post-leak: ret-align + pop_rdi + binsh + system); the `orw` layout
57
+ strikes through the leaked buffer address. The `ret2csu_v2` pack
58
+ teaches its own chain (the canonical csu leak) in `build:` - packs
59
+ teach, the builder follows.
60
+
61
+ Under `seccomp("true")` the `sh` stub is a booby trap (execve dies):
62
+ the builder switches the stub to the orw chain (open/read/write,
63
+ clean exit) with the flag path embedded.
64
+
65
+ The armor validates a pack's `build:` section: the technique must be
66
+ the pack's own, the layout must be a slot list from the vocabulary
67
+ (padding, qword, dword, shellcode, chain), the refs must name
68
+ existing predicates. A refused layout never reaches the builder.
69
+
70
+ ## What metasm does here
71
+
72
+ 1. **The frame read** (`StaticOffset`): decode the ELF, resolve the
73
+ unsafe imports' PLT stubs (`.rela.plt` order; both the classic
74
+ `.plt` and the CFI `.plt.sec` layouts), disassemble `.text` from
75
+ every function symbol, net the `e8 rel32` call to an unsafe sink,
76
+ read the buffer's `rbp` displacement: offset = saved rbp + return
77
+ slot - displacement. Validated against the canonical numbers
78
+ (ROP Emporium ret2win x64 = 40, split = 40).
79
+ 2. **The shellcode**: `Metasm::Shellcode.assemble` on committed
80
+ assembly text; the call-back layout keeps every stub
81
+ position-independent; the live-execution test in the suite runs our
82
+ own assembled bytes.
83
+ 3. **The gadget verification**: the bytes at a claimed gadget address
84
+ are re-decoded and must match the advertised semantics.
85
+ 4. **The crash identification**: the static side names the vulnerable
86
+ function and its sink; the control evidence is the known pattern
87
+ found in the registers - exact, not inferred from 0x41414141 shapes.
88
+ 5. **The general gadget hunt**: every offset of the executable
89
+ sections, a decoded chain ending on ret, classified semantically -
90
+ the multi-instruction gadgets (pop rsi; pop r15; ret) the byte
91
+ patterns never see.
92
+ 6. **The dynamic layer**: the write-site proof (a watchpoint on the
93
+ return slot catches the instruction that plants the overflow - the
94
+ taint proven, not inferred), the runtime map (libc, stack of a live
95
+ run), and lictor's harvest (the service's banner becomes
96
+ leaked_address facts).
97
+ 7. **The encoders**: the keyed self-decoder with the backward-call
98
+ get-pc (a negative rel32 carries ff bytes, never 00), the payload
99
+ nop-padded under 128 bytes, the keys per seed. The shikata
100
+ inheritance, auditable: the seed reproduces the exact bytes. A
101
+ payload wider than 120 bytes on a 0x00 channel is refused honestly.
102
+
103
+ No emulation: the suite's live-execution tests are the truth.
104
+
105
+ ## Teaching a buildable technique
106
+
107
+ Write the pack (rules, kb, mcts, priors per the pack format), add its
108
+ `build:` section with the layout, run the armor:
109
+
110
+ ```sh
111
+ lictor rule validate my_pack.yml # the armor + the corpus guard
112
+ lictor rule install my_pack.yml # the technique becomes decidable AND buildable
113
+ lictor plan target.facts --packs packs/
114
+ lictor build ret2mything --facts target.facts
115
+ ```
116
+
117
+ The LLM path goes through the same armor: `validate_pack` and
118
+ `scaffold_exploit` on the MCP surface are the same doors, staged for
119
+ review.
@@ -0,0 +1,74 @@
1
+ # Architecture: why rules-as-data
2
+
3
+ *Explanation. The design rationale.*
4
+
5
+ ## The one decision that shapes everything
6
+
7
+ > **Separate what the target IS (facts) from what you DO about it (rules),
8
+ > and make both data.**
9
+
10
+ Everything else follows. The fact schema is a schema; the rules are rows;
11
+ the engine is an interpreter over the rows. That separation is what makes
12
+ an auditable decision layer possible at all:
13
+
14
+ - **Provenance**: only data can answer "which rule fired because of which
15
+ facts" - a case statement cannot explain itself.
16
+ - **A second engine**: rules-as-data can be generated into a the frozen corpus .dl
17
+ program, so a compiled Datalog engine and the Ruby engine can be
18
+ conformance-tested against each other - the frozen corpus catches any
19
+ drift between them.
20
+ - **The reverse reading**: "which protections kill which techniques" is
21
+ a table traversal, not a code trace.
22
+
23
+ ## The engine is boring on purpose
24
+
25
+ Forward-chaining fixpoint over a tiny rule set: derive tuples until nothing
26
+ new derives. No backtracking, no unification beyond equality, no cuts. The
27
+ entire semantics fits in one file and one afternoon of reading. Boring is
28
+ the point: auditable systems are systems a reviewer can finish.
29
+
30
+ Negation is stratified by convention (only on input facts) and enforced by
31
+ the corpus spec. Defaults are closed-world (`nx` absent means `nx("true")`)
32
+
33
+ - documented, tested, and the single most surprising line in the engine.
34
+
35
+ ## Uncertainty, in three layers
36
+
37
+ ```mermaid
38
+ graph TD
39
+ AG["Genetic algorithm<br/>offline, synthetic targets"] -- "finds weight inversions" --> P["prior table<br/>(ret2plt 15,5 vs the human ROP anchor)"]
40
+ P --> B["Bandit: Thompson sampling<br/>online, flat, auditable (seeded)"]
41
+ B -- "blind spot: choices look independent" --> M["MCTS over STAGE_MODEL<br/>a leak is worth what it unlocks"]
42
+ M --> D["plan: fmtstr_leak -> ret2libc"]
43
+ ```
44
+
45
+ A flat bandit ranks `ret2libc` above a leak; MCTS plans the leak first
46
+ because the transition table says what it unlocks. The planner's lesson,
47
+ as a diagram.
48
+
49
+ 1. **Genetic algorithm (offline).** Evolved weight vectors over synthetic
50
+ targets; found the inversions humans miss. Research tool, stays in
51
+ Python - it is the narrative, not the SDK.
52
+ 2. **Bandit (online, flat).** Thompson sampling over Beta posteriors.
53
+ Adapts per target, remains auditable (seeded draws, logged feedback).
54
+ Blind spot: treats choices as independent.
55
+ 3. **MCTS (sequential).** Plans multi-step trajectories: a leak is worth
56
+ what it unlocks, which no flat weight can express. The STAGE_MODEL is a
57
+ transition table - data again, so the planner stays inspectable.
58
+
59
+ ## Subprocesses are the API policy
60
+
61
+ SMT solvers, LLM providers: every external capability enters
62
+ through a serialization boundary (facts text, SMT-LIB text, HTTP). That is
63
+ what makes the zero-runtime-dependency gem possible, keeps native-extension
64
+ attack surface at zero, and lets a consumer swap bitwuzla for cvc5 or an
65
+ LLM backend without touching the core.
66
+
67
+ ## New proposal sources, and why they do not change this
68
+
69
+ The profiler, the fuzzer's triage, and any future LLM feature extractor
70
+ all emit *facts* into the same boundary. A hypothetical LLM hypothesis
71
+ generator stays behind the whitelist: it proposes facts, the schema
72
+ rejects anything else, the engine never sees a prose sentence. The
73
+ architecture accepts new proposal sources because the contract was never
74
+ "trust the source"; it is "speak fact, or leave".
@@ -0,0 +1,73 @@
1
+ # How to: plug a real SMT solver
2
+
3
+ Assumes augure is installed. Goal: route the numerical checks through z3,
4
+ bitwuzla or cvc5 instead of the arithmetic fast path.
5
+
6
+ ## 1. Install a solver
7
+
8
+ ```sh
9
+ apt install z3 # or brew install z3
10
+ # bitwuzla and cvc5 have upstream binaries
11
+ ```
12
+
13
+ ## 2. Emit the SMT-LIB and solve it
14
+
15
+ ```ruby
16
+ require "augure"
17
+
18
+ smt = Augure::Verifier.payload_fits_smtlib(256, 40)
19
+ puts smt
20
+ # (set-logic QF_LIA)
21
+ # (declare-const buffer_size Int)
22
+ # (assert (= buffer_size 256))
23
+ # (assert (>= buffer_size 40))
24
+ # (check-sat)
25
+
26
+ status = Augure::SmtProcess.solve(smt, solver: "z3")
27
+ # => :sat
28
+ ```
29
+
30
+ The contract is text-in, status-atom-out:
31
+
32
+ | Return | Meaning |
33
+ |---|---|
34
+ | `:sat` | constraint satisfied |
35
+ | `:unsat` | constraint violated |
36
+ | `:unknown` | solver missing, timed out, or answered garbage |
37
+
38
+ `:unknown` is deliberate: a verification *failure* must be visible and
39
+ logged, never a crash. Treat `:unknown` as "do not claim verified".
40
+
41
+ ## 3. Solver options
42
+
43
+ ```ruby
44
+ Augure::SmtProcess.solve(smt, solver: "bitwuzla", timeout: 5)
45
+ ```
46
+
47
+ - `solver` - any binary that reads SMT-LIB v2 on stdin.
48
+ - `timeout` - seconds (default 10). Expiry returns `:unknown` and kills the
49
+ child process.
50
+
51
+ ## 4. Which path should you use?
52
+
53
+ The arithmetic fast path answers the same questions without a subprocess:
54
+
55
+ | | arithmetic | SMT solver |
56
+ |---|---|---|
57
+ | latency | nanoseconds | milliseconds (process spawn) |
58
+ | dependencies | none | solver binary |
59
+ | audit value | provable by inspection | provable by solver certificate |
60
+
61
+ Default to arithmetic; reach for the solver when a check outgrows integer
62
+ arithmetic (bit-vectors, arrays, string constraints). The corpus spec pins
63
+ both paths to the same answers, so switching never silently changes a
64
+ verdict.
65
+
66
+ ## Troubleshooting
67
+
68
+ - **`:unknown` with a solver installed** - run the emitted text manually:
69
+ `z3 < smt.txt` and read its stdout. Augure only
70
+ parses a bare `sat`/`unsat` token; solver banners are fine, they are
71
+ ignored.
72
+ - **Permission denied** - the solver path is not executable; give the
73
+ absolute path.
@@ -0,0 +1,71 @@
1
+ # How to: write a technique rule
2
+
3
+ Assumes you have run the tutorial and understand the fact format. Goal: add
4
+ a rule to the applicability table with test coverage and provenance.
5
+
6
+ ## 1. Add the rule to the table
7
+
8
+ Open `lib/augure/rules.rb`. Rules live in one place - the table IS the
9
+ engine. Add yours where it belongs in the reading order:
10
+
11
+ ```ruby
12
+ b.rule :app_my_technique, ["applicable", "my_technique"],
13
+ source: "one line: when does this fire and why" do |c|
14
+ c.fact "vuln", "sof"
15
+ c.fact "nx", "true"
16
+ c.fact "plt", "pwnme"
17
+ end
18
+ ```
19
+
20
+ Condition vocabulary:
21
+
22
+ | Condition | Meaning |
23
+ |---|---|
24
+ | `c.fact rel, val` | a tuple `rel("val")` exists |
25
+ | `c.not_fact rel, val` | that tuple is absent (input relations only) |
26
+ | `c.match rel, idx, "pattern"` | arg `idx` matches the regex |
27
+ | `c.cmp rel, :ge, 26` | an integer arg satisfies the comparison |
28
+
29
+ ## 2. Test it (before the rule exists in your head, after in the file)
30
+
31
+ ```ruby
32
+ # spec/augure/engine_spec.rb
33
+ it "fires my_technique when pwnme is imported" do
34
+ f = "vuln(\"sof\").\nnx(\"true\").\nplt(\"pwnme\").\n"
35
+ expect(applicable(f)).to include("my_technique")
36
+ end
37
+
38
+ it "does not fire my_technique without NX" do
39
+ f = "vuln(\"sof\").\nnx(\"false\").\nplt(\"pwnme\").\n"
40
+ expect(applicable(f)).not_to include("my_technique")
41
+ end
42
+ ```
43
+
44
+ Run `bundle exec rspec`. If the frozen CTF corpus conformance spec now fails,
45
+ your rule changed an existing decision: that is either a bug in the rule or
46
+ an improvement - update the corpus entry and say which, in the commit.
47
+
48
+ ## 3. Check the provenance appears
49
+
50
+ ```sh
51
+ bundle exec ruby -e '
52
+ require "augure"
53
+ r = Augure::Pipeline.analyze(facts: "vuln(\"sof\").\nnx(\"true\").\nplt(\"pwnme\").\n")
54
+ p r[:explain][:provenance]["my_technique"]
55
+ '
56
+ ```
57
+
58
+ You should see `rule: :app_my_technique` with the evidence pairs. That is
59
+ the audit payload - it is automatic, you earned it by using the table.
60
+
61
+ ## Rules of the road
62
+
63
+ - **One rule, one reason.** Split OR-conditions into sibling rules sharing a
64
+ head; the provenance then tells *which* reason fired.
65
+ - **Negation on inputs only.** `not_fact` on a derived relation breaks
66
+ stratification; the corpus conformance spec will catch it loudly.
67
+ - **Defaults are closed-world.** Absent `nx` means `nx("true")` - document
68
+ any rule that depends on absence.
69
+ - **The corpus is the contract.** A rules commit that moves a corpus verdict
70
+ must update `lib/augure/ctf_corpus.json` in the same commit and justify
71
+ the move in the message.
@@ -0,0 +1,137 @@
1
+ # Pack format: teach augure a new technique (as data)
2
+
3
+ *How-to. For operators and LLM agents. You write a YAML file; the armor
4
+ decides whether augure learns it.*
5
+
6
+ ## The one rule before anything
7
+
8
+ **You write data, never code.** A pack is a constrained YAML document.
9
+ If your intent needs something the format cannot express, the format
10
+ will refuse it - that is the design, not a bug. The vocabulary exists
11
+ so that a wrong pack is impossible, not merely discouraged.
12
+
13
+ ## Anatomy
14
+
15
+ ```yaml
16
+ technique: my_new_technique # [a-z0-9_]+ - the NEW technique's name
17
+ author: claude-code (session 2026-10-03) # mandatory: who taught augure
18
+ source: https://... # mandatory: a verifiable reference
19
+ rules:
20
+ - id: app_my_new_technique # convention: app_<technique>
21
+ head: [applicable, my_new_technique]
22
+ conditions: # ONLY the 4 forms below
23
+ - [fact, vuln, sof] # a tuple vuln("sof") exists
24
+ - [not_fact, relro, full] # absent (input facts ONLY)
25
+ - [match, gadget, 0, pop] # gadget arg 0 matches /pop/
26
+ - [cmp, glibc_minor, ge, 26] # integer arg >= 26
27
+ kb:
28
+ - id: my_new_technique_001
29
+ technique: my_new_technique
30
+ description: >
31
+ What the technique is, when it applies, what it needs. This is
32
+ what the agent (or human) will read back from `augure explain`.
33
+ chain: [step_one, step_two]
34
+ unlocks: [libc_base]
35
+ success_rate: 0.7 # 0..1 - authored, say so if uncertain
36
+ mcts:
37
+ requires: [] # capabilities needed before this runs
38
+ provides: [libc_base] # capabilities it unlocks
39
+ terminal: false # true only if it yields a shell
40
+ success: 0.7 # the planner's base success
41
+ priors: [14, 6] # Beta(alpha, beta): mean 0.7 here
42
+
43
+ # All three sections below are OPTIONAL:
44
+ build: # (check 8) the assembly order - packs
45
+ technique: my_new_technique # teach, lictor's builder follows
46
+ layout: # slots: padding/qword/dword/shellcode/chain
47
+ - [padding, measured_offset]
48
+ - [chain, "gadget:pop_rdi_ret", "plt_addr:system"]
49
+
50
+ detection: # (check 9) the DEFENDER side - what the
51
+ - channel: wire # technique looks like to the defenses;
52
+ signature: "cyclic padding then a non-mapped return" # channel from
53
+ note: "the padding phase" # wire/syslog/syscall/crash/file
54
+
55
+ mitre: # (check 10) the ATT&CK mapping
56
+ - T1068
57
+ ```
58
+
59
+ ## The checks (in the order the loader runs them)
60
+
61
+ 1. **Condition vocabulary**: only `fact`, `not_fact`, `match`, `cmp`.
62
+ Anything else - including "clever" ones - is refused.
63
+ 2. **Existing predicates only**: your conditions may reference the 33
64
+ input facts (see `docs/reference.md#schema` - the binary vocabulary:
65
+ nx/pie/canary/plt/gadget/plt_addr/got_addr...; the network vocabulary,
66
+ emitted from nmap scans: service/software_version/remote; the
67
+ confidence marker: verified; the leaks: leaked_address) and the
68
+ built-in derived relations (`vuln`, `has_reg_control`,
69
+ `has_write_primitive`, `has_syscall_gadget`, `has_csu_gadget`,
70
+ `has_pivot_gadget`, `enough_gadgets`). You cannot invent a fact
71
+ source.
72
+ 3. **Stratification**: `not_fact` on input facts only.
73
+ 4. **New heads only**: your technique must be NEW. Overriding
74
+ `ret2plt` or any built-in is refused - the corpus owns those verdicts.
75
+ 5. **Priors and rates in range**: priors are exactly `[alpha, beta]`
76
+ numbers; success rates in `[0, 1]`.
77
+ 6. **Corpus guard**: with your pack loaded, all 33 frozen corpus
78
+ targets must keep their documented ranking (your technique may rank
79
+ BELOW the documented top). A pack that takes the top-1 of a
80
+ documented target is refused - claiming better on frozen truth is a
81
+ dev-time corpus update, not a runtime pack. Two documented
82
+ exceptions: an entry with an EMPTY built-in verdict has nothing to
83
+ move (additive packs extend it), and an entry documenting
84
+ `pack_verdict: <technique>` polices the pack answer's ARRIVAL
85
+ instead - the pwnable/orw case.
86
+ 7. **match patterns and cmp operands fail at load**: a `match` pattern
87
+ is precompiled (`RegexpError` at load, not at decision); `cmp`
88
+ operands must be integers and operators from the engine's table -
89
+ `"26"` or a typo'd operator never reaches the engine.
90
+ 8. **build: (optional)**: must name its own technique; the layout must
91
+ be a non-empty slot list from the vocabulary
92
+ (padding/qword/dword/shellcode/chain). The builder interprets; the
93
+ armor gates the vocabulary.
94
+ 9. **detection: (optional)**: each entry needs a `channel` from
95
+ wire/syslog/syscall/crash/file plus a concrete `signature` and
96
+ `note` - a vague hint is not detection content.
97
+ 10. **mitre: (optional)**: an array of ATT&CK technique codes
98
+ (`T1068`, `T1068.001`) only.
99
+
100
+ ## The optional `build:` and `detection:` sections
101
+
102
+ `build:` teaches the BUILDER the technique's assembly order (packs
103
+ teach, lictor's builder follows): the layout must be a slot list from
104
+ the vocabulary (padding/qword/dword/shellcode/chain - see
105
+ build-format.md), the technique must be the pack's own. Armor check 8.
106
+
107
+ `detection:` is the DEFENDER side of the technique - the corpus read
108
+ backwards. One entry per observable: `channel` from the constrained
109
+ vocabulary (wire/syslog/syscall/crash/file), a concrete `signature`,
110
+ and a `note` saying what the observable means. A vague hint is not
111
+ detection content - the armor rejects it (check 9). The blue team
112
+ consumes these with `augure rules --mitre` and the hardened-coverage
113
+ flow; a pack without `detection:` teaches the offense but leaves the
114
+ defense blind.
115
+
116
+ ## The cycle
117
+
118
+ ```sh
119
+ lictor rule validate my_pack.yml # dry-run: schema + corpus guard
120
+ lictor rule install my_pack.yml # interactive confirm, versioned, trailed
121
+ augure analyze target.facts # your technique is now decidable
122
+ ```
123
+
124
+ Via an MCP agent: the rules change every future decision - the
125
+ operator reads the validated pack before installing it.
126
+
127
+ ## Writer's discipline (for agents especially)
128
+
129
+ - Copy `packs/ret2csu_v2.yml` (the canonical example) - change the
130
+ substance, keep the structure.
131
+ - `success_rate` and `priors` are authored numbers: ground them in the
132
+ source you cite, and say so in the description if they are estimates.
133
+ - If validation refuses you, read the refusal - each check names its
134
+ reason. Fix the data; never look for a way around the armor.
135
+ - One pack = one technique. Split OR-conditions into sibling rules
136
+ sharing the head; the provenance then carries EVERY reason that
137
+ fired, each attributed to its own rule.