skills/rule-authoring/SKILL.md
Writing SAiST static analysis rules — both shipped rules in the auditor-addon repo and custom per-engagement rules in audit workspaces. Use when the user wants to create a new detection rule, add a security check, implement a code smell detector, turn a confirmed finding into a reusable rule, or extend the rule set. Covers rule types (shallow, deep, MapRule), the trait system, language scoping, finding kinds, custom rules, and testing patterns.
npx skillsauth add artifex1/auditor-addon rule-authoringInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Rules live in two places:
src/static/rules/): part of auditor-addon, bundled into the server at build time. IDs use standard prefixes: SOL-, GEN-, MAP-. Adding a new shipped rule requires two files: the rule itself and an import in src/static/rules/index.ts..ts or .js file): per-engagement rules in the audit workspace. IDs must use the CUSTOM- prefix. Loaded at runtime via customRulePaths on sast_run_rules. TypeScript files are compiled on-the-fly by tsx — no build step needed.Both use the same interfaces. Each rule is a single .ts file that default-exports a Rule or MapRule object.
Rule without deep)Walks every AST node in every file. Use for patterns detectable within a single function or file without following call edges.
enter(node: Node, ctx: RuleContext): void // pre-order DFS
exit(node: Node, ctx: RuleContext): void // post-order DFS
finalize(ctx: RuleContext): FindingInstance[]
reset(): void
Use when: The pattern is visible in a single function body — node type checks, operator patterns, modifier presence. Examples: SOL-011 (div before mul), SOL-006 (floating pragma), SOL-023 (malformed modifier).
Rule with deep: { maxDepth: N })Same interface but the walker follows call edges across function boundaries. depth increments at each function transition, not each AST level.
Use when: The pattern spans multiple functions — e.g. external call in callee followed by state write in caller. Example: SOL-002 (reentrancy, deep: { maxDepth: 6 }).
Runs once against the completed SymbolMap after all files are processed. No AST traversal — operates on SymbolEntry metadata.
check(symbolMap: SymbolMap, ctx: RuleContext): FindingInstance[]
Use when: The detection requires cross-function or cross-file reasoning over the symbol table — caller counts, visibility analysis, state variable usage patterns. Examples: MAP-001 (broad visibility), MAP-002 (unused function), SOL-017 (variable could be constant).
import { SupportedLanguage } from "../../engine/types.js";
import type { Rule, FindingInstance, RuleContext } from "../../engine/types.js";
import type { Node } from "web-tree-sitter";
function createRule(): Rule {
let findings: FindingInstance[] = [];
return {
id: 'SOL-NNN',
severity: 'medium', // critical | high | medium | low | info
title: 'Short label',
description: 'What it detects and why it matters.',
kind: 'smell', // issue | smell | pointer
appliesTo: {
languages: [SupportedLanguage.Solidity],
domains: ['on-chain'], // optional: 'on-chain' | 'off-chain'
},
enter(node: Node, ctx: RuleContext) {
// pattern detection logic
},
finalize() { return findings; },
reset() { findings = []; },
};
}
export default createRule();
ctx.trait)Rules are language-agnostic through ctx.trait — the LanguageAdapter for the current language. Use trait methods instead of hardcoding node types:
| Trait method | Returns | Use for |
|---|---|---|
| isFunctionDef(node) | boolean | Detecting function boundaries |
| isExternalCall(node) | boolean | External/cross-contract calls |
| isStateWrite(node) | boolean | Storage mutations |
| isStateRead(node) | boolean | Storage reads |
| isPublicFn(node) | boolean | Public/external visibility |
| isEmitStatement(node) | boolean | Event emissions |
| getFunctionName(node) | string? | Extracting function name |
| getCallTarget(node) | string? | Extracting call target |
When to use traits vs direct node types: Use traits for concepts that exist across languages (function def, state write, external call). Use direct node.type checks for language-specific syntax (modifier_definition, pragma_directive).
appliesTo — always explicitShipped rules use the SupportedLanguage enum (available via the relative import ../../engine/types.js):
appliesTo: {
languages: [SupportedLanguage.Solidity, SupportedLanguage.Cairo],
domains: ['on-chain'], // optional
inheritanceModels: ['classical'], // optional
}
Custom rules use string literals instead — no runtime import of SupportedLanguage needed:
appliesTo: {
languages: ['solidity', 'cairo'] as any,
domains: ['on-chain'],
}
Never use empty appliesTo: {} — that matches everything. List supported languages explicitly. Only include languages whose grammar you have verified.
For Solidity-specific node types, field names, and expression-unwrapping patterns, see references/solidity-ast.md.
| Kind | When to use |
|---|---|
| issue | High confidence — confirmed defect pattern |
| smell | Medium confidence — likely problem, anti-pattern |
| pointer | Low confidence — structural pattern historically linked to bugs |
Design principle: All three kinds must have a syntactic anchor — a structural AST pattern. If detection requires understanding what a variable means (name-matching heuristics like "fee", "onBehalf"), it belongs to the agent, not a rule.
Acceptable vocabulary: well-known library functions (mulFloor, mulDiv), standards (TYPEHASH for EIP-712), language keywords. Not acceptable: arbitrary naming conventions.
Custom rules let auditors codify a pattern found during an audit and immediately test it against the codebase. The flywheel:
./rules/CUSTOM-001-unbounded-loop.ts)// ./rules/CUSTOM-001-unbounded-loop.ts
// These imports are optional — only needed for IDE type hints, erased at runtime by tsx
import type { Rule, FindingInstance, RuleContext } from "auditor-addon";
import type { Node } from "web-tree-sitter";
function createRule(): Rule {
let findings: FindingInstance[] = [];
return {
id: 'CUSTOM-001', // MUST use CUSTOM- prefix
severity: 'high',
title: 'Unbounded loop over user-controlled array',
description: 'A for-loop iterates over a storage array with no upper bound. An attacker can grow the array to cause out-of-gas reverts.',
kind: 'smell',
appliesTo: { languages: ['solidity'] as any }, // string literal, not SupportedLanguage enum
enter(node: Node, ctx: RuleContext) { /* ... */ },
finalize() { return findings; },
reset() { findings = []; },
};
}
export default createRule();
sast_run_rules({
scanId: "abc123",
customRulePaths: ["./rules/CUSTOM-001-unbounded-loop.ts"],
})
Both .ts and .js paths are accepted. Custom rules run alongside shipped rules. Use ruleIds filter to isolate custom rules if needed.
When promoting a custom rule to a shipped rule:
src/static/rules/ with a standard ID (SOL-, GEN-, MAP-)src/static/rules/index.ts and append it to the shippedRules arraytests/languages/<lang>/rules/<RULE-ID>.test.tsOne test file per rule: tests/languages/<lang>/rules/<RULE-ID>.test.ts
Helpers in tests/languages/<lang>/rules/helpers.ts: buildContext(sources) → { ctx, symbolMap }, runRule(ctx, file, rule), runMapRule(ctx, symbolMap, rule), runDeepRuleOnFunction(ctx, symbolMap, funcLabel, rule).
Each rule needs a positive case (flags the pattern) and a negative case (safe variant). Multi-language rules: one test file per affected language.
Run: npx vitest run tests/languages/<lang>/rules/<RULE-ID>
development
Analyzing codebases to systematically identify and categorize potential security threats, producing a threat model report before code-level auditing. Use when starting an engagement and wanting to map the attack surface, identify high-value assets, and enumerate threat agents before diving into code-level analysis.
development
Conducting interactive security audits using the Map & Probe methodology. Use when the user wants to perform a security review of source code, find vulnerabilities, audit a codebase, or analyze code for security issues.
testing
Technical writing for formal security audit reports. Use when the user wants to write up a security finding, create a formal issue report, or draft system overview and security model sections for an audit report.
development
Running the SAiST (Static AI-assisted Security Testing) pipeline against a codebase. Use when the user wants to run static analysis rules, detect code smells, find vulnerability patterns, or scan code with the built-in rule engine. Covers the full init → resolve gaps → run rules flow.