# How the Behavioral AST Analyzer Identifies Dangerous Code Patterns in SkillSpector

> Discover how the SkillSpector Behavioral AST analyzer identifies dangerous code patterns by parsing Python and matching calls to high-risk functions like exec() and eval(). Enhance your code security today.

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

---

**The Behavioral AST analyzer in SkillSpector detects dangerous code patterns by parsing Python files with the `ast` module and matching calls against predefined sets of high-risk functions like `exec()`, `eval()`, `subprocess`, and `os` system calls, emitting findings with rule IDs AST1 through AST8 based on severity and confidence levels.**

The **behavioral AST analyzer** is a static analysis component of NVIDIA's SkillSpector project that scans Python source code for potentially malicious constructs. Implemented in [`src/skillspector/nodes/analyzers/behavioral_ast.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_ast.py), this analyzer leverages Python's built-in `ast` module to build an abstract syntax tree and identify eight specific categories of dangerous execution patterns. Each detection maps to a specific rule ID with calibrated severity and confidence scores to help security teams prioritize remediation efforts.

## Detection Pipeline Architecture

### AST Parsing and Traversal

The analyzer begins by loading file content and constructing an AST using `ast.parse`. It then walks the entire tree with `ast.walk(tree)` to inspect every `ast.Call` node for potential security risks.

### Call Name Resolution

For each call node encountered, the helper function `resolve_call_name` (defined in [`src/skillspector/nodes/analyzers/common.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/common.py)) resolves the dotted name of the function being invoked. This converts complex attribute chains like `subprocess.run` or `os.system` into canonical strings for comparison.

### Pattern Matching and Emission

The resolved call name is compared against three predefined sets: `_DANGEROUS_BUILTINS`, `_SUBPROCESS_CALLS`, and `_OS_EXEC_CALLS`. When a match occurs, the internal `_emit` function creates an `AnalyzerFinding` object containing the rule ID, descriptive message, severity level, confidence score, and the tag **"Dangerous Code Execution"**.

## Dangerous Code Patterns Detected

### AST1: Direct exec() Calls

Any invocation of the built-in `exec()` function triggers rule AST1 with **HIGH** severity and 0.85 confidence. This pattern enables arbitrary Python code execution at runtime.

```python
code = "print('hello')"
exec(code)  # Triggers AST1: "exec() call detected"

```

### AST2: Direct eval() Calls

Calls to `eval()` are flagged as AST2, also with **HIGH** severity and 0.85 confidence, identifying dynamic evaluation of string expressions.

```python
expr = "2 + 2"
result = eval(expr)  # Triggers AST2: "eval() call detected"

```

### AST3: Dynamic Imports via __import__()

Usage of `__import__()` for runtime module loading triggers AST3 with **MEDIUM** severity and 0.75 confidence.

```python
module_name = "os"
mod = __import__(module_name)  # Triggers AST3: "Dynamic import via __import__()"

```

### AST4: Subprocess Module Calls

Any call starting with `subprocess.` (such as `subprocess.run` or `subprocess.Popen`) triggers AST4 with **MEDIUM** severity and 0.70 confidence.

```python
import subprocess
subprocess.run(["ls", "-l"])  # Triggers AST4: "subprocess module call"

```

### AST5: OS Exec-Family Calls

Calls to `os.system`, `os.execv`, `os.spawn`, and other `os` module execution functions trigger AST5 with **HIGH** severity and 0.85 confidence.

```python
import os
os.system("rm -rf /tmp/evil")  # Triggers AST5: "os.system() or os exec-family call"

```

### AST6: Runtime Code Compilation

The `compile()` function, which creates code objects dynamically, triggers AST6 with **MEDIUM** severity and 0.65 confidence.

```python
source = "a = 1"
code_obj = compile(source, "<string>", "exec")  # Triggers AST6: "compile() call detected"

```

### AST7: Dynamic Attribute Access

Using `getattr()` with a non-constant attribute name triggers AST7 with **LOW** severity and 0.50 confidence, flagging potential obfuscation techniques.

```python
obj = {}
attr_name = "dynamic_attr"
getattr(obj, attr_name)  # Triggers AST7: "Dynamic attribute access via getattr()"

```

## Critical Execution Chain Detection (AST8)

The analyzer performs additional taint analysis when it encounters `exec()` or `eval()` calls. The `_contains_dangerous_source` function inspects the argument subtree to determine if the executed code wraps another dangerous source, such as a subprocess call or dynamic import. When detected, this triggers rule **AST8** with **CRITICAL** severity and 0.95 confidence.

```python
import subprocess
payload = "import os; os.system('whoami')"
exec(payload)  # Triggers AST8: "Dangerous execution chain"

```

## Severity and Confidence Calibration

Each rule defined in [`behavioral_ast.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_ast.py) has specific severity and confidence values hardcoded in the source. **Severity** ranges from LOW to CRITICAL, while **confidence** scores (0.50 to 0.95) reflect the likelihood of true positives. The definitions reside in dictionaries at lines 82-91 (severity) and lines 93-101 (confidence), ensuring consistent classification across all findings.

## Summary

- The **behavioral AST analyzer** in NVIDIA/SkillSpector parses Python files using the `ast` module to detect eight categories of dangerous execution patterns.
- Rules AST1 through AST8 cover `exec()`, `eval()`, `__import__()`, `subprocess` calls, `os` exec-family, `compile()`, dynamic `getattr()`, and dangerous execution chains.
- Detection relies on the `resolve_call_name` helper and predefined sets (`_DANGEROUS_BUILTINS`, `_SUBPROCESS_CALLS`, `_OS_EXEC_CALLS`) to identify risky function calls.
- The analyzer assigns **severity** and **confidence** scores to each finding, with execution chains (AST8) flagged as **CRITICAL** due to their compound risk.
- All findings are tagged with **"Dangerous Code Execution"** and emitted through the `_emit` function in [`behavioral_ast.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_ast.py).

## Frequently Asked Questions

### What is the difference between AST1 and AST8 detections?

AST1 flags direct calls to `exec()` regardless of content, while AST8 specifically identifies when `exec()` or `eval()` wraps another dangerous source (such as a subprocess call or dynamic import) creating an execution chain. AST8 carries **CRITICAL** severity (0.95 confidence) compared to AST1's **HIGH** severity (0.85 confidence) because chained execution represents a significantly higher security risk.

### How does the analyzer resolve complex call names like subprocess.run?

The analyzer uses the `resolve_call_name` helper function imported from [`common.py`](https://github.com/NVIDIA/SkillSpector/blob/main/common.py) to flatten attribute chains into dotted strings (e.g., converting an `ast.Attribute` chain of `subprocess.run` into the string `"subprocess.run"`). This allows the analyzer to match against string prefixes like `"subprocess."` and `"os."` reliably.

### Why does getattr() only trigger a LOW severity warning?

Dynamic attribute access via `getattr()` with a non-constant name (AST7) receives **LOW** severity and 0.50 confidence because it is commonly used in legitimate metaprogramming scenarios. The analyzer flags it as a potential obfuscation vector but recognizes the higher false-positive rate compared to direct code execution functions like `exec()` or `eval()`.

### Where are the rule definitions and severity scores stored in the source code?

The message strings, severity levels, and confidence scores for rules AST1-AST8 are defined in [`src/skillspector/nodes/analyzers/behavioral_ast.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_ast.py) at lines 71-80 (messages), lines 82-91 (severity), and lines 93-101 (confidence). These dictionaries map rule IDs to their respective metadata used when creating `AnalyzerFinding` objects.