diff --git a/pyrit/scenario/core/attack_technique_factory.py b/pyrit/scenario/core/attack_technique_factory.py index af0c59c851..d929b6eb24 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, next_message_system_prompt_path: str | Path | None = None, num_turns: int = 3, @@ -173,6 +179,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``. @@ -224,6 +232,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, @@ -320,6 +329,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 4507697454..488eb5d34c 100644 --- a/pyrit/setup/initializers/techniques/core.py +++ b/pyrit/setup/initializers/techniques/core.py @@ -46,43 +46,52 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="role_play", attack_class=RolePlayAttack, + description="Frames the objective as a fictional movie script the target treats as creative writing.", technique_tags=["single_turn", "light"], attack_kwargs={"role_play_definition_path": RolePlayPaths.MOVIE_SCRIPT.value}, ), 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="Crescendo escalation cast as a movie-director persona coaxing the target scene by scene.", technique_tags=["single_turn"], ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_history_lecture", + description="Crescendo escalation framed as an academic history lecture to normalize the objective.", technique_tags=["single_turn"], ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_journalist_interview", + description="Crescendo escalation staged as a journalist interview 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( name="context_compliance", attack_class=ContextComplianceAttack, + description="Injects a fabricated prior exchange so the target continues as if it already agreed.", technique_tags=["single_turn", "light"], ), ] diff --git a/pyrit/setup/initializers/techniques/extra.py b/pyrit/setup/initializers/techniques/extra.py index 9202d77840..dd3fd9e8d8 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-teaming 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 bd7de9a576..ce1c61e6d4 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(