How SkillSpector Uses Abstract Syntax Trees (AST) for Python Security Analysis

SkillSpector parses Python source files into Abstract Syntax Trees using ast.parse(), then walks these trees to detect dangerous built-in calls and perform source-to-sink taint tracking on credential flows.

NVIDIA's SkillSpector is a security analysis framework that inspects Python code for potentially dangerous patterns. By utilizing Abstract Syntax Trees (AST) rather than simple text matching, it achieves precise, context-aware detection of risky behaviors like dynamic code execution and credential leakage. The framework traverses AST nodes to identify specific call patterns, track variable assignments, and analyze data flows within the exact syntactic structure of the code.

The Core AST Pipeline in SkillSpector

Parsing and Caching Strategy

Each analysis begins when SkillSpector discovers Python files and converts them into AST representations. In skillspector/nodes/build_context.py, the framework parses each file once using ast.parse(content, filename) and caches the resulting tree in state["ast_cache"]. This caching strategy prevents redundant parsing during multi-pass analysis, ensuring efficient performance when multiple analyzers inspect the same codebase.

Walking the AST for Pattern Detection

Once parsed, analyzers traverse the tree using ast.walk(tree), which yields every node in the source file. The framework inspects ast.Call nodes to identify function invocations, ast.Assign nodes to track variable bindings, and other node types depending on the specific security rule. This structured traversal allows SkillSpector to understand the exact syntactic context of each operation, distinguishing between actual function calls and string occurrences.

Behavioral AST Analysis: Detecting Dangerous Built-ins

The behavioral_ast analyzer, located in skillspector/nodes/analyzers/behavioral_ast.py, scans for high-risk Python built-ins like exec, eval, compile, and __import__. It also detects dangerous subprocess patterns and OS execution functions.

The analyzer uses the helper resolve_call_name from skillspector/nodes/analyzers/common.py to extract fully-qualified function names from call nodes. When it encounters a risky call, it invokes _contains_dangerous_source to recursively examine the call's arguments, detecting dangerous chains where one risky call wraps another (e.g., exec(compile(...))).

import ast
from skillspector.nodes.analyzers.common import resolve_call_name

def find_dangerous_calls(source: str, file_path: str):
    tree = ast.parse(source, filename=file_path)
    findings = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            name = resolve_call_name(node)
            if name in {"exec", "eval", "compile", "__import__"}:
                findings.append((name, node.lineno))
    return findings

Taint Tracking Analysis: Following Credential Flows

The behavioral_taint_tracking analyzer performs data-flow analysis to track sensitive values from their origins (sources) to dangerous destinations (sinks). Located in skillspector/nodes/analyzers/behavioral_taint_tracking.py, this analyzer monitors assignments from sources like os.getenv or file reads, marks variables as "tainted," and checks if they reach sinks such as subprocess.run or requests.post.

The analyzer builds a type map using build_type_map to resolve attribute lookups and dotted names. When it finds an assignment from a sensitive source, it marks the target variables via _mark_targets. For each call node, it checks if the function name matches known sinks in _ALL_SINKS, and if any tainted variables appear in the arguments, it reports a finding showing the complete source-to-sink flow.

import ast
from skillspector.nodes.analyzers.common import resolve_call_name_typed, build_type_map

SINKS = {"subprocess.run", "os.system"}

def taint_flow(source: str, file_path: str):
    tree = ast.parse(source, filename=file_path)
    type_map = build_type_map(tree)
    tainted = set()
    findings = []

    for node in ast.walk(tree):
        # Mark assignments from secret sources

        if isinstance(node, ast.Assign):
            if isinstance(node.value, ast.Call) and resolve_call_name_typed(node.value, type_map) == "os.getenv":
                for target in node.targets:
                    if isinstance(target, ast.Name):
                        tainted.add(target.id)

        # Detect sinks receiving tainted data

        if isinstance(node, ast.Call):
            call_name = resolve_call_name_typed(node, type_map)
            if call_name in SINKS:
                for arg in node.args:
                    if isinstance(arg, ast.Name) and arg.id in tainted:
                        findings.append((call_name, node.lineno, arg.id))
    return findings

Shared Utilities and AST Helpers

Both analyzers rely on common utilities defined in skillspector/nodes/analyzers/common.py. The resolve_call_name function extracts the fully-qualified name of a function call from an ast.Call node, handling simple names, attributes, and complex expressions. The get_context_from_lines helper provides source context for findings. These shared utilities ensure consistent AST interpretation across different analysis modules.

Summary

  • SkillSpector uses AST parsing via ast.parse() to create structured representations of Python source code, cached in state["ast_cache"] to optimize performance.
  • The behavioral_ast analyzer detects dangerous built-ins and execution chains by walking the AST and inspecting ast.Call nodes with resolve_call_name.
  • The behavioral_taint_tracking analyzer performs data-flow analysis by tracking tainted variables from sources like os.getenv to dangerous sinks using type maps and assignment tracking.
  • Shared utilities in common.py provide standardized AST node resolution and context extraction across all analyzers.

Frequently Asked Questions

Why does SkillSpector use AST instead of regex for security analysis?

Abstract Syntax Trees provide structured, semantic understanding of code whereas regex only matches text patterns. AST analysis allows SkillSpector to distinguish between actual function calls and string literals containing the same text, accurately resolve imported names, and track data flow through variable assignments. This precision eliminates false positives that plague pattern-based scanning.

How does SkillSpector handle multiple analyses on the same file efficiently?

The framework caches parsed ASTs in state["ast_cache"] during the build context phase (build_context.py). When multiple analyzers need to inspect the same Python file, they retrieve the pre-parsed tree from the cache rather than invoking ast.parse() repeatedly. This optimization significantly reduces overhead when running both behavioral_ast and behavioral_taint_tracking on the same codebase.

What Python constructs can the taint tracker analyze?

The taint tracker handles variable assignments from function calls (like os.getenv), attribute lookups via type maps, and tracks these values through to dangerous sinks. It recognizes ast.Name nodes for simple variables and uses build_type_map to resolve dotted names like subprocess.run. However, it focuses on direct data flows and may not track complex transformations or interprocedural flows across function boundaries.

Where are the AST analyzers located in the SkillSpector repository?

The core AST analyzers reside in skillspector/nodes/analyzers/behavioral_ast.py for dangerous call detection and skillspector/nodes/analyzers/behavioral_taint_tracking.py for data-flow analysis. Shared helper functions are in skillspector/nodes/analyzers/common.py, while AST caching logic is implemented in skillspector/nodes/build_context.py.

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 →