How the Behavioral AST Analyzer Identifies Dangerous Code Patterns in SkillSpector

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, 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) 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.

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.

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.

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.

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.

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.

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.

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.

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 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.

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 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 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.

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 →