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 .py files)
  • 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 – includes call, run, Popen, check_output (triggers AST4)
  • _OS_EXEC_CALLS – includes system, 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_lines in common.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.py by walking the Python syntax tree to find dangerous Call nodes.
  • 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.py converts AST nodes into qualified names like os.system or subprocess.run for pattern matching.
  • Findings are constructed as AnalyzerFinding objects with severity, confidence, and 3-line context snippets, then converted to public Finding types in static_runner.py.
  • The system operates within a larger graph pipeline where graph.Scanner handles file discovery and the node(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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →