Skip to content

Commit 0fe2aec

Browse files
committed
reject dataset size limits that a scenario would ignore
1 parent f916d8e commit 0fe2aec

6 files changed

Lines changed: 84 additions & 3 deletions

File tree

‎doc/scanner/garak.ipynb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@
616616
"**CLI example:**\n",
617617
"\n",
618618
"```bash\n",
619-
"pyrit_scan run garak.web_injection --target openai_chat --techniques xss --max-dataset-size 1\n",
619+
"pyrit_scan run garak.web_injection --target openai_chat --techniques xss\n",
620620
"```\n",
621621
"\n",
622622
"**Available techniques** (8 probes): MarkdownImageExfil, ColabAIDataLeakage,\n",

‎doc/scanner/garak.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@
217217
# **CLI example:**
218218
#
219219
# ```bash
220-
# pyrit_scan run garak.web_injection --target openai_chat --techniques xss --max-dataset-size 1
220+
# pyrit_scan run garak.web_injection --target openai_chat --techniques xss
221221
# ```
222222
#
223223
# **Available techniques** (8 probes): MarkdownImageExfil, ColabAIDataLeakage,

‎pyrit/backend/services/scenario_configuration_resolver.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ def resolve_configuration(
148148
resolved["technique_converters"] = technique_converters
149149

150150
if dataset_names or has_total_override or filters:
151+
cls._validate_dataset_overrides(
152+
scenario_name=scenario_name,
153+
scenario=introspection_instance,
154+
has_total_override=has_total_override,
155+
)
151156
default_config = introspection_instance._default_dataset_config
152157
config = default_config.with_overrides(filters=filters)
153158
if dataset_names:
@@ -163,6 +168,30 @@ def resolve_configuration(
163168

164169
return resolved
165170

171+
@staticmethod
172+
def _validate_dataset_overrides(*, scenario_name: str, scenario: Scenario, has_total_override: bool) -> None:
173+
"""
174+
Reject dataset overrides the scenario would not apply.
175+
176+
Args:
177+
scenario_name: Scenario name used in error messages.
178+
scenario: Introspection instance of the scenario.
179+
has_total_override: Whether a non-default total limit was requested.
180+
181+
Raises:
182+
ValueError: If the scenario has a fixed dataset, or ignores the total limit.
183+
"""
184+
if not any(parameter.name == "dataset_config" for parameter in scenario.supported_parameters()):
185+
raise ValueError(
186+
f"Scenario '{scenario_name}' uses a fixed dataset, so it doesn't take dataset names, "
187+
"dataset filters or a dataset size limit."
188+
)
189+
if has_total_override and not scenario.USES_DATASET_SIZE_LIMIT:
190+
raise ValueError(
191+
f"Scenario '{scenario_name}' doesn't use a dataset size limit; its own settings decide "
192+
"how many prompts it runs. Leave the dataset size at its default."
193+
)
194+
166195
@classmethod
167196
def resolve_techniques_and_converters(
168197
cls,

‎tests/unit/backend/test_scenario_configuration_resolver.py‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
"""Adversarial target resolution validates without changing execution scopes."""
55

6-
from typing import Literal
6+
from typing import Any, Literal
77
from unittest.mock import AsyncMock, MagicMock, patch
88

99
import pytest
@@ -23,7 +23,11 @@
2323
from pyrit.scenario.scenarios.adaptive.text_adaptive import TextAdaptive
2424
from pyrit.scenario.scenarios.airt.rapid_response import RapidResponse
2525
from pyrit.scenario.scenarios.garak.api_key import ApiKey
26+
from pyrit.scenario.scenarios.garak.exploitation import Exploitation
27+
from pyrit.scenario.scenarios.garak.package_hallucination import PackageHallucination
2628
from pyrit.scenario.scenarios.garak.prompt_inject import PromptInject, PromptInjectDatasetConfiguration
29+
from pyrit.scenario.scenarios.garak.system_prompt_extraction import SystemPromptExtraction
30+
from pyrit.scenario.scenarios.garak.web_injection import WebInjection
2731
from pyrit.score import TrueFalseScorer
2832
from unit.mocks import MockPromptTarget
2933

@@ -183,3 +187,45 @@ def test_resolve_adversarial_target_rejects_invalid_selection_without_changing_s
183187
with pytest.raises(ValueError, match=message):
184188
ScenarioConfigurationResolver.resolve_adversarial_target(target_name=selection)
185189
assert get_default_adversarial_target() is outer
190+
191+
192+
@pytest.mark.usefixtures("patch_central_database")
193+
@pytest.mark.parametrize("scenario_class", [SystemPromptExtraction, WebInjection, PackageHallucination])
194+
@pytest.mark.parametrize("limit", [3, "all"])
195+
def test_total_limit_is_rejected_when_the_scenario_ignores_it(
196+
*, scenario_class: type[Scenario], limit: DatasetLimit
197+
) -> None:
198+
with (
199+
patch.object(Scenario, "_get_default_objective_scorer", return_value=MagicMock(spec=TrueFalseScorer)),
200+
pytest.raises(ValueError, match="doesn't use a dataset size limit"),
201+
):
202+
ScenarioConfigurationResolver.resolve_configuration(
203+
scenario_name="test", scenario_class=scenario_class, max_dataset_size=limit
204+
)
205+
206+
207+
@pytest.mark.usefixtures("patch_central_database")
208+
@pytest.mark.parametrize("scenario_class", [SystemPromptExtraction, WebInjection, PackageHallucination])
209+
def test_dataset_names_still_reach_a_scenario_that_ignores_the_total_limit(scenario_class: type[Scenario]) -> None:
210+
with patch.object(Scenario, "_get_default_objective_scorer", return_value=MagicMock(spec=TrueFalseScorer)):
211+
names = scenario_class()._default_dataset_config.dataset_names
212+
resolved = ScenarioConfigurationResolver.resolve_configuration(
213+
scenario_name="test", scenario_class=scenario_class, dataset_names=names, max_dataset_size="default"
214+
)
215+
assert resolved["dataset_config"].dataset_names == names
216+
217+
218+
@pytest.mark.usefixtures("patch_central_database")
219+
@pytest.mark.parametrize(
220+
"overrides",
221+
[
222+
{"max_dataset_size": 3},
223+
{"dataset_names": ["garak_exploitation_sql_injection"]},
224+
{"dataset_filters": {"data_types": ["text"]}},
225+
],
226+
)
227+
def test_dataset_overrides_are_rejected_for_a_fixed_dataset_scenario(overrides: dict[str, Any]) -> None:
228+
with pytest.raises(ValueError, match="uses a fixed dataset"):
229+
ScenarioConfigurationResolver.resolve_configuration(
230+
scenario_name="garak.exploitation", scenario_class=Exploitation, **overrides
231+
)

‎tests/unit/backend/test_scenario_run_service.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ def mock_all_registries(mock_memory):
286286
mock_scenario_class = MagicMock(return_value=mock_scenario_instance)
287287
mock_scenario_instance._technique_class = MagicMock()
288288
mock_scenario_instance._default_dataset_config = MagicMock()
289+
mock_scenario_instance.supported_parameters.return_value = Scenario.supported_parameters()
289290

290291
mock_sr = MagicMock()
291292
mock_sr.get_class.return_value = mock_scenario_class

‎tests/unit/backend/test_scenario_service.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ def construct() -> MagicMock:
364364
introspected.append(get_default_adversarial_target())
365365
scenario = MagicMock(spec=Scenario)
366366
scenario._default_dataset_config = DatasetAttackConfiguration(dataset_names=["test_dataset"])
367+
scenario.supported_parameters.return_value = Scenario.supported_parameters()
367368
return scenario
368369

369370
async def estimate_async(**kwargs: object) -> ScenarioRunSizeEstimate:
@@ -1531,6 +1532,7 @@ async def test_estimate_resolves_total_limit_async(
15311532
original = DatasetAttackConfiguration(dataset_names=["harmbench"], max_total=20)
15321533
scenario_class = MagicMock()
15331534
scenario_class.return_value._default_dataset_config = original
1535+
scenario_class.return_value.supported_parameters.return_value = Scenario.supported_parameters()
15341536
with patch.object(ScenarioService, "__init__", lambda self: None):
15351537
service = ScenarioService()
15361538
service._registry = MagicMock()
@@ -1572,6 +1574,7 @@ async def test_configured_estimate_uses_shared_launch_resolution(self) -> None:
15721574
introspection_instance = MagicMock()
15731575
introspection_instance._technique_class = _EstimateTechnique
15741576
introspection_instance._default_dataset_config = DatasetAttackConfiguration(dataset_names=["harmbench"])
1577+
introspection_instance.supported_parameters.return_value = Scenario.supported_parameters()
15751578
scenario_class = MagicMock(return_value=introspection_instance)
15761579
scenario_class.supported_parameters.return_value = [
15771580
Parameter(name=name, description="", param_type=int)
@@ -1631,6 +1634,7 @@ async def test_configured_estimate_rejects_incompatible_v4_jailbreak_technique(s
16311634
introspection_instance = MagicMock()
16321635
introspection_instance._technique_class = _EstimateTechnique
16331636
introspection_instance._default_dataset_config = DatasetAttackConfiguration(dataset_names=["harmbench"])
1637+
introspection_instance.supported_parameters.return_value = Scenario.supported_parameters()
16341638
scenario_class = MagicMock(return_value=introspection_instance)
16351639

16361640
with patch.object(ScenarioService, "__init__", lambda self: None):
@@ -1657,6 +1661,7 @@ async def test_configured_estimate_without_target_does_not_resolve_or_send_to_ta
16571661
introspection_instance = MagicMock()
16581662
introspection_instance._technique_class = _EstimateTechnique
16591663
introspection_instance._default_dataset_config = DatasetAttackConfiguration(dataset_names=["harmbench"])
1664+
introspection_instance.supported_parameters.return_value = Scenario.supported_parameters()
16601665
scenario_class = MagicMock(return_value=introspection_instance)
16611666

16621667
with (

0 commit comments

Comments
 (0)