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
70 changes: 70 additions & 0 deletions docs/user-guides/community/ai-defense.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Cisco AI Defense Integration

[Cisco AI Defense](https://www.cisco.com/site/us/en/products/security/ai-defense/index.html?utm_medium=github&utm_campaign=nemo-guardrails) allows you to protect LLM interactions. This integration enables NeMo Guardrails to use Cisco AI Defense to protect input and output flows.

You'll need to set the following env variables to work with Cisco AI Defense:

1. AI_DEFENSE_API_ENDPOINT - This is the URL for the Cisco AI Defense inspection API endpoint. This will look like https://[REGION].api.inspect.aidefense.security.cisco.com/api/v1/inspect/chat where REGION is us, ap, eu, etc.
2. AI_DEFENSE_API_KEY - This is the API key for Cisco AI Defense. It is used to authenticate the API request. It can be generated from the Cisco Security Cloud Control UI at https://security.cisco.com

## Setup

1. Ensure that you have access to the Cisco AI Defense endpoints (SaaS or in your private deployment)
2. Enable Cisco AI Defense flows in your `config.yml` file:

```yaml
rails:
config:
ai_defense:
timeout: 30.0
fail_open: false

input:
flows:
- ai defense inspect prompt

output:
flows:
- ai defense inspect response
```

Don't forget to set the `AI_DEFENSE_API_ENDPOINT` and `AI_DEFENSE_API_KEY` environment variables.

### Configuration Options

The AI Defense integration supports the following configuration options under `rails.config.ai_defense`:

- **`timeout`** (float, default: 30.0): Timeout in seconds for API requests to the AI Defense service.
- **`fail_open`** (boolean, default: false): Determines the behavior when AI Defense API calls fail:
- `false` (fail closed): Block content when API calls fail or return malformed responses
- `true` (fail open): Allow content when API calls fail or return malformed responses

**Note**: Configuration validation failures (missing API key or endpoint) will always block content regardless of the `fail_open` setting.

## Usage

Once configured, the Cisco AI Defense integration will automatically:

1. Protect prompts before they are processed by the LLM.
2. Protect LLM outputs before they are sent back to the user.

The `ai_defense_inspect` action in `nemoguardrails/library/ai_defense/actions.py` handles the protection process.

## Error Handling

The AI Defense integration provides configurable error handling through the `fail_open` setting:

- **Fail Closed (default)**: When `fail_open: false`, API failures and malformed responses will block the content (conservative approach)
- **Fail Open**: When `fail_open: true`, API failures and malformed responses will allow the content to proceed

This allows you to choose between security (fail closed) and availability (fail open) based on your requirements.

### Error Scenarios

1. **API Failures** (network errors, timeouts, HTTP errors): Behavior determined by `fail_open` setting
2. **Malformed Responses** (missing required fields): Behavior determined by `fail_open` setting
3. **Configuration Errors** (missing API key/endpoint): Always fail closed regardless of `fail_open` setting

## Notes

For more information on Cisco AI Defense capabilities and configuration, please refer to the [Cisco AI Defense documentation](https://securitydocs.cisco.com/docs/scc/admin/108321.dita?utm_medium=github&utm_campaign=nemo-guardrails).
22 changes: 22 additions & 0 deletions docs/user-guides/guardrails-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ NeMo Guardrails comes with a library of built-in guardrails that you can easily
- [Pangea AI Guard](#pangea-ai-guard)
- [Trend Micro Vision One AI Application Security](#trend-micro-vision-one-ai-application-security)
- OpenAI Moderation API - *[COMING SOON]*
- [Cisco AI Defense](#cisco-ai-defense)

4. Other
- [Jailbreak Detection](#jailbreak-detection)
Expand Down Expand Up @@ -937,6 +938,27 @@ rails:

For more details, check out the [Trend Micro Vision One AI Application Security](./community/trend-micro.md) page.

### Cisco AI Defense

NeMo Guardrails supports using [Cisco AI Defense Inspection](https://www.cisco.com/site/us/en/products/security/ai-defense/index.html?utm_medium=github&utm_campaign=nemo-guardrails) for protecting input and output flows.

To activate the protection, you need to set the `AI_DEFENSE_API_KEY` and `AI_DEFENSE_API_ENDPOINT` environment variables.

#### Example usage

```yaml
rails:
input:
flows:
- ai defense inspect prompt

output:
flows:
- ai defense inspect response
```

For more details, check out the [Cisco AI Defense Integration](./community/ai-defense.md) page.

## Other

### Jailbreak Detection
Expand Down
16 changes: 16 additions & 0 deletions examples/configs/ai_defense/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Cisco AI Defense Configuration Example

This example contains configuration files for using Cisco AI Defense in your NeMo Guardrails project.

## Files

- **`config.yml`**: AI Defense configuration with optional settings

## Configuration Options

The AI Defense integration supports configurable timeout and error handling behavior:

- **`timeout`**: API request timeout in seconds (default: 30.0)
- **`fail_open`**: Behavior when API calls fail (default: false for fail closed)

For more details on the Cisco AI Defense integration, see [Cisco AI Defense Integration User Guide](../../../docs/user-guides/community/ai-defense.md).
18 changes: 18 additions & 0 deletions examples/configs/ai_defense/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
models:
- type: main
engine: openai
model: gpt-4o-mini

rails:
config:
ai_defense:
# Optional: Configure AI Defense behavior
timeout: 30.0 # API request timeout in seconds (default: 30.0)
fail_open: false # Fail closed on API errors (default: false)
# Set to true for fail open behavior
input:
flows:
- ai defense inspect prompt
output:
flows:
- ai defense inspect response
14 changes: 14 additions & 0 deletions nemoguardrails/library/ai_defense/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2023 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.
154 changes: 154 additions & 0 deletions nemoguardrails/library/ai_defense/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# SPDX-FileCopyrightText: Copyright (c) 2023 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.

"""Prompt/Response protection using Cisco AI Defense."""

import logging
import os
from typing import Any, Dict, Optional

import httpx

from nemoguardrails import RailsConfig
from nemoguardrails.actions import action

log = logging.getLogger(__name__)

# Default timeout for AI Defense API calls in seconds
DEFAULT_TIMEOUT = 30.0


def is_ai_defense_text_blocked(result: Dict[str, Any]) -> bool:
"""
Mapping for inspect API response.
Expects result to be a dict with:
- "is_blocked": a boolean indicating if the prompt or response sent to AI Defense should be blocked.
Returns:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it the intent?

# default to not blocked (safe/fail-open) if is_blocked is missing

then shouldn't we change the default value to False?

is_blocked = result.get("is_blocked", False)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaning this up to remove is_blocked and keep jsut a single value and have it default to fail closed.

True if "is_blocked" is True (i.e., the response should be blocked),
False otherwise.
"""
# The fail_open behavior is handled in the main function
# This function just extracts the is_blocked value from the result
is_blocked = result.get("is_blocked", True)
return is_blocked


@action(is_system_action=True, output_mapping=is_ai_defense_text_blocked)
async def ai_defense_inspect(
config: RailsConfig,
user_prompt: Optional[str] = None,
bot_response: Optional[str] = None,
**kwargs,
):
# Get configuration with defaults
ai_defense_config = getattr(config.rails.config, "ai_defense", None)
timeout = ai_defense_config.timeout if ai_defense_config else DEFAULT_TIMEOUT
fail_open = ai_defense_config.fail_open if ai_defense_config else False

api_key = os.environ.get("AI_DEFENSE_API_KEY")
if not api_key:
msg = "AI_DEFENSE_API_KEY environment variable not set."
log.error(msg)
raise ValueError(msg)

api_endpoint = os.environ.get("AI_DEFENSE_API_ENDPOINT")
if not api_endpoint:
msg = "AI_DEFENSE_API_ENDPOINT environment variable not set."
log.error(msg)
raise ValueError(msg)

headers = {
"X-Cisco-AI-Defense-API-Key": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}

if bot_response is not None:
role = "assistant"
text = str(bot_response)
elif user_prompt is not None:
role = "user"
text = str(user_prompt)
else:
msg = "Either user_prompt or bot_response must be provided"
log.error(msg)
raise ValueError(msg)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No timeout configured. If we expect the AI Defense API hangs for any reason, this will block indefinitely.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may have started your review before my most recent changes where I added a timeout (and a fail open config). Please let me know if that's not the case and I'm still missing something.


messages = [{"role": role, "content": text}]

metadata = None
user = kwargs.get("user")
if user is not None:
metadata = {"user": user}

payload: Dict[str, Any] = {"messages": messages}
if metadata:
payload["metadata"] = metadata
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code assumes data.get("is_safe") exists but doesn't validate the response structure. If API returns unexpected format, this could fail silently.

Copy link
Author

@rucpande rucpande Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, added those checks in my commit from yesterday.


async with httpx.AsyncClient() as client:
try:
resp = await client.post(
api_endpoint, headers=headers, json=payload, timeout=timeout
)
resp.raise_for_status()
data = resp.json()
except (httpx.HTTPStatusError, httpx.TimeoutException, httpx.RequestError) as e:
msg = f"Error calling AI Defense API: {e}"
log.error(msg)
if fail_open:
# Fail open: allow content when API call fails
log.warning(
"AI Defense API call failed, but fail_open=True, allowing content"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both is_blocked and is_safe are returned, which are redundant (one is just not of the other). Are you expecting this to evolve differently?

Copy link
Author

@rucpande rucpande Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will clean it up so it only uses is_safe.

)
return {"is_blocked": False, "is_safe": True}
else:
# Fail closed: block content when API call fails
raise ValueError(msg)

# Compose a consistent return structure for flows
# Handle malformed responses based on fail_open setting
if "is_safe" not in data:
# Malformed response - respect fail_open setting
if fail_open:
log.warning(
"AI Defense API returned malformed response (missing 'is_safe'), but fail_open=True, allowing content"
)
is_safe = True
else:
log.warning(
"AI Defense API returned malformed response (missing 'is_safe'), fail_open=False, blocking content"
)
is_safe = False
else:
is_safe = bool(data.get("is_safe", False))

rules = data.get("rules") or []
if not is_safe and rules:
entries = [
f"{r.get('rule_name')} ({r.get('classification')})"
for r in rules
if isinstance(r, dict)
]
if entries:
log.debug("AI Defense matched rules: %s", ", ".join(entries))

# Ensure flows can check explicit block flag
result: Dict[str, Any] = {
"is_blocked": (not is_safe),
"is_safe": is_safe,
}

return result
24 changes: 24 additions & 0 deletions nemoguardrails/library/ai_defense/flows.co
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# INPUT RAILS

flow ai defense inspect prompt
"""Check if the prompt is safe according to AI Defense."""
$result = await AiDefenseInspectAction(user_prompt=$user_message)
if $result["is_blocked"]
if $system.config.enable_rails_exceptions
send AIDefenseRailException(message="Prompt not allowed. The prompt was blocked by the 'ai defense inspect prompt' flow.")
else
bot refuse to respond
abort


# OUTPUT RAILS

flow ai defense inspect response
"""Check if the response is safe according to AI Defense."""
$result = await AiDefenseInspectAction(bot_response=$bot_message)
if $result["is_blocked"]
if $system.config.enable_rails_exceptions
send AIDefenseRailException(message="Response not allowed. The response was blocked by the 'ai defense inspect response' flow.")
else
bot refuse to respond
abort
24 changes: 24 additions & 0 deletions nemoguardrails/library/ai_defense/flows.v1.co
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# INPUT RAILS

define subflow ai defense inspect prompt
"""Check if the prompt is safe according to AI Defense."""
$result = execute ai_defense_inspect(user_prompt=$user_message)
if $result["is_blocked"]
if $config.enable_rails_exceptions
create event AIDefenseRailException(message="Prompt not allowed. The prompt was blocked by the 'ai defense inspect prompt' flow.")
else
bot refuse to respond
stop


# OUTPUT RAILS

define subflow ai defense inspect response
"""Check if the response is safe according to AI Defense."""
$result = execute ai_defense_inspect(bot_response=$bot_message)
if $result["is_blocked"]
if $config.enable_rails_exceptions
create event AIDefenseRailException(message="Response not allowed. The response was blocked by the 'ai defense inspect response' flow.")
else
bot refuse to respond
stop
19 changes: 19 additions & 0 deletions nemoguardrails/rails/llm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,20 @@ def get_api_key(self) -> Optional[str]:
return None


class AIDefenseRailConfig(BaseModel):
"""Configuration data for the Cisco AI Defense API"""

timeout: float = Field(
default=30.0,
description="Timeout in seconds for API requests to AI Defense service",
)

fail_open: bool = Field(
default=False,
description="If True, allow content when AI Defense API call fails (fail open). If False, block content when API call fails (fail closed). Does not affect missing configuration validation.",
)


class RailsConfigData(BaseModel):
"""Configuration data for specific rails that are supported out-of-the-box."""

Expand Down Expand Up @@ -960,6 +974,11 @@ class RailsConfigData(BaseModel):
description="Configuration for Trend Micro.",
)

ai_defense: Optional[AIDefenseRailConfig] = Field(
default_factory=AIDefenseRailConfig,
description="Configuration for Cisco AI Defense.",
)


class Rails(BaseModel):
"""Configuration of specific rails."""
Expand Down
Loading