From 4670ca814af100f74b2c1a16c8ae3a4aeee09bd5 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 10 Jul 2026 19:16:19 -0700 Subject: [PATCH 1/2] FEAT: Adding descriptions to attack technique factories Add an optional description field to AttackTechniqueFactory (decorative metadata, excluded from the behavioral identity hash) and populate a concise description for every technique in the core, extra, and airt catalogs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scenario/core/attack_technique_factory.py | 14 +++++++++ pyrit/setup/initializers/techniques/airt.py | 2 ++ pyrit/setup/initializers/techniques/core.py | 9 ++++++ pyrit/setup/initializers/techniques/extra.py | 2 ++ .../core/test_attack_technique_factory.py | 29 +++++++++++++++++++ 5 files changed, 56 insertions(+) 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( From c2741dc98437764c2fbd2103c3819a27e6b8f412 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Tue, 14 Jul 2026 16:42:55 -0700 Subject: [PATCH 2/2] Normalize technique descriptions to active-verb openings Address review nits on PR #2167: open every description with an active verb and use consistent 'Frames' wording for the role_play variants (was cast/staged/framed synonyms), and rewrite the crescendo entries from noun phrases to active verbs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pyrit/setup/initializers/techniques/core.py | 16 ++++++++-------- pyrit/setup/initializers/techniques/extra.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyrit/setup/initializers/techniques/core.py b/pyrit/setup/initializers/techniques/core.py index 8c9ebf39ae..be77112768 100644 --- a/pyrit/setup/initializers/techniques/core.py +++ b/pyrit/setup/initializers/techniques/core.py @@ -53,7 +53,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: return [ AttackTechniqueFactory.with_simulated_conversation( name="role_play_movie_script", - description="Casts the adversarial chat as a screenwriter so the objective is delivered as a movie beat.", + 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" ), @@ -63,7 +63,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_video_game", - description="Casts the adversarial chat as a game designer so the objective becomes an in-game mechanic.", + 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" ), @@ -73,7 +73,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_trivia_game", - description="Casts the adversarial chat as a trivia host so the objective is answered as a quiz question.", + 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" ), @@ -83,7 +83,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_persuasion", - description="Stages a persuasion-resistance test so the target drafts a persuasive spoken script.", + 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" ), @@ -93,7 +93,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="role_play_persuasion_written", - description="Stages a persuasion-resistance test so the target drafts written material for the objective.", + 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" ), @@ -120,17 +120,17 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: ), AttackTechniqueFactory.with_simulated_conversation( name="crescendo_movie_director", - description="Crescendo escalation cast as a movie-director persona coaxing the target scene by scene.", + 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="Crescendo escalation framed as an academic history lecture to normalize the objective.", + 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="Crescendo escalation staged as a journalist interview drawing the target out.", + description="Escalates gradually while posing as a journalist drawing the target out.", technique_tags=["single_turn"], ), AttackTechniqueFactory( diff --git a/pyrit/setup/initializers/techniques/extra.py b/pyrit/setup/initializers/techniques/extra.py index dd3fd9e8d8..dcbc7da1e3 100644 --- a/pyrit/setup/initializers/techniques/extra.py +++ b/pyrit/setup/initializers/techniques/extra.py @@ -32,7 +32,7 @@ def get_technique_factories() -> list[AttackTechniqueFactory]: AttackTechniqueFactory( name="violent_durian", attack_class=RedTeamingAttack, - description="Red-teaming with a 'violent durian' persona role-playing a criminal mastermind.", + 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"),