# How SkillSpector Performs AST-Based Behavioral Analysis: A Deep Dive into the Detection Engine

> Discover how SkillSpector uses AST-based behavioral analysis to identify dangerous Python code patterns like exec eval and subprocess calls by parsing code and matching nodes against rules.

- Repository: [NVIDIA Corporation/SkillSpector](https://github.com/NVIDIA/SkillSpector)
- Tags: deep-dive
- Published: 2026-06-14

---

**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)](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/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/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:

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

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

## Practical Examples

### Detecting Arbitrary Code Execution

The following code triggers **AST1** for the direct `exec` call:

```python

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

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

```

### Dangerous Chain Detection

Nested dangerous calls trigger **AST8**:

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

```python
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/cli.py)](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/cli.py)):

```bash
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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_ast.py) to store raw detection results. The [`static_runner.py`](https://github.com/NVIDIA/SkillSpector/blob/main/static_runner.py) converts these into the public `Finding` type, adding remediation guidance, tags, and standardized categories suitable for SARIF output and CLI reporting.