Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/configure-rails/guardrail-catalog/agentic-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,46 @@ Before you begin, install the `yara-python` package or you can install the NeMo
*Example Output*

<Code src="../../../examples/configs/injection_detection/demo-out.txt" language="text" title="demo-out.txt" lines="2-2" />

## Agent Threat Rules (ATR)

The NeMo Guardrails library can evaluate input against [Agent Threat Rules (ATR)](https://github.com/Agent-Threat-Rule/agent-threat-rules), an open, MIT-licensed detection standard for AI-agent attacks such as prompt injection, jailbreak, tool poisoning, MCP attacks, and skill compromise.

The rules are bundled inside the [`pyatr`](https://pypi.org/project/pyatr/) package and run locally -- no API key or network call.
As an input rail, the rail evaluates the user message and flags content matching a rule at or above a configured severity.
It is intended as a fast, deterministic first gate as part of a defense-in-depth strategy.
A flagged input emits the standard `bot refuse to respond` intent, so the refusal wording is configured in one place for every rail; set `enable_rails_exceptions` to raise `AtrDetectionRailException` instead.

### Configuring Agent Threat Rules

Install the optional dependency with `pip install "nemoguardrails[atr]"` (or `pip install pyatr`).

To activate the rail, include the `atr detection` input flow:

```yaml
rails:
config:
atr:
block_severities:
- critical
- high

input:
flows:
- atr detection
```

Refer to the following table for the `rails.config.atr` field syntax reference:

```{list-table}
:header-rows: 1

* - Field
- Description
- Default Value

* - `block_severities`
- The ATR match severities that flag the input.
Matches below these severities are ignored.
- `["critical", "high"]`
```
14 changes: 14 additions & 0 deletions nemoguardrails/library/atr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
92 changes: 92 additions & 0 deletions nemoguardrails/library/atr/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Agent Threat Rules (ATR) detection rail.

Evaluates the input against Agent Threat Rules -- an open, community-maintained
detection standard for AI-agent attacks (like Sigma, but for prompt injection,
jailbreak, tool poisoning, MCP attacks, and skill compromise) -- via the
``pyatr`` package. As an input rail it operates on the user message, so it is
most effective against input-borne attacks such as prompt injection and
jailbreak. Rules are bundled inside ``pyatr``; no rule files or API keys needed.
"""

import logging
from typing import Optional, Set

from nemoguardrails import RailsConfig
from nemoguardrails.actions import action
from nemoguardrails.actions.rail_outcome import RailOutcome
from nemoguardrails.library.atr.rail_config import DEFAULT_BLOCK_SEVERITIES

log = logging.getLogger(__name__)


def _allow(rules: Optional[list] = None) -> RailOutcome:
"""Allow, recording any sub-threshold matches for traces and monitoring."""
return RailOutcome.allow(metadata={"rules": rules or [], "max_severity": None})


def _block_severities(config: Optional[RailsConfig]) -> Set[str]:
"""Read block severities from the rail's ``atr`` config section.

The section is declared by ``rail_config.build_config_spec`` and is absent
when a config does not mention the rail. An explicitly configured
``block_severities: []`` is honored — it flags nothing, giving a
monitor-only rail — so only an absent section falls back to the default.
"""
atr_config = getattr(getattr(config, "rails", None), "config", None)
atr_config = getattr(atr_config, "atr", None)
if atr_config is None:
return {s.lower() for s in DEFAULT_BLOCK_SEVERITIES}
return {str(s).lower() for s in atr_config.block_severities}


@action()
async def atr_detection(text: str, config: RailsConfig) -> RailOutcome:
"""Detect AI-agent threats in *text* using Agent Threat Rules.

Args:
text: The text to evaluate (typically the user message for an input rail).
config: The Rails configuration; ``rails.config.atr.block_severities``
overrides the default ``["critical", "high"]`` block list.

Returns:
A blocking RailOutcome when a rule at or above a block severity matched,
otherwise an allowing one. Both carry ``rules`` (matched ATR rule IDs)
and ``max_severity`` in their metadata.

Raises:
ImportError: If the ``pyatr`` package is not installed.
"""
try:
from pyatr import scan
except ImportError as exc:
raise ImportError(
"The `pyatr` package is required for the ATR rail. Install it with: pip install pyatr"
) from exc

if not text:
return _allow()

block = _block_severities(config)
matches = scan(text) # bundled ATR rules; returns matches sorted by severity
blocking = [match for match in matches if match.severity.lower() in block]
if not blocking:
return _allow([match.rule_id for match in matches])

rule_ids = [match.rule_id for match in blocking]
log.info("ATR rail flagged input on rule(s): %s", ", ".join(rule_ids))
return RailOutcome.block(metadata={"rules": rule_ids, "max_severity": blocking[0].severity})
14 changes: 14 additions & 0 deletions nemoguardrails/library/atr/flows.co
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
flow atr detection
"""
Block user input that matches Agent Threat Rules (prompt injection, jailbreak,
tool poisoning, MCP attacks, skill compromise). This rail operates on the
$user_message.
"""
$response = await AtrDetectionAction(text=$user_message)

if $response.is_blocked
if $system.config.enable_rails_exceptions
send AtrDetectionRailException(message="Input not allowed. The input was blocked by the 'atr detection' flow.")
else
bot refuse to respond
abort
13 changes: 13 additions & 0 deletions nemoguardrails/library/atr/flows.v1.co
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
define subflow atr detection
"""
Block user input that matches Agent Threat Rules (prompt injection, jailbreak,
tool poisoning, MCP attacks, skill compromise).
"""
$response = execute atr_detection(text=$user_message)

if $response.is_blocked
if $config.enable_rails_exceptions
create event AtrDetectionRailException(message="Input not allowed. The input was blocked by the 'atr detection' flow.")
else
bot refuse to respond
stop
73 changes: 73 additions & 0 deletions nemoguardrails/library/atr/rail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from nemoguardrails.manifests import (
ActionRef,
Binding,
ConfigSpecRef,
RailActions,
RailConfigSchema,
RailDirection,
RailFlows,
RailManifest,
RailMetadata,
RailPrivacy,
RailRequirements,
RailSpec,
RailSurface,
)

ATR_DETECTION = ActionRef(
name="atr_detection",
target="nemoguardrails.library.atr.actions:atr_detection",
)

RAIL = RailManifest(
name="atr",
metadata=RailMetadata(
display_name="Agent Threat Rules",
description="Matches user input against the open Agent Threat Rules detection catalog.",
long_description="Evaluates the user message against Agent Threat Rules, a "
"community-maintained detection catalog for AI-agent attacks such as prompt "
"injection, jailbreak, tool poisoning, MCP attacks, and skill compromise. "
"Rules ship inside the optional `pyatr` package and are evaluated in-process, "
"so the rail needs no API key, no service endpoint, and no network access, and "
"the same input yields the same verdict on every run.",
categories=("input",),
capabilities=("allow", "block", "classify", "detect_jailbreak"),
tags=("security", "agentic", "offline", "deterministic"),
docs_url="docs/configure-rails/guardrail-catalog/agentic-security.mdx",
),
spec=RailSpec(
config_schema=RailConfigSchema(
key="atr",
spec=ConfigSpecRef(target="nemoguardrails.library.atr.rail_config:build_config_spec"),
),
flows=RailFlows(flow_names=("atr detection",)),
actions=RailActions(refs=(ATR_DETECTION,)),
surfaces=(
RailSurface(
name="atr detection",
direction=RailDirection.INPUT,
action=ATR_DETECTION,
bindings=(Binding.context("text", "user_message"),),
),
),
requirements=RailRequirements(optional_dependencies=("pyatr",)),
# Everything is evaluated in-process: no text leaves the machine and no
# remote service is contacted, so every disclosure flag stays false.
privacy=RailPrivacy(),
),
)
47 changes: 47 additions & 0 deletions nemoguardrails/library/atr/rail_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List, Optional

from nemoguardrails.manifests.config_schema import (
Field,
RailConfigBaseModel,
RailConfigSpec,
rail_field,
)

DEFAULT_BLOCK_SEVERITIES = ["critical", "high"]


class ATRDetection(RailConfigBaseModel):
block_severities: List[str] = Field(
default_factory=lambda: list(DEFAULT_BLOCK_SEVERITIES),
description="ATR match severities that flag the input. Options are 'critical', "
"'high', 'medium', and 'low'. Matches below these severities are still "
"returned but do not flag, which keeps false positives low. An explicit "
"empty list flags nothing, giving a monitor-only rail that records "
"matches without blocking.",
)


def build_config_spec() -> RailConfigSpec:
return RailConfigSpec(
annotation=Optional[ATRDetection],
field_info=rail_field(
default_factory=ATRDetection,
description="Configuration for Agent Threat Rules detection.",
),
exports={"ATRDetection": ATRDetection},
)
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ multilingual = ["fast-langdetect (>=1.0.0)"]

chat-ui = ["chainlit (>=2.11.0,<3.0.0)"]

# agent threat rules (ATR) detection rail
atr = ["pyatr (>=0.2.6)"]

# eval
eval = [
"tqdm (>=4.65,<5.0)",
Expand Down Expand Up @@ -94,6 +97,7 @@ all = [
"uvicorn (>=0.23)",
"watchdog (>=3.0.0)",
"chainlit (>=2.11.0,<3.0.0)",
"pyatr (>=0.2.6)",
]

[dependency-groups]
Expand Down Expand Up @@ -127,6 +131,7 @@ test_integration = [
"langchain-community>=0.2.5,<2.0.0",
"langchain-openai>=0.1.0",
"langchain-nvidia-ai-endpoints>=0.2.0",
"pyatr>=0.2.6",
]
dev = [
"pre-commit>=3.1.1",
Expand Down
25 changes: 25 additions & 0 deletions schemas/rails_config.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@
"title": "AIDefenseRailConfig",
"type": "object"
},
"ATRDetection": {
"properties": {
"block_severities": {
"description": "ATR match severities that flag the input. Options are 'critical', 'high', 'medium', and 'low'. Matches below these severities are still returned but do not flag, which keeps false positives low. An explicit empty list flags nothing, giving a monitor-only rail that records matches without blocking.",
"items": {
"type": "string"
},
"title": "Block Severities",
"type": "array"
}
},
"title": "ATRDetection",
"type": "object"
},
"AutoAlignOptions": {
"description": "List of guardrails that are activated",
"properties": {
Expand Down Expand Up @@ -1124,6 +1138,17 @@
],
"description": "Configuration for Cisco AI Defense."
},
"atr": {
"anyOf": [
{
"$ref": "#/$defs/ATRDetection"
},
{
"type": "null"
}
],
"description": "Configuration for Agent Threat Rules detection."
},
"autoalign": {
"$ref": "#/$defs/AutoAlignRailConfig",
"description": "Configuration data for the AutoAlign guardrails API."
Expand Down
2 changes: 2 additions & 0 deletions tests/rails/llm/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def test_builtin_rails_config_fields_canonical_set_and_legacy_exports():
"LocalHFClassifierConfig",
"RemoteHFClassifierConfig",
),
"atr": ("ATRDetection",),
"injection_detection": ("InjectionDetection",),
"jailbreak_detection": ("JailbreakDetectionConfig",),
"pangea": ("PangeaRailConfig", "PangeaRailOptions"),
Expand Down Expand Up @@ -153,6 +154,7 @@ def test_builtin_rails_config_fields_canonical_set_and_legacy_exports():
}
expected_config_keys = {
"ai_defense",
"atr",
"autoalign",
"clavata",
"content_safety",
Expand Down
Loading