How Does SkillSpector Perform AST-Based Behavioral Analysis?

SkillSpector performs AST-based behavioral analysis by walking the abstract syntax tree of Python source files to detect dangerous execution patterns like eval(), subprocess calls, and dynamic attribute access, converting these findings into structured security alerts with precise line numbers and context.

NVIDIA's SkillSpector implements a dedicated Behavioral AST analyzer that inspects Python code for potentially dangerous capabilities without executing it. According to the SkillSpector source code, this static analysis technique parses discovered .py files into abstract syntax trees and applies heuristic security rules to identify code that could enable arbitrary command execution at runtime.

Entry Point: The Behavioral AST Analyzer Node

The analysis begins in [behavioral_ast.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_ast.py), which implements the analyzer node that receives pipeline state from the static pattern runner.

The node(state) function accepts a SkillspectorState object containing:

  • components: A list of discovered Python file paths
  • file_cache: A mapping of path → source-code for each component

For each .py file that satisfies the size constraint defined by MAX_FILE_BYTES 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) to begin inspection.

Parsing Python Source into AST

Inside _analyze_python, the analyzer uses Python's built-in ast module to parse source code into a traversable tree structure:

tree = ast.parse(content, filename=file_path)

If ast.parse raises a SyntaxError, the analyzer aborts processing for that specific file and logs the failure, allowing the scan to continue against the remaining codebase. This tolerant approach ensures that malformed or incomplete files do not crash the entire analysis pipeline.

Detecting Hazardous Code Constructs

The core detection logic iterates over every node in the AST using ast.walk(tree) and filters for ast.Call nodes:

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 utility (located in [common.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/common.py)) converts a Call node into a dotted string representation (e.g., os.system, subprocess.run, exec).

Dangerous Builtins and Dynamic Imports

The analyzer checks resolved call names against pre-computed rule sets to identify high-risk builtins:

  • _DANGEROUS_BUILTINS: Contains exec, eval, compile, and __import__
    • AST1: exec() call detected
    • AST2: eval() call detected
    • AST3: Dynamic import via __import__()
    • AST6: compile() call detected

Subprocess and OS-Level Execution

System-level execution vectors are detected via two additional sets:

  • _SUBPROCESS_CALLS: Includes call, run, Popen, check_output, and similar subprocess module functions (AST4)
  • _OS_EXEC_CALLS: Covers system, execl, posix_spawn, and other os-module execution functions (AST5)

Dynamic Attribute Access

Non-constant attribute access via getattr triggers AST7 ("Dynamic attribute access via getattr()"), flagging code that dynamically resolves object methods or properties based on runtime input.

Nested Execution Chains

When exec, eval, or compile wrap another dangerous call (such as exec(os.system('rm -rf /'))), the _contains_dangerous_source helper walks the argument subtree searching for nested dangerous calls. If found, the analyzer emits AST8 ("Dangerous execution chain").

Emitting Structured Security Findings

Each detection invokes _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). These objects include:

  • Rule ID: The specific AST code (e.g., AST1)
  • Severity: Drawn from _RULE_SEVERITIES (HIGH, MEDIUM, etc.)
  • Confidence: Based on _RULE_CONFIDENCES
  • Location: File path, start line, and optional end line
  • Context: A 3-line code snippet extracted via get_context_from_lines in common.py
  • Matched text: First 200 characters from get_source_segment

The analyzer_finding_to_finding function in [static_runner.py](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_runner.py) subsequently converts these internal objects into public Finding types, adding remediation data, tags, and user-friendly categories for the final report.

Integration with the Static Analysis Pipeline

The AST-based behavioral analysis integrates into SkillSpector's broader scanning architecture through four stages:

  1. File Discovery: graph.Scanner collects all target files and populates state["file_cache"] with source contents
  2. Static Pattern Execution: run_static_patterns in static_runner.py iterates over analyzer modules, including behavioral_ast
  3. Node Execution: The node function returns {"findings": all_findings} to update the graph state
  4. Reporting: Downstream nodes (such as report.py) aggregate findings and render SARIF or CLI-formatted output

Code Examples

Detecting Direct Code Execution

A simple exec call triggers AST1:


# unsafe.py

exec("print('Hello world')")

SkillSpector output:


[AST1] exec() call detected
Location: unsafe.py:1
Context:
exec("print('Hello world')")

Identifying Dynamic Imports

Using __import__ triggers AST3:

mod = __import__('os')

Flagging Dangerous Evaluation Chains

Nested dangerous calls trigger AST8:

eval("subprocess.run(['rm', '-rf', '/'])")

Output:


[AST8] Dangerous chain: eval() wrapping subprocess.run
Location: unsafe.py:1

Spotting Dynamic Attribute Access

Non-constant getattr usage triggers AST7:

obj = SomeClass()
method = getattr(obj, user_supplied_name)

Running the Analyzer from Command Line

To run only the behavioral AST analyzer against a project:

skill-spector scan ./my_project \
  --analyzers behavioral_ast \
  --output sarif

Summary

  • SkillSpector analyzes Python code by parsing files into ASTs using ast.parse() and walking the tree to find ast.Call nodes.
  • Detection relies on sets of dangerous function names (_DANGEROUS_BUILTINS, _SUBPROCESS_CALLS, _OS_EXEC_CALLS) to flag risky capabilities.
  • The analyzer emits structured AnalyzerFinding objects that include severity, confidence, and source context extracted via helpers in common.py.
  • Rule IDs AST1 through AST8 cover builtins, subprocess calls, dynamic imports, attribute access, and nested execution chains.
  • Findings flow through static_runner.py to become public Finding objects suitable for SARIF or CLI reporting.

Frequently Asked Questions

What specific Python constructs does SkillSpector flag as dangerous?

SkillSpector flags four primary categories of dangerous constructs according to its rule tables: direct code execution builtins (exec, eval, compile), dynamic import mechanisms (__import__), system command execution via subprocess and os modules, and dynamic attribute resolution through getattr with variable arguments. Each category maps to specific AST rule IDs (AST1-AST8) with assigned severity levels.

How does SkillSpector handle files with syntax errors?

The analyzer wraps ast.parse() in exception handling that catches SyntaxError and logs the failure without stopping the scan. This design choice, implemented in _analyze_python within behavioral_ast.py, ensures that malformed files or template fragments do not prevent analysis of the remainder of the codebase.

What is the difference between AnalyzerFinding and Finding in SkillSpector?

AnalyzerFinding is the internal data class used by individual analyzer nodes (defined in models.py) to store raw detection results with rule IDs and source locations. Finding is the public-facing type that includes additional metadata such as remediation guidance, tags, and formatted messages. The conversion happens in static_runner.py via analyzer_finding_to_finding.

Can SkillSpector detect obfuscated code or only direct function calls?

The current AST-based analyzer primarily detects direct function calls and simple wrapper chains (AST8). It resolves call names using resolve_call_name from common.py, which handles standard import patterns. While it can detect one level of nesting (e.g., eval(subprocess.run())), it does not perform deep symbolic execution or deobfuscation of heavily encoded strings or dynamically constructed code.

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 →