How the Behavioral AST Analyzer Identifies Dangerous Code Patterns in SkillSpector
The Behavioral AST analyzer in SkillSpector detects dangerous code patterns by parsing Python files with the ast module and matching calls against predefined sets of high-risk functions like exec(), eval(), subprocess, and os system calls, emitting findings with rule IDs AST1 through AST8 based on severity and confidence levels.
The behavioral AST analyzer is a static analysis component of NVIDIA's SkillSpector project that scans Python source code for potentially malicious constructs. Implemented in src/skillspector/nodes/analyzers/behavioral_ast.py, this analyzer leverages Python's built-in ast module to build an abstract syntax tree and identify eight specific categories of dangerous execution patterns. Each detection maps to a specific rule ID with calibrated severity and confidence scores to help security teams prioritize remediation efforts.
Detection Pipeline Architecture
AST Parsing and Traversal
The analyzer begins by loading file content and constructing an AST using ast.parse. It then walks the entire tree with ast.walk(tree) to inspect every ast.Call node for potential security risks.
Call Name Resolution
For each call node encountered, the helper function resolve_call_name (defined in src/skillspector/nodes/analyzers/common.py) resolves the dotted name of the function being invoked. This converts complex attribute chains like subprocess.run or os.system into canonical strings for comparison.
Pattern Matching and Emission
The resolved call name is compared against three predefined sets: _DANGEROUS_BUILTINS, _SUBPROCESS_CALLS, and _OS_EXEC_CALLS. When a match occurs, the internal _emit function creates an AnalyzerFinding object containing the rule ID, descriptive message, severity level, confidence score, and the tag "Dangerous Code Execution".
Dangerous Code Patterns Detected
AST1: Direct exec() Calls
Any invocation of the built-in exec() function triggers rule AST1 with HIGH severity and 0.85 confidence. This pattern enables arbitrary Python code execution at runtime.
code = "print('hello')"
exec(code) # Triggers AST1: "exec() call detected"
AST2: Direct eval() Calls
Calls to eval() are flagged as AST2, also with HIGH severity and 0.85 confidence, identifying dynamic evaluation of string expressions.
expr = "2 + 2"
result = eval(expr) # Triggers AST2: "eval() call detected"
AST3: Dynamic Imports via import()
Usage of __import__() for runtime module loading triggers AST3 with MEDIUM severity and 0.75 confidence.
module_name = "os"
mod = __import__(module_name) # Triggers AST3: "Dynamic import via __import__()"
AST4: Subprocess Module Calls
Any call starting with subprocess. (such as subprocess.run or subprocess.Popen) triggers AST4 with MEDIUM severity and 0.70 confidence.
import subprocess
subprocess.run(["ls", "-l"]) # Triggers AST4: "subprocess module call"
AST5: OS Exec-Family Calls
Calls to os.system, os.execv, os.spawn, and other os module execution functions trigger AST5 with HIGH severity and 0.85 confidence.
import os
os.system("rm -rf /tmp/evil") # Triggers AST5: "os.system() or os exec-family call"
AST6: Runtime Code Compilation
The compile() function, which creates code objects dynamically, triggers AST6 with MEDIUM severity and 0.65 confidence.
source = "a = 1"
code_obj = compile(source, "<string>", "exec") # Triggers AST6: "compile() call detected"
AST7: Dynamic Attribute Access
Using getattr() with a non-constant attribute name triggers AST7 with LOW severity and 0.50 confidence, flagging potential obfuscation techniques.
obj = {}
attr_name = "dynamic_attr"
getattr(obj, attr_name) # Triggers AST7: "Dynamic attribute access via getattr()"
Critical Execution Chain Detection (AST8)
The analyzer performs additional taint analysis when it encounters exec() or eval() calls. The _contains_dangerous_source function inspects the argument subtree to determine if the executed code wraps another dangerous source, such as a subprocess call or dynamic import. When detected, this triggers rule AST8 with CRITICAL severity and 0.95 confidence.
import subprocess
payload = "import os; os.system('whoami')"
exec(payload) # Triggers AST8: "Dangerous execution chain"
Severity and Confidence Calibration
Each rule defined in behavioral_ast.py has specific severity and confidence values hardcoded in the source. Severity ranges from LOW to CRITICAL, while confidence scores (0.50 to 0.95) reflect the likelihood of true positives. The definitions reside in dictionaries at lines 82-91 (severity) and lines 93-101 (confidence), ensuring consistent classification across all findings.
Summary
- The behavioral AST analyzer in NVIDIA/SkillSpector parses Python files using the
astmodule to detect eight categories of dangerous execution patterns. - Rules AST1 through AST8 cover
exec(),eval(),__import__(),subprocesscalls,osexec-family,compile(), dynamicgetattr(), and dangerous execution chains. - Detection relies on the
resolve_call_namehelper and predefined sets (_DANGEROUS_BUILTINS,_SUBPROCESS_CALLS,_OS_EXEC_CALLS) to identify risky function calls. - The analyzer assigns severity and confidence scores to each finding, with execution chains (AST8) flagged as CRITICAL due to their compound risk.
- All findings are tagged with "Dangerous Code Execution" and emitted through the
_emitfunction inbehavioral_ast.py.
Frequently Asked Questions
What is the difference between AST1 and AST8 detections?
AST1 flags direct calls to exec() regardless of content, while AST8 specifically identifies when exec() or eval() wraps another dangerous source (such as a subprocess call or dynamic import) creating an execution chain. AST8 carries CRITICAL severity (0.95 confidence) compared to AST1's HIGH severity (0.85 confidence) because chained execution represents a significantly higher security risk.
How does the analyzer resolve complex call names like subprocess.run?
The analyzer uses the resolve_call_name helper function imported from common.py to flatten attribute chains into dotted strings (e.g., converting an ast.Attribute chain of subprocess.run into the string "subprocess.run"). This allows the analyzer to match against string prefixes like "subprocess." and "os." reliably.
Why does getattr() only trigger a LOW severity warning?
Dynamic attribute access via getattr() with a non-constant name (AST7) receives LOW severity and 0.50 confidence because it is commonly used in legitimate metaprogramming scenarios. The analyzer flags it as a potential obfuscation vector but recognizes the higher false-positive rate compared to direct code execution functions like exec() or eval().
Where are the rule definitions and severity scores stored in the source code?
The message strings, severity levels, and confidence scores for rules AST1-AST8 are defined in src/skillspector/nodes/analyzers/behavioral_ast.py at lines 71-80 (messages), lines 82-91 (severity), and lines 93-101 (confidence). These dictionaries map rule IDs to their respective metadata used when creating AnalyzerFinding objects.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →