This guide walks you through creating, validating, packaging, and sharing a custom Sanctifier rule using the YAML rule format.
- Sanctifier CLI installed (
cargo install sanctifieror see Getting Started) - A Soroban smart-contract project to lint
- Basic familiarity with YAML
A Sanctifier YAML rule has the following top-level fields:
- id: <unique_snake_case_id> # required — must be unique in the file
name: <Human Readable Name> # required
description: <what it catches> # required
severity: error | warning | info
matcher:
type: <matcher_type> # see §2 for available types
# ... type-specific fieldsRules are stored in a file you reference from .sanctify.toml:
[rules]
custom = ["custom-rules.yaml"]Soroban contracts must never call panic! directly in contract entry-points.
A panic unwinds the WASM host and gives no structured error to callers.
Use Result<T, E> and return Err(...) instead.
Create custom-rules.yaml in your project root:
# custom-rules.yaml
- id: no_panic_in_contractimpl
name: No panic! in #[contractimpl] blocks
description: >
Calling panic!() inside a #[contractimpl] block crashes the WASM host
without a structured error. Return a typed Err(...) instead.
severity: error
matcher:
type: regex
pattern: 'panic!\s*\('
scope: contractimpl # only flag matches inside #[contractimpl] blocksAdd it to .sanctify.toml:
[rules]
custom = ["custom-rules.yaml"]sanctifier check --manifest-path Cargo.tomlSample output when a violation is found:
ERROR [no_panic_in_contractimpl] src/lib.rs:42:9
No panic! in #[contractimpl] blocks
| panic!("transfer failed");
= help: return Err(ContractError::TransferFailed) instead
If a particular panic! is intentional, annotate the line:
panic!("unreachable"); // sanctifier: ignore[no_panic_in_contractimpl]type |
Key fields | Use for |
|---|---|---|
regex |
pattern, scope? |
Raw text patterns |
function_call |
name, args? |
Calls to a named free function |
method_call |
method, receiver? |
Method calls (obj.method(...)) |
storage_operation |
operation (get/set/remove), key_pattern? |
DataStore read/write patterns |
See custom-rules.example.yaml for one example of each type.
Before committing, validate your rule file:
sanctifier rules validate custom-rules.yamlCommon validation errors:
| Error | Fix |
|---|---|
duplicate id |
Each id: must be unique across all loaded rule files |
unknown matcher type |
Check spelling — types are lowercase |
invalid severity |
Must be error, warning, or info |
my-soroban-rules/
├── rules/
│ └── no-panic.yaml
└── README.md
Reference it from any project:
[rules]
remote = [
{ git = "https://github.com/your-org/my-soroban-rules", rev = "v1.0.0" }
]Add a sanctifier/ directory to your crate:
[package.metadata.sanctifier]
rules = ["sanctifier/rules.yaml"]Downstream users who add your crate automatically inherit the rules.
- Open a PR to HyperSafeD/Sanctifier adding your rule to
custom-rules.example.yaml - Include a test fixture in
tests/fixtures/with a pass and fail case - Maintainers will review severity, description clarity, and matcher correctness
When writing built-in Rust rules that perform taint analysis, be aware that taint
must be propagated through all pattern-binding forms — not just simple let x = ...
assignments.
// user_data is a tainted function parameter
let (key, val) = user_data; // key AND val must inherit taint
env.storage().persistent().set(&key, &val); // should be flaggedIn the AST this is a syn::Stmt::Local whose pat is syn::Pat::Tuple. Iterate
pt.elems and mark every bound identifier as tainted.
let MyRecord { key, value } = record; // key AND value must inherit taintThe pattern is syn::Pat::Struct; iterate ps.fields and recurse into each
field.pat.
fn collect_pat_idents(pat: &Pat, out: &mut HashSet<String>) {
match pat {
Pat::Ident(pi) => { out.insert(pi.ident.to_string()); }
Pat::Tuple(pt) => pt.elems.iter().for_each(|e| collect_pat_idents(e, out)),
Pat::Struct(ps) => ps.fields.iter().for_each(|f| collect_pat_idents(&f.pat, out)),
Pat::TupleStruct(pts) => pts.elems.iter().for_each(|e| collect_pat_idents(e, out)),
Pat::Reference(pr) => collect_pat_idents(&pr.pat, out),
_ => {}
}
}Not handling Pat::Tuple / Pat::Struct is the most common source of false negatives
in taint passes — taint silently disappears at the destructure boundary.
custom-rules.example.yaml— full example rule settooling/sanctifier-core/src/custom_yaml_rules.rs— rule engine sourcetooling/sanctifier-core/src/rules/taint_propagation.rs— reference taint implementation- Troubleshooting Guide
- Contributing