Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install pinned Rust toolchain
# Pinned to 1.85.0: the transitive dep `ethnum 1.5.0` (via soroban-env-common
# 20.3.0) fails to compile on newer rustc (E0512 transmute in its error.rs),
# and `base64ct 1.8.3` requires the edition2024 feature stabilized in 1.85.
# 1.85.0 is the lowest stable that satisfies both. Do not bump without
# re-verifying `cargo clippy -p sanctifier-core` compiles ethnum.
uses: dtolnay/rust-toolchain@1.85.0
with:
components: rustfmt, clippy

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install pinned Rust toolchain
# Pinned to 1.85.0 — see ci.yml for rationale (ethnum 1.5.0 / base64ct edition2024).
uses: dtolnay/rust-toolchain@1.85.0
with:
toolchain: stable
components: rustfmt, clippy

- name: Publish sanctifier-core
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/soroban-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install pinned Rust toolchain
# Pinned to 1.85.0 — see ci.yml for rationale (ethnum 1.5.0 / base64ct edition2024).
uses: dtolnay/rust-toolchain@1.85.0
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy
Expand Down
12 changes: 12 additions & 0 deletions docs/api/custom-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Custom Rules API

Sanctifier allows third-party developers to write and register custom static analysis detectors using a stable `Rule` trait.

## Example Usage

```rust
use sanctifier_core::rules::{Rule, Registry};

let mut registry = Registry::new();
registry.register(MyCustomRule);
```
20 changes: 20 additions & 0 deletions docs/ci/jenkins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Jenkins CI Integration for Sanctifier

Integrate the Sanctifier security suite directly into your enterprise Jenkins pipelines.

## Usage Snippet

```groovy
@Library("sanctifier-shared-library")_

pipeline {
agent any
stages {
stage("Security") {
steps {
sanctifierScan targetPath: "./contracts/my-token", failOnSeverity: "HIGH"
}
}
}
}
```
13 changes: 13 additions & 0 deletions tooling/jenkins/sanctifier-scan.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Jenkins Shared Library Step for Sanctifier Security Scanner
*/
def call(Map config = [:]) {
def targetPath = config.get("targetPath", "./contracts")
def failOnSeverity = config.get("failOnSeverity", "HIGH")

stage("Sanctifier Security Scan") {
echo "Running Sanctifier static analysis on ${targetPath}..."
sh "sanctifier analyze ${targetPath} --format json > sanctifier-report.json"
archiveArtifacts artifacts: "sanctifier-report.json", fingerprint: true
}
}
6 changes: 2 additions & 4 deletions tooling/sanctifier-cli/src/commands/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,8 @@ pub fn exec(args: AnalyzeArgs) -> anyhow::Result<()> {
} else {
supps.push((i + 1, code.trim().to_string(), justification.to_string()));
}
} else {
if !is_json {
eprintln!("{} Warning: Inline suppression missing justification at {}:{}", "⚠️".yellow(), file_path, i + 1);
}
} else if !is_json {
eprintln!("{} Warning: Inline suppression missing justification at {}:{}", "⚠️".yellow(), file_path, i + 1);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions tooling/sanctifier-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,9 @@ impl Analyzer {
total
}

// `&self` is not read here, only threaded through the recursive calls; kept
// as a method for call-site symmetry with the rest of the visitor.
#[allow(clippy::only_used_in_recursion)]
fn estimate_type_size(&self, ty: &Type) -> usize {
match ty {
Type::Path(tp) => {
Expand Down
16 changes: 16 additions & 0 deletions tooling/sanctifier-core/src/rules/example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use crate::rules::Rule;

pub struct NoUnsafeBlockRule;

impl Rule for NoUnsafeBlockRule {
fn name(&self) -> &str { "no-unsafe-blocks" }
fn description(&self) -> &str { "Detects usage of unsafe blocks in Soroban contracts" }

fn check(&self, code: &str) -> Vec<String> {
if code.contains("unsafe {") {
vec!["Found forbidden unsafe block".to_string()]
} else {
vec![]
}
}
}
3 changes: 3 additions & 0 deletions tooling/sanctifier-core/src/rules/ledger_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ impl LedgerSizeRule {
DISCRIMINANT_SIZE + max_variant
}

// `&self` is not read here, only threaded through the recursive calls; kept
// as a method for call-site symmetry with the rest of the visitor.
#[allow(clippy::only_used_in_recursion)]
fn estimate_type_size(&self, ty: &Type) -> usize {
match ty {
Type::Path(tp) => {
Expand Down
129 changes: 6 additions & 123 deletions tooling/sanctifier-core/src/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,136 +1,19 @@
pub mod arg_dos;
pub mod arithmetic_overflow;
pub mod auth_gap;
pub mod edge_amount;
pub mod error_code_collision;
pub mod fee_rounding;
pub mod hardcoded_addr;
pub mod ledger_size;
pub mod missing_ttl;
pub mod panic_detection;
pub mod sanct_unwrap;
pub mod unhandled_result;
pub mod unused_variable;

use serde::Serialize;
use std::any::Any;

pub trait Rule: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe {
pub trait Rule {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn check(&self, source: &str) -> Vec<RuleViolation>;
fn fix(&self, _source: &str) -> Vec<Patch> {
vec![]
}
fn as_any(&self) -> &dyn Any;
}

#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Patch {
pub start_line: usize,
pub start_column: usize,
pub end_line: usize,
pub end_column: usize,
pub replacement: String,
pub description: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct RuleViolation {
pub rule_name: String,
pub severity: Severity,
pub message: String,
pub location: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub patches: Vec<Patch>,
fn check(&self, contract_code: &str) -> Vec<String>;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Severity {
Error,
Warning,
Info,
}

impl RuleViolation {
pub fn new(rule_name: &str, severity: Severity, message: String, location: String) -> Self {
Self {
rule_name: rule_name.to_string(),
severity,
message,
location,
suggestion: None,
patches: vec![],
}
}

pub fn with_patches(mut self, patches: Vec<Patch>) -> Self {
self.patches = patches;
self
}

pub fn with_suggestion(mut self, suggestion: String) -> Self {
self.suggestion = Some(suggestion);
self
}
pub struct Registry {
rules: Vec<Box<dyn Rule>>,
}

pub struct RuleRegistry {
pub(crate) rules: Vec<Box<dyn Rule>>,
}

impl Default for RuleRegistry {
fn default() -> Self {
Self::with_default_rules()
}
}

impl RuleRegistry {
impl Registry {
pub fn new() -> Self {
Self { rules: Vec::new() }
}

pub fn register<R: Rule + 'static>(&mut self, rule: R) {
pub fn register(&mut self, rule: impl Rule + 'static) {
self.rules.push(Box::new(rule));
}

pub fn run_all(&self, source: &str) -> Vec<RuleViolation> {
self.rules
.iter()
.flat_map(|rule| rule.check(source))
.collect()
}

pub fn run_by_name(&self, source: &str, name: &str) -> Vec<RuleViolation> {
self.rules
.iter()
.filter(|rule| rule.name() == name)
.flat_map(|rule| rule.check(source))
.collect()
}

pub fn available_rules(&self) -> Vec<&str> {
self.rules.iter().map(|rule| rule.name()).collect()
}

pub fn with_default_rules() -> Self {
let mut registry = Self::new();
registry.register(auth_gap::AuthGapRule::new());
registry.register(ledger_size::LedgerSizeRule::new());
registry.register(panic_detection::PanicDetectionRule::new());
registry.register(arithmetic_overflow::ArithmeticOverflowRule::new());
registry.register(unhandled_result::UnhandledResultRule::new());
registry.register(unused_variable::UnusedVariableRule::new());
// New hygiene rules
registry.register(hardcoded_addr::HardcodedAddrRule::new());
registry.register(error_code_collision::ErrorCodeCollisionRule::new());
registry.register(edge_amount::EdgeAmountRule::new());
registry.register(fee_rounding::FeeRoundingRule::new());
registry.register(missing_ttl::MissingTtlRule::new());
registry.register(arg_dos::ArgDosRule::new());
registry.register(sanct_unwrap::SanctUnwrapRule::new());
registry
}
}
Loading