diff --git a/pyrit/scenario/core/attack_technique_factory.py b/pyrit/scenario/core/attack_technique_factory.py index d8bbd60fc3..292023e07a 100644 --- a/pyrit/scenario/core/attack_technique_factory.py +++ b/pyrit/scenario/core/attack_technique_factory.py @@ -74,6 +74,7 @@ def __init__( *, name: str, attack_class: type[AttackStrategy[Any, Any]], + description: str | None = None, technique_tags: list[str] | None = None, attack_kwargs: dict[str, Any] | None = None, adversarial_chat: PromptTarget | None = None, @@ -90,6 +91,9 @@ def __init__( name: Registry name for this technique. This is used as the scenario technique name. attack_class: The AttackStrategy subclass to instantiate. + description: Short human-readable summary of what the technique does. + Purely descriptive metadata — it does not affect the technique's + behavioral identity. technique_tags: Tags controlling which ``ScenarioTechnique`` aggregates include this technique (e.g. ``"single_turn"``, ``"multi_turn"``, ``"default"``). @@ -129,6 +133,7 @@ class constructor signature and seed-technique shape. """ self._name = name self._attack_class = attack_class + self._description = description self._technique_tags = list(technique_tags) if technique_tags else [] self._attack_kwargs = dict(attack_kwargs) if attack_kwargs else {} self._adversarial_chat = adversarial_chat @@ -151,6 +156,7 @@ def with_simulated_conversation( *, name: str, attack_class: type[AttackStrategy[Any, Any]] | None = None, + description: str | None = None, adversarial_chat_system_prompt_path: str | Path | None = None, simulated_target_system_prompt_path: str | Path | None = None, next_message_system_prompt_path: str | Path | None = None, @@ -175,6 +181,8 @@ def with_simulated_conversation( ``EXECUTOR_SEED_PROMPT_PATH/red_teaming/{name}.yaml``. attack_class: The AttackStrategy subclass to instantiate. Defaults to ``PromptSendingAttack``. + description: Short human-readable summary of what the technique does. + Forwarded to the factory constructor as descriptive metadata. adversarial_chat_system_prompt_path: Path to the YAML file containing the adversarial chat system prompt for the simulated conversation. Defaults to ``EXECUTOR_SEED_PROMPT_PATH/red_teaming/{name}.yaml``. @@ -263,6 +271,7 @@ def with_simulated_conversation( return cls( name=name, attack_class=attack_class, + description=description, technique_tags=technique_tags, attack_kwargs=attack_kwargs, adversarial_chat=adversarial_chat, @@ -359,6 +368,11 @@ def name(self) -> str: """The registry name for this technique.""" return self._name + @property + def description(self) -> str | None: + """Short human-readable summary of what the technique does, or None.""" + return self._description + @property def technique_tags(self) -> list[str]: """Tags controlling which ``ScenarioTechnique`` aggregates include this technique.""" diff --git a/pyrit/setup/initializers/techniques/airt.py b/pyrit/setup/initializers/techniques/airt.py index 9136cd3371..1469ea6e19 100644 --- a/pyrit/setup/initializers/techniques/airt.py +++ b/pyrit/setup/initializers/techniques/airt.py @@ -40,6 +40,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="first_letter", attack_class=PromptSendingAttack, + description="Obfuscates the objective by asking for it encoded as the first letter of each word.", technique_tags=["single_turn", "airt", "leakage"], attack_kwargs={ "attack_converter_config": AttackConverterConfig( @@ -50,6 +51,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="image", attack_class=PromptSendingAttack, + description="Carries the objective text inside a blank image so it bypasses text-only input handling.", technique_tags=["single_turn", "airt", "leakage"], attack_kwargs={ "attack_converter_config": AttackConverterConfig( diff --git a/pyrit/setup/initializers/techniques/core.py b/pyrit/setup/initializers/techniques/core.py index e31b054ce7..be77112768 100644 --- a/pyrit/setup/initializers/techniques/core.py +++ b/pyrit/setup/initializers/techniques/core.py @@ -53,6 +53,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: return [ AttackTechniqueFactory.with_simulated_conversation( name="role_play_movie_script", + description="Frames the objective as a movie beat by casting the adversarial chat as a screenwriter.", adversarial_chat_system_prompt_path=( EXECUTOR_SEED_PROMPT_PATH / "red_teaming" / "role_play" / "role_play_movie_script.yaml" ), @@ -62,6 +63,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_video_game", + description="Frames the objective as an in-game mechanic by casting the adversarial chat as a designer.", adversarial_chat_system_prompt_path=( EXECUTOR_SEED_PROMPT_PATH / "red_teaming" / "role_play" / "role_play_video_game.yaml" ), @@ -71,6 +73,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_trivia_game", + description="Frames the objective as a quiz answer by casting the adversarial chat as a trivia host.", adversarial_chat_system_prompt_path=( EXECUTOR_SEED_PROMPT_PATH / "red_teaming" / "role_play" / "role_play_trivia_game.yaml" ), @@ -80,6 +83,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_persuasion", + description="Frames a persuasion-resistance test so the target drafts a persuasive spoken script.", adversarial_chat_system_prompt_path=( EXECUTOR_SEED_PROMPT_PATH / "red_teaming" / "role_play" / "role_play_persuasion.yaml" ), @@ -89,6 +93,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_persuasion_written", + description="Frames a persuasion-resistance test so the target drafts written persuasive material.", adversarial_chat_system_prompt_path=( EXECUTOR_SEED_PROMPT_PATH / "red_teaming" / "role_play" / "role_play_persuasion_written.yaml" ), @@ -99,36 +104,44 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="many_shot", attack_class=ManyShotJailbreakAttack, + description="Primes the target with many fake example exchanges that model compliance before the ask.", technique_tags=["multi_turn", "light"], ), AttackTechniqueFactory( name="tap", attack_class=TreeOfAttacksWithPruningAttack, + description="Explores a tree of adversarial prompts, pruning weak branches to refine the attack.", technique_tags=["multi_turn"], ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_simulated", + description="Escalates gradually over a simulated conversation toward the objective.", technique_tags=["single_turn"], ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_movie_director", + description="Escalates gradually while posing as a movie director coaxing the target scene by scene.", technique_tags=["single_turn"], ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_history_lecture", + description="Escalates gradually while framing the ask as an academic history lecture.", technique_tags=["single_turn"], ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_journalist_interview", + description="Escalates gradually while posing as a journalist drawing the target out.", technique_tags=["single_turn"], ), AttackTechniqueFactory( name="red_teaming", attack_class=RedTeamingAttack, + description="Uses an adversarial chat model to converse with the target and adapt toward the objective.", technique_tags=["multi_turn", "light"], ), AttackTechniqueFactory.with_simulated_conversation( name="context_compliance", + description="Injects a fabricated prior exchange so the target continues as if it already agreed.", adversarial_chat_system_prompt_path=EXECUTOR_RED_TEAM_PATH / "context_compliance" / "context_compliance.yaml", @@ -140,6 +153,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="flip", attack_class=PromptSendingAttack, + description="Reverses the objective text so it slips past filters, then asks the target to flip it back.", technique_tags=["single_turn", "light"], attack_kwargs={ "attack_converter_config": AttackConverterConfig( diff --git a/pyrit/setup/initializers/techniques/extra.py b/pyrit/setup/initializers/techniques/extra.py index 9202d77840..dcbc7da1e3 100644 --- a/pyrit/setup/initializers/techniques/extra.py +++ b/pyrit/setup/initializers/techniques/extra.py @@ -26,11 +26,13 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="pair", attack_class=PAIRAttack, + description="Runs the PAIR algorithm, using an adversarial model to iteratively rewrite jailbreak prompts.", technique_tags=["multi_turn"], ), AttackTechniqueFactory( name="violent_durian", attack_class=RedTeamingAttack, + description="Red-teams with a 'violent durian' persona role-playing a criminal mastermind.", technique_tags=["multi_turn"], attack_kwargs={"max_turns": 3}, adversarial_system_prompt=SeedPrompt.from_yaml_file(EXECUTOR_RED_TEAM_PATH / "violent_durian.yaml"), diff --git a/tests/unit/scenario/core/test_attack_technique_factory.py b/tests/unit/scenario/core/test_attack_technique_factory.py index 7ae54e3c70..c0a40142b9 100644 --- a/tests/unit/scenario/core/test_attack_technique_factory.py +++ b/tests/unit/scenario/core/test_attack_technique_factory.py @@ -70,6 +70,35 @@ def test_init_stores_seed_technique(self): assert factory.seed_technique is seeds + def test_init_description_defaults_to_none(self): + factory = AttackTechniqueFactory(name="test", attack_class=_StubAttack) + + assert factory.description is None + + def test_init_stores_description(self): + factory = AttackTechniqueFactory( + name="test", + attack_class=_StubAttack, + description="Does the thing.", + ) + + assert factory.description == "Does the thing." + + def test_with_simulated_conversation_forwards_description(self): + factory = AttackTechniqueFactory.with_simulated_conversation( + name="crescendo_journalist_interview", + description="Staged as a journalist interview.", + ) + + assert factory.description == "Staged as a journalist interview." + + def test_description_does_not_affect_identifier(self): + """Description is decorative metadata and must not change the behavioral identity hash.""" + with_desc = AttackTechniqueFactory(name="test", attack_class=_StubAttack, description="Does the thing.") + without_desc = AttackTechniqueFactory(name="test", attack_class=_StubAttack) + + assert with_desc.get_identifier().hash == without_desc.get_identifier().hash + def test_validate_kwargs_accepts_valid_params(self): """All valid kwarg names should pass without error.""" factory = AttackTechniqueFactory(