A static analyzer for Solana/Anchor programs, written in Rust. It parses real
Rust source with syn (no regex pattern matching) and reports security findings
with file/line/snippet evidence.
solguard is syntactic. It walks the AST and reasons about collected items, context structs, fields, attributes and expressions. It has no dataflow analysis, cannot track values across function or instruction boundaries, cannot evaluate arbitrary Anchor constraints, and cannot prove anything. It finds suspicious patterns, not proven vulnerabilities. It does not replace a manual audit. Treat every finding as a lead to verify by hand.
| ID | Check | Severity | Reference |
|---|---|---|---|
| SG000 | File could not be read/parsed (not analyzed) | error | no reference |
| SG001 | Missing signer check: raw AccountInfo/UncheckedAccount used in a handler without Signer typing, is_signer, seeds, or #[account(signer)] |
error | CWE-284 |
| SG002 | Missing owner check: raw account with no constraint/seeds/address attribute and no /// CHECK: doc |
error | CWE-345 |
| SG003 | Unchecked arithmetic (+,-,*) in a handler (not a checked_/saturating_/wrapping_ variant) |
warning | CWE-190 |
| SG004 | .unwrap()/.expect() in a handler — panic-as-DoS |
warning | CWE-670 (approximation) |
| SG005 | init_if_needed — reinitialization risk; requires justification |
error | Anchor docs (reinit attack) |
| SG006 | State with an authority/owner field used via Account<T> without has_one/constraint/seeds/close |
warning | CWE-862 |
| SG007 | PDA bump re-derived at runtime instead of stored (bump = x.bump) |
info | no reference |
| SG008 | close with no visible zeroing of user data — account revival risk |
info | Anchor close docs / account revival |
Severity model:
- error — pattern that is a direct enabler of a real attack class; fix or justify.
- warning — likely exploitable but context-dependent (panics, overflow, missing has_one).
- info — hygiene / defense-in-depth; often intentional.
These are documented heuristics that reduce false positives; they can also hide a real bug, so read them as "the tool stays quiet", not "the code is safe":
/// CHECK: ...doc comment on the field (Anchor's manual-review convention).seeds = [...](PDA-derived address),#[account(signer)],constraint,address,closeattributes.- Handler body mentions
is_signer,.owner, or arequire*/assert*macro.
solguard <PATH> # human output (default)
solguard <PATH> -f json # machine-readable findings
solguard <PATH> -f sarif # SARIF 2.1.0 (GitHub code scanning)
solguard <PATH> --fail-on error # CI gate: exit 1 if any error finding
Exit codes: 0 = no findings at/above threshold, 1 = threshold exceeded
(only with --fail-on), 2 = usage/IO error.
Top level is an array of findings:
{
"check_id": "SG001", // stable check identifier
"severity": "error", // "error" | "warning" | "info"
"reference": "CWE-284", // verified reference or "no reference"
"file": "programs/x/src/lib.rs",
"line": 17, // 1-based
"column": 4, // 1-based
"snippet": "pub authority: UncheckedAccount<'info>,",
"message": "short summary",
"explanation":"why this is a problem (attack class)"
}SARIF output maps severity: error→error, warning→warning, info→note,
validated against the official sarif-2.1.0 JSON schema.
- name: solguard
run: |
cargo install --path . || true
solguard programs/ --fail-on error--fail-on accepts error, warning or info; only findings at or above the
threshold cause exit 1.
- No dataflow or taint analysis — a value crossing functions is invisible.
- No semantic evaluation of arbitrary
constraint = <expr>predicates. - No cross-instruction / cross-transaction reasoning (reentrancy-style state confusion, oracle manipulation across instructions, etc.).
- No CPI-target analysis, no token-standard awareness (SPL/TOKEN-2022 rules).
- Cannot verify whether a
/// CHECK:justification is actually correct — it trusts the comment. - SG003 assumes Anchor defaults (overflow panics); non-Anchor builds without
overflow-checkssilently wrap, which is worse than reported. - It will miss anything requiring semantic understanding. It does not replace a manual audit.
Run against coral-xyz/anchor's own example programs (real, public Anchor code):
tests/escrow— 2 findings (both SG008, info). True positives ~50%: Anchor'scloseon a PDA does leave user data intact; the fixture handlers do not zero it, but exploitability depends on PDA re-creation, which is impossible for these fixed-seed PDAs. Honest rating: plausible-but-not-exploitable.tests/multisig— 5 findings, all SG006 (warning). Mixed: onApproveandExecuteTransactionthe multisig IS protected viahas_oneon other fields plus a derived-signer pattern the analyzer does not model; theCreateMultisighit is on#[account(zero)](init path), which the tool does not recognize. Mostly false positives — SG006 is the weakest check.tests/misc— 77 findings. The SG005 hits (init_if_needed) are true positives by design — that test program exists to exerciseinit_if_needed. SG004.unwrap()hits are true positives (real panics in handlers, deliberately in the test suite). SG007/SG008 are accurate pattern matches but low-severity hygiene.
Overall FP assessment: SG001/SG002/SG005/SG004 are high-precision on these
repos; SG006 is noisy (~70% FP on real code) and SG007/SG008 are accurate but
low-value. Use --fail-on error to gate on the reliable subset.
cargo build
cargo test # fixtures + CI-gate exit-code test
Fixtures live in tests/fixtures/{vulnerable,safe}/ — one vulnerable and one
safe program asserting zero false positives.
MIT