22# Licensed under the MIT license.
33
44"""
5- AttackTechniqueRegistry — Singleton registry of reusable attack technique factories.
6-
7- Scenarios and initializers register self-describing ``AttackTechniqueFactory``
8- instances. Scenarios retrieve factories via ``get_factories()`` and filter
9- them in-place (e.g. by ``factory.uses_adversarial`` or strategy tags) before
10- calling ``factory.create()`` with the scenario's objective target and scorer.
5+ Attack technique registry for PyRIT.
6+
7+ A registry for ``AttackTechniqueFactory`` instances that scenarios and
8+ initializers register and later retrieve. Like ``ConverterRegistry`` it is a
9+ ``Registry`` whose pre-configured instances live under the ``instances``
10+ property; unlike converters, its buildable class catalog is intentionally empty
11+ for now — the factory still owns its own construction, and the catalog is lit up
12+ later when the factory is decoupled into a buildable component.
13+
14+ Scenarios and initializers register self-describing factories (via
15+ ``register_from_factories``), retrieve them with ``get_factories`` /
16+ ``get_factories_or_raise``, filter them in-place by factory properties (e.g.
17+ ``factory.uses_adversarial`` or strategy tags), and call ``factory.create()``
18+ with the scenario's objective target and scorer.
1119"""
1220
1321from __future__ import annotations
1422
1523import logging
24+ from dataclasses import dataclass
1625from typing import TYPE_CHECKING
1726
18- from pyrit .registry .object_registries . base_instance_registry import (
19- BaseInstanceRegistry ,
20- )
27+ from pyrit .registry .base import ClassRegistryEntry
28+ from pyrit . registry . instance_registry import DefaultInstanceRegistry , InstanceRegistry
29+ from pyrit . registry . registry import Registry
2130
2231if TYPE_CHECKING :
2332 from pyrit .registry .tag_query import TagQuery
24- from pyrit .scenario import AttackTechniqueFactory
25- from pyrit .scenario .core .attack_technique_factory import ScorerOverridePolicy
33+ from pyrit .scenario .core .attack_technique_factory import (
34+ AttackTechniqueFactory ,
35+ ScorerOverridePolicy ,
36+ )
2637
2738logger = logging .getLogger (__name__ )
2839
2940
30- class AttackTechniqueRegistry (BaseInstanceRegistry ["AttackTechniqueFactory" ]):
41+ def _attack_technique_factory_type () -> type [AttackTechniqueFactory ]:
42+ """
43+ Return the ``AttackTechniqueFactory`` class, importing it lazily.
44+
45+ Used as the ``instance_type`` for the registry's ``instances`` container so a
46+ non-factory cannot be registered, without importing the factory module (which
47+ pulls in the executor/attack stack) at registry import time.
48+
49+ Returns:
50+ type[AttackTechniqueFactory]: The ``AttackTechniqueFactory`` class.
3151 """
32- Singleton registry of reusable attack technique factories.
52+ from pyrit .scenario .core .attack_technique_factory import AttackTechniqueFactory
53+
54+ return AttackTechniqueFactory
55+
56+
57+ @dataclass (frozen = True )
58+ class AttackTechniqueMetadata (ClassRegistryEntry ):
59+ """
60+ Metadata describing a registered attack-technique class.
61+
62+ Placeholder for the buildable catalog, which is intentionally empty until the
63+ factory is decoupled into a buildable component. It carries only the common
64+ ``ClassRegistryEntry`` fields today; technique-specific fields are added when
65+ the catalog is lit up.
66+ """
67+
68+
69+ class AttackTechniqueRegistry (Registry ["AttackTechniqueFactory" , AttackTechniqueMetadata ]):
70+ """
71+ Registry that holds reusable ``AttackTechniqueFactory`` instances.
3372
3473 Scenarios and initializers register self-describing
35- ``AttackTechniqueFactory`` instances. Scenarios retrieve factories via
36- ``get_factories()`` and call ``factory.create()`` with the scenario's
37- objective target and scorer.
74+ ``AttackTechniqueFactory`` instances; scenarios retrieve them via
75+ ``get_factories`` / ``get_factories_or_raise`` and call ``factory.create()``
76+ with the scenario's objective target and scorer.
77+
78+ It is a ``Registry``: pre-configured factories live under the ``instances``
79+ property (``register``, ``get``, ``get_all_instances``, ``get_by_tag``, …),
80+ a ``DefaultInstanceRegistry``. The buildable class catalog is intentionally
81+ empty for now — the factory still owns construction — so ``_discover``
82+ registers no classes.
3883 """
3984
40- def __init__ (self ) -> None :
41- """Initialize the registry with the default scorer override policy."""
85+ def __init__ (self , * , lazy_discovery : bool = True ) -> None :
86+ """
87+ Initialize the registry.
88+
89+ Args:
90+ lazy_discovery (bool): If True, class discovery is deferred until first
91+ access. If False, discovery runs immediately. The buildable catalog
92+ is empty either way; the flag is accepted for parity with other
93+ registries.
94+ """
4295 from pyrit .scenario .core .attack_technique_factory import ScorerOverridePolicy
4396
44- super ().__init__ ()
97+ super ().__init__ (lazy_discovery = lazy_discovery )
98+ self .instances : InstanceRegistry [AttackTechniqueFactory ] = DefaultInstanceRegistry (
99+ instance_type = _attack_technique_factory_type
100+ )
45101 self ._scorer_override_policy = ScorerOverridePolicy .WARN
46102
103+ def _discover (self ) -> None :
104+ """Register no classes: the factory owns construction; the catalog is lit up later."""
105+
106+ def _metadata_class (self ) -> type [AttackTechniqueMetadata ]:
107+ """Return ``AttackTechniqueMetadata``; unused while the buildable catalog is empty."""
108+ return AttackTechniqueMetadata
109+
47110 def register_technique (
48111 self ,
49112 * ,
@@ -55,16 +118,13 @@ def register_technique(
55118 Register an attack technique factory.
56119
57120 Args:
58- name: The registry name for this technique.
59- factory: The factory that produces attack techniques.
60- tags: Optional tags for categorisation. Accepts a ``dict[str, str]``
61- or a ``list[str]`` (each string becomes a key with value ``""``).
121+ name (str): The registry name for this technique.
122+ factory (AttackTechniqueFactory): The factory that produces attack techniques.
123+ tags (dict[str, str] | list[str] | None): Optional tags for categorisation.
124+ Accepts a ``dict[str, str]`` or a ``list[str]`` (each string becomes a
125+ key with value ``""``).
62126 """
63- self .register (
64- factory ,
65- name = name ,
66- tags = tags ,
67- )
127+ self .instances .register (factory , name = name , tags = tags )
68128 logger .debug (f"Registered attack technique factory: { name } ({ factory .attack_class .__name__ } )" )
69129
70130 def get_factories (self ) -> dict [str , AttackTechniqueFactory ]:
@@ -77,7 +137,7 @@ def get_factories(self) -> dict[str, AttackTechniqueFactory]:
77137 Returns:
78138 dict[str, AttackTechniqueFactory]: Mapping of technique name to factory.
79139 """
80- return {name : entry .instance for name , entry in self ._registry_items . items ()}
140+ return {entry . name : entry .instance for entry in self .instances . get_all_instances ()}
81141
82142 def get_factories_or_raise (self ) -> dict [str , AttackTechniqueFactory ]:
83143 """
@@ -131,14 +191,15 @@ def build_strategy_class_from_factories(
131191 technique factories belong to it.
132192
133193 Args:
134- class_name: Name for the generated enum class.
135- factories: Technique factories to include as enum members.
136- aggregate_tags: Maps aggregate member names to a ``TagQuery``
137- that selects which techniques belong to the aggregate.
194+ class_name (str): Name for the generated enum class.
195+ factories (list[AttackTechniqueFactory]): Technique factories to include
196+ as enum members.
197+ aggregate_tags (dict[str, TagQuery]): Maps aggregate member names to a
198+ ``TagQuery`` that selects which techniques belong to the aggregate.
138199 An ``ALL`` aggregate (expanding to all techniques) is always added.
139200
140201 Returns:
141- A ``ScenarioStrategy`` subclass with the generated members.
202+ type: A ``ScenarioStrategy`` subclass with the generated members.
142203 """
143204 from pyrit .scenario import ScenarioStrategy
144205
@@ -179,16 +240,17 @@ def register_from_factories(
179240 Per-name idempotent: existing entries are not overwritten.
180241
181242 Args:
182- factories: Self-describing factories to register. Each factory's
183- ``name`` and ``strategy_tags`` properties are used directly.
243+ factories (list[AttackTechniqueFactory]): Self-describing factories to
244+ register. Each factory's ``name`` and ``strategy_tags`` properties are
245+ used directly.
184246 """
185247 for factory in factories :
186- if factory .name not in self :
248+ if factory .name not in self . instances :
187249 tags : dict [str , str ] = dict .fromkeys (factory .strategy_tags , "" )
188250 self .register_technique (
189251 name = factory .name ,
190252 factory = factory ,
191253 tags = tags ,
192254 )
193255
194- logger .debug ("Technique registration complete (%d total in registry)" , len (self ))
256+ logger .debug ("Technique registration complete (%d total in registry)" , len (self . instances ))
0 commit comments