How SkillSpector Performs AST-Based Behavioral Analysis: A Deep Dive into the Detection Engine
SkillSpector's AST-based behavioral analysis parses Python source code into an abstract syntax tree to detect dangerous execution patterns like exec(), eval(), and subprocess calls by walking the tree and matching nodes against predefined rule sets.
The NVIDIA SkillSpector project implements a static analysis pipeline that uses Python's built-in ast module to identify potentially malicious behavioral patterns without executing the code. This system focuses on dangerous built-ins, dynamic execution, and subprocess invocations that could indicate security risks in AI-generated or third-party Python code.
Core Architecture and Entry Points
The Analyzer Node Interface
The behavioral analysis originates in [src/skillspector/nodes/analyzers/behavioral_ast.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_ast.py). The node(state) function serves as the primary entry point, receiving a SkillspectorState object containing the scan context.
The state dictionary provides two critical keys:
components– A list of file paths discovered by the scanner (e.g., all.pyfiles)file_cache– A mapping of path strings to source code content for every component
For each Python file that fits within the size limit (MAX_FILE_BYTES defined in [static_runner.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_runner.py)), the node invokes the internal helper _analyze_python(content, path).
File Discovery and State Management
The analysis operates on files already collected by the graph.Scanner class (defined in [graph.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/graph.py)). This separation of concerns allows the behavioral AST analyzer to focus purely on detection logic while the orchestration layer handles I/O and caching.
Parsing and AST Traversal
Syntax Tree Construction
For each target file, SkillSpector constructs the AST using Python's standard library:
tree = ast.parse(content, filename=file_path)
The parser operates in fail-open mode. If a SyntaxError occurs, the analyzer logs the failure and continues to the next file, ensuring that malformed or incomplete Python files do not crash the entire scan.
The Detection Loop
The core detection mechanism iterates over every node in the tree using ast.walk():
for ast_node in ast.walk(tree):
if not isinstance(ast_node, ast.Call):
continue
call_name = resolve_call_name(ast_node) # defined in common.py
The resolve_call_name function (located in [common.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/common.py)) converts Call nodes into fully qualified dotted strings like os.system, subprocess.run, or exec.
Dangerous Pattern Detection
The analyzer checks resolved call names against several pre-computed rule sets to categorize findings.
Built-in Execution Functions (AST1-AST3, AST6)
The _DANGEROUS_BUILTINS set contains high-risk functions that execute arbitrary code:
exec,eval,compile,__import__
When detected, these trigger rule IDs AST1 through AST3 and AST6, with messages like "exec() call detected" or "Dynamic import via import()".
Subprocess and OS Execution Calls (AST4-AST5)
Two specialized sets track system-level execution:
_SUBPROCESS_CALLS– includescall,run,Popen,check_output(triggers AST4)_OS_EXEC_CALLS– includessystem,execl,posix_spawn(triggers AST5)
These detections flag "subprocess module call" and "os.system() or os exec-family call" respectively.
Dynamic Attribute Access (AST7)
SkillSpector identifies non-constant attribute access via getattr() calls. When the attribute parameter is not a string literal, the analyzer emits AST7 with the message "Dynamic attribute access via getattr()".
Dangerous Execution Chains (AST8)
For nested dangerous calls (e.g., exec(os.system('rm -rf /'))), the analyzer employs _contains_dangerous_source. This helper walks the argument subtree of exec/eval/compile calls looking for nested dangerous executions. If found, the analyzer emits AST8 – "Dangerous execution chain".
Finding Generation and Reporting
Constructing AnalyzerFindings
Each detection calls _emit(rule_id, lineno, end_lineno, msg_override=None), which constructs an AnalyzerFinding object (defined in [models.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/models.py)). The finding includes:
- Rule ID – The specific AST code (e.g.,
AST1) - Severity – Drawn from
_RULE_SEVERITIES(HIGH, MEDIUM, etc.) - Confidence – Per
_RULE_CONFIDENCES - Location – File path, start line, and optional end line
- Context – A 3-line code snippet around the match (via
get_context_from_linesincommon.py) - Matched text – First 200 characters of the source segment (via
get_source_segment)
Pipeline Integration
All AnalyzerFinding objects are converted to the public Finding type via analyzer_finding_to_finding in [static_runner.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_runner.py`). This transformation adds remediation data, tags, and user-friendly categories.
The node returns a dictionary {"findings": all_findings} that downstream nodes (e.g., report.py) aggregate into SARIF or CLI output.
Practical Examples
Detecting Arbitrary Code Execution
The following code triggers AST1 for the direct exec call:
# unsafe.py
exec("print('Hello world')")
SkillSpector outputs:
[AST1] exec() call detected
Location: unsafe.py:1
Context:
exec("print('Hello world')")
Dynamic Import Detection
Using __import__ dynamically triggers AST3:
mod = __import__('os')
Dangerous Chain Detection
Nested dangerous calls trigger AST8:
eval("subprocess.run(['rm', '-rf', '/'])")
Result:
[AST8] Dangerous chain: eval() wrapping subprocess.run
Location: unsafe.py:1
Dynamic Attribute Access
Non-constant getattr usage triggers AST7:
obj = SomeClass()
method = getattr(obj, user_supplied_name)
Running the Analysis
To execute only the behavioral AST analyzer from the command line (defined in [cli.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/cli.py)):
skill-spector scan ./my_project \
--analyzers behavioral_ast \
--output sarif
This command scans all Python files, applies the AST walker, and writes a SARIF report containing each finding with its rule ID, severity, and source snippet.
Summary
- SkillSpector implements AST-based behavioral analysis in
behavioral_ast.pyby walking the Python syntax tree to find dangerousCallnodes. - The analyzer detects eight specific rule categories (AST1-AST8) covering built-in execution functions, subprocess calls, OS exec families, dynamic attribute access, and dangerous execution chains.
- Resolution logic in
common.pyconverts AST nodes into qualified names likeos.systemorsubprocess.runfor pattern matching. - Findings are constructed as
AnalyzerFindingobjects with severity, confidence, and 3-line context snippets, then converted to publicFindingtypes instatic_runner.py. - The system operates within a larger graph pipeline where
graph.Scannerhandles file discovery and thenode(state)interface processes cached file contents.
Frequently Asked Questions
What is the maximum file size SkillSpector can analyze with the AST-based behavioral analyzer?
The analyzer respects the MAX_FILE_BYTES limit defined in static_runner.py. Files exceeding this size are skipped to prevent memory issues and performance degradation during the AST construction phase.
How does SkillSpector handle Python files with syntax errors?
The parser deliberately uses a fail-open strategy. When ast.parse() raises a SyntaxError, the analyzer logs the failure for that specific file and continues processing the remaining files in the scan batch.
Can SkillSpector detect dangerous code inside string literals or obfuscated calls?
The current AST-based analysis targets actual Call nodes in the syntax tree. While it detects eval("malicious_code") as a dangerous execution pattern (AST1), it does not execute or introspect the string content itself unless that content contains additional dangerous calls that form an execution chain (AST8).
What is the difference between AnalyzerFinding and Finding in SkillSpector?
AnalyzerFinding is the internal data class used by individual analyzers like behavioral_ast.py to store raw detection results. The static_runner.py converts these into the public Finding type, adding remediation guidance, tags, and standardized categories suitable for SARIF output and CLI reporting.
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 →