-
Notifications
You must be signed in to change notification settings - Fork 548
feat: Add Cisco AI Defense integration #1433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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). |
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). |
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 |
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. |
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: | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. code assumes There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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 |
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.