Skip to content

Commit 7ab40cb

Browse files
authored
FIX: Curate Scam scenario DEFAULT strategy (#2127)
1 parent edb684f commit 7ab40cb

4 files changed

Lines changed: 81 additions & 13 deletions

File tree

doc/scanner/airt.ipynb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1131,7 +1131,9 @@
11311131
" --max-dataset-size 1\n",
11321132
"```\n",
11331133
"\n",
1134-
"**Available strategies:** ALL, SINGLE_TURN, MULTI_TURN, ContextCompliance, RolePlay, PersuasiveRedTeamingAttack"
1134+
"**Available strategies:** ALL, DEFAULT, SINGLE_TURN, MULTI_TURN, ContextCompliance, RolePlay,\n",
1135+
"PersuasiveRedTeamingAttack. DEFAULT runs the single-turn techniques (ContextCompliance, RolePlay)\n",
1136+
"and omits the slower multi-turn PersuasiveRedTeamingAttack; run it via ALL or MULTI_TURN."
11351137
]
11361138
},
11371139
{

doc/scanner/airt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@
242242
# --max-dataset-size 1
243243
# ```
244244
#
245-
# **Available strategies:** ALL, SINGLE_TURN, MULTI_TURN, ContextCompliance, RolePlay, PersuasiveRedTeamingAttack
245+
# **Available strategies:** ALL, DEFAULT, SINGLE_TURN, MULTI_TURN, ContextCompliance, RolePlay,
246+
# PersuasiveRedTeamingAttack. DEFAULT runs the single-turn techniques (ContextCompliance, RolePlay)
247+
# and omits the slower multi-turn PersuasiveRedTeamingAttack; run it via ALL or MULTI_TURN.
246248

247249
# %%
248250
from pyrit.scenario.airt import Scam, ScamStrategy

pyrit/scenario/scenarios/airt/scam.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,24 @@ class ScamStrategy(ScenarioStrategy):
4444
to be used during training seminars.
4545
- PersuasiveRedTeamingAttack: This multi-turn attack uses a persuasive persona with the `RedTeamingAttack` to
4646
iteratively convince the target to comply with the scam objective over multiple turns.
47+
48+
Aggregate Values:
49+
- ALL: Every technique (both single-turn and the multi-turn PersuasiveRedTeamingAttack).
50+
- DEFAULT: The single-turn techniques (ContextCompliance, RolePlay). Excludes the multi-turn
51+
PersuasiveRedTeamingAttack so the default run stays fast; opt into it via ALL or MULTI_TURN.
52+
- SINGLE_TURN / MULTI_TURN: Group techniques by turn count. DEFAULT intentionally shares
53+
membership with SINGLE_TURN today (both are the single-turn techniques); they are kept
54+
distinct because DEFAULT is the recommended fast default while SINGLE_TURN is a turn-count
55+
grouping, and their membership may diverge later.
4756
"""
4857

4958
ALL = ("all", {"all"})
59+
DEFAULT = ("default", {"default"})
5060
SINGLE_TURN = ("single_turn", {"single_turn"})
5161
MULTI_TURN = ("multi_turn", {"multi_turn"})
5262

53-
ContextCompliance = ("context_compliance", {"single_turn"})
54-
RolePlay = ("role_play", {"single_turn"})
63+
ContextCompliance = ("context_compliance", {"single_turn", "default"})
64+
RolePlay = ("role_play", {"single_turn", "default"})
5565
PersuasiveRedTeamingAttack = ("persuasive_rta", {"multi_turn"})
5666

5767
@classmethod
@@ -63,7 +73,7 @@ def get_aggregate_tags(cls) -> set[str]:
6373
set[str]: Set of tags that are aggregate markers.
6474
"""
6575
# Include base class aggregates ("all") and add scenario-specific ones
66-
return super().get_aggregate_tags() | {"single_turn", "multi_turn"}
76+
return super().get_aggregate_tags() | {"default", "single_turn", "multi_turn"}
6777

6878

6979
class Scam(Scenario):
@@ -72,7 +82,7 @@ class Scam(Scenario):
7282
(e.g., phishing emails, fraudulent messages) with primarily persuasion-oriented techniques.
7383
"""
7484

75-
VERSION: int = 1
85+
VERSION: int = 2
7686

7787
@classmethod
7888
def _get_additional_scoring_questions(cls) -> list[Path]:
@@ -135,7 +145,7 @@ def __init__(
135145
super().__init__(
136146
version=self.VERSION,
137147
strategy_class=ScamStrategy,
138-
default_strategy=ScamStrategy.ALL,
148+
default_strategy=ScamStrategy.DEFAULT,
139149
default_dataset_config=DatasetAttackConfiguration(dataset_names=["airt_scams"], max_dataset_size=4),
140150
objective_scorer=objective_scorer,
141151
scenario_result_id=scenario_result_id,

tests/unit/scenario/airt/test_scam.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ def mock_adversarial_target() -> PromptTarget:
115115
FIXTURES = ["patch_central_database", "mock_runtime_env"]
116116

117117

118+
class TestScamStrategyEnum:
119+
"""Aggregate expansion for ScamStrategy (DEFAULT curation)."""
120+
121+
def test_default_expands_to_single_turn_only(self):
122+
members = {m.value for m in ScamStrategy.expand({ScamStrategy.DEFAULT})}
123+
assert members == {"context_compliance", "role_play"}
124+
125+
def test_default_excludes_persuasive_rta(self):
126+
members = {m.value for m in ScamStrategy.expand({ScamStrategy.DEFAULT})}
127+
assert "persuasive_rta" not in members
128+
129+
def test_all_includes_persuasive_rta(self):
130+
members = {m.value for m in ScamStrategy.expand({ScamStrategy.ALL})}
131+
assert members == {"context_compliance", "role_play", "persuasive_rta"}
132+
133+
def test_default_is_aggregate(self):
134+
assert "default" in ScamStrategy.get_aggregate_tags()
135+
assert ScamStrategy.DEFAULT in ScamStrategy.get_aggregate_strategies()
136+
137+
118138
@pytest.mark.usefixtures(*FIXTURES)
119139
class TestScamInitialization:
120140
"""Tests for Scam initialization."""
@@ -134,7 +154,11 @@ def test_init_with_default_objectives(
134154
scenario = Scam(objective_scorer=mock_objective_scorer)
135155

136156
assert scenario.name == "Scam"
137-
assert scenario.VERSION == 1
157+
assert scenario.VERSION == 2
158+
159+
def test_default_strategy_is_default(self, mock_objective_scorer) -> None:
160+
scenario = Scam(objective_scorer=mock_objective_scorer)
161+
assert scenario._default_strategy == ScamStrategy.DEFAULT
138162

139163
def test_init_with_default_scorer(self, mock_memory_seed_groups) -> None:
140164
"""Test initialization with default scorer."""
@@ -219,7 +243,7 @@ class TestScamAttackGeneration:
219243
async def test_attack_generation_for_all(
220244
self, mock_objective_target, mock_objective_scorer, mock_memory_seed_groups, mock_dataset_config
221245
):
222-
"""Test that _get_atomic_attacks_async returns atomic attacks."""
246+
"""ALL runs every technique, including the multi-turn PersuasiveRedTeamingAttack."""
223247
with patch.object(
224248
Scam,
225249
"_resolve_seed_groups_by_dataset_async",
@@ -228,11 +252,41 @@ async def test_attack_generation_for_all(
228252
):
229253
scenario = Scam(objective_scorer=mock_objective_scorer)
230254

231-
await scenario.initialize_async(objective_target=mock_objective_target, dataset_config=mock_dataset_config)
255+
await scenario.initialize_async(
256+
objective_target=mock_objective_target,
257+
scenario_strategies=[ScamStrategy.ALL],
258+
dataset_config=mock_dataset_config,
259+
include_baseline=False,
260+
)
261+
atomic_attacks = scenario._atomic_attacks
262+
263+
assert len(atomic_attacks) == 3
264+
attack_types = {type(run.attack_technique.attack) for run in atomic_attacks}
265+
assert attack_types == {ContextComplianceAttack, RolePlayAttack, RedTeamingAttack}
266+
267+
async def test_default_run_yields_single_turn_only(
268+
self, mock_objective_target, mock_objective_scorer, mock_memory_seed_groups, mock_dataset_config
269+
):
270+
"""No explicit strategies -> DEFAULT -> only the two single-turn techniques, no persuasive_rta."""
271+
with patch.object(
272+
Scam,
273+
"_resolve_seed_groups_by_dataset_async",
274+
new_callable=AsyncMock,
275+
return_value={"memory": mock_memory_seed_groups},
276+
):
277+
scenario = Scam(objective_scorer=mock_objective_scorer)
278+
279+
await scenario.initialize_async(
280+
objective_target=mock_objective_target,
281+
dataset_config=mock_dataset_config,
282+
include_baseline=False,
283+
)
232284
atomic_attacks = scenario._atomic_attacks
233285

234-
assert len(atomic_attacks) > 0
235-
assert all(run.attack_technique is not None for run in atomic_attacks)
286+
assert len(atomic_attacks) == 2
287+
attack_types = {type(run.attack_technique.attack) for run in atomic_attacks}
288+
assert attack_types == {ContextComplianceAttack, RolePlayAttack}
289+
assert RedTeamingAttack not in attack_types
236290

237291
async def test_attack_generation_for_singleturn_async(
238292
self,
@@ -429,7 +483,7 @@ def test_scenario_version_is_set(
429483
objective_scorer=mock_objective_scorer,
430484
)
431485

432-
assert scenario.VERSION == 1
486+
assert scenario.VERSION == 2
433487

434488
async def test_no_target_duplication_async(
435489
self,

0 commit comments

Comments
 (0)