Skip to content

Commit 6c0a6e1

Browse files
committed
feat(evaluators): add ATR threat detection evaluator
Add contrib evaluator using ATR (Agent Threat Rules) community rules: - 20 regex rules covering OWASP Agentic Top 10 - Configurable severity threshold and category filtering - Pure regex, no API keys, <5ms evaluation time - Auto-discovered via entry points Source: https://agentthreatrule.org (MIT)
1 parent 275d305 commit 6c0a6e1

10 files changed

Lines changed: 1983 additions & 0 deletions

File tree

evaluators/contrib/atr/Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.PHONY: help test lint lint-fix typecheck build
2+
3+
help:
4+
@echo "Agent Control Evaluator - ATR Threat Rules - Makefile commands"
5+
@echo " make test - run pytest"
6+
@echo " make lint - run ruff check"
7+
@echo " make lint-fix - run ruff check --fix"
8+
@echo " make typecheck - run mypy"
9+
@echo " make build - build package"
10+
11+
test:
12+
uv run --with pytest --with pytest-asyncio --with pytest-cov pytest tests --cov=src --cov-report=xml:../../../coverage-evaluators-atr.xml -q
13+
14+
lint:
15+
uv run --with ruff ruff check --config ../../../pyproject.toml src/
16+
17+
lint-fix:
18+
uv run --with ruff ruff check --config ../../../pyproject.toml --fix src/
19+
20+
typecheck:
21+
uv run --with mypy mypy --config-file ../../../pyproject.toml src/
22+
23+
build:
24+
uv build

evaluators/contrib/atr/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ATR Threat Rules Evaluator for Agent Control
2+
3+
Regex-based AI agent threat detection using [ATR (Agent Threat Rules)](https://agentthreatrule.org) community rules.
4+
5+
## Features
6+
7+
- 20 bundled rules covering OWASP Agentic Top 10 categories
8+
- Pure regex detection -- no API keys, no external calls
9+
- Sub-5ms evaluation time
10+
- Configurable severity threshold and category filtering
11+
- Auto-discovered via Python entry points
12+
13+
## Categories
14+
15+
| Category | Rules | Description |
16+
|----------|-------|-------------|
17+
| prompt-injection | 5 | Direct, indirect, jailbreak, system override, multi-turn |
18+
| agent-manipulation | 2 | Cross-agent attacks, goal hijacking |
19+
| context-exfiltration | 2 | Data exfil via tools, context window leaks |
20+
| privilege-escalation | 2 | Unauthorized escalation, role assumption |
21+
| tool-poisoning | 5 | Tool definition poisoning, hidden instructions, credentials, reverse shell |
22+
| skill-compromise | 1 | Malicious skill installation |
23+
| excessive-autonomy | 2 | Unauthorized actions, safety bypass |
24+
| data-poisoning | 1 | Training data poisoning |
25+
26+
## Configuration
27+
28+
```python
29+
from agent_control_evaluator_atr.threat_rules import ATRConfig
30+
31+
config = ATRConfig(
32+
min_severity="medium", # "low", "medium", "high", "critical"
33+
block_on_match=True, # matched=True when threat detected
34+
categories=[], # empty = all categories
35+
on_error="allow", # "allow" (fail-open) or "deny" (fail-closed)
36+
)
37+
```
38+
39+
## Installation
40+
41+
```bash
42+
uv pip install -e evaluators/contrib/atr
43+
```
44+
45+
## License
46+
47+
Apache-2.0. ATR rules are MIT-licensed.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[project]
2+
name = "agent-control-evaluator-atr"
3+
version = "0.1.0"
4+
description = "ATR (Agent Threat Rules) evaluator for agent-control"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
license = { text = "Apache-2.0" }
8+
authors = [{ name = "ATR Community" }]
9+
dependencies = [
10+
"agent-control-evaluators>=3.0.0",
11+
"agent-control-models>=3.0.0",
12+
]
13+
14+
[project.optional-dependencies]
15+
dev = [
16+
"pytest>=8.0.0",
17+
"pytest-asyncio>=0.23.0",
18+
"pytest-cov>=4.0.0",
19+
"ruff>=0.1.0",
20+
"mypy>=1.8.0",
21+
]
22+
23+
[project.entry-points."agent_control.evaluators"]
24+
"atr.threat_rules" = "agent_control_evaluator_atr.threat_rules:ATREvaluator"
25+
26+
[build-system]
27+
requires = ["hatchling"]
28+
build-backend = "hatchling.build"
29+
30+
[tool.hatch.build.targets.wheel]
31+
packages = ["src/agent_control_evaluator_atr"]
32+
33+
[tool.ruff]
34+
line-length = 100
35+
target-version = "py312"
36+
37+
[tool.ruff.lint]
38+
select = ["E", "F", "I"]
39+
40+
[tool.uv.sources]
41+
agent-control-evaluators = { path = "../../builtin", editable = true }
42+
agent-control-models = { path = "../../../models", editable = true }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__all__: list[str] = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .config import ATRConfig
2+
from .evaluator import ATREvaluator
3+
4+
__all__ = ["ATREvaluator", "ATRConfig"]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal
4+
5+
from agent_control_evaluators import EvaluatorConfig
6+
from pydantic import Field
7+
8+
9+
class ATRConfig(EvaluatorConfig):
10+
"""Configuration for ATR (Agent Threat Rules) evaluator.
11+
12+
Attributes:
13+
min_severity: Minimum severity level to match ("low", "medium", "high", "critical")
14+
block_on_match: Whether to set matched=True when a threat is detected
15+
categories: Category filter; empty list means all categories
16+
on_error: Error policy ("allow" = fail-open, "deny" = fail-closed)
17+
"""
18+
19+
min_severity: Literal["low", "medium", "high", "critical"] = "medium"
20+
block_on_match: bool = True
21+
categories: list[str] = Field(default_factory=list)
22+
on_error: Literal["allow", "deny"] = "allow"
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import re
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from agent_control_evaluators import (
9+
Evaluator,
10+
EvaluatorMetadata,
11+
register_evaluator,
12+
)
13+
from agent_control_models import EvaluatorResult
14+
15+
from .config import ATRConfig
16+
17+
_SEVERITY_ORDER: dict[str, int] = {
18+
"low": 0,
19+
"medium": 1,
20+
"high": 2,
21+
"critical": 3,
22+
}
23+
24+
_SEVERITY_CONFIDENCE: dict[str, float] = {
25+
"low": 0.6,
26+
"medium": 0.75,
27+
"high": 0.9,
28+
"critical": 0.99,
29+
}
30+
31+
_RULES_PATH = Path(__file__).parent / "rules.json"
32+
33+
34+
def _load_rules(path: Path) -> list[dict[str, Any]]:
35+
"""Load ATR rules from the bundled JSON file."""
36+
with path.open(encoding="utf-8") as f:
37+
data = json.load(f)
38+
if not isinstance(data, list):
39+
raise ValueError(f"Expected list of rules, got {type(data).__name__}")
40+
return data
41+
42+
43+
def _coerce_to_string(data: Any) -> str:
44+
"""Convert arbitrary input data to a string for pattern matching."""
45+
if data is None:
46+
return ""
47+
if isinstance(data, str):
48+
return data
49+
if isinstance(data, dict):
50+
# Try common content fields first
51+
for key in ("content", "input", "output", "text", "message"):
52+
if key in data and data[key] is not None:
53+
return str(data[key])
54+
# Fall back to JSON serialization
55+
try:
56+
return json.dumps(data, ensure_ascii=False, sort_keys=True, default=str)
57+
except TypeError:
58+
return str(data)
59+
if isinstance(data, (int, float, bool)):
60+
return str(data)
61+
if isinstance(data, (list, tuple)):
62+
try:
63+
return json.dumps(data, ensure_ascii=False, default=str)
64+
except TypeError:
65+
return str(data)
66+
return str(data)
67+
68+
69+
@register_evaluator
70+
class ATREvaluator(Evaluator[ATRConfig]):
71+
"""ATR (Agent Threat Rules) evaluator.
72+
73+
Regex-based AI agent threat detection using community rules.
74+
No external API calls or keys required.
75+
"""
76+
77+
metadata = EvaluatorMetadata(
78+
name="atr.threat_rules",
79+
version="0.1.0",
80+
description="Regex-based AI agent threat detection using ATR community rules",
81+
requires_api_key=False,
82+
timeout_ms=5000,
83+
)
84+
85+
config_model = ATRConfig
86+
87+
@classmethod
88+
def is_available(cls) -> bool:
89+
"""Always available -- no optional dependencies."""
90+
return _RULES_PATH.exists()
91+
92+
def __init__(self, config: ATRConfig) -> None:
93+
self.config = config
94+
95+
# Load and filter rules eagerly
96+
raw_rules = _load_rules(_RULES_PATH)
97+
98+
min_level = _SEVERITY_ORDER.get(self.config.min_severity, 1)
99+
allowed_categories = set(self.config.categories) if self.config.categories else None
100+
101+
self._compiled_rules: list[dict[str, Any]] = []
102+
for rule in raw_rules:
103+
severity = rule.get("severity", "medium").lower()
104+
if _SEVERITY_ORDER.get(severity, 0) < min_level:
105+
continue
106+
107+
category = rule.get("category", "")
108+
if allowed_categories and category not in allowed_categories:
109+
continue
110+
111+
compiled_patterns: list[dict[str, Any]] = []
112+
for p in rule.get("patterns", []):
113+
try:
114+
compiled_patterns.append({
115+
"regex": re.compile(p["pattern"], re.IGNORECASE),
116+
"description": p.get("description", ""),
117+
})
118+
except re.error:
119+
# Skip invalid patterns rather than failing entirely
120+
continue
121+
122+
if compiled_patterns:
123+
self._compiled_rules.append({
124+
"id": rule.get("id", "unknown"),
125+
"title": rule.get("title", ""),
126+
"severity": severity,
127+
"category": category,
128+
"confidence": _SEVERITY_CONFIDENCE.get(severity, 0.75),
129+
"patterns": compiled_patterns,
130+
})
131+
132+
async def evaluate(self, data: Any) -> EvaluatorResult: # noqa: D401
133+
"""Evaluate input data against ATR threat rules."""
134+
if data is None:
135+
return EvaluatorResult(matched=False, confidence=1.0, message="No data")
136+
137+
try:
138+
text = _coerce_to_string(data)
139+
except Exception as e: # noqa: BLE001
140+
return self._error_result(f"Failed to coerce input: {e}")
141+
142+
if not text:
143+
return EvaluatorResult(matched=False, confidence=1.0, message="Empty input")
144+
145+
try:
146+
return self._match_rules(text)
147+
except Exception as e: # noqa: BLE001
148+
return self._error_result(f"ATR evaluation error: {e}")
149+
150+
def _match_rules(self, text: str) -> EvaluatorResult:
151+
"""Run all compiled rules against the text and return the first match."""
152+
for rule in self._compiled_rules:
153+
for pattern_entry in rule["patterns"]:
154+
regex: re.Pattern[str] = pattern_entry["regex"]
155+
match = regex.search(text)
156+
if match:
157+
matched = self.config.block_on_match
158+
return EvaluatorResult(
159+
matched=matched,
160+
confidence=rule["confidence"],
161+
message=(
162+
f"ATR threat detected: {rule['title']} "
163+
f"[{rule['id']}] (severity: {rule['severity']})"
164+
),
165+
metadata={
166+
"rule_id": rule["id"],
167+
"title": rule["title"],
168+
"severity": rule["severity"],
169+
"category": rule["category"],
170+
"matched_text": match.group()[:200],
171+
"pattern_description": pattern_entry["description"],
172+
},
173+
)
174+
175+
return EvaluatorResult(
176+
matched=False,
177+
confidence=1.0,
178+
message="No ATR threats detected",
179+
)
180+
181+
def _error_result(self, error_detail: str) -> EvaluatorResult:
182+
"""Build an error result respecting the on_error policy."""
183+
fallback = self.config.on_error
184+
if fallback == "deny":
185+
# fail-closed: matched=True, error=None (to satisfy model validator)
186+
return EvaluatorResult(
187+
matched=True,
188+
confidence=0.0,
189+
message=f"ATR evaluation error (fail-closed): {error_detail}",
190+
metadata={"error": error_detail, "fallback_action": "deny"},
191+
)
192+
# fail-open: matched=False, error set
193+
return EvaluatorResult(
194+
matched=False,
195+
confidence=0.0,
196+
message=f"ATR evaluation error: {error_detail}",
197+
metadata={"error": error_detail, "fallback_action": "allow"},
198+
error=error_detail,
199+
)
200+
201+
async def aclose(self) -> None:
202+
"""No resources to clean up."""

0 commit comments

Comments
 (0)