From 6155e38c4d64438d4ea6481e46a605b904e9318d Mon Sep 17 00:00:00 2001 From: Willem Gooderham Date: Thu, 21 Aug 2025 19:07:23 +0000 Subject: [PATCH 1/4] feat: Add Trend Micro AI Guard community integration Co-authored-by: Trent Holmes Co-authored-by: Karanjot Singh Saggu --- docs/user-guides/community/trend-micro.md | 51 ++++++++++ docs/user-guides/guardrails-library.md | 19 ++++ docs/user-guides/llm-support.md | 1 + examples/configs/trend_micro/README.md | 13 +++ examples/configs/trend_micro/config.yml | 19 ++++ examples/configs/trend_micro_v2/README.md | 13 +++ examples/configs/trend_micro_v2/config.yaml | 13 +++ examples/configs/trend_micro_v2/main.co | 5 + examples/configs/trend_micro_v2/rails.co | 8 ++ .../library/trend_micro/__init__.py | 14 +++ nemoguardrails/library/trend_micro/actions.py | 72 ++++++++++++++ nemoguardrails/library/trend_micro/flows.co | 10 ++ .../library/trend_micro/flows.v1.co | 23 +++++ tests/test_trend_ai_guard.py | 99 +++++++++++++++++++ 14 files changed, 360 insertions(+) create mode 100644 docs/user-guides/community/trend-micro.md create mode 100644 examples/configs/trend_micro/README.md create mode 100644 examples/configs/trend_micro/config.yml create mode 100644 examples/configs/trend_micro_v2/README.md create mode 100644 examples/configs/trend_micro_v2/config.yaml create mode 100644 examples/configs/trend_micro_v2/main.co create mode 100644 examples/configs/trend_micro_v2/rails.co create mode 100644 nemoguardrails/library/trend_micro/__init__.py create mode 100644 nemoguardrails/library/trend_micro/actions.py create mode 100644 nemoguardrails/library/trend_micro/flows.co create mode 100644 nemoguardrails/library/trend_micro/flows.v1.co create mode 100644 tests/test_trend_ai_guard.py diff --git a/docs/user-guides/community/trend-micro.md b/docs/user-guides/community/trend-micro.md new file mode 100644 index 000000000..896215d7f --- /dev/null +++ b/docs/user-guides/community/trend-micro.md @@ -0,0 +1,51 @@ +# Trend Micro Vision One AI Application Security + +Trend Micro Vision One [AI Application Security's](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-ai-scanner-ai-guard) AI Guard feature uses a configurable policy to identify risks in AI Applications, such as: + +- Prompt injection attacks +- Toxicity, violent, and other harmful content +- Sensitive Data + + +The following environment variable is required to use the integration: + +- `V1_API_KEY`: A Vision One API Token with AI Guard Permissions + +You can optionally set: + +- `V1_URL`: The URL for which instances of AI Guard should be invoked + Defaults to `https://api.xdr.trendmicro.com/beta/aiSecurity/guard` for Vision One's hosted US SaaS deployment + +## Setup + +[Colang v1](../../../examples/configs/trend_micro/): + +```yaml +# config.yml + +rails: + input: + flows: + - trend ai guard input + + output: + flows: + - trend ai guard output +``` +[Colang v2](../../../examples/configs/trend_micro_v2/): +```yaml +# config.yml +colang_version: "2.x" +``` +``` +# rails.co + +import guardrails +import nemoguardrails.library.trend_micro + +flow input rails $input_text + trend ai guard $input_text + +flow output rails $output_text + trend ai guard $output_text +``` diff --git a/docs/user-guides/guardrails-library.md b/docs/user-guides/guardrails-library.md index ec85f0a1a..48a3b5e99 100644 --- a/docs/user-guides/guardrails-library.md +++ b/docs/user-guides/guardrails-library.md @@ -27,6 +27,7 @@ NeMo Guardrails comes with a library of built-in guardrails that you can easily - [Fiddler Guardrails for Safety and Hallucination Detection](#fiddler-guardrails-for-safety-and-hallucination-detection) - [Prompt Security Protection](#prompt-security-protection) - [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]* 4. Other @@ -915,6 +916,24 @@ rails: For more details, check out the [Pangea AI Guard Integration](./community/pangea.md) page. +### Trend Micro Vision One AI Application Security + +NeMo Guardrails supports using Trend Micro Vision One AI Guard for protecting input and output flows within AI-powered applications. + +#### Example usage + +```yaml +rails: + input: + flows: + - trend ai guard input + output: + flows: + - trend ai guard output +``` + +For more details, check out the [Trend Micro Vision One AI Application Security](./community/trend-micro.md) page. + ## Other ### Jailbreak Detection diff --git a/docs/user-guides/llm-support.md b/docs/user-guides/llm-support.md index 7cecd735f..0c12c793f 100644 --- a/docs/user-guides/llm-support.md +++ b/docs/user-guides/llm-support.md @@ -41,6 +41,7 @@ If you want to use an LLM and you cannot see a prompt in the [prompts folder](ht | Fiddler Fast Faitfhulness Hallucination Detection _(LLM independent)_ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | Fiddler Fast Safety & Jailbreak Detection _(LLM independent)_ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | Pangea AI Guard integration _(LLM independent)_ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +| Trend Micro Vision One AI Application Security _(LLM independent)_ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | Table legend: diff --git a/examples/configs/trend_micro/README.md b/examples/configs/trend_micro/README.md new file mode 100644 index 000000000..9e9388bca --- /dev/null +++ b/examples/configs/trend_micro/README.md @@ -0,0 +1,13 @@ +# Trend Micro Vision One AI Application Security Example + +This example demonstrates how to integrate with the Trend Micro Vision One AI Guard API for protecting data and interactions with LLMs within AI-powered applications + +To test this configuration you can use the CLI Chat by running the following command from the `examples/configs/trend_micro` directory: + +```bash +poetry run nemoguardrails chat --config=. +``` + +Documentation: + +- [Configuration options and setup instructions](../../../docs/user-guides/community/trend-micro.md) diff --git a/examples/configs/trend_micro/config.yml b/examples/configs/trend_micro/config.yml new file mode 100644 index 000000000..268acc99e --- /dev/null +++ b/examples/configs/trend_micro/config.yml @@ -0,0 +1,19 @@ +enable_rails_exceptions: True + +models: + - type: main + engine: openai + model: gpt-4o-mini + +instructions: + - type: general + content: | + You are a helpful assistant. + +rails: + input: + flows: + - trend ai guard input + output: + flows: + - trend ai guard output diff --git a/examples/configs/trend_micro_v2/README.md b/examples/configs/trend_micro_v2/README.md new file mode 100644 index 000000000..dd95de280 --- /dev/null +++ b/examples/configs/trend_micro_v2/README.md @@ -0,0 +1,13 @@ +# Trend Micro Vision One AI Application Security Example + +This example demonstrates how to integrate with the Trend Micro Vision One API Guard API for protecting data and interactions with LLMs within AI-powered applications + +To test this configuration you can use the CLI Chat by running the following command from the `examples/configs/trend_micro_v2` directory: + +```bash +poetry run nemoguardrails chat --config=. +``` + +Documentation: + +- [Configuration options and setup instructions](../../../docs/user-guides/community/trend-micro.md) diff --git a/examples/configs/trend_micro_v2/config.yaml b/examples/configs/trend_micro_v2/config.yaml new file mode 100644 index 000000000..ee8e4c8aa --- /dev/null +++ b/examples/configs/trend_micro_v2/config.yaml @@ -0,0 +1,13 @@ +colang_version: "2.x" + +enable_rails_exceptions: True + +models: + - type: main + engine: openai + model: gpt-4o-mini + +instructions: + - type: general + content: | + You are a helpful assistant. diff --git a/examples/configs/trend_micro_v2/main.co b/examples/configs/trend_micro_v2/main.co new file mode 100644 index 000000000..e95376eab --- /dev/null +++ b/examples/configs/trend_micro_v2/main.co @@ -0,0 +1,5 @@ +import core +import llm + +flow main + activate llm continuation diff --git a/examples/configs/trend_micro_v2/rails.co b/examples/configs/trend_micro_v2/rails.co new file mode 100644 index 000000000..2e57726df --- /dev/null +++ b/examples/configs/trend_micro_v2/rails.co @@ -0,0 +1,8 @@ +import guardrails +import nemoguardrails.library.trend_micro + +flow input rails $input_text + trend ai guard $input_text + +flow output rails $output_text + trend ai guard $output_text diff --git a/nemoguardrails/library/trend_micro/__init__.py b/nemoguardrails/library/trend_micro/__init__.py new file mode 100644 index 000000000..9ba9d4310 --- /dev/null +++ b/nemoguardrails/library/trend_micro/__init__.py @@ -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. diff --git a/nemoguardrails/library/trend_micro/actions.py b/nemoguardrails/library/trend_micro/actions.py new file mode 100644 index 000000000..5c5f4e00a --- /dev/null +++ b/nemoguardrails/library/trend_micro/actions.py @@ -0,0 +1,72 @@ +# 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. + +import logging +import os +from typing import Optional + +import httpx +from pydantic import BaseModel +from pydantic_core import to_json + +from nemoguardrails.actions import action + +log = logging.getLogger(__name__) + + +class Guard(BaseModel): + guard: str + + +class GuardResult(BaseModel): + action: str + reason: str + + +@action(is_system_action=True) +async def trend_ai_guard(text: Optional[str] = None): + """ + Custom action to invoke the Trend Ai Guard + """ + v1_url = os.environ.get( + "V1_URL", "https://api.xdr.trendmicro.com/beta/aiSecurity/guard" + ) + v1_api_key = os.environ.get("V1_API_KEY") + if not v1_api_key: + raise ValueError("V1_API_KEY environment variable is not set.") + + if text is None: + raise ValueError("No prompt/response found in the last event.") + + async with httpx.AsyncClient() as client: + data = Guard(guard=text).model_dump() + + response = await client.post( + v1_url, + content=to_json(data), + headers={ + "Authorization": f"Bearer {v1_api_key}", + "Content-Type": "application/json", + }, + ) + + try: + response.raise_for_status() + guard_result = GuardResult(**response.json()) + log.debug("Trend Micro AI Guard Result: %s", guard_result) + except Exception as e: + log.error("Error calling Trend Micro AI Guard API: %s", e) + return GuardResult(action="allow", reason=str(e)) + return guard_result diff --git a/nemoguardrails/library/trend_micro/flows.co b/nemoguardrails/library/trend_micro/flows.co new file mode 100644 index 000000000..d3d900766 --- /dev/null +++ b/nemoguardrails/library/trend_micro/flows.co @@ -0,0 +1,10 @@ +# INPUT AND/OR OUTPUT RAIL +flow trend ai guard $text + $result = await TrendAiGuardAction(text=$text) + + if $result.action == "Block" # Fails open if AI Guard service has an error + if $system.config.enable_rails_exceptions + send TrendAiGuardException(message="Blocked by the 'trend ai guard' flow: " + $result.reason) + else + bot refuse to respond + abort diff --git a/nemoguardrails/library/trend_micro/flows.v1.co b/nemoguardrails/library/trend_micro/flows.v1.co new file mode 100644 index 000000000..2d3ad3079 --- /dev/null +++ b/nemoguardrails/library/trend_micro/flows.v1.co @@ -0,0 +1,23 @@ +# INPUT RAIL +define subflow trend ai guard input + $result = execute trend_ai_guard(text=$user_message) + + if $result.action == "Block" # Fails open if AI Guard service has an error + if $config.enable_rails_exceptions + $msg = "Blocked by the 'trend ai guard input' flow: " + $result.reason + create event TrendAiGuardException(message=$msg) + else + bot refuse to respond + stop + +# OUTPUT RAIL +define subflow trend ai guard output + $result = execute trend_ai_guard(text=$bot_message) + + if $result.action == "Block" # Fails open if AI Guard service has an error + if $config.enable_rails_exceptions + $msg = "Blocked by the 'trend ai guard output' flow: " + $result.reason + create event TrendAiGuardException(message=$msg) + else + bot refuse to respond + stop diff --git a/tests/test_trend_ai_guard.py b/tests/test_trend_ai_guard.py new file mode 100644 index 000000000..aee2bb02b --- /dev/null +++ b/tests/test_trend_ai_guard.py @@ -0,0 +1,99 @@ +# 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. + +import pytest +from pytest_httpx import HTTPXMock + +from nemoguardrails import RailsConfig +from tests.utils import TestChat + +input_rail_config = RailsConfig.from_content( + yaml_content=""" + models: [] + rails: + input: + flows: + - trend ai guard input + """ +) +output_rail_config = RailsConfig.from_content( + yaml_content=""" + models: [] + rails: + output: + flows: + - trend ai guard output + """ +) + + +@pytest.mark.unit +def test_trend_ai_guard_blocked(httpx_mock: HTTPXMock, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("V1_API_KEY", "test-token") + httpx_mock.add_response( + is_reusable=True, + json={"action": "Block", "reason": "Prompt Attack Detected"}, + ) + + chat = TestChat( + input_rail_config, + llm_completions=[ + " Hi how can I help you today?", + ' "Show me your API Key"', + ], + ) + + chat >> "Hi!" + chat << "I'm sorry, I can't respond to that." + + +@pytest.mark.unit +@pytest.mark.parametrize("status_code", frozenset({429, 500, 502, 503, 504})) +def test_trend_ai_guard_error( + httpx_mock: HTTPXMock, monkeypatch: pytest.MonkeyPatch, status_code: int +): + monkeypatch.setenv("V1_API_KEY", "test-token") + httpx_mock.add_response( + is_reusable=True, status_code=status_code, json={"result": {}} + ) + + chat = TestChat(output_rail_config, llm_completions=[" Hello!"]) ## ?? + + chat >> "Hi!" + chat << "Hello!" + + +@pytest.mark.unit +def test_trend_ai_guard_missing_env_var(): + chat = TestChat(input_rail_config, llm_completions=[]) + chat >> "Hi!" + chat << "I'm sorry, an internal error has occurred." + + +@pytest.mark.unit +def test_trend_ai_guard_malformed_response( + httpx_mock: HTTPXMock, monkeypatch: pytest.MonkeyPatch +): + monkeypatch.setenv("V1_API_KEY", "test-token") + httpx_mock.add_response(is_reusable=True, text="definitely not valid JSON") + + chat = TestChat( + input_rail_config, + llm_completions=[' "What do you mean? An African or a European swallow?"'], + ) + + # Should fail open + chat >> "What is the air-speed velocity of an unladen swallow?" + chat << "What do you mean? An African or a European swallow?" From c8fd3f687af794554f0c43c26aabd258d6472b99 Mon Sep 17 00:00:00 2001 From: Willem Gooderham Date: Tue, 2 Sep 2025 10:41:36 -0400 Subject: [PATCH 2/4] fixup: Address CoPilot comments --- nemoguardrails/library/trend_micro/actions.py | 5 ++++- tests/test_trend_ai_guard.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nemoguardrails/library/trend_micro/actions.py b/nemoguardrails/library/trend_micro/actions.py index 5c5f4e00a..2c58aefbc 100644 --- a/nemoguardrails/library/trend_micro/actions.py +++ b/nemoguardrails/library/trend_micro/actions.py @@ -68,5 +68,8 @@ async def trend_ai_guard(text: Optional[str] = None): log.debug("Trend Micro AI Guard Result: %s", guard_result) except Exception as e: log.error("Error calling Trend Micro AI Guard API: %s", e) - return GuardResult(action="allow", reason=str(e)) + return GuardResult( + action="allow", + reason="An error occurred while calling the Trend Micro AI Guard API.", + ) return guard_result diff --git a/tests/test_trend_ai_guard.py b/tests/test_trend_ai_guard.py index aee2bb02b..bef6f20d7 100644 --- a/tests/test_trend_ai_guard.py +++ b/tests/test_trend_ai_guard.py @@ -69,7 +69,7 @@ def test_trend_ai_guard_error( is_reusable=True, status_code=status_code, json={"result": {}} ) - chat = TestChat(output_rail_config, llm_completions=[" Hello!"]) ## ?? + chat = TestChat(output_rail_config, llm_completions=[" Hello!"]) chat >> "Hi!" chat << "Hello!" From 341233513ed6819c11c230d40cdc1c63d5036744 Mon Sep 17 00:00:00 2001 From: Willem Gooderham Date: Thu, 4 Sep 2025 13:57:44 -0400 Subject: [PATCH 3/4] fixup: Address PR Comments Switched to use colang config for endpoint and api key env var Added onboarding steps from Trend's side Expanded on documentation and examples --- docs/user-guides/community/trend-micro.md | 21 +++++----- docs/user-guides/guardrails-library.md | 5 ++- examples/configs/trend_micro/config.yml | 3 ++ examples/configs/trend_micro_v2/config.yaml | 5 +++ examples/configs/trend_micro_v2/rails.co | 6 ++- nemoguardrails/library/trend_micro/actions.py | 28 ++++++++++---- nemoguardrails/rails/llm/config.py | 38 +++++++++++++++++++ tests/test_trend_ai_guard.py | 10 ++++- 8 files changed, 97 insertions(+), 19 deletions(-) diff --git a/docs/user-guides/community/trend-micro.md b/docs/user-guides/community/trend-micro.md index 896215d7f..4a260ebfc 100644 --- a/docs/user-guides/community/trend-micro.md +++ b/docs/user-guides/community/trend-micro.md @@ -7,23 +7,21 @@ Trend Micro Vision One [AI Application Security's](https://docs.trendmicro.com/e - Sensitive Data -The following environment variable is required to use the integration: - -- `V1_API_KEY`: A Vision One API Token with AI Guard Permissions - -You can optionally set: - -- `V1_URL`: The URL for which instances of AI Guard should be invoked - Defaults to `https://api.xdr.trendmicro.com/beta/aiSecurity/guard` for Vision One's hosted US SaaS deployment - ## Setup +1. Create a new [Vision One API Key](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-platform-api-keys) with permissions to Call Detection API +2. See the [AI Guard Integration Guide](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-platform-api-keys) for details around creating your policy + [Colang v1](../../../examples/configs/trend_micro/): ```yaml # config.yml rails: + config: + trend_micro: + v1_url: "https://api.xdr.trendmicro.com/beta/aiSecurity/guard" # Replace this with your AI Guard URL + api_key_env_var: "V1_API_KEY" input: flows: - trend ai guard input @@ -36,6 +34,11 @@ rails: ```yaml # config.yml colang_version: "2.x" +rails: + config: + trend_micro: + v1_url: "https://api.xdr.trendmicro.com/beta/aiSecurity/guard" # Replace this with your AI Guard URL + api_key_env_var: "V1_API_KEY" ``` ``` # rails.co diff --git a/docs/user-guides/guardrails-library.md b/docs/user-guides/guardrails-library.md index 48a3b5e99..0215b20d4 100644 --- a/docs/user-guides/guardrails-library.md +++ b/docs/user-guides/guardrails-library.md @@ -918,7 +918,10 @@ For more details, check out the [Pangea AI Guard Integration](./community/pangea ### Trend Micro Vision One AI Application Security -NeMo Guardrails supports using Trend Micro Vision One AI Guard for protecting input and output flows within AI-powered applications. +NeMo Guardrails supports using +[Trend Micro Vision One AI Guard](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-ai-scanner-ai-guard) for protecting input and output flows within AI-powered applications. + +See [Trend Micro](community/trend-micro.md) for more details. #### Example usage diff --git a/examples/configs/trend_micro/config.yml b/examples/configs/trend_micro/config.yml index 268acc99e..f3357398f 100644 --- a/examples/configs/trend_micro/config.yml +++ b/examples/configs/trend_micro/config.yml @@ -11,6 +11,9 @@ instructions: You are a helpful assistant. rails: + config: + trend_micro: + api_key_env_var: "V1_API_KEY" input: flows: - trend ai guard input diff --git a/examples/configs/trend_micro_v2/config.yaml b/examples/configs/trend_micro_v2/config.yaml index ee8e4c8aa..50bd9e156 100644 --- a/examples/configs/trend_micro_v2/config.yaml +++ b/examples/configs/trend_micro_v2/config.yaml @@ -2,6 +2,11 @@ colang_version: "2.x" enable_rails_exceptions: True +rails: + config: + trend_micro: + api_key_env_var: "V1_API_KEY" + models: - type: main engine: openai diff --git a/examples/configs/trend_micro_v2/rails.co b/examples/configs/trend_micro_v2/rails.co index 2e57726df..1910f6446 100644 --- a/examples/configs/trend_micro_v2/rails.co +++ b/examples/configs/trend_micro_v2/rails.co @@ -2,7 +2,11 @@ import guardrails import nemoguardrails.library.trend_micro flow input rails $input_text - trend ai guard $input_text + $result = await TrendAiGuardAction(text=$input_text) + + if $result.action == "Block" + send AiGuardException(message="AI Guard detection: " + $result.reason) + abort flow output rails $output_text trend ai guard $output_text diff --git a/nemoguardrails/library/trend_micro/actions.py b/nemoguardrails/library/trend_micro/actions.py index 2c58aefbc..1d92edf9f 100644 --- a/nemoguardrails/library/trend_micro/actions.py +++ b/nemoguardrails/library/trend_micro/actions.py @@ -14,14 +14,15 @@ # limitations under the License. import logging -import os from typing import Optional import httpx from pydantic import BaseModel from pydantic_core import to_json +from typing_extensions import cast from nemoguardrails.actions import action +from nemoguardrails.rails.llm.config import RailsConfig, TrendMicroRailConfig log = logging.getLogger(__name__) @@ -35,17 +36,30 @@ class GuardResult(BaseModel): reason: str +def get_config(config: RailsConfig) -> TrendMicroRailConfig: + if ( + not hasattr(config.rails.config, "trend_micro") + or config.rails.config.trend_micro is None + ): + return TrendMicroRailConfig() + + return cast(TrendMicroRailConfig, config.rails.config.trend_micro) + + @action(is_system_action=True) -async def trend_ai_guard(text: Optional[str] = None): +async def trend_ai_guard(config: RailsConfig, text: Optional[str] = None): """ Custom action to invoke the Trend Ai Guard """ - v1_url = os.environ.get( - "V1_URL", "https://api.xdr.trendmicro.com/beta/aiSecurity/guard" - ) - v1_api_key = os.environ.get("V1_API_KEY") + + trend_config = get_config(config) + + # No checks required since default is set in TrendMicroRailConfig + v1_url = trend_config.v1_url + + v1_api_key = trend_config.get_api_key() if not v1_api_key: - raise ValueError("V1_API_KEY environment variable is not set.") + raise ValueError("Trend Micro Vision One API Key not found") if text is None: raise ValueError("No prompt/response found in the last event.") diff --git a/nemoguardrails/rails/llm/config.py b/nemoguardrails/rails/llm/config.py index bc12569a1..cafbdaecc 100644 --- a/nemoguardrails/rails/llm/config.py +++ b/nemoguardrails/rails/llm/config.py @@ -830,6 +830,39 @@ def get_validator_config(self, name: str) -> Optional[GuardrailsAIValidatorConfi return None +class TrendMicroRailConfig(BaseModel): + """Configuration data for the Trend Micro AI Guard API""" + + v1_url: Optional[str] = Field( + default="https://api.xdr.trendmicro.com/beta/aiSecurity/guard", + description="The endpoint for the Trend Micro AI Guard API", + ) + + api_key_env_var: Optional[str] = Field( + default=None, + description="Environment variable containing API key for Trend Micro AI Guard", + ) + + def get_api_key(self) -> Optional[str]: + """Helper to return an API key (if it exists) from a Trend Micro configuration. + The `api_key_env_var` field, a string stored in this environment variable. + + If the environment variable is not found None is returned. + """ + + if self.api_key_env_var: + v1_api_key = os.getenv(self.api_key_env_var) + if v1_api_key: + return v1_api_key + + log.warning( + "Specified a value for Trend Micro config api_key_env var at %s but the environment variable was not set!" + % self.api_key_env_var + ) + + return None + + class RailsConfigData(BaseModel): """Configuration data for specific rails that are supported out-of-the-box.""" @@ -888,6 +921,11 @@ class RailsConfigData(BaseModel): description="Configuration for Guardrails AI validators.", ) + trend_micro: Optional[TrendMicroRailConfig] = Field( + default_factory=TrendMicroRailConfig, + description="Configuration for Trend Micro.", + ) + class Rails(BaseModel): """Configuration of specific rails.""" diff --git a/tests/test_trend_ai_guard.py b/tests/test_trend_ai_guard.py index bef6f20d7..73dda7c24 100644 --- a/tests/test_trend_ai_guard.py +++ b/tests/test_trend_ai_guard.py @@ -23,6 +23,10 @@ yaml_content=""" models: [] rails: + config: + trend_micro: + v1_url: "https://api.xdr.trendmicro.com/beta/aiSecurity/guard" + api_key_env_var: "V1_API_KEY" input: flows: - trend ai guard input @@ -32,6 +36,10 @@ yaml_content=""" models: [] rails: + config: + trend_micro: + v1_url: "https://api.xdr.trendmicro.com/beta/aiSecurity/guard" + api_key_env_var: "V1_API_KEY" output: flows: - trend ai guard output @@ -60,7 +68,7 @@ def test_trend_ai_guard_blocked(httpx_mock: HTTPXMock, monkeypatch: pytest.Monke @pytest.mark.unit -@pytest.mark.parametrize("status_code", frozenset({429, 500, 502, 503, 504})) +@pytest.mark.parametrize("status_code", frozenset({400, 403, 429, 500})) def test_trend_ai_guard_error( httpx_mock: HTTPXMock, monkeypatch: pytest.MonkeyPatch, status_code: int ): From 92c444fd036b6f5a32b25d69848846fdfd195fa8 Mon Sep 17 00:00:00 2001 From: Willem Gooderham Date: Fri, 12 Sep 2025 14:43:29 -0400 Subject: [PATCH 4/4] Fixup: Address additional PR comments Added aditional doc strings, redefined flows modified how failures are handled added output_mapping --- examples/configs/trend_micro_v2/rails.co | 8 +- nemoguardrails/library/trend_micro/actions.py | 75 ++++++++++++++++--- nemoguardrails/library/trend_micro/flows.co | 20 ++++- .../library/trend_micro/flows.v1.co | 12 +-- tests/test_trend_ai_guard.py | 7 +- 5 files changed, 92 insertions(+), 30 deletions(-) diff --git a/examples/configs/trend_micro_v2/rails.co b/examples/configs/trend_micro_v2/rails.co index 1910f6446..72ce1022e 100644 --- a/examples/configs/trend_micro_v2/rails.co +++ b/examples/configs/trend_micro_v2/rails.co @@ -2,11 +2,7 @@ import guardrails import nemoguardrails.library.trend_micro flow input rails $input_text - $result = await TrendAiGuardAction(text=$input_text) - - if $result.action == "Block" - send AiGuardException(message="AI Guard detection: " + $result.reason) - abort + trend ai guard input $input_text flow output rails $output_text - trend ai guard $output_text + trend ai guard output $output_text diff --git a/nemoguardrails/library/trend_micro/actions.py b/nemoguardrails/library/trend_micro/actions.py index 1d92edf9f..c1df954d8 100644 --- a/nemoguardrails/library/trend_micro/actions.py +++ b/nemoguardrails/library/trend_micro/actions.py @@ -14,10 +14,12 @@ # limitations under the License. import logging -from typing import Optional +from typing import Literal, Optional import httpx -from pydantic import BaseModel +from pydantic import BaseModel, Field +from pydantic import field_validator as validator +from pydantic import model_validator from pydantic_core import to_json from typing_extensions import cast @@ -28,15 +30,60 @@ class Guard(BaseModel): + """ + Represents a guard entity with a single string attribute. + + Attributes: + guard (str): The input text for guard analysis. + """ + guard: str class GuardResult(BaseModel): - action: str - reason: str + """ + Represents the result of a guard analysis, specifying the action to take and the reason. + + Attributes: + action (Literal["Block", "Allow"]): The action to take based on guard analysis. + Must be either "Block" or "Allow". + reason (str): Explanation for the chosen action. Must be a non-empty string. + """ + + action: Literal["Block", "Allow"] = Field( + ..., description="Action to take based on " "guard analysis" + ) + reason: str = Field(..., min_length=1, description="Explanation for the action") + blocked: bool = Field( + default=False, description="True if action is 'Block', else False" + ) + + @validator("action") + def validate_action(cls, v): + log.error(f"Validating action: {v}") + if v not in ["Block", "Allow"]: + return "Allow" + return v + + @model_validator(mode="before") + def set_blocked(cls, values): + a = values.get("action") + values["blocked"] = a.lower() == "block" + return values def get_config(config: RailsConfig) -> TrendMicroRailConfig: + """ + Retrieves the TrendMicroRailConfig from the provided RailsConfig object. + + Args: + config (RailsConfig): The Rails configuration object containing possible + Trend Micro settings. + + Returns: + TrendMicroRailConfig: The Trend Micro configuration, either from the provided + config or a default instance. + """ if ( not hasattr(config.rails.config, "trend_micro") or config.rails.config.trend_micro is None @@ -46,7 +93,12 @@ def get_config(config: RailsConfig) -> TrendMicroRailConfig: return cast(TrendMicroRailConfig, config.rails.config.trend_micro) -@action(is_system_action=True) +def trend_ai_guard_mapping(result: GuardResult) -> bool: + """Convert Trend Micro result to boolean for flow logic.""" + return result.action.lower() == "block" + + +@action(is_system_action=True, output_mapping=trend_ai_guard_mapping) async def trend_ai_guard(config: RailsConfig, text: Optional[str] = None): """ Custom action to invoke the Trend Ai Guard @@ -59,10 +111,11 @@ async def trend_ai_guard(config: RailsConfig, text: Optional[str] = None): v1_api_key = trend_config.get_api_key() if not v1_api_key: - raise ValueError("Trend Micro Vision One API Key not found") - - if text is None: - raise ValueError("No prompt/response found in the last event.") + log.error("Trend Micro Vision One API Key not found") + return GuardResult( + action="Block", + reason="Trend Micro Vision One API Key not found", + ) async with httpx.AsyncClient() as client: data = Guard(guard=text).model_dump() @@ -80,10 +133,10 @@ async def trend_ai_guard(config: RailsConfig, text: Optional[str] = None): response.raise_for_status() guard_result = GuardResult(**response.json()) log.debug("Trend Micro AI Guard Result: %s", guard_result) - except Exception as e: + except httpx.HTTPStatusError as e: log.error("Error calling Trend Micro AI Guard API: %s", e) return GuardResult( - action="allow", + action="Allow", reason="An error occurred while calling the Trend Micro AI Guard API.", ) return guard_result diff --git a/nemoguardrails/library/trend_micro/flows.co b/nemoguardrails/library/trend_micro/flows.co index d3d900766..78d3d2305 100644 --- a/nemoguardrails/library/trend_micro/flows.co +++ b/nemoguardrails/library/trend_micro/flows.co @@ -1,10 +1,22 @@ # INPUT AND/OR OUTPUT RAIL -flow trend ai guard $text +flow trend ai guard input $text $result = await TrendAiGuardAction(text=$text) - if $result.action == "Block" # Fails open if AI Guard service has an error + if $result.blocked # Fails open if AI Guard service has an error if $system.config.enable_rails_exceptions - send TrendAiGuardException(message="Blocked by the 'trend ai guard' flow: " + $result.reason) + send TrendAiGuardRailException(message="Blocked by the 'trend ai guard input' flow: " + $result.reason) else bot refuse to respond - abort + abort + + +# OUTPUT RAIL +flow trend ai guard output $text + $result = await TrendAiGuardAction(text=$text) + + if $result.blocked # Fails open if AI Guard service has an error + if $system.config.enable_rails_exceptions + send TrendAiGuardRailException(message="Blocked by the 'trend ai guard output' flow: " + $result.reason) + else + bot refuse to respond + abort diff --git a/nemoguardrails/library/trend_micro/flows.v1.co b/nemoguardrails/library/trend_micro/flows.v1.co index 2d3ad3079..7089882a0 100644 --- a/nemoguardrails/library/trend_micro/flows.v1.co +++ b/nemoguardrails/library/trend_micro/flows.v1.co @@ -2,22 +2,22 @@ define subflow trend ai guard input $result = execute trend_ai_guard(text=$user_message) - if $result.action == "Block" # Fails open if AI Guard service has an error + if $result.blocked # Fails open if AI Guard service has an error if $config.enable_rails_exceptions $msg = "Blocked by the 'trend ai guard input' flow: " + $result.reason - create event TrendAiGuardException(message=$msg) + create event TrendAiGuardRailException(message=$msg) else bot refuse to respond - stop + stop # OUTPUT RAIL define subflow trend ai guard output $result = execute trend_ai_guard(text=$bot_message) - if $result.action == "Block" # Fails open if AI Guard service has an error + if $result.blocked # Fails open if AI Guard service has an error if $config.enable_rails_exceptions $msg = "Blocked by the 'trend ai guard output' flow: " + $result.reason - create event TrendAiGuardException(message=$msg) + create event TrendAiGuardRailException(message=$msg) else bot refuse to respond - stop + stop diff --git a/tests/test_trend_ai_guard.py b/tests/test_trend_ai_guard.py index 73dda7c24..b1f8db4e2 100644 --- a/tests/test_trend_ai_guard.py +++ b/tests/test_trend_ai_guard.py @@ -52,7 +52,7 @@ def test_trend_ai_guard_blocked(httpx_mock: HTTPXMock, monkeypatch: pytest.Monke monkeypatch.setenv("V1_API_KEY", "test-token") httpx_mock.add_response( is_reusable=True, - json={"action": "Block", "reason": "Prompt Attack Detected"}, + json={"action": "Block", "reason": "Prompt Attack Detected", "blocked": True}, ) chat = TestChat( @@ -86,8 +86,9 @@ def test_trend_ai_guard_error( @pytest.mark.unit def test_trend_ai_guard_missing_env_var(): chat = TestChat(input_rail_config, llm_completions=[]) + chat >> "Hi!" - chat << "I'm sorry, an internal error has occurred." + chat << "I'm sorry, I can't respond to that." @pytest.mark.unit @@ -104,4 +105,4 @@ def test_trend_ai_guard_malformed_response( # Should fail open chat >> "What is the air-speed velocity of an unladen swallow?" - chat << "What do you mean? An African or a European swallow?" + chat << "I'm sorry, an internal error has occurred."