Semgrep Rule Creator for MultiversX
Create custom Semgrep rules to automatically detect MultiversX-specific security patterns, coding violations, and best practice issues. This skill enables scalable security scanning across codebases.
When to Use
-
Setting up automated security scanning for CI/CD
-
Enforcing MultiversX coding standards across teams
-
Scaling security reviews with automated pattern detection
-
Creating custom rules after finding manual vulnerabilities
-
Building organizational security rule libraries
- Semgrep Basics for Rust
Rule Structure
rules:
- id: rule-identifier
languages: [rust]
message: "Description of the issue and why it matters"
severity: ERROR # ERROR, WARNING, INFO
patterns:
- pattern: <code pattern to match>
metadata:
category: security
technology:
- multiversx
- pattern: <code pattern to match>
metadata:
category: security
technology:
Pattern Syntax
Syntax Meaning Example
$VAR
Any expression $X + $Y matches a + b
...
Zero or more statements { ... } matches any block
$...VAR
Zero or more arguments func($...ARGS)
<... $X ...>
Contains expression <... panic!(...) ...>
- Common MultiversX Patterns
Unsafe Arithmetic Detection
rules:
-
id: mvx-unsafe-addition languages: [rust] message: "Potential arithmetic overflow. Use BigUint or checked arithmetic for financial calculations." severity: ERROR patterns:
- pattern: $X + $Y
- pattern-not: $X.checked_add($Y)
- pattern-not: BigUint::from($X) + BigUint::from($Y)
paths:
include:
- "/src/.rs" metadata: category: security subcategory: arithmetic cwe: "CWE-190: Integer Overflow"
-
id: mvx-unsafe-multiplication languages: [rust] message: "Potential multiplication overflow. Use BigUint or checked_mul." severity: ERROR patterns:
- pattern: $X * $Y
- pattern-not: $X.checked_mul($Y)
- pattern-not: BigUint::from($X) * BigUint::from($Y)
Floating Point Detection
rules:
- id: mvx-float-forbidden
languages: [rust]
message: "Floating point arithmetic is non-deterministic and forbidden in smart contracts."
severity: ERROR
pattern-either:
- pattern: "let $X: f32 = ..."
- pattern: "let $X: f64 = ..."
- pattern: "$X as f32"
- pattern: "$X as f64" metadata: category: security subcategory: determinism
Payable Endpoint Without Value Check
rules:
- id: mvx-payable-no-check
languages: [rust]
message: "Payable endpoint does not check payment value. Verify token ID and amount."
severity: WARNING
patterns:
- pattern: | #[payable("*")] #[endpoint] fn $FUNC(&self, $...PARAMS) { $...BODY }
- pattern-not: | #[payable("*")] #[endpoint] fn $FUNC(&self, $...PARAMS) { <... self.call_value() ...> } metadata: category: security subcategory: input-validation
Unsafe Unwrap Usage
rules:
- id: mvx-unsafe-unwrap
languages: [rust]
message: "unwrap() can panic. Use unwrap_or_else with sc_panic! or proper error handling."
severity: ERROR
patterns:
- pattern: $EXPR.unwrap()
- pattern-not-inside: | #[test] fn $FUNC() { ... } fix: "$EXPR.unwrap_or_else(|| sc_panic!("Error message"))" metadata: category: security subcategory: error-handling
Missing Owner Check
rules:
- id: mvx-sensitive-no-owner-check
languages: [rust]
message: "Sensitive operation without owner check. Add #[only_owner] or explicit verification."
severity: ERROR
patterns:
- pattern: | #[endpoint] fn $FUNC(&self, $...PARAMS) { <... self.$MAPPER().set(...) ...> }
- pattern-not: | #[only_owner] #[endpoint] fn $FUNC(&self, $...PARAMS) { ... }
- pattern-not: | #[endpoint] fn $FUNC(&self, $...PARAMS) { <... self.blockchain().get_owner_address() ...> }
- metavariable-regex: metavariable: $MAPPER regex: "(admin|owner|config|fee|rate)"
- Advanced Patterns
Callback Without Error Handling
rules:
- id: mvx-callback-no-error-handling
languages: [rust]
message: "Callback does not handle error case. Async call failures will silently proceed."
severity: ERROR
patterns:
- pattern: | #[callback] fn $FUNC(&self, $...PARAMS) { $...BODY }
- pattern-not: | #[callback] fn $FUNC(&self, #[call_result] $RESULT: ManagedAsyncCallResult<$TYPE>) { ... }
Unbounded Iteration
rules:
- id: mvx-unbounded-iteration
languages: [rust]
message: "Iterating over storage mapper without bounds. Can cause DoS via gas exhaustion."
severity: ERROR
pattern-either:
- pattern: self.$MAPPER().iter()
- pattern: | for $ITEM in self.$MAPPER().iter() { ... } metadata: category: security subcategory: dos cwe: "CWE-400: Uncontrolled Resource Consumption"
Storage Key Collision Risk
rules:
- id: mvx-storage-key-short
languages: [rust]
message: "Storage key is very short, increasing collision risk. Use descriptive keys."
severity: WARNING
patterns:
- pattern: '#[storage_mapper("$KEY")]'
- metavariable-regex: metavariable: $KEY regex: "^.{1,3}$"
Reentrancy Pattern Detection
rules:
- id: mvx-reentrancy-risk
languages: [rust]
message: "External call before state update. Follow Checks-Effects-Interactions pattern."
severity: ERROR
patterns:
- pattern: | fn $FUNC(&self, $...PARAMS) { ... self.send().$SEND_METHOD(...); ... self.$STORAGE().set(...); ... }
- pattern: | fn $FUNC(&self, $...PARAMS) { ... self.tx().to(...).transfer(); ... self.$STORAGE().set(...); ... }
- Creating Rules from Findings
Workflow
-
Find a bug manually during audit
-
Abstract the pattern - what makes this a bug?
-
Write a Semgrep rule to catch similar issues
-
Test on the codebase - find all variants
-
Refine to reduce false positives
Example: From Bug to Rule
Bug Found:
#[endpoint] fn withdraw(&self, amount: BigUint) { let caller = self.blockchain().get_caller(); self.send().direct_egld(&caller, &amount); // Sends before balance check! self.balances(&caller).update(|b| *b -= &amount); // Can underflow }
Pattern Abstracted:
-
Send/transfer before state update
-
No balance validation before deduction
Rule Created:
rules:
- id: mvx-withdraw-pattern-unsafe
languages: [rust]
message: "Withdrawal sends funds before updating balance. Risk of reentrancy and underflow."
severity: ERROR
patterns:
- pattern: | fn $FUNC(&self, $...PARAMS) { ... self.send().$METHOD(...); ... self.$BALANCE(...).update(|$B| *$B -= ...); ... }
- pattern: | fn $FUNC(&self, $...PARAMS) { ... self.tx().to(...).transfer(); ... self.$BALANCE(...).update(|$B| *$B -= ...); ... }
- Running Semgrep
Command Line Usage
Run single rule
semgrep --config rules/mvx-unsafe-arithmetic.yaml src/
Run all rules in directory
semgrep --config rules/ src/
Output JSON for processing
semgrep --config rules/ --json -o results.json src/
Ignore test files
semgrep --config rules/ --exclude="*_test.rs" --exclude="tests/" src/
CI/CD Integration
GitHub Actions example
- name: Run Semgrep uses: returntocorp/semgrep-action@v1 with: config: >- rules/mvx-security.yaml rules/mvx-best-practices.yaml
- Rule Library Organization
semgrep-rules/ ├── security/ │ ├── mvx-arithmetic.yaml # Overflow/underflow │ ├── mvx-access-control.yaml # Auth issues │ ├── mvx-reentrancy.yaml # CEI violations │ └── mvx-input-validation.yaml ├── best-practices/ │ ├── mvx-storage.yaml # Storage patterns │ ├── mvx-gas.yaml # Gas optimization │ └── mvx-error-handling.yaml └── style/ ├── mvx-naming.yaml # Naming conventions └── mvx-documentation.yaml # Doc requirements
- Testing Rules
Test File Format
test/mvx-unsafe-unwrap.test.yaml
rules:
- id: mvx-unsafe-unwrap
... rule definition ...
Test cases
test_cases:
-
name: "Should match unwrap" code: | fn test() { let x = some_option.unwrap(); } should_match: true
-
name: "Should not match unwrap_or_else" code: | fn test() { let x = some_option.unwrap_or_else(|| sc_panic!("Error")); } should_match: false
Running Tests
semgrep --test rules/
- Best Practices for Rule Writing
-
Start specific, then generalize: Begin with exact pattern, relax constraints carefully
-
Include fix suggestions: Use fix: field when automated fixes are safe
-
Document the "why": Message should explain impact, not just what's detected
-
Include CWE references: Link to standard vulnerability classifications
-
Test with real codebases: Validate against actual MultiversX projects
-
Version your rules: Rules evolve as framework APIs change
-
Categorize by severity: ERROR for security, WARNING for best practices