Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ AZURE_OPENAI_GPT4O_UNSAFE_CHAT_KEY2="xxxxx"
AZURE_OPENAI_GPT4O_UNSAFE_CHAT_MODEL2="deployment-name"
AZURE_OPENAI_GPT4O_UNSAFE_CHAT_UNDERLYING_MODEL2=""

# Adversarial chat target (used by scenario attack techniques, e.g. role-play, TAP)
ADVERSARIAL_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/v1"
ADVERSARIAL_CHAT_KEY="xxxxx"
ADVERSARIAL_CHAT_MODEL="deployment-name"

AZURE_FOUNDRY_DEEPSEEK_ENDPOINT="https://xxxxx.eastus2.models.ai.azure.com"
AZURE_FOUNDRY_DEEPSEEK_KEY="xxxxx"
AZURE_FOUNDRY_DEEPSEEK_MODEL=""
Expand Down
28 changes: 22 additions & 6 deletions .github/instructions/scenarios.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,41 @@ Options:

## Strategy Enum

Strategies should be selectable by an axis. E.g. it could be harm category or and attack type, but likely not both or it gets confusing.
Strategy members should represent **attack techniques** — the *how* of an attack (e.g., prompt sending, role play, TAP). Datasets control *what* is tested (e.g., harm categories, compliance topics). Avoid mixing dataset/category selection into the strategy enum; use `DatasetConfiguration` and `--dataset-names` for that axis.

```python
class MyStrategy(ScenarioStrategy):
ALL = ("all", {"all"}) # Required aggregate
EASY = ("easy", {"easy"})
ALL = ("all", {"all"}) # Required aggregate
DEFAULT = ("default", {"default"}) # Recommended default aggregate
SINGLE_TURN = ("single_turn", {"single_turn"}) # Category aggregate

Base64 = ("base64", {"easy", "converter"})
Crescendo = ("crescendo", {"difficult", "multi_turn"})
PromptSending = ("prompt_sending", {"single_turn", "default"})
RolePlay = ("role_play", {"single_turn"})
ManyShot = ("many_shot", {"multi_turn", "default"})

@classmethod
def get_aggregate_tags(cls) -> set[str]:
return {"all", "easy", "difficult"}
return {"all", "default", "single_turn", "multi_turn"}
```

- `ALL` aggregate is always required
- Each member: `NAME = ("string_value", {tag_set})`
- Aggregates expand to all strategies matching their tag

### `_build_atomic_attack_name()` — Result Grouping

Override `_build_atomic_attack_name()` on the `Scenario` base class to control how attack results are grouped:

```python
def _build_atomic_attack_name(self, *, technique_name: str, seed_group_name: str) -> str:
# Default: group by technique name (most common)
return technique_name

# Override examples:
# Group by dataset/harm category: return seed_group_name
# Cross-product: return f"{technique_name}_{seed_group_name}"
```

## AtomicAttack Construction

```python
Expand Down
20 changes: 13 additions & 7 deletions doc/code/scenarios/0_scenarios.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@
"\n",
"### Required Components\n",
"\n",
"1. **Strategy Enum**: Create a `ScenarioStrategy` enum that defines the available strategies for your scenario.\n",
" - Each enum member is defined as `(value, tags)` where value is a string and tags is a set of strings\n",
"1. **Strategy Enum**: Create a `ScenarioStrategy` enum that defines the available attack techniques for your scenario.\n",
" - Each enum member represents an **attack technique** (the *how* of an attack)\n",
" - Each member is defined as `(value, tags)` where value is a string and tags is a set of strings\n",
" - Include an `ALL` aggregate strategy that expands to all available strategies\n",
" - Optionally implement `supports_composition()` and `validate_composition()` for strategy composition rules\n",
"\n",
Expand All @@ -63,15 +64,19 @@
" - `get_default_strategy()`: Return the default strategy (typically `YourStrategy.ALL`)\n",
" - `_get_atomic_attacks_async()`: Build and return a list of `AtomicAttack` instances\n",
"\n",
"3. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:\n",
"3. **Default Dataset**: Implement `default_dataset_config()` to specify the datasets your scenario uses out of the box.\n",
" - Returns a `DatasetConfiguration` with one or more named datasets (e.g., `DatasetConfiguration(dataset_names=[\"my_dataset\"])`)\n",
" - Users can override this at runtime via `--dataset-names` in the CLI or by passing a custom `dataset_config` programmatically\n",
"\n",
"4. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:\n",
" - `name`: Descriptive name for your scenario\n",
" - `version`: Integer version number\n",
" - `strategy_class`: The strategy enum class for this scenario\n",
" - `objective_scorer_identifier`: Identifier dict for the scoring mechanism (optional)\n",
" - `include_default_baseline`: Whether to include a baseline attack (default: True)\n",
" - `scenario_result_id`: Optional ID to resume an existing scenario (optional)\n",
"\n",
"4. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:\n",
"5. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:\n",
" - `objective_target`: The target system being tested (required)\n",
" - `scenario_strategies`: List of strategies to execute (optional, defaults to ALL)\n",
" - `max_concurrency`: Number of concurrent operations (default: 1)\n",
Expand Down Expand Up @@ -117,8 +122,9 @@
"\n",
"class MyStrategy(ScenarioStrategy):\n",
" ALL = (\"all\", {\"all\"})\n",
" StrategyA = (\"strategy_a\", {\"tag1\", \"tag2\"})\n",
" StrategyB = (\"strategy_b\", {\"tag1\"})\n",
" # Strategy members represent attack techniques\n",
" PromptSending = (\"prompt_sending\", {\"single_turn\"})\n",
" RolePlay = (\"role_play\", {\"single_turn\"})\n",
"\n",
"\n",
"class MyScenario(Scenario):\n",
Expand Down Expand Up @@ -178,7 +184,7 @@
" # self._dataset_config is set by the parent class\n",
" seed_groups = self._dataset_config.get_all_seed_groups()\n",
"\n",
" # Create attack instances based on strategy\n",
" # Create attack instances based on the selected technique\n",
" attack = PromptSendingAttack(\n",
" objective_target=self._objective_target,\n",
" attack_scoring_config=self._scorer_config,\n",
Expand Down
20 changes: 13 additions & 7 deletions doc/code/scenarios/0_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@
#
# ### Required Components
#
# 1. **Strategy Enum**: Create a `ScenarioStrategy` enum that defines the available strategies for your scenario.
# - Each enum member is defined as `(value, tags)` where value is a string and tags is a set of strings
# 1. **Strategy Enum**: Create a `ScenarioStrategy` enum that defines the available attack techniques for your scenario.
# - Each enum member represents an **attack technique** (the *how* of an attack)
# - Each member is defined as `(value, tags)` where value is a string and tags is a set of strings
# - Include an `ALL` aggregate strategy that expands to all available strategies
# - Optionally implement `supports_composition()` and `validate_composition()` for strategy composition rules
#
Expand All @@ -69,15 +70,19 @@
# - `get_default_strategy()`: Return the default strategy (typically `YourStrategy.ALL`)
# - `_get_atomic_attacks_async()`: Build and return a list of `AtomicAttack` instances
#
# 3. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:
# 3. **Default Dataset**: Implement `default_dataset_config()` to specify the datasets your scenario uses out of the box.
# - Returns a `DatasetConfiguration` with one or more named datasets (e.g., `DatasetConfiguration(dataset_names=["my_dataset"])`)
# - Users can override this at runtime via `--dataset-names` in the CLI or by passing a custom `dataset_config` programmatically
#
# 4. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:
# - `name`: Descriptive name for your scenario
# - `version`: Integer version number
# - `strategy_class`: The strategy enum class for this scenario
# - `objective_scorer_identifier`: Identifier dict for the scoring mechanism (optional)
# - `include_default_baseline`: Whether to include a baseline attack (default: True)
# - `scenario_result_id`: Optional ID to resume an existing scenario (optional)
#
# 4. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:
# 5. **Initialization**: Call `await scenario.initialize_async()` to populate atomic attacks:
# - `objective_target`: The target system being tested (required)
# - `scenario_strategies`: List of strategies to execute (optional, defaults to ALL)
# - `max_concurrency`: Number of concurrent operations (default: 1)
Expand Down Expand Up @@ -105,8 +110,9 @@

class MyStrategy(ScenarioStrategy):
ALL = ("all", {"all"})
StrategyA = ("strategy_a", {"tag1", "tag2"})
StrategyB = ("strategy_b", {"tag1"})
# Strategy members represent attack techniques
PromptSending = ("prompt_sending", {"single_turn"})
RolePlay = ("role_play", {"single_turn"})


class MyScenario(Scenario):
Expand Down Expand Up @@ -166,7 +172,7 @@ async def _get_atomic_attacks_async(self) -> list[AtomicAttack]:
# self._dataset_config is set by the parent class
seed_groups = self._dataset_config.get_all_seed_groups()

# Create attack instances based on strategy
# Create attack instances based on the selected technique
attack = PromptSendingAttack(
objective_target=self._objective_target,
attack_scoring_config=self._scorer_config,
Expand Down
4 changes: 4 additions & 0 deletions doc/code/scenarios/1_scenario_parameters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"strategies, baseline execution, and custom scorers. All examples use `RedTeamAgent` but the\n",
"patterns apply to any scenario.\n",
"\n",
"> **Two selection axes**: *Strategies* select attack techniques (*how* attacks run — e.g., prompt\n",
"> sending, role play, TAP). *Datasets* select objectives (*what* is tested — e.g., harm categories,\n",
"> compliance topics). Use `--dataset-names` on the CLI to filter by content category.\n",
"\n",
"> **Running scenarios from the command line?** See the [Scanner documentation](../../scanner/0_scanner.md).\n",
"\n",
"## Setup\n",
Expand Down
4 changes: 4 additions & 0 deletions doc/code/scenarios/1_scenario_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
# strategies, baseline execution, and custom scorers. All examples use `RedTeamAgent` but the
# patterns apply to any scenario.
#
# > **Two selection axes**: *Strategies* select attack techniques (*how* attacks run — e.g., prompt
# > sending, role play, TAP). *Datasets* select objectives (*what* is tested — e.g., harm categories,
# > compliance topics). Use `--dataset-names` on the CLI to filter by content category.
#
# > **Running scenarios from the command line?** See the [Scanner documentation](../../scanner/0_scanner.md).
#
# ## Setup
Expand Down
2 changes: 2 additions & 0 deletions pyrit/registry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
RetrievableInstanceRegistry,
ScorerRegistry,
TargetRegistry,
TechniqueSpec,
)

__all__ = [
Expand All @@ -45,4 +46,5 @@
"ScenarioRegistry",
"ScorerRegistry",
"TargetRegistry",
"TechniqueSpec",
]
16 changes: 16 additions & 0 deletions pyrit/registry/class_registries/scenario_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ def _discover_builtin_scenarios(self) -> None:
logger.debug(f"Skipping deprecated alias: {scenario_class.__name__}")
continue

# Skip re-exported aliases: if the class was defined in a different
# module than the one being discovered, it's an alias (e.g.,
# ContentHarms in content_harms.py is really RapidResponse from
# rapid_response.py).
class_module = getattr(scenario_class, "__module__", "")
expected_module_suffix = registry_name.replace(".", "/")
if not class_module.endswith(registry_name.replace("/", ".")):
# Build the full expected module name for comparison
expected_module = f"pyrit.scenario.scenarios.{registry_name.replace('/', '.')}"
if class_module != expected_module:
logger.debug(
f"Skipping alias '{scenario_class.__name__}' in '{registry_name}' "
f"(defined in {class_module})"
)
continue

# Check for registry key collision
if registry_name in self._class_entries:
logger.warning(
Expand Down
2 changes: 2 additions & 0 deletions pyrit/registry/object_registries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from pyrit.registry.object_registries.attack_technique_registry import (
AttackTechniqueRegistry,
TechniqueSpec,
)
from pyrit.registry.object_registries.base_instance_registry import (
BaseInstanceRegistry,
Expand Down Expand Up @@ -41,4 +42,5 @@
"ConverterRegistry",
"ScorerRegistry",
"TargetRegistry",
"TechniqueSpec",
]
Loading
Loading