Skip to content

Commit edb684f

Browse files
authored
MAINT: Add DEFAULT strategy alias to airt.cyber scenario (#2061)
1 parent 71d5e8a commit edb684f

4 files changed

Lines changed: 47 additions & 10 deletions

File tree

doc/scanner/airt.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@
373373
" --max-dataset-size 1\n",
374374
"```\n",
375375
"\n",
376-
"**Available strategies:** ALL, MULTI_TURN, red_teaming"
376+
"**Available strategies:** ALL, DEFAULT, MULTI_TURN, red_teaming"
377377
]
378378
},
379379
{

doc/scanner/airt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
# --max-dataset-size 1
130130
# ```
131131
#
132-
# **Available strategies:** ALL, MULTI_TURN, red_teaming
132+
# **Available strategies:** ALL, DEFAULT, MULTI_TURN, red_teaming
133133

134134
# %%
135135
from pyrit.scenario.airt import Cyber, CyberStrategy

pyrit/scenario/scenarios/airt/cyber.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323

2424
logger = logging.getLogger(__name__)
2525

26+
# Techniques Cyber selects from the shared catalog. ``DEFAULT`` is wired to ``any_of("core")``
27+
# (see _build_cyber_strategy), so adding a technique here that carries the ``core`` tag pulls it
28+
# into DEFAULT, while a technique lacking ``core`` (e.g. an ``extra``-group technique) would stay
29+
# in ALL but be silently dropped from DEFAULT. Either case breaks the current DEFAULT == ALL
30+
# invariant (guarded by test_default_matches_all); revisit the aggregate wiring if that happens.
2631
_CYBER_TECHNIQUE_NAMES = {"red_teaming"}
2732

2833

@@ -36,6 +41,9 @@ def _build_cyber_strategy() -> type[ScenarioStrategy]:
3641
prepended automatically by ``Scenario._build_baseline_atomic_attack`` via
3742
``BaselineAttackPolicy.Enabled``.
3843
44+
The ``DEFAULT`` aggregate is the curated default run; for Cyber it expands to the
45+
same single ``red_teaming`` technique as ``ALL``.
46+
3947
Returns:
4048
type[ScenarioStrategy]: The dynamically generated strategy enum class.
4149
"""
@@ -50,6 +58,11 @@ def _build_cyber_strategy() -> type[ScenarioStrategy]:
5058
class_name="CyberStrategy",
5159
factories=cyber_factories,
5260
aggregate_tags={
61+
# Cyber curates a single technique (red_teaming) at the scenario level. That
62+
# technique carries the canonical ``core`` tag but not the catalog-wide
63+
# ``default`` tag, so DEFAULT matches ``core`` here to select it (rather than
64+
# tagging red_teaming ``default`` globally, which would alter other scenarios).
65+
"default": TagQuery.any_of("core"),
5366
"multi_turn": TagQuery.any_of("multi_turn"),
5467
},
5568
)
@@ -101,7 +114,7 @@ def __init__(
101114
version=self.VERSION,
102115
objective_scorer=self._objective_scorer,
103116
strategy_class=strategy_class,
104-
default_strategy=strategy_class("all"),
117+
default_strategy=strategy_class("default"),
105118
default_dataset_config=DatasetAttackConfiguration(dataset_names=["airt_malware"], max_dataset_size=4),
106119
scenario_result_id=scenario_result_id,
107120
)

tests/unit/scenario/airt/test_cyber.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pyrit.models import ComponentIdentifier, SeedAttackGroup, SeedObjective, SeedPrompt
1212
from pyrit.prompt_target import PromptTarget
1313
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
14-
from pyrit.scenario.core.dataset_configuration import DatasetAttackConfiguration, DatasetConfiguration
14+
from pyrit.scenario.core.dataset_configuration import DatasetAttackConfiguration
1515
from pyrit.scenario.scenarios.airt.cyber import Cyber
1616
from pyrit.score import TrueFalseScorer
1717
from pyrit.setup.initializers.techniques import (
@@ -133,13 +133,37 @@ def test_get_strategy_class(self):
133133
strat = _strategy_class()
134134
assert Cyber()._strategy_class is strat
135135

136-
def test_get_default_strategy_returns_all(self):
136+
def test_get_default_strategy_returns_default(self):
137137
strat = _strategy_class()
138-
assert Cyber()._default_strategy == strat.ALL
138+
assert Cyber()._default_strategy == strat.DEFAULT
139+
140+
def test_default_aggregate_expands_to_red_teaming(self):
141+
"""DEFAULT must be non-empty and select the single curated technique.
142+
143+
Guards against wiring the ``default`` aggregate to a tag ``red_teaming`` lacks
144+
(e.g. the catalog ``default`` tag), which would silently make DEFAULT empty and
145+
collapse the default run to baseline-only.
146+
"""
147+
strat = _strategy_class()
148+
assert "default" in strat.get_aggregate_tags()
149+
default_members = strat.expand({strat.DEFAULT})
150+
assert default_members == [strat("red_teaming")]
151+
152+
def test_default_matches_all(self):
153+
"""DEFAULT must expand to exactly the same techniques as ALL.
154+
155+
Cyber curates a single technique, so its DEFAULT run is a no-op alias of ALL. Asserting
156+
equality (not just subset) guards against a future technique landing in ALL but being
157+
silently excluded from DEFAULT (or vice versa) once the aggregate wiring changes.
158+
"""
159+
strat = _strategy_class()
160+
assert set(strat.expand({strat.DEFAULT})) == set(strat.expand({strat.ALL}))
139161

140162
def test_default_dataset_config_has_malware_dataset(self):
141163
config = Cyber()._default_dataset_config
142-
assert isinstance(config, DatasetConfiguration)
164+
# Concrete DatasetAttackConfiguration (not the base) so the scenario's async
165+
# get_attack_groups_by_dataset_async() resolve path is available.
166+
assert isinstance(config, DatasetAttackConfiguration)
143167
names = config.dataset_names
144168
assert "airt_malware" in names
145169
assert len(names) == 1
@@ -166,15 +190,15 @@ def test_scenario_name_is_cyber(self, mock_objective_scorer):
166190
new_callable=AsyncMock,
167191
return_value={"malware": _make_seed_groups("malware")},
168192
)
169-
async def test_initialization_defaults_to_all_strategy(
193+
async def test_initialization_defaults_to_default_strategy(
170194
self,
171195
_mock_groups,
172196
mock_objective_target,
173197
mock_objective_scorer,
174198
):
175199
scenario = Cyber(objective_scorer=mock_objective_scorer)
176200
await scenario.initialize_async(objective_target=mock_objective_target)
177-
# ALL expands to red_teaming (the only registered Cyber technique); a
201+
# DEFAULT expands to red_teaming (the only registered Cyber technique); a
178202
# PromptSendingAttack baseline is added separately via the baseline
179203
# policy, not as a strategy.
180204
assert len(scenario._scenario_strategies) == 1
@@ -276,7 +300,7 @@ async def test_multi_turn_strategy_produces_red_teaming(self, mock_objective_tar
276300
assert technique_classes == {RedTeamingAttack}
277301

278302
async def test_default_strategy_produces_red_teaming(self, mock_objective_target, mock_objective_scorer):
279-
"""Default (ALL) should produce RedTeaming. PromptSendingAttack baseline is
303+
"""Default (DEFAULT) should produce RedTeaming. PromptSendingAttack baseline is
280304
prepended automatically by BaselineAttackPolicy.Enabled when
281305
include_baseline=True (the helper here uses include_baseline=False)."""
282306
attacks = await self._init_and_get_attacks(

0 commit comments

Comments
 (0)