-
Notifications
You must be signed in to change notification settings - Fork 19
How to Write Templates
version: 0.1.0
author: forefy
accent: anchor
name: Issue name here
description: Issue technical description here
severity: Medium (filter templates for example via --ignore low,medium)
certainty: Medium (filter templates for example via --ignore uncertain)
vulnerable_example: https://github.com/Auditware/radar/blob/main/api/tests/mocks/arbitrary_cpi/bad/src/lib.rs#L11-L14 (Each template is shipped with a god/ and bad/ minimal compilable contracts, and is tested to work as expected)
rule: |
for source, nodes in ast:
try:
# Your python logic here, for example:
funcs = nodes.find_all_functions().exit_on_none()
invoke_refs = funcs.find_by_names("invoke")
if len(invoke_refs) == 0:
continue
spl_token_checks = funcs.find_by_names("spl_token", "ID")
spl_token_2022_checks = funcs.find_by_names("spl_token_2022", "ID")
if len(spl_token_checks) >= 2 or len(spl_token_2022_checks) >= 2:
continue
for invoke_ref in invoke_refs:
print(invoke_ref.parent.to_result())
except:
continueA template consists of descriptive fields, and a logical rule.
The descriptive fields represent the information associated with the vulnerability and the template itself.
The rule is python based statements through which we iterate on the AST (Abstract Syntax Tree) of the contract and extract insightful information.
At a first glance, we can see that we have the ast variable magically available to us, and that we can iterate on it in source and nodes pairs.
source is the file path of the current iteration
An example source looks like
/contract/programs/program_name/insecure/src/lib.rs
nodes are the nodes of that specific file path
Example nodes looks like
{"mod": {"attrs": [...], "vis": "pub", "ident": "node_name", "content": [...], "src": {...}}}We have setup ease-of-use rule functions, and there are operations that can be done on each.
These functions live in a single file in the repo dsl_ast_iterator.py, and to deep dive and understand the different methods available that's the place to be.
We can learn from the template at the top of this page how example calls are made on the AST. Take this line for example:
cpi_groups = nodes.find_chained_calls("solana_program", "program", "invoke").exit_on_none()find_chained_calls is a method implemented by the ASTNode iterator, and to understand how to use it we can look up the function name in the dsl_ast_iterator.py file.
def find_chained_calls(self, *idents: tuple[str, ...]) -> ASTNodeListGroup:
...By the signature alone, we can understand that the function receives a dynamic number of string arguments, and returns a group of AST node lists.
In a high level, there are three important classes: ASTNode, ASTNodeList and ASTNodeListGroup, all give us an abstraction to iterate over the rust JSON AST radar generates.
ASTNode represent an AST node, and the layers above it (List, ListGroup) can be thought of similarly - the functions implemented on ASTNode can be called in the Node, List, or ListGroup level accordingly.
In the code snippet above we iterate on an ASTNodeList, retreiving occurrences of solana_program::program::invoke(..).
That returns us List Groups (i.e. list of ast node lists) of the nodes involved in those occurrences, including further relevant data like line positioning, metadata, child nodes, parent nodes etc.
We can then use this info and pass it to more methods, filtering results further, or print nodes based on conditions we choose.
When we want to indicate a result, we just print the vulnerable node found (or the node whose line information we want to include in the raised vulnerability/insight):
for cpi_group in cpi_groups:
print(cpi_group.first().parent.to_result())In that example we printed the first CPI's parent from each node list group, using the .to_result() to ensure it's picked up as a vulnerability item.
For an easy learning curve, we've setup demo.ipynb in which you can start playing with a simulated rule and see results, no docker setup needed!