|
53 | 53 | "\n", |
54 | 54 | "### Required Components\n", |
55 | 55 | "\n", |
56 | | - "1. **Strategy Enum**: Create a `ScenarioStrategy` enum that defines the available strategies for your scenario.\n", |
57 | | - " - Each enum member is defined as `(value, tags)` where value is a string and tags is a set of strings\n", |
| 56 | + "1. **Strategy Enum**: Create a `ScenarioStrategy` enum that defines the available attack techniques for your scenario.\n", |
| 57 | + " - Each enum member represents an **attack technique** (the *how* of an attack)\n", |
| 58 | + " - Each member is defined as `(value, tags)` where value is a string and tags is a set of strings\n", |
58 | 59 | " - Include an `ALL` aggregate strategy that expands to all available strategies\n", |
59 | 60 | " - Optionally override `_prepare_strategies()` for custom composition logic (see `FoundryComposite`)\n", |
60 | 61 | "\n", |
61 | 62 | "2. **Scenario Class**: Extend `Scenario` and implement these abstract methods:\n", |
62 | 63 | " - `get_strategy_class()`: Return your strategy enum class\n", |
63 | 64 | " - `get_default_strategy()`: Return the default strategy (typically `YourStrategy.ALL`)\n", |
64 | | - " - `_get_atomic_attacks_async()`: Build and return a list of `AtomicAttack` instances\n", |
| 65 | + " - The base class provides a default `_get_atomic_attacks_async()` that uses the factory/registry\n", |
| 66 | + " pattern. Override it only if your scenario needs custom attack construction logic.\n", |
65 | 67 | "\n", |
66 | | - "3. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:\n", |
| 68 | + "3. **Default Dataset**: Implement `default_dataset_config()` to specify the datasets your scenario uses out of the box.\n", |
| 69 | + " - Returns a `DatasetConfiguration` with one or more named datasets (e.g., `DatasetConfiguration(dataset_names=[\"my_dataset\"])`)\n", |
| 70 | + " - Users can override this at runtime via `--dataset-names` in the CLI or by passing a custom `dataset_config` programmatically\n", |
| 71 | + "\n", |
| 72 | + "4. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:\n", |
67 | 73 | " - `name`: Descriptive name for your scenario\n", |
68 | 74 | " - `version`: Integer version number\n", |
69 | 75 | " - `strategy_class`: The strategy enum class for this scenario\n", |
70 | 76 | " - `objective_scorer_identifier`: Identifier dict for the scoring mechanism (optional)\n", |
71 | 77 | " - `include_default_baseline`: Whether to include a baseline attack (default: True)\n", |
72 | 78 | " - `scenario_result_id`: Optional ID to resume an existing scenario (optional)\n", |
73 | 79 | "\n", |
74 | | - "4. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:\n", |
| 80 | + "5. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:\n", |
75 | 81 | " - `objective_target`: The target system being tested (required)\n", |
76 | 82 | " - `scenario_strategies`: List of strategies to execute (optional, defaults to ALL)\n", |
77 | 83 | " - `max_concurrency`: Number of concurrent operations (default: 1)\n", |
78 | 84 | " - `max_retries`: Number of retry attempts on failure (default: 0)\n", |
79 | 85 | " - `memory_labels`: Optional labels for tracking (optional)\n", |
80 | 86 | "\n", |
81 | | - "### Example Structure" |
| 87 | + "### Example Structure\n", |
| 88 | + "\n", |
| 89 | + "The simplest approach uses the **factory/registry pattern**: define your strategy,\n", |
| 90 | + "dataset config, and constructor — the base class handles building atomic attacks\n", |
| 91 | + "automatically from registered attack techniques." |
82 | 92 | ] |
83 | 93 | }, |
84 | 94 | { |
|
98 | 108 | } |
99 | 109 | ], |
100 | 110 | "source": [ |
101 | | - "from typing import Optional\n", |
102 | | - "\n", |
103 | 111 | "from pyrit.common import apply_defaults\n", |
104 | | - "from pyrit.executor.attack import AttackScoringConfig, PromptSendingAttack\n", |
105 | 112 | "from pyrit.scenario import (\n", |
106 | | - " AtomicAttack,\n", |
107 | 113 | " DatasetConfiguration,\n", |
108 | 114 | " Scenario,\n", |
109 | 115 | " ScenarioStrategy,\n", |
|
116 | 122 | "\n", |
117 | 123 | "class MyStrategy(ScenarioStrategy):\n", |
118 | 124 | " ALL = (\"all\", {\"all\"})\n", |
119 | | - " StrategyA = (\"strategy_a\", {\"tag1\", \"tag2\"})\n", |
120 | | - " StrategyB = (\"strategy_b\", {\"tag1\"})\n", |
| 125 | + " DEFAULT = (\"default\", {\"default\"})\n", |
| 126 | + " SINGLE_TURN = (\"single_turn\", {\"single_turn\"})\n", |
| 127 | + " # Strategy members represent attack techniques\n", |
| 128 | + " PromptSending = (\"prompt_sending\", {\"single_turn\", \"default\"})\n", |
| 129 | + " RolePlay = (\"role_play\", {\"single_turn\"})\n", |
121 | 130 | "\n", |
122 | 131 | "\n", |
123 | 132 | "class MyScenario(Scenario):\n", |
124 | | - " version: int = 1\n", |
| 133 | + " \"\"\"Quick-check scenario for testing model behavior across harm categories.\"\"\"\n", |
| 134 | + "\n", |
| 135 | + " VERSION: int = 1\n", |
125 | 136 | "\n", |
126 | | - " # A strategy definition helps callers define how to run your scenario (e.g. from the scanner CLI)\n", |
127 | 137 | " @classmethod\n", |
128 | 138 | " def get_strategy_class(cls) -> type[ScenarioStrategy]:\n", |
129 | 139 | " return MyStrategy\n", |
130 | 140 | "\n", |
131 | 141 | " @classmethod\n", |
132 | 142 | " def get_default_strategy(cls) -> ScenarioStrategy:\n", |
133 | | - " return MyStrategy.ALL\n", |
| 143 | + " return MyStrategy.DEFAULT\n", |
134 | 144 | "\n", |
135 | | - " # This is the default dataset configuration for this scenario (e.g. prompts to send)\n", |
136 | 145 | " @classmethod\n", |
137 | 146 | " def default_dataset_config(cls) -> DatasetConfiguration:\n", |
138 | | - " return DatasetConfiguration(dataset_names=[\"dataset_name\"])\n", |
| 147 | + " return DatasetConfiguration(dataset_names=[\"dataset_name\"], max_dataset_size=4)\n", |
139 | 148 | "\n", |
140 | 149 | " @apply_defaults\n", |
141 | 150 | " def __init__(\n", |
142 | 151 | " self,\n", |
143 | 152 | " *,\n", |
144 | | - " objective_scorer: Optional[TrueFalseScorer] = None,\n", |
145 | | - " scenario_result_id: Optional[str] = None,\n", |
146 | | - " ):\n", |
147 | | - " self._objective_scorer = objective_scorer\n", |
148 | | - " self._scorer_config = AttackScoringConfig(objective_scorer=objective_scorer)\n", |
| 153 | + " objective_scorer: TrueFalseScorer | None = None,\n", |
| 154 | + " scenario_result_id: str | None = None,\n", |
| 155 | + " ) -> None:\n", |
| 156 | + " self._objective_scorer: TrueFalseScorer = (\n", |
| 157 | + " objective_scorer if objective_scorer else self._get_default_objective_scorer()\n", |
| 158 | + " )\n", |
149 | 159 | "\n", |
150 | | - " # Call parent constructor - note: objective_target is NOT passed here\n", |
151 | 160 | " super().__init__(\n", |
152 | | - " name=\"My Custom Scenario\",\n", |
153 | | - " version=self.version,\n", |
154 | | - " strategy_class=MyStrategy,\n", |
155 | | - " objective_scorer=objective_scorer,\n", |
| 161 | + " version=self.VERSION,\n", |
| 162 | + " objective_scorer=self._objective_scorer,\n", |
| 163 | + " strategy_class=self.get_strategy_class(),\n", |
156 | 164 | " scenario_result_id=scenario_result_id,\n", |
157 | 165 | " )\n", |
158 | 166 | "\n", |
159 | | - " async def _get_atomic_attacks_async(self) -> list[AtomicAttack]:\n", |
160 | | - " \"\"\"\n", |
161 | | - " Build atomic attacks based on selected strategies.\n", |
162 | | - "\n", |
163 | | - " This method is called by initialize_async() after strategies are prepared.\n", |
164 | | - " Use self._scenario_strategies to access the resolved strategy list.\n", |
165 | | - " \"\"\"\n", |
166 | | - " atomic_attacks = []\n", |
167 | | - "\n", |
168 | | - " # objective_target is guaranteed to be non-None by parent class validation\n", |
169 | | - " assert self._objective_target is not None\n", |
170 | | - "\n", |
171 | | - " for strategy in self._scenario_strategies:\n", |
172 | | - " # self._dataset_config is set by the parent class\n", |
173 | | - " seed_groups = self._dataset_config.get_all_seed_groups()\n", |
174 | | - "\n", |
175 | | - " # Create attack instances based on strategy\n", |
176 | | - " attack = PromptSendingAttack(\n", |
177 | | - " objective_target=self._objective_target,\n", |
178 | | - " attack_scoring_config=self._scorer_config,\n", |
179 | | - " )\n", |
180 | | - " atomic_attacks.append(\n", |
181 | | - " AtomicAttack(\n", |
182 | | - " atomic_attack_name=strategy.value,\n", |
183 | | - " attack=attack,\n", |
184 | | - " seed_groups=seed_groups, # type: ignore[arg-type]\n", |
185 | | - " memory_labels=self._memory_labels,\n", |
186 | | - " )\n", |
187 | | - " )\n", |
188 | | - " return atomic_attacks" |
| 167 | + " # Optional: override _build_display_group to customize result grouping.\n", |
| 168 | + " # Default groups by technique name; override to group by dataset instead:\n", |
| 169 | + " def _build_display_group(self, *, technique_name: str, seed_group_name: str) -> str:\n", |
| 170 | + " return seed_group_name\n", |
| 171 | + "\n", |
| 172 | + " # No _get_atomic_attacks_async override needed!\n", |
| 173 | + " # The base class builds attacks from the (technique x dataset) cross-product\n", |
| 174 | + " # using the factory/registry pattern automatically." |
189 | 175 | ] |
190 | 176 | }, |
191 | 177 | { |
|
0 commit comments