What Dangerous Code Patterns Does the Behavioral AST Analyzer Detect?

The Behavioral AST analyzer in NVIDIA SkillSpector detects eight dangerous code patterns ranging from dynamic code execution via exec() and eval() to process spawning and dangerous execution chains, each tagged with specific severity levels and confidence scores.

The Behavioral AST analyzer (behavioral_ast.py) is a static analysis component of the NVIDIA SkillSpector repository that parses Python source files using the built-in ast module. It identifies constructs that enable dynamic code execution, privilege escalation, or runtime code generation by walking the abstract syntax tree and matching call nodes against known dangerous patterns.

How the Behavioral AST Analyzer Detects Threats

The analyzer processes each file through a multi-stage pipeline defined in src/skillspector/nodes/analyzers/behavioral_ast.py. First, it parses the source into an AST using ast.parse(). Then it walks the tree with ast.walk() to inspect every ast.Call node.

For each call, the resolve_call_name helper (imported from common.py) resolves the dotted name (e.g., subprocess.run). The analyzer compares these names against internal sets: _DANGEROUS_BUILTINS, _SUBPROCESS_CALLS, and _OS_EXEC_CALLS. When matched, the _emit function creates an AnalyzerFinding with a specific rule ID, message, severity, confidence score, and the tag "Dangerous Code Execution".

The 8 Dangerous Code Patterns Detected

The analyzer assigns a unique rule ID (AST1 through AST8) to each pattern, with severities ranging from LOW to CRITICAL.

AST1: Direct Use of exec()

Severity: HIGH | Confidence: 0.85

Detected in lines 72-78 of behavioral_ast.py, this rule triggers on any direct call to the exec() builtin, which enables arbitrary Python code execution from string inputs.

code = "print('hello')"
exec(code)  # Triggers AST1

AST2: Direct Use of eval()

Severity: HIGH | Confidence: 0.85

Lines 78-85 flag calls to eval(), which evaluates string expressions as Python code, presenting similar risks to exec().

expr = "2 + 2"
result = eval(expr)  # Triggers AST2

AST3: Dynamic Import via __import__()

Severity: MEDIUM | Confidence: 0.75

Lines 86-88 detect runtime imports using __import__(), which can bypass static analysis and load untrusted modules.

module_name = "os"
mod = __import__(module_name)  # Triggers AST3

AST4: Subprocess Module Calls

Severity: MEDIUM | Confidence: 0.70

Lines 92-96 identify any call prefixed with subprocess. (e.g., subprocess.run, subprocess.Popen, subprocess.call), which spawn external processes and may enable shell injection.

import subprocess
subprocess.run(["ls", "-l"])  # Triggers AST4

AST5: OS Exec-Family Calls

Severity: HIGH | Confidence: 0.85

Lines 97-101 detect calls to os.system() or any os. exec-family function (os.execv, os.spawn*, etc.), providing direct shell access.

import os
os.system("rm -rf /tmp/evil")  # Triggers AST5

AST6: Runtime Code Compilation via compile()

Severity: MEDIUM | Confidence: 0.65

Lines 89-92 flag the compile() builtin when used to create code objects at runtime, which often precedes dynamic execution.

source = "a = 1"
code_obj = compile(source, "<string>", "exec")  # Triggers AST6

AST7: Dynamic Attribute Access with getattr()

Severity: LOW | Confidence: 0.50

Lines 102-105 detect getattr() calls where the attribute name is not a constant string, enabling dynamic method resolution that can bypass access controls.

obj = {}
attr_name = "dynamic_attr"
getattr(obj, attr_name)  # Triggers AST7

AST8: Dangerous Execution Chains

Severity: CRITICAL | Confidence: 0.95

The most severe detection (lines 71-78) triggers when exec() or eval() wraps another dangerous source, such as a subprocess call, dynamic import, or network-loaded code. The _contains_dangerous_source() helper recursively checks the argument subtree for nested risks.

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

Detection Configuration and Metadata

The rule definitions reside at the top of behavioral_ast.py:

  • Messages: Lines 71-80 define human-readable descriptions like "exec() call detected"
  • Severities: Lines 82-91 map rule IDs to HIGH, MEDIUM, LOW, or CRITICAL
  • Confidences: Lines 93-101 assign confidence scores (0.50 to 0.95) based on detection reliability

All findings receive the tag "Dangerous Code Execution" (lines 104-108), enabling filtered reporting across the SkillSpector pipeline.

Summary

  • The Behavioral AST analyzer in src/skillspector/nodes/analyzers/behavioral_ast.py detects 8 specific dangerous patterns using Python's ast module.
  • Critical execution chains (AST8) receive the highest severity when exec() or eval() wrap other dangerous operations.
  • Direct code execution via exec(), eval(), and compile() are flagged as HIGH severity with 0.85 confidence.
  • Process spawning via subprocess.* and os.* exec calls are flagged as MEDIUM to HIGH severity.
  • Dynamic imports (__import__) and reflection (getattr with variables) are detected with lower confidence but still tagged for review.

Frequently Asked Questions

What is the difference between AST1 and AST8 detections?

AST1 triggers on any direct exec() call regardless of its arguments, while AST8 specifically detects when exec() or eval() wraps another dangerous source (like a subprocess call or dynamic import). AST8 is rated CRITICAL with 0.95 confidence because execution chains represent compounded security risks.

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

The analyzer uses the resolve_call_name helper function defined in src/skillspector/nodes/analyzers/common.py to transform an ast.Call node into its fully qualified dotted name. This allows the detection logic to match against prefixes like subprocess. and os. regardless of how the module was imported.

Why does the getattr() detection (AST7) have lower confidence?

AST7 carries a LOW severity and 0.50 confidence because dynamic attribute access via getattr() is a common, legitimate Python pattern. The analyzer lowers the confidence to reduce false positives while still flagging the construct for security review when the attribute name is not a string constant.

Can the analyzer detect dangerous code in imported modules?

No, the Behavioral AST analyzer only inspects the immediate file being analyzed. It does not recursively follow imports or analyze dependencies. It focuses on the AST of the current file, checking for calls to dangerous builtins and modules as defined in the detection sets.

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 →