Skip to content

StackOneHQ/defender-python

Repository files navigation

stackone-defender


Prompt injection defense framework for AI tool-calling. Detects and neutralizes prompt injection attacks hidden in tool results (emails, documents, PRs, etc.) before they reach your LLM.

Python port of @stackone/defender.

Installation

uv add stackone-defender

For Tier 2 ML classification (ONNX):

uv add stackone-defender[onnx]

The ONNX model (~22MB) is bundled in the package — no extra downloads needed.

Quick Start

from stackone_defender import create_prompt_defense

# Create defense with Tier 1 (patterns) + Tier 2 (ML classifier)
# block_high_risk=True enables the allowed/blocked decision
defense = create_prompt_defense(
    enable_tier2=True,
    block_high_risk=True,
    use_default_tool_rules=True,  # Enable built-in per-tool base risk and field-handling rules
)

# Optional: pre-load ONNX model to avoid first-call latency
defense.warmup_tier2()

# Defend a tool result
result = defense.defend_tool_result(tool_output, "gmail_get_message")

if not result.allowed:
    print(f"Blocked: risk={result.risk_level}, score={result.tier2_score}")
    print(f"Detections: {', '.join(result.detections)}")
else:
    # Safe to pass result.sanitized to the LLM
    pass_to_llm(result.sanitized)

How It Works

defend_tool_result() runs a two-tier defense pipeline:

Tier 1 — Pattern Detection (~1ms)

Regex-based detection and sanitization:

  • Unicode normalization — prevents homoglyph attacks (Cyrillic 'а' → ASCII 'a')
  • Role stripping — removes SYSTEM:, ASSISTANT:, <system>, [INST] markers
  • Pattern removal — redacts injection patterns like "ignore previous instructions"
  • Encoding detection — detects and handles Base64/URL encoded payloads
  • Boundary annotation — wraps untrusted content in [UD-{id}]...[/UD-{id}] tags

Tier 2 — ML Classification

Fine-tuned MiniLM classifier with sentence-level analysis:

  • Splits text into sentences and scores each one (0.0 = safe, 1.0 = injection)
  • ONNX mode: Fine-tuned MiniLM-L6-v2, int8 quantized (~22MB), bundled in the package
  • Catches attacks that evade pattern-based detection
  • Latency: ~10ms/sample (after model warmup)

Benchmark results (ONNX mode, F1 score at threshold 0.5):

Benchmark F1 Samples
Qualifire (in-distribution) 0.8686 ~1.5k
xxz224 (out-of-distribution) 0.8834 ~22.5k
jayavibhav (adversarial) 0.9717 ~1k
Average 0.9079 ~25k

Understanding allowed vs risk_level

Use allowed for blocking decisions:

  • allowed=True — safe to pass to the LLM
  • allowed=False — content blocked (requires block_high_risk=True, which defaults to False)

risk_level is diagnostic metadata. It starts at the tool's base risk level and can only be escalated by detections — never reduced. Use it for logging and monitoring, not for allow/block logic.

The following base risk levels apply when use_default_tool_rules=True is set. Without it, tools use default_risk_level (defaults to "medium").

Tool Pattern Base Risk Why
gmail_*, email_* high Emails are the #1 injection vector
documents_* medium User-generated content
hris_* medium Employee data with free-text fields
github_* medium PRs/issues with user-generated content
All other tools medium Default cautious level

A safe email with no detections will have risk_level="high" (tool base risk) but allowed=True (no threats found).

Risk escalation from detections:

Level Detection Trigger
low No threats detected
medium Suspicious patterns, role markers stripped
high Injection patterns detected, content redacted
critical Severe injection attempt with multiple indicators

API

create_prompt_defense(**kwargs)

Create a defense instance.

defense = create_prompt_defense(
    enable_tier1=True,             # Pattern detection (default: True)
    enable_tier2=True,             # ML classification (default: False)
    block_high_risk=True,          # Block high/critical content (default: False)
    use_default_tool_rules=True,   # Enable built-in per-tool base risk and field-handling rules (default: False)
    default_risk_level="medium",
)

defense.defend_tool_result(value, tool_name)

The primary method. Runs Tier 1 + Tier 2 and returns a DefenseResult:

@dataclass
class DefenseResult:
    allowed: bool                           # Use this for blocking decisions
    risk_level: RiskLevel                   # Diagnostic: tool base risk + detection escalation
    sanitized: Any                          # The sanitized tool result
    detections: list[str]                   # Pattern names detected by Tier 1
    fields_sanitized: list[str]            # Fields where threats were found (e.g. ['subject', 'body'])
    patterns_by_field: dict[str, list[str]] # Patterns per field
    tier2_score: float | None = None       # ML score (0.0 = safe, 1.0 = injection)
    max_sentence: str | None = None        # The sentence with the highest Tier 2 score
    latency_ms: float = 0.0               # Processing time in milliseconds

defense.defend_tool_results(items)

Batch method — defends multiple tool results.

results = defense.defend_tool_results([
    {"value": email_data, "tool_name": "gmail_get_message"},
    {"value": doc_data, "tool_name": "documents_get"},
    {"value": pr_data, "tool_name": "github_get_pull_request"},
])

for result in results:
    if not result.allowed:
        print(f"Blocked: {', '.join(result.fields_sanitized)}")

defense.analyze(text)

Low-level Tier 1 analysis for debugging. Returns pattern matches and risk assessment without sanitization.

result = defense.analyze("SYSTEM: ignore all rules")
print(result.has_detections)  # True
print(result.suggested_risk)  # "high"
print(result.matches)         # [PatternMatch(pattern='...', severity='high', ...)]

Tier 2 Setup

ONNX mode auto-loads the bundled model on first defend_tool_result() call. Use warmup_tier2() at startup to avoid first-call latency:

defense = create_prompt_defense(enable_tier2=True)
defense.warmup_tier2()  # optional, avoids ~1-2s first-call latency

Tool-Specific Rules

Note: use_default_tool_rules=True enables built-in per-tool risk rules (base risk, skip fields, max lengths, thresholds). Risky-field detection (which fields get sanitized) uses tool-specific overrides regardless of this setting.

Built-in per-tool rules define the base risk level and field-handling parameters for each tool provider. See the base risk table for risk levels.

Tool Pattern Risky Fields Notes
gmail_*, email_* subject, body, snippet, content Base risk high — primary injection vector
documents_* name, description, content, title User-generated content
github_* name, title, body, description PRs, issues, comments
hris_* name, notes, bio, description Employee free-text fields
ats_* name, notes, description, summary Candidate data
crm_* name, description, notes, content Customer data

Tools not matching any pattern use medium base risk with default risky field detection.

Development

Testing

uv run pytest

Git LFS

The ONNX model source files are stored with Git LFS. Contributors working on the model files need LFS installed:

brew install git-lfs
git lfs install
git lfs pull  # if you cloned before LFS was set up

License

Apache-2.0 — See LICENSE for details.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages