mirror of
https://github.com/alibaba/higress.git
synced 2026-05-22 19:57:29 +08:00
649 lines
21 KiB
Go
649 lines
21 KiB
Go
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
|
//
|
|
// 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.
|
|
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"testing"
|
|
"testing/quick"
|
|
)
|
|
|
|
// validSensitiveLevels are the valid sensitive data levels in ascending order.
|
|
var validSensitiveLevels = []string{"S0", "S1", "S2", "S3", "S4"}
|
|
|
|
// Feature: sensitive-data-mask-threshold, Property 1: Above-threshold mask produces RiskMask
|
|
// **Validates: Requirements 1.1, 4.1**
|
|
//
|
|
// For any valid sensitive level L and threshold T where LevelToInt(L) >= LevelToInt(T),
|
|
// when evaluateRiskMultiModal is called with a single Detail of Type=sensitiveData,
|
|
// Suggestion=mask, Level=L, config SensitiveDataAction=mask, SensitiveDataLevelBar=T,
|
|
// and no other blocking conditions, the result SHALL be RiskMask.
|
|
func TestProperty1_AboveThresholdMaskProducesRiskMask(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
// Use seed to deterministically pick a (level, threshold) pair
|
|
// where LevelToInt(level) >= LevelToInt(threshold)
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick threshold index [0..4], then level index [thresholdIdx..4]
|
|
thresholdIdx := r.Intn(len(validSensitiveLevels))
|
|
levelIdx := thresholdIdx + r.Intn(len(validSensitiveLevels)-thresholdIdx)
|
|
|
|
level := validSensitiveLevels[levelIdx]
|
|
threshold := validSensitiveLevels[thresholdIdx]
|
|
|
|
// Sanity: level >= threshold
|
|
if LevelToInt(level) < LevelToInt(threshold) {
|
|
t.Errorf("generator bug: level=%s (%d) < threshold=%s (%d)", level, LevelToInt(level), threshold, LevelToInt(threshold))
|
|
return false
|
|
}
|
|
|
|
config := baseConfig()
|
|
config.SensitiveDataAction = "mask"
|
|
config.SensitiveDataLevelBar = threshold
|
|
// Set all other thresholds to max (most permissive) to avoid interference
|
|
config.ContentModerationLevelBar = MaxRisk
|
|
config.PromptAttackLevelBar = MaxRisk
|
|
config.MaliciousUrlLevelBar = MaxRisk
|
|
config.ModelHallucinationLevelBar = MaxRisk
|
|
config.CustomLabelLevelBar = MaxRisk
|
|
config.RiskAction = "block"
|
|
|
|
data := Data{
|
|
RiskLevel: "none", // Avoid top-level gate triggering
|
|
Detail: []Detail{
|
|
{
|
|
Type: SensitiveDataType,
|
|
Suggestion: "mask",
|
|
Level: level,
|
|
Result: []Result{{Ext: Ext{Desensitization: "masked-content"}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskMask {
|
|
t.Errorf("expected RiskMask for level=%s, threshold=%s, got %d", level, threshold, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 1 failed: %v", err)
|
|
fmt.Printf("Property 1 counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Feature: sensitive-data-mask-threshold, Property 2: Below-threshold mask produces RiskPass
|
|
// **Validates: Requirements 1.2, 1.3**
|
|
//
|
|
// For any valid sensitive level L and threshold T where LevelToInt(L) < LevelToInt(T),
|
|
// when evaluateRiskMultiModal is called with a single Detail of Type=sensitiveData,
|
|
// Suggestion=mask, Level=L, config SensitiveDataAction=mask, SensitiveDataLevelBar=T,
|
|
// and no other blocking conditions, the result SHALL be RiskPass.
|
|
func TestProperty2_BelowThresholdMaskProducesRiskPass(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
// Use seed to deterministically pick a (level, threshold) pair
|
|
// where LevelToInt(level) < LevelToInt(threshold)
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick threshold index [1..4], then level index [0..thresholdIdx-1]
|
|
thresholdIdx := 1 + r.Intn(len(validSensitiveLevels)-1) // [1..4]
|
|
levelIdx := r.Intn(thresholdIdx) // [0..thresholdIdx-1]
|
|
|
|
level := validSensitiveLevels[levelIdx]
|
|
threshold := validSensitiveLevels[thresholdIdx]
|
|
|
|
// Sanity: level < threshold
|
|
if LevelToInt(level) >= LevelToInt(threshold) {
|
|
t.Errorf("generator bug: level=%s (%d) >= threshold=%s (%d)", level, LevelToInt(level), threshold, LevelToInt(threshold))
|
|
return false
|
|
}
|
|
|
|
config := baseConfig()
|
|
config.SensitiveDataAction = "mask"
|
|
config.SensitiveDataLevelBar = threshold
|
|
config.ContentModerationLevelBar = MaxRisk
|
|
config.PromptAttackLevelBar = MaxRisk
|
|
config.MaliciousUrlLevelBar = MaxRisk
|
|
config.ModelHallucinationLevelBar = MaxRisk
|
|
config.CustomLabelLevelBar = MaxRisk
|
|
config.RiskAction = "block"
|
|
|
|
data := Data{
|
|
RiskLevel: "none",
|
|
Detail: []Detail{
|
|
{
|
|
Type: SensitiveDataType,
|
|
Suggestion: "mask",
|
|
Level: level,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskPass {
|
|
t.Errorf("expected RiskPass for level=%s, threshold=%s, got %d", level, threshold, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 2 failed: %v", err)
|
|
fmt.Printf("Property 2 counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Feature: sensitive-data-mask-threshold, Property 3: Per-detail threshold independence
|
|
// **Validates: Requirements 1.4**
|
|
//
|
|
// For any list of sensitiveData Details each with Suggestion=mask and varying levels,
|
|
// and a threshold T, when evaluateRiskMultiModal is called with SensitiveDataAction=mask
|
|
// and no blocking conditions: the result SHALL be RiskMask if and only if at least one
|
|
// Detail has LevelToInt(Level) >= LevelToInt(T).
|
|
func TestProperty3_PerDetailThresholdIndependence(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick a random threshold from validSensitiveLevels
|
|
thresholdIdx := r.Intn(len(validSensitiveLevels))
|
|
threshold := validSensitiveLevels[thresholdIdx]
|
|
|
|
// Generate 1-5 random sensitiveData details
|
|
numDetails := 1 + r.Intn(5)
|
|
details := make([]Detail, numDetails)
|
|
expectMask := false
|
|
|
|
for i := 0; i < numDetails; i++ {
|
|
levelIdx := r.Intn(len(validSensitiveLevels))
|
|
level := validSensitiveLevels[levelIdx]
|
|
|
|
detail := Detail{
|
|
Type: SensitiveDataType,
|
|
Suggestion: "mask",
|
|
Level: level,
|
|
}
|
|
|
|
// Details that meet threshold should have Result with Desensitization content
|
|
if LevelToInt(level) >= LevelToInt(threshold) {
|
|
expectMask = true
|
|
detail.Result = []Result{{Ext: Ext{Desensitization: "masked-content"}}}
|
|
}
|
|
|
|
details[i] = detail
|
|
}
|
|
|
|
config := baseConfig()
|
|
config.SensitiveDataAction = "mask"
|
|
config.SensitiveDataLevelBar = threshold
|
|
// Set all other thresholds to max to avoid interference
|
|
config.ContentModerationLevelBar = MaxRisk
|
|
config.PromptAttackLevelBar = MaxRisk
|
|
config.MaliciousUrlLevelBar = MaxRisk
|
|
config.ModelHallucinationLevelBar = MaxRisk
|
|
config.CustomLabelLevelBar = MaxRisk
|
|
config.RiskAction = "block"
|
|
|
|
data := Data{
|
|
RiskLevel: "none",
|
|
Detail: details,
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
|
|
if expectMask {
|
|
if result != RiskMask {
|
|
t.Errorf("expected RiskMask: threshold=%s, details=%v, got %d", threshold, describeLevels(details), result)
|
|
return false
|
|
}
|
|
} else {
|
|
if result != RiskPass {
|
|
t.Errorf("expected RiskPass: threshold=%s, details=%v, got %d", threshold, describeLevels(details), result)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 3 failed: %v", err)
|
|
fmt.Printf("Property 3 counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// describeLevels returns a slice of level strings from the given details for error reporting.
|
|
func describeLevels(details []Detail) []string {
|
|
levels := make([]string, len(details))
|
|
for i, d := range details {
|
|
levels[i] = d.Level
|
|
}
|
|
return levels
|
|
}
|
|
|
|
// validGeneralRiskLevels are the valid general risk levels in ascending order.
|
|
var validGeneralRiskLevels = []string{"none", "low", "medium", "high", "max"}
|
|
|
|
// knownDetailTypes are the known dimension types used for generating random details.
|
|
var knownDetailTypes = []string{
|
|
SensitiveDataType,
|
|
ContentModerationType,
|
|
PromptAttackType,
|
|
MaliciousUrlDataType,
|
|
ModelHallucinationDataType,
|
|
CustomLabelType,
|
|
}
|
|
|
|
// Feature: sensitive-data-mask-threshold, Property 4: Block triggers always produce RiskBlock
|
|
// **Validates: Requirements 3.1, 3.2**
|
|
//
|
|
// Sub-property 4a: For any Detail with Suggestion=block, regardless of type, level,
|
|
// dimAction, or threshold configuration, evaluateRiskMultiModal SHALL return RiskBlock.
|
|
//
|
|
// Sub-property 4b: For any Detail where the resolved dimAction is "block" and the
|
|
// detail's level exceeds the configured threshold, evaluateRiskMultiModal SHALL return RiskBlock.
|
|
func TestProperty4a_SuggestionBlockAlwaysProducesRiskBlock(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick a random detail type
|
|
detailType := knownDetailTypes[r.Intn(len(knownDetailTypes))]
|
|
|
|
// Pick a random level based on type
|
|
var level string
|
|
if detailType == SensitiveDataType {
|
|
level = validSensitiveLevels[r.Intn(len(validSensitiveLevels))]
|
|
} else {
|
|
level = validGeneralRiskLevels[r.Intn(len(validGeneralRiskLevels))]
|
|
}
|
|
|
|
// Random config: pick random dimAction (block or mask) and random thresholds
|
|
config := baseConfig()
|
|
|
|
// Randomly assign dimension actions
|
|
actions := []string{"block", "mask"}
|
|
config.ContentModerationAction = actions[r.Intn(2)]
|
|
config.PromptAttackAction = actions[r.Intn(2)]
|
|
config.SensitiveDataAction = actions[r.Intn(2)]
|
|
config.MaliciousUrlAction = actions[r.Intn(2)]
|
|
config.ModelHallucinationAction = actions[r.Intn(2)]
|
|
config.CustomLabelAction = actions[r.Intn(2)]
|
|
|
|
// Random thresholds
|
|
config.ContentModerationLevelBar = validGeneralRiskLevels[1+r.Intn(len(validGeneralRiskLevels)-1)]
|
|
config.PromptAttackLevelBar = validGeneralRiskLevels[1+r.Intn(len(validGeneralRiskLevels)-1)]
|
|
config.SensitiveDataLevelBar = validSensitiveLevels[r.Intn(len(validSensitiveLevels))]
|
|
config.MaliciousUrlLevelBar = validGeneralRiskLevels[1+r.Intn(len(validGeneralRiskLevels)-1)]
|
|
config.ModelHallucinationLevelBar = validGeneralRiskLevels[1+r.Intn(len(validGeneralRiskLevels)-1)]
|
|
config.CustomLabelLevelBar = validGeneralRiskLevels[1+r.Intn(len(validGeneralRiskLevels)-1)]
|
|
|
|
data := Data{
|
|
RiskLevel: "none", // Avoid top-level gate interference
|
|
Detail: []Detail{
|
|
{
|
|
Type: detailType,
|
|
Suggestion: "block", // Always block suggestion
|
|
Level: level,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskBlock {
|
|
t.Errorf("expected RiskBlock for Suggestion=block, type=%s, level=%s, got %d", detailType, level, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 4a failed: %v", err)
|
|
fmt.Printf("Property 4a counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
func TestProperty4b_DimActionBlockExceedsThresholdProducesRiskBlock(t *testing.T) {
|
|
// Test with dimension types that support block action and have configurable thresholds.
|
|
// For each iteration, pick a dimension type, set its action to "block",
|
|
// and set level >= threshold to ensure exceeds=true.
|
|
type dimConfig struct {
|
|
detailType string
|
|
levels []string
|
|
setThreshold func(config *AISecurityConfig, threshold string)
|
|
}
|
|
|
|
dims := []dimConfig{
|
|
{
|
|
detailType: ContentModerationType,
|
|
levels: validGeneralRiskLevels,
|
|
setThreshold: func(c *AISecurityConfig, t string) {
|
|
c.ContentModerationAction = "block"
|
|
c.ContentModerationLevelBar = t
|
|
},
|
|
},
|
|
{
|
|
detailType: PromptAttackType,
|
|
levels: validGeneralRiskLevels,
|
|
setThreshold: func(c *AISecurityConfig, t string) {
|
|
c.PromptAttackAction = "block"
|
|
c.PromptAttackLevelBar = t
|
|
},
|
|
},
|
|
{
|
|
detailType: SensitiveDataType,
|
|
levels: validSensitiveLevels,
|
|
setThreshold: func(c *AISecurityConfig, t string) {
|
|
c.SensitiveDataAction = "block"
|
|
c.SensitiveDataLevelBar = t
|
|
},
|
|
},
|
|
{
|
|
detailType: MaliciousUrlDataType,
|
|
levels: validGeneralRiskLevels,
|
|
setThreshold: func(c *AISecurityConfig, t string) {
|
|
c.MaliciousUrlAction = "block"
|
|
c.MaliciousUrlLevelBar = t
|
|
},
|
|
},
|
|
{
|
|
detailType: ModelHallucinationDataType,
|
|
levels: validGeneralRiskLevels,
|
|
setThreshold: func(c *AISecurityConfig, t string) {
|
|
c.ModelHallucinationAction = "block"
|
|
c.ModelHallucinationLevelBar = t
|
|
},
|
|
},
|
|
{
|
|
detailType: CustomLabelType,
|
|
levels: validGeneralRiskLevels,
|
|
setThreshold: func(c *AISecurityConfig, t string) {
|
|
c.CustomLabelAction = "block"
|
|
c.CustomLabelLevelBar = t
|
|
},
|
|
},
|
|
}
|
|
|
|
f := func(seed uint64) bool {
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick a random dimension
|
|
dim := dims[r.Intn(len(dims))]
|
|
|
|
// Pick threshold index, then level index >= threshold
|
|
thresholdIdx := r.Intn(len(dim.levels))
|
|
levelIdx := thresholdIdx + r.Intn(len(dim.levels)-thresholdIdx)
|
|
|
|
threshold := dim.levels[thresholdIdx]
|
|
level := dim.levels[levelIdx]
|
|
|
|
// Sanity: level >= threshold
|
|
if LevelToInt(level) < LevelToInt(threshold) {
|
|
t.Errorf("generator bug: level=%s (%d) < threshold=%s (%d)", level, LevelToInt(level), threshold, LevelToInt(threshold))
|
|
return false
|
|
}
|
|
|
|
config := baseConfig()
|
|
// Set all other thresholds to max to avoid interference
|
|
config.ContentModerationLevelBar = MaxRisk
|
|
config.PromptAttackLevelBar = MaxRisk
|
|
config.SensitiveDataLevelBar = S4Sensitive
|
|
config.MaliciousUrlLevelBar = MaxRisk
|
|
config.ModelHallucinationLevelBar = MaxRisk
|
|
config.CustomLabelLevelBar = MaxRisk
|
|
|
|
// Configure the chosen dimension with block action and threshold
|
|
dim.setThreshold(&config, threshold)
|
|
|
|
// Use a non-block suggestion so we test the dimAction=block + exceeds path
|
|
// (not the Suggestion=block shortcut tested in 4a)
|
|
suggestion := "pass"
|
|
|
|
data := Data{
|
|
RiskLevel: "none", // Avoid top-level gate interference
|
|
Detail: []Detail{
|
|
{
|
|
Type: dim.detailType,
|
|
Suggestion: suggestion,
|
|
Level: level,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskBlock {
|
|
t.Errorf("expected RiskBlock for dimAction=block, type=%s, level=%s, threshold=%s, got %d",
|
|
dim.detailType, level, threshold, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 4b failed: %v", err)
|
|
fmt.Printf("Property 4b counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Feature: sensitive-data-mask-threshold, Property 5: Top-level gates produce RiskBlock
|
|
// **Validates: Requirements 3.3, 3.4**
|
|
//
|
|
// Sub-property 5a: For any Data.RiskLevel and contentModerationLevelBar where
|
|
// LevelToInt(RiskLevel) >= LevelToInt(contentModerationLevelBar),
|
|
// evaluateRiskMultiModal SHALL return RiskBlock regardless of Detail content.
|
|
//
|
|
// Sub-property 5b: For any Data.AttackLevel and promptAttackLevelBar where
|
|
// LevelToInt(AttackLevel) >= LevelToInt(promptAttackLevelBar),
|
|
// evaluateRiskMultiModal SHALL return RiskBlock regardless of Detail content.
|
|
func TestProperty5a_TopLevelRiskLevelGateProducesRiskBlock(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick (riskLevel, threshold) where LevelToInt(riskLevel) >= LevelToInt(threshold)
|
|
// Use validGeneralRiskLevels [none, low, medium, high, max]
|
|
thresholdIdx := r.Intn(len(validGeneralRiskLevels))
|
|
levelIdx := thresholdIdx + r.Intn(len(validGeneralRiskLevels)-thresholdIdx)
|
|
|
|
riskLevel := validGeneralRiskLevels[levelIdx]
|
|
threshold := validGeneralRiskLevels[thresholdIdx]
|
|
|
|
// Sanity check
|
|
if LevelToInt(riskLevel) < LevelToInt(threshold) {
|
|
t.Errorf("generator bug: riskLevel=%s (%d) < threshold=%s (%d)",
|
|
riskLevel, LevelToInt(riskLevel), threshold, LevelToInt(threshold))
|
|
return false
|
|
}
|
|
|
|
config := baseConfig()
|
|
config.ContentModerationLevelBar = threshold
|
|
// Set promptAttackLevelBar to max so it doesn't interfere
|
|
config.PromptAttackLevelBar = MaxRisk
|
|
|
|
// Generate random details to show they don't matter
|
|
numDetails := r.Intn(4) // 0-3 random details
|
|
details := make([]Detail, numDetails)
|
|
for i := 0; i < numDetails; i++ {
|
|
detailType := knownDetailTypes[r.Intn(len(knownDetailTypes))]
|
|
var level string
|
|
if detailType == SensitiveDataType {
|
|
level = validSensitiveLevels[r.Intn(len(validSensitiveLevels))]
|
|
} else {
|
|
level = validGeneralRiskLevels[r.Intn(len(validGeneralRiskLevels))]
|
|
}
|
|
details[i] = Detail{
|
|
Type: detailType,
|
|
Suggestion: "pass",
|
|
Level: level,
|
|
}
|
|
}
|
|
|
|
data := Data{
|
|
RiskLevel: riskLevel,
|
|
Detail: details,
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskBlock {
|
|
t.Errorf("expected RiskBlock for RiskLevel=%s, contentModerationLevelBar=%s, got %d",
|
|
riskLevel, threshold, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 5a failed: %v", err)
|
|
fmt.Printf("Property 5a counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
func TestProperty5b_TopLevelAttackLevelGateProducesRiskBlock(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Pick (attackLevel, threshold) where LevelToInt(attackLevel) >= LevelToInt(threshold)
|
|
thresholdIdx := r.Intn(len(validGeneralRiskLevels))
|
|
levelIdx := thresholdIdx + r.Intn(len(validGeneralRiskLevels)-thresholdIdx)
|
|
|
|
attackLevel := validGeneralRiskLevels[levelIdx]
|
|
threshold := validGeneralRiskLevels[thresholdIdx]
|
|
|
|
// Sanity check
|
|
if LevelToInt(attackLevel) < LevelToInt(threshold) {
|
|
t.Errorf("generator bug: attackLevel=%s (%d) < threshold=%s (%d)",
|
|
attackLevel, LevelToInt(attackLevel), threshold, LevelToInt(threshold))
|
|
return false
|
|
}
|
|
|
|
config := baseConfig()
|
|
config.PromptAttackLevelBar = threshold
|
|
// Set contentModerationLevelBar to max so it doesn't interfere
|
|
config.ContentModerationLevelBar = MaxRisk
|
|
|
|
// Generate random details to show they don't matter
|
|
numDetails := r.Intn(4) // 0-3 random details
|
|
details := make([]Detail, numDetails)
|
|
for i := 0; i < numDetails; i++ {
|
|
detailType := knownDetailTypes[r.Intn(len(knownDetailTypes))]
|
|
var level string
|
|
if detailType == SensitiveDataType {
|
|
level = validSensitiveLevels[r.Intn(len(validSensitiveLevels))]
|
|
} else {
|
|
level = validGeneralRiskLevels[r.Intn(len(validGeneralRiskLevels))]
|
|
}
|
|
details[i] = Detail{
|
|
Type: detailType,
|
|
Suggestion: "pass",
|
|
Level: level,
|
|
}
|
|
}
|
|
|
|
data := Data{
|
|
AttackLevel: attackLevel,
|
|
RiskLevel: "none", // Avoid contentModeration gate interference
|
|
Detail: details,
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskBlock {
|
|
t.Errorf("expected RiskBlock for AttackLevel=%s, promptAttackLevelBar=%s, got %d",
|
|
attackLevel, threshold, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 5b failed: %v", err)
|
|
fmt.Printf("Property 5b counterexample: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Feature: sensitive-data-mask-threshold, Property 6: Data.Suggestion=block fallback
|
|
// **Validates: Requirements 3.5**
|
|
//
|
|
// For any set of Details that do not individually trigger block, when Data.Suggestion=block,
|
|
// evaluateRiskMultiModal SHALL return RiskBlock.
|
|
func TestProperty6_DataSuggestionBlockFallbackProducesRiskBlock(t *testing.T) {
|
|
f := func(seed uint64) bool {
|
|
r := rand.New(rand.NewSource(int64(seed)))
|
|
|
|
// Generate 0-4 random non-blocking details.
|
|
// Strategy: use Suggestion="pass" or "watch" with levels below their thresholds
|
|
// so that no detail individually triggers block.
|
|
numDetails := r.Intn(5) // 0-4 details
|
|
nonBlockSuggestions := []string{"pass", "watch"}
|
|
details := make([]Detail, numDetails)
|
|
|
|
for i := 0; i < numDetails; i++ {
|
|
detailType := knownDetailTypes[r.Intn(len(knownDetailTypes))]
|
|
suggestion := nonBlockSuggestions[r.Intn(len(nonBlockSuggestions))]
|
|
|
|
// Use "none" level (0) which is always below any meaningful threshold
|
|
// since all thresholds are set to max.
|
|
var level string
|
|
if detailType == SensitiveDataType {
|
|
level = "S0"
|
|
} else {
|
|
level = "none"
|
|
}
|
|
|
|
details[i] = Detail{
|
|
Type: detailType,
|
|
Suggestion: suggestion,
|
|
Level: level,
|
|
}
|
|
}
|
|
|
|
config := baseConfig()
|
|
// Set all thresholds to max so no detail exceeds threshold
|
|
config.ContentModerationLevelBar = MaxRisk
|
|
config.PromptAttackLevelBar = MaxRisk
|
|
config.SensitiveDataLevelBar = S4Sensitive
|
|
config.MaliciousUrlLevelBar = MaxRisk
|
|
config.ModelHallucinationLevelBar = MaxRisk
|
|
config.CustomLabelLevelBar = MaxRisk
|
|
config.RiskAction = "block"
|
|
|
|
data := Data{
|
|
RiskLevel: "none", // Avoid top-level RiskLevel gate
|
|
AttackLevel: "", // Avoid top-level AttackLevel gate
|
|
Suggestion: "block", // The fallback that should trigger RiskBlock
|
|
Detail: details,
|
|
}
|
|
|
|
result := EvaluateRisk(MultiModalGuard, data, config, "")
|
|
if result != RiskBlock {
|
|
t.Errorf("expected RiskBlock for Data.Suggestion=block with %d non-blocking details, got %d",
|
|
numDetails, result)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
cfg := &quick.Config{MaxCount: 200}
|
|
if err := quick.Check(f, cfg); err != nil {
|
|
t.Errorf("Property 6 failed: %v", err)
|
|
fmt.Printf("Property 6 counterexample: %v\n", err)
|
|
}
|
|
}
|