# How Does SkillSpector Perform AST-Based Behavioral Analysis?

> Discover how SkillSpector uses AST-based behavioral analysis to scan Python code for dangerous execution patterns like eval and subprocess, providing structured security alerts with precise line numbers.

- Repository: [NVIDIA Corporation/SkillSpector](https://github.com/NVIDIA/SkillSpector)
- Tags: internals
- Published: 2026-06-23

---

**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/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/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:

```python
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:

```python
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/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/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/report.py)) aggregate findings and render SARIF or CLI-formatted output

## Code Examples

### Detecting Direct Code Execution

A simple `exec` call triggers AST1:

```python

# 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:

```python
mod = __import__('os')

```

### Flagging Dangerous Evaluation Chains

Nested dangerous calls trigger AST8:

```python
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:

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

```

### Running the Analyzer from Command Line

To run only the behavioral AST analyzer against a project:

```bash
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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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.