Skip to content

Commit de7c5f0

Browse files
committed
Enforce inline-check thresholds and cover fail-safe/prompt-attack paths
- Reject non-finite or out-of-[0,1] SeverityThreshold/ConfidenceThreshold at assignment: Bedrock caps scores at 1.0, so a threshold >1 (or NaN) can never be met and would silently allow every detection (fail-open). - Add mapper tests proving a flagged finding with no score denies (documented fail-safe) for both content-filter severity and PII confidence. - Add a prompt-attack backend test verifying the emitted config and severity-based denial. - Decouple InlineChecksIntegrationTests from the provisioning GuardrailFixture (use IntegrationConfig.Region) so it runs with inline-check-only permissions and truly tests the no-guardrail path.
1 parent f9e4870 commit de7c5f0

4 files changed

Lines changed: 132 additions & 10 deletions

File tree

src/AWS.Bedrock.MAG/Policy/GuardrailChecksOptions.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
using System;
45
using System.Collections.Generic;
56

67
namespace AWS.Bedrock.MAG
@@ -24,10 +25,33 @@ public sealed class GuardrailChecksOptions
2425
/// <summary>
2526
/// Content-filter and prompt-attack severity at or above which the call is denied (0.0 to 1.0).
2627
/// </summary>
27-
public double SeverityThreshold { get; set; } = 0.5;
28+
public double SeverityThreshold
29+
{
30+
get => _severityThreshold;
31+
set => _severityThreshold = ValidateThreshold(value, nameof(SeverityThreshold));
32+
}
2833

2934
/// <summary>PII confidence at or above which the call is denied (0.0 to 1.0).</summary>
30-
public double ConfidenceThreshold { get; set; } = 0.5;
35+
public double ConfidenceThreshold
36+
{
37+
get => _confidenceThreshold;
38+
set => _confidenceThreshold = ValidateThreshold(value, nameof(ConfidenceThreshold));
39+
}
40+
41+
private double _severityThreshold = 0.5;
42+
private double _confidenceThreshold = 0.5;
43+
44+
// Bedrock caps these scores at 1.0, so a threshold outside [0, 1] (or NaN) can never be met and would
45+
// silently allow every detection through — a fail-open misconfiguration. Reject it at assignment.
46+
private static double ValidateThreshold(double value, string name)
47+
{
48+
if (!double.IsFinite(value) || value < 0.0 || value > 1.0)
49+
{
50+
throw new ArgumentOutOfRangeException(name, value, "Threshold must be a finite value between 0.0 and 1.0 inclusive.");
51+
}
52+
53+
return value;
54+
}
3155

3256
/// <summary>True when at least one category or entity is configured.</summary>
3357
public bool HasAnyCheck =>

test/AWS.Bedrock.MAG.IntegrationTests/InlineChecksIntegrationTests.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,18 @@ namespace AWS.Bedrock.MAG.IntegrationTests
1212
{
1313
/// <summary>
1414
/// Runs the policy backend in inline-checks mode against real InvokeGuardrailChecks, i.e. with no
15-
/// pre-created guardrail (PR: InvokeGuardrailChecks).
15+
/// pre-created guardrail (PR: InvokeGuardrailChecks). Deliberately does NOT join the
16+
/// "bedrock-integration" collection: the shared fixture provisions a real guardrail and log group, which
17+
/// this mode neither needs nor should require, so these tests can run with inline-check-only permissions
18+
/// and genuinely exercise the no-pre-created-guardrail path.
1619
/// </summary>
17-
[Collection("bedrock-integration")]
1820
public class InlineChecksIntegrationTests
1921
{
20-
private readonly GuardrailFixture _fx;
21-
22-
public InlineChecksIntegrationTests(GuardrailFixture fx) => _fx = fx;
23-
24-
private BedrockGuardrailsPolicyBackend Backend()
22+
private static BedrockGuardrailsPolicyBackend Backend()
2523
{
2624
var options = new BedrockGuardrailsPolicyOptions
2725
{
28-
Region = _fx.Region,
26+
Region = IntegrationConfig.Region,
2927
InlineChecks = new GuardrailChecksOptions { ConfidenceThreshold = 0.1 }
3028
};
3129
options.InlineChecks.SensitiveInformationEntities.Add("US_SOCIAL_SECURITY_NUMBER");

test/AWS.Bedrock.MAG.UnitTests/GuardrailResponseMapperTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,53 @@ public void SummarizeAssessment_includes_action_and_pii_types()
105105
Assert.Contains("action=GUARDRAIL_INTERVENED", summary);
106106
Assert.Contains("NAME", summary);
107107
}
108+
109+
[Fact]
110+
public void ChecksTripped_treats_a_content_finding_with_no_severity_score_as_tripped()
111+
{
112+
// Fail-safe: a flagged entry with no score must deny rather than slip through. A very high
113+
// threshold ensures the entry only trips because the missing score is treated as meeting it.
114+
var response = new InvokeGuardrailChecksResponse
115+
{
116+
Results = new GuardrailChecksResults
117+
{
118+
ContentFilter = new GuardrailChecksContentFilterResult
119+
{
120+
Results = new List<GuardrailChecksContentFilterResultEntry>
121+
{
122+
new() { Category = "HATE" } // SeverityScore deliberately unset (null).
123+
}
124+
}
125+
}
126+
};
127+
128+
var tripped = GuardrailResponseMapper.ChecksTripped(response, severityThreshold: 1.0, confidenceThreshold: 1.0, out var summary);
129+
130+
Assert.True(tripped);
131+
Assert.Contains("HATE", summary);
132+
}
133+
134+
[Fact]
135+
public void ChecksTripped_treats_a_pii_finding_with_no_confidence_score_as_tripped()
136+
{
137+
var response = new InvokeGuardrailChecksResponse
138+
{
139+
Results = new GuardrailChecksResults
140+
{
141+
SensitiveInformation = new GuardrailChecksSensitiveInformationResult
142+
{
143+
Results = new List<GuardrailChecksSensitiveInformationResultEntry>
144+
{
145+
new() { Type = "US_SOCIAL_SECURITY_NUMBER" } // ConfidenceScore deliberately unset (null).
146+
}
147+
}
148+
}
149+
};
150+
151+
var tripped = GuardrailResponseMapper.ChecksTripped(response, severityThreshold: 1.0, confidenceThreshold: 1.0, out var summary);
152+
153+
Assert.True(tripped);
154+
Assert.Contains("US_SOCIAL_SECURITY_NUMBER", summary);
155+
}
108156
}
109157
}

test/AWS.Bedrock.MAG.UnitTests/Policy/InlineChecksPolicyBackendTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,30 @@ private static BedrockGuardrailsPolicyOptions ContentFilterOptions(double severi
4949
}
5050
};
5151

52+
private static BedrockGuardrailsPolicyOptions PromptAttackOptions(double severityThreshold = 0.5)
53+
{
54+
var options = new BedrockGuardrailsPolicyOptions
55+
{
56+
InlineChecks = new GuardrailChecksOptions { SeverityThreshold = severityThreshold }
57+
};
58+
options.InlineChecks.PromptAttackCategories.Add("PROMPT_INJECTION");
59+
return options;
60+
}
61+
62+
private static InvokeGuardrailChecksResponse PromptAttackResult(string category, double severity) => new()
63+
{
64+
Results = new GuardrailChecksResults
65+
{
66+
PromptAttack = new GuardrailChecksPromptAttackResult
67+
{
68+
Results = new List<GuardrailChecksPromptAttackResultEntry>
69+
{
70+
new() { Category = category, SeverityScore = severity }
71+
}
72+
}
73+
}
74+
};
75+
5276
private static readonly Dictionary<string, object> Context = new() { ["tool"] = "send_email" };
5377

5478
[Fact]
@@ -116,5 +140,33 @@ public async Task Allows_when_score_is_below_the_severity_threshold()
116140

117141
Assert.True(decision.Allowed);
118142
}
143+
144+
[Fact]
145+
public async Task Emits_prompt_attack_config_and_denies_on_severity()
146+
{
147+
InvokeGuardrailChecksRequest? captured = null;
148+
var mock = Mock(PromptAttackResult("PROMPT_INJECTION", 0.9), r => captured = r);
149+
var backend = new BedrockGuardrailsPolicyBackend(PromptAttackOptions(severityThreshold: 0.5), mock.Object);
150+
151+
var decision = await backend.EvaluateAsync(Context);
152+
153+
Assert.NotNull(captured);
154+
Assert.NotNull(captured!.Checks.PromptAttack);
155+
Assert.Single(captured.Checks.PromptAttack.Categories);
156+
Assert.False(decision.Allowed);
157+
Assert.Contains("PROMPT_INJECTION", decision.Reason);
158+
}
159+
160+
[Theory]
161+
[InlineData(-0.1)]
162+
[InlineData(1.1)]
163+
[InlineData(double.NaN)]
164+
[InlineData(double.PositiveInfinity)]
165+
public void Threshold_setters_reject_out_of_range_or_non_finite_values(double bad)
166+
{
167+
// An unenforced threshold > 1 (or NaN) can never be met and would silently allow every detection.
168+
Assert.Throws<ArgumentOutOfRangeException>(() => new GuardrailChecksOptions { SeverityThreshold = bad });
169+
Assert.Throws<ArgumentOutOfRangeException>(() => new GuardrailChecksOptions { ConfidenceThreshold = bad });
170+
}
119171
}
120172
}

0 commit comments

Comments
 (0)