NVIDIA SkillSpector Behavioral AST Patterns (AST1-AST9): Security Detection Guide
SkillSpector's behavioral AST analyzer identifies nine dangerous Python code patterns (AST1-AST9) by scanning abstract syntax trees for risky execution flows, dynamic imports, and reflective calls, with severity and confidence scoring defined in behavioral_ast.py.
NVIDIA's SkillSpector is an open-source security analysis tool that detects high-risk code execution patterns in Python source files. The behavioral AST analyzer—implemented in src/skillspector/nodes/analyzers/behavioral_ast.py—maps nine distinct detection rules to specific dangerous programming practices. Each pattern (AST1 through AST9) targets a specific vector for code injection, arbitrary execution, or reflective attacks.
What Are Behavioral AST Patterns?
Behavioral AST patterns are static analysis rules that detect dangerous function calls and execution chains by inspecting Python's Abstract Syntax Tree (AST). Unlike simple string matching, these patterns analyze the semantic structure of code to identify exec() calls, dynamic imports, and reflective attribute access that could enable malicious behavior.
The analyzer assigns severity levels (CRITICAL, HIGH, MEDIUM, LOW) and confidence scores to each finding based on the _RULE_SEVERITIES and _RULE_CONFIDENCES dictionaries defined in the source. When SkillSpector scans a file, it generates a finding containing the rule ID, location, and specific message associated with the triggered pattern.
The Nine Behavioral AST Patterns Explained
AST1: Direct exec() Call Detection
AST1 triggers when the analyzer detects a direct call to Python's built-in exec() function. This pattern identifies code that dynamically executes Python code strings.
# Triggers AST1
exec("print('hello')")
AST2: Direct eval() Call Detection
AST2 identifies calls to the built-in eval() function, which evaluates arbitrary string expressions and poses significant code injection risks.
# Triggers AST2
eval("2 + 2")
AST3: Dynamic Import via import()
AST3 detects runtime module imports using __import__() rather than standard import statements. This pattern catches attempts to load modules dynamically based on string variables.
# Triggers AST3
mod = __import__("os")
AST4: Subprocess Module Execution
AST4 monitors calls to functions within the subprocess module that match the whitelist _SUBPROCESS_CALLS (including subprocess.run, subprocess.call, and similar execution functions).
# Triggers AST4
import subprocess
subprocess.run(["ls", "-l"])
AST5: OS System and Exec Family Calls
AST5 detects calls to operating system execution functions defined in the _OS_EXEC_CALLS whitelist, including os.system(), os.spawnv(), and other process-spawning utilities in the os module.
# Triggers AST5
import os
os.system("rm -rf /tmp/*")
AST6: Direct compile() Call Detection
AST6 identifies usage of the built-in compile() function, which can prepare code objects for later execution via exec() or eval().
# Triggers AST6
code = "x = 1"
compile(code, "<string>", "exec")
AST7: Dynamic Attribute Access via getattr()
AST7 triggers when getattr() is called with a non-constant attribute name (e.g., a variable). This pattern detects reflective attribute access that could bypass static analysis.
# Triggers AST7
attr = "dynamic_name"
getattr(some_obj, attr)
AST8: Dangerous Execution Chain
AST8 detects nested dangerous calls where an exec() or eval() argument itself contains a dangerous source (such as a subprocess call). This pattern identifies multi-layered execution attacks.
# Triggers AST8
exec("import subprocess; subprocess.call(['ls'])")
AST9: Reflective Dangerous Call with Literal Sink
AST9 identifies getattr() calls where the attribute name is a constant literal belonging to the dangerous set {exec, eval, system, popen, __import__}. Unlike AST7, this detects reflective access to specific sinks like getattr(builtins, "exec").
# Triggers AST9
getattr(builtins, "exec")("print('hi')")
Implementation Architecture
The behavioral AST analyzer operates through a coordinated set of modules within the SkillSpector codebase:
-
src/skillspector/nodes/analyzers/behavioral_ast.py: Contains the core detection logic, rule definitions, and theBehavioralASTAnalyzerclass that implements the visitor pattern for AST traversal. -
src/skillspector/nodes/analyzers/common.py: Provides helper utilities for import alias resolution and source extraction used by the behavioral analyzer. -
src/skillspector/models.py: Defines theFindingdata models that store rule IDs, messages, severities, and locations for each detected pattern. -
src/skillspector/state.py: Maintains the global analysis state and file cache consumed by the AST node during scanning.
Summary
- AST1-AST3 detect direct execution risks:
exec(),eval(), and__import__()calls. - AST4-AST5 identify operating system command execution through
subprocessandosmodule functions. - AST6 catches code object compilation via
compile(). - AST7 and AST9 distinguish between variable-based and literal-based reflective attribute access via
getattr(). - AST8 specifically targets nested execution chains where dangerous functions wrap other dangerous calls.
- All nine patterns are defined in
src/skillspector/nodes/analyzers/behavioral_ast.pywith configurable severities and confidence scores.
Frequently Asked Questions
What is the difference between AST7 and AST9?
AST7 triggers when getattr() uses a variable or non-constant attribute name, while AST9 triggers when getattr() uses a string literal that matches dangerous function names like "exec", "eval", "system", "popen", or "__import__". AST7 detects dynamic reflection, whereas AST9 detects reflective access to specific execution sinks.
How does SkillSpector determine the severity of an AST pattern finding?
The analyzer references the _RULE_SEVERITIES dictionary in behavioral_ast.py to assign levels (CRITICAL, HIGH, MEDIUM, LOW) based on the specific AST pattern detected. Each rule ID maps to a severity level that reflects the potential security impact of the dangerous code construct.
Can AST4 detect any subprocess function call?
No, AST4 only detects calls to functions explicitly listed in the _SUBPROCESS_CALLS whitelist, which includes common execution functions like subprocess.run() and subprocess.call() but does not necessarily include every function in the subprocess module. The whitelist ensures the analyzer focuses on high-risk execution paths.
What makes AST8 different from a standard AST1 detection?
While AST1 simply detects any exec() call, AST8 specifically identifies exec() or eval() calls where the argument string contains another dangerous source—such as a subprocess invocation—creating a compound execution chain. This pattern catches sophisticated attacks where code execution wraps additional system commands.
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 →