Skip to content

Commit c299739

Browse files
rlundeen2Copilot
andauthored
FEAT: Adding AttackTechnique Registry (microsoft#2101)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d6f0abd commit c299739

24 files changed

Lines changed: 202 additions & 138 deletions

doc/code/scenarios/0_attack_techniques.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"## Where techniques come from: initializers\n",
5050
"\n",
5151
"Techniques are registered into a singleton\n",
52-
"[`AttackTechniqueRegistry`](../../../pyrit/registry/object_registries/attack_technique_registry.py)\n",
52+
"[`AttackTechniqueRegistry`](../../../pyrit/registry/components/attack_technique_registry.py)\n",
5353
"by an **initializer**. The canonical catalog lives in\n",
5454
"[`ScenarioTechniqueInitializer`](../../../pyrit/setup/initializers/components/scenario_techniques.py),\n",
5555
"which registers a flat list of\n",
@@ -92,7 +92,7 @@
9292
"source": [
9393
"import pandas as pd\n",
9494
"\n",
95-
"from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry\n",
95+
"from pyrit.registry import AttackTechniqueRegistry\n",
9696
"from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n",
9797
"from pyrit.setup.initializers.components import ScenarioTechniqueInitializer\n",
9898
"\n",
@@ -169,7 +169,7 @@
169169
"\n",
170170
"```python\n",
171171
"from pyrit.executor.attack import RolePlayAttack, RolePlayPaths\n",
172-
"from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry\n",
172+
"from pyrit.registry import AttackTechniqueRegistry\n",
173173
"from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory\n",
174174
"\n",
175175
"AttackTechniqueRegistry.get_registry_singleton().register_from_factories(\n",

doc/code/scenarios/0_attack_techniques.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
# ## Where techniques come from: initializers
4949
#
5050
# Techniques are registered into a singleton
51-
# [`AttackTechniqueRegistry`](../../../pyrit/registry/object_registries/attack_technique_registry.py)
51+
# [`AttackTechniqueRegistry`](../../../pyrit/registry/components/attack_technique_registry.py)
5252
# by an **initializer**. The canonical catalog lives in
5353
# [`ScenarioTechniqueInitializer`](../../../pyrit/setup/initializers/components/scenario_techniques.py),
5454
# which registers a flat list of
@@ -65,7 +65,7 @@
6565
# %%
6666
import pandas as pd
6767

68-
from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
68+
from pyrit.registry import AttackTechniqueRegistry
6969
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
7070
from pyrit.setup.initializers.components import ScenarioTechniqueInitializer
7171

@@ -132,7 +132,7 @@
132132
#
133133
# ```python
134134
# from pyrit.executor.attack import RolePlayAttack, RolePlayPaths
135-
# from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
135+
# from pyrit.registry import AttackTechniqueRegistry
136136
# from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory
137137
#
138138
# AttackTechniqueRegistry.get_registry_singleton().register_from_factories(

pyrit/registry/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
ScenarioRegistry,
1515
)
1616
from pyrit.registry.components import (
17+
AttackTechniqueMetadata,
18+
AttackTechniqueRegistry,
1719
ConverterMetadata,
1820
ConverterRegistry,
1921
)
@@ -28,7 +30,6 @@
2830
SupportsInstances,
2931
)
3032
from pyrit.registry.object_registries import (
31-
AttackTechniqueRegistry,
3233
BaseInstanceRegistry,
3334
RegistryEntry,
3435
RetrievableInstanceRegistry,
@@ -40,6 +41,7 @@
4041

4142
__all__ = [
4243
"AttackTechniqueRegistry",
44+
"AttackTechniqueMetadata",
4345
"BaseClassRegistry",
4446
"BaseInstanceRegistry",
4547
"ConverterRegistry",

pyrit/registry/components/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@
1414
``DefaultInstanceRegistry``) live at the top level of ``pyrit.registry``.
1515
"""
1616

17+
from pyrit.registry.components.attack_technique_registry import (
18+
AttackTechniqueMetadata,
19+
AttackTechniqueRegistry,
20+
)
1721
from pyrit.registry.components.converter_registry import (
1822
ConverterMetadata,
1923
ConverterRegistry,
2024
)
2125

2226
__all__ = [
27+
"AttackTechniqueRegistry",
28+
"AttackTechniqueMetadata",
2329
"ConverterRegistry",
2430
"ConverterMetadata",
2531
]

pyrit/registry/object_registries/attack_technique_registry.py renamed to pyrit/registry/components/attack_technique_registry.py

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,111 @@
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

1321
from __future__ import annotations
1422

1523
import logging
24+
from dataclasses import dataclass
1625
from 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

2231
if 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

2738
logger = 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))

pyrit/registry/object_registries/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111
For registries that store classes (type[T]), see class_registries/.
1212
"""
1313

14-
from pyrit.registry.object_registries.attack_technique_registry import (
15-
AttackTechniqueRegistry,
16-
)
1714
from pyrit.registry.object_registries.base_instance_registry import (
1815
BaseInstanceRegistry,
1916
RegistryEntry,
@@ -34,7 +31,6 @@
3431
"RetrievableInstanceRegistry",
3532
"RegistryEntry",
3633
# Concrete registries
37-
"AttackTechniqueRegistry",
3834
"ScorerRegistry",
3935
"TargetRegistry",
4036
]

pyrit/registry/object_registries/base_instance_registry.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,17 @@
1111
build instances by name) and hold pre-configured instances via the
1212
``.instances`` property (a ``DefaultInstanceRegistry``). See
1313
``ConverterRegistry`` for the target shape. This class and
14-
``RetrievableInstanceRegistry`` remain only because ``TargetRegistry``,
15-
``ScorerRegistry``, and ``AttackTechniqueRegistry`` still subclass them;
16-
the whole stack is removed once those migrate.
14+
``RetrievableInstanceRegistry`` remain only because ``TargetRegistry`` and
15+
``ScorerRegistry`` still subclass them; the whole stack is removed once
16+
those migrate.
1717
1818
This module provides ``BaseInstanceRegistry``, the shared infrastructure for
1919
registries that store ``Identifiable`` objects (not classes): singleton
2020
lifecycle, registration, tags, metadata, container protocol.
2121
2222
Subclass directly for registries that store factories or other
23-
non-retrievable items (e.g., ``AttackTechniqueRegistry``). For registries
24-
where callers retrieve stored objects directly, subclass
25-
``RetrievableInstanceRegistry`` instead.
23+
non-retrievable items. For registries where callers retrieve stored objects
24+
directly, subclass ``RetrievableInstanceRegistry`` instead.
2625
2726
For registries that store classes (type[T]), see ``class_registries/``.
2827
"""
@@ -59,9 +58,8 @@ class BaseInstanceRegistry(ABC, RegistryProtocol[ComponentIdentifier], Generic[T
5958
via the ``.instances`` property (``DefaultInstanceRegistry``), which
6059
carries this same surface (``register``/``get``/``get_by_tag``/
6160
``add_tags``/``find_dependents_of_tag``/``list_metadata``). This class
62-
survives only for the not-yet-migrated ``TargetRegistry``,
63-
``ScorerRegistry``, and ``AttackTechniqueRegistry`` and is removed once
64-
they move to ``.instances``.
61+
survives only for the not-yet-migrated ``TargetRegistry`` and
62+
``ScorerRegistry`` and is removed once they move to ``.instances``.
6563
6664
Provides singleton lifecycle, registration, tag-based lookup, metadata
6765
filtering, and the standard container protocol (``__contains__``,

pyrit/scenario/core/scenario.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def _get_attack_technique_factories(self) -> dict[str, "AttackTechniqueFactory"]
334334
Raises:
335335
RuntimeError: If the registry is empty (no initializer has run).
336336
"""
337-
from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
337+
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
338338

339339
registry = AttackTechniqueRegistry.get_registry_singleton()
340340
return registry.get_factories_or_raise()

pyrit/scenario/scenarios/adaptive/text_adaptive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from pyrit.common import apply_defaults
2020
from pyrit.models.parameter import Parameter
21-
from pyrit.registry.object_registries.attack_technique_registry import (
21+
from pyrit.registry.components.attack_technique_registry import (
2222
AttackTechniqueRegistry,
2323
)
2424
from pyrit.registry.tag_query import TagQuery

pyrit/scenario/scenarios/airt/cyber.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _build_cyber_strategy() -> type[ScenarioStrategy]:
3737
Returns:
3838
type[ScenarioStrategy]: The dynamically generated strategy enum class.
3939
"""
40-
from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
40+
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
4141
from pyrit.registry.tag_query import TagQuery
4242

4343
registry = AttackTechniqueRegistry.get_registry_singleton()

0 commit comments

Comments
 (0)