How AST-Based Behavioral Analysis Detects Dangerous Code in SkillSpector
AST-based behavioral analysis in SkillSpector detects dangerous code by parsing Python files into abstract syntax trees and walking them to identify risky function calls like exec(), eval(), and subprocess operations, including chained execution patterns that wrap multiple dangerous operations.
SkillSpector, NVIDIA's open-source security analysis tool, uses AST-based behavioral analysis to automatically identify potentially dangerous execution patterns in Python source code. This static analysis approach inspects the abstract syntax tree of each file to catch risky behaviors such as dynamic code execution, shell commands, and malicious import chains before they reach production.
The Core Analysis Pipeline
The behavioral analysis engine lives in [behavioral_ast.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_ast.py) and follows a three-stage pipeline to inspect source files.
Parsing Python Source Files
The analyzer first filters files by size, respecting the MAX_FILE_BYTES limit defined in [static_runner.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_runner.py). For qualifying files, it calls ast.parse() to convert raw Python source into an abstract syntax tree. This parsing step captures the complete structure of the code while discarding formatting details like comments and whitespace.
Walking the Abstract Syntax Tree
Once parsed, the analyzer uses ast.walk(tree) to iterate over every node in the AST. The walker specifically targets ast.Call nodes representing function invocations. For each call encountered, the analyzer extracts the fully-qualified name using resolve_call_name() from [common.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/common.py), handling both plain names and attribute chains like os.system or subprocess.Popen.
Detecting Dangerous Call Patterns
SkillSpector matches extracted call names against several rule sets to identify eight distinct categories of dangerous behavior. Each detection generates an AnalyzerFinding with specific severity and confidence levels defined in _RULE_SEVERITIES and _RULE_CONFIDENCES.
The detection rules include:
- AST1: Direct
exec()calls found in_DANGEROUS_BUILTINS - AST2: Direct
eval()calls found in_DANGEROUS_BUILTINS - AST3: Dynamic imports via
__import__()found in_DANGEROUS_BUILTINS - AST4: Subprocess invocations like
subprocess.runorsubprocess.Popenfound in_SUBPROCESS_CALLS - AST5: OS execution family calls like
os.systemfound in_OS_EXEC_CALLS - AST6: Use of
compile()to create executable code objects found in_DANGEROUS_BUILTINS - AST7: Dynamic attribute access using
getattr()where the attribute argument is not a constant - AST8: Chained dangerous calls detected by
_contains_dangerous_source
Identifying Dangerous Execution Chains
The AST8 rule specifically targets complex attack vectors where dangerous calls wrap secondary risky sources. When the analyzer detects exec(), eval(), or compile() calls, it examines the first argument (ast_node.args[0]) using the _contains_dangerous_source() helper.
If this argument contains calls to subprocess modules, OS execution functions, __import__, or obfuscated loaders (such as base64/urllib patterns), SkillSpector flags the pattern as a dangerous execution chain. This catches sophisticated evasion techniques like exec(eval("os.system('whoami')")) that might bypass simpler signature-based detection.
Name Resolution and Context Extraction
The [common.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/common.py) module provides utilities for accurate call identification. The resolve_call_name() function constructs dotted names from AST nodes, while _build_import_aliases() and build_type_map() enable future extensions to resolve import aliases (e.g., import subprocess as sp; sp.run(...)).
Each finding includes location metadata (file path, start/end lines) and a source snippet extracted via get_source_segment() to provide developers with immediate context for remediation.
Practical Examples
Direct Dangerous Calls
# vulnerable.py
exec(user_input) # flagged as AST1 (high severity)
Running SkillSpector on this file produces a finding with the message "exec() call detected".
Subprocess Execution
import subprocess
subprocess.run("rm -rf /", shell=True) # flagged as AST4 (medium severity)
Dangerous Execution Chains
import os
exec(eval("os.system('whoami')")) # flagged as AST8 (critical severity)
The outer exec triggers AST8 because _contains_dangerous_source discovers the nested os.system call.
Dynamic Attribute Access
obj = SomeClass()
method_name = get_user_input()
getattr(obj, method_name)() # flagged as AST7 (low severity)
Summary
AST-based behavioral analysis in SkillSpector provides lightweight but powerful static security scanning for Python codebases. Key implementation details include:
- File parsing using
ast.parse()with size limits enforced byMAX_FILE_BYTES - Tree walking via
ast.walk()targetingast.Callnodes specifically - Eight detection rules (AST1-AST8) covering builtins, subprocess, OS execution, and dynamic attribute access
- Chain detection through
_contains_dangerous_source()for nested dangerous calls - Rich findings converted via
analyzer_finding_to_finding()instatic_runner.pyto include severity, confidence, and source context
Frequently Asked Questions
What is AST-based behavioral analysis in SkillSpector?
AST-based behavioral analysis is a static security scanning technique that parses Python source code into an abstract syntax tree and inspects function call patterns. As implemented in NVIDIA/SkillSpector, it walks the AST to detect dangerous operations like exec(), eval(), and subprocess calls without executing the code.
How does SkillSpector detect dangerous execution chains?
SkillSpector detects dangerous execution chains through the AST8 rule, which triggers when exec(), eval(), or compile() wrap other risky operations. The analyzer examines the first argument of these calls using _contains_dangerous_source() to identify nested subprocess calls, OS execution functions, or dynamic imports.
What types of dangerous calls does the AST analyzer identify?
The analyzer identifies calls in _DANGEROUS_BUILTINS (like exec and eval), _SUBPROCESS_CALLS (like subprocess.run), _OS_EXEC_CALLS (like os.system), and dynamic getattr() usage. Each category maps to specific AST rule IDs ranging from AST1 to AST8 with assigned severity levels.
How are findings converted to the final output format?
Raw AnalyzerFinding objects created in behavioral_ast.py are converted to generic Finding objects via analyzer_finding_to_finding() defined in static_runner.py. This transformation standardizes the output format with location data, severity scores, and source snippets for integration into SkillSpector's analysis pipeline.
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 →