Skip to content

Repository files navigation

Semora

CI license rust

Semantic diff for Anchor/Solana programs. Semora parses two versions of a program, normalizes each into a Semantic Model, diffs them, and classifies the result as compatible / warning / breaking plus surfaces security regressions (e.g. a dropped signer check).

frontend → model::Program → diff::SemDiff → analysis → report

Install

# from source (not yet on crates.io)
cargo install --git https://github.com/narsibhati-dev/semora --locked

Usage

# terminal report (default)
semora diff ./path/to/v1 ./path/to/v2

# machine-readable, for CI
semora diff ./v1 ./v2 --format json

# SARIF for GitHub Code Scanning (findings anchored to source lines)
semora diff ./v1 ./v2 --format sarif --output semora.sarif

# markdown for a PR comment, or an LLM review prompt
semora diff ./v1 ./v2 --format markdown
semora diff ./v1 ./v2 --format prompt | llm

# inspect the rule catalog
semora rules
semora explain SIGNER_REMOVED

Sample output:

instructions
  ~ withdraw
      ~ account authority signer: true -> false

verdict: COMPATIBLE

security findings
  HIGH SIGNER_REMOVED  instruction withdraw / account authority
      signer check removed

Each side is a source directory (scanned for .rs) or a single file.

Each side is auto-detected: a .json file is parsed as an Anchor IDL, anything else (a directory or .rs file) as Anchor source. A directory that is an Anchor workspace (has Anchor.toml) is expanded to its [workspace] members / programs/* crates automatically. IDL and source inputs can be mixed on the two sides.

semora diff ./target/idl/prog.json ./programs/prog   # old IDL vs new source

CI gate --fail-on <off|low|medium|high|critical> (default high) sets the exit code: 1 on a breaking change or a security finding at/above the threshold, 0 otherwise, 2 on error. --fail-on off disables gating.

Config an optional semora.toml (next to <NEW>, in the cwd, or via --config) sets the gate and per-rule overrides. CLI flags win. See semora.example.toml:

[gate]
fail_on = "high"

[diff]
rename_threshold = 0.5   # rename-detection sensitivity, 0.0..=1.0

[rules.INIT_IF_NEEDED_ADDED]
enabled = false          # drop this rule's findings

[rules.ADDRESS_REMOVED]
severity = "high"        # override severity

Renames are detected when a removed and an added item share enough member names (--rename-threshold, default 0.5); a lower value pairs more aggressively, a higher value only pairs near-identical bodies.

Baseline --baseline accepted.json suppresses already-accepted findings so CI only fails on new regressions:

{ "findings": [ { "rule": "SIGNER_REMOVED", "target": "instruction withdraw / account authority" } ] }

For PR gating with results in the GitHub Security tab, copy examples/github-actions/semora.yml into a consumer Anchor project it diffs the PR base against HEAD and uploads the SARIF.

Layout

module role
model/ normalized Program the spine everything else speaks
frontend/ input → Program (source = Anchor .rs via syn; idl stubbed)
diff/ Program × Program → SemDiff (serde-serializable core object)
analysis/ classify the diff: breaking (compatibility) + security (findings)
report/ render only human (terminal) and json (CI)

Tests

tests/diff_cases.rs runs one case per fixture pair under tests/fixtures/, with golden SemDiff JSON checked in per case. Regenerate goldens after an intentional shape change:

BLESS=1 cargo test

About

Semantic analysis and version intelligence for smart contracts.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages