AI Guard: end-to-end tests for SDS redaction - #7385
Conversation
Adds a proof-of-concept test suite for AI Guard sensitive-data redaction:
- Test_Redaction: asserts the tracer applies the backend
redaction_replacements to the message payload stored in the ai_guard
meta struct (single value, multi-message, multi-finding, mixed, tool
result, tool arguments, system prompt).
- Test_NoRedaction: benign payloads are left untouched.
- Test_RedactionInSDKResponse: the SDK evaluate() response exposes the
{path, replacement} contract alongside sds_findings.
Scenarios live in tests/ai_guard/redaction_scenarios.json and the
matching VCR cassettes are generated from it by
utils/scripts/gen_redaction_cassettes.py, so payloads and expected
replacements cannot drift.
Marked missing_feature in every manifest: no tracer implements the
redaction contract yet.
APPSEC-68867
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
|
…path
Extends the SDS redaction end-to-end tests to the surfaces the redaction RFC
makes normative, aligned with the reference implementation in dd-trace-py#19360.
Metrics:
- Test_AIGuardTelemetryRedacted asserts the new `redacted` tag on the
ai_guard.requests metric, alongside the existing action/block/error tags.
- Every redaction assertion now also checks the ai_guard.redacted span tag.
- Fix the telemetry lookup: AI Guard metrics are emitted in the appsec
namespace as ai_guard.requests, not in an ai_guard namespace as requests.
Both identities are accepted so the tests survive the planned namespace move.
- Set the telemetry heartbeat on the scenarios that read metrics mid-run;
metrics are only sent at the heartbeat, 60s by default.
Kill-switch:
- New AI_GUARD_REDACTION_DISABLED scenario (DD_AI_GUARD_REDACTION_ENABLED=false)
hosting Test_RedactionDisabled and Test_RedactionDisabledTelemetry. Neither
the span tag nor the telemetry tag may be emitted, so an absent tag ("off")
stays distinguishable from false ("on, nothing redacted").
Blocked path:
- Test_RedactionOnBlock sends a redacting scenario with blocking enabled: the
span must still report the redaction and the abort error must carry the
redacted messages. The flask weblog now exposes those messages on its 403.
Scenarios and cassettes stay generated together by gen_redaction_cassettes.py,
whose reference implementation of the RFC algorithm cross-checks every expected
outcome. All new classes are declared missing_feature in every manifest.
Also restores the scenario reference in tests/appsec/smoke_tests, which pointed
at a scenario that does not exist.
RFC: https://datadoghq.atlassian.net/browse/APPSEC-68806
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-redaction-tests # Conflicts: # manifests/cpp_httpd.yml # manifests/cpp_nginx.yml # manifests/dotnet.yml # manifests/golang.yml # manifests/java.yml # manifests/nodejs.yml # manifests/php.yml # manifests/python.yml # manifests/ruby.yml
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d0cf6add9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
christophe-papazian
left a comment
There was a problem hiding this comment.
Reviewed the fixture machinery first: regeneration is deterministic (no drift), and I re-derived all 31 cassette filenames from their request bodies, including the 6 previously recorded ones — the hash formula is correct. mypy/ruff clean, all 9 manifests complete, CI wiring matches AI_GUARD_TELEMETRY.
The issues below are mostly about the test oracles. Blocking ones IMO: the truncation/cassette mismatch on Test_AIGuardTelemetryRedacted, and the cassette deletion in the generator.
(Reviewed with Claude Code + codex.)
cbeauchesne
left a comment
There was a problem hiding this comment.
From framework usage, AGTM
Motivation
This is a proof of concept. No tracer implements the AI Guard redaction contract yet, so every new test is marked
missing_featurein all manifests. The goal is to agree on the shape of the cross-language assertions before libraries start implementing.Jira: APPSEC-68867
RFC: SDS for AI Guard
Python Implementation: DataDog/dd-trace-py#19360
What this PR does
Adds three test classes to
tests/ai_guard/test_ai_guard_sdk.py:Test_Redaction— the backend returns a top-levelredaction_replacementsarray of{path, replacement}pairs; the tracer must overwrite each path verbatim and store the redacted messages in theai_guardmeta struct. Assertions run against the meta struct because that is the cross-language, cross-provider surface. Covered shapes: single value, multiple messages with one finding, one message with several findings, mixed, tool result content, tool call arguments (must stay valid JSON), and system prompt.Test_NoRedaction— benign payloads (plain message, benign tool-call conversation) come back with noredaction_replacementsand unmodified messages.Test_RedactionInSDKResponse— the SDKevaluate()response exposes theredaction_replacementscontract, independent of thesds_findingsdetection metadata, which must still be present.Keeping fixtures in sync
Scenarios (messages, expected replacements, raw sensitive values) live in
tests/ai_guard/redaction_scenarios.json. The matching VCR cassettes underutils/build/docker/vcr/cassettes/aiguard/are generated from that same file byutils/scripts/gen_redaction_cassettes.py, which computes the cassette filename hash from the request body — so the payloads the tests send and the responses the mock backend replays cannot drift. Regenerate both after editing a scenario.Notes for reviewers
missing_featurefor cpp_httpd, cpp_nginx, dotnet, golang, java, nodejs, php, python, ruby.redaction_replacementsfield name and the path grammar (messages[0].content,messages[1].tool_calls[0].function.arguments) are the main things worth reviewing — they're taken from the RFC and encoded in_resolve_path..gitignoreaddition for a local tooling directory (.omc/).🤖 Generated with Claude Code