Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/code/scenarios/0_attack_techniques.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"source": [
"import pandas as pd\n",
"\n",
"from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry\n",
"from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry\n",
"from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n",
"from pyrit.setup.initializers.components import ScenarioTechniqueInitializer\n",
"\n",
Expand Down Expand Up @@ -169,7 +169,7 @@
"\n",
"```python\n",
"from pyrit.executor.attack import RolePlayAttack, RolePlayPaths\n",
"from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry\n",
"from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry\n",
"from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory\n",
"\n",
"AttackTechniqueRegistry.get_registry_singleton().register_from_factories(\n",
Expand Down
4 changes: 2 additions & 2 deletions doc/code/scenarios/0_attack_techniques.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
# %%
import pandas as pd

from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we shorten this?

from pyrit.setup import IN_MEMORY, initialize_pyrit_async
from pyrit.setup.initializers.components import ScenarioTechniqueInitializer

Expand Down Expand Up @@ -132,7 +132,7 @@
#
# ```python
# from pyrit.executor.attack import RolePlayAttack, RolePlayPaths
# from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
# from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
# from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory
#
# AttackTechniqueRegistry.get_registry_singleton().register_from_factories(
Expand Down
4 changes: 3 additions & 1 deletion pyrit/registry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
ScenarioRegistry,
)
from pyrit.registry.components import (
AttackTechniqueMetadata,
AttackTechniqueRegistry,
ConverterMetadata,
ConverterRegistry,
)
Expand All @@ -28,7 +30,6 @@
SupportsInstances,
)
from pyrit.registry.object_registries import (
AttackTechniqueRegistry,
BaseInstanceRegistry,
RegistryEntry,
RetrievableInstanceRegistry,
Expand All @@ -40,6 +41,7 @@

__all__ = [
"AttackTechniqueRegistry",
"AttackTechniqueMetadata",
"BaseClassRegistry",
"BaseInstanceRegistry",
"ConverterRegistry",
Expand Down
6 changes: 6 additions & 0 deletions pyrit/registry/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@
``DefaultInstanceRegistry``) live at the top level of ``pyrit.registry``.
"""

from pyrit.registry.components.attack_technique_registry import (
AttackTechniqueMetadata,
AttackTechniqueRegistry,
)
from pyrit.registry.components.converter_registry import (
ConverterMetadata,
ConverterRegistry,
)

__all__ = [
"AttackTechniqueRegistry",
"AttackTechniqueMetadata",
"ConverterRegistry",
"ConverterMetadata",
]
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,111 @@
# Licensed under the MIT license.

"""
AttackTechniqueRegistry — Singleton registry of reusable attack technique factories.

Scenarios and initializers register self-describing ``AttackTechniqueFactory``
instances. Scenarios retrieve factories via ``get_factories()`` and filter
them in-place (e.g. by ``factory.uses_adversarial`` or strategy tags) before
calling ``factory.create()`` with the scenario's objective target and scorer.
Attack technique registry for PyRIT.

A registry for ``AttackTechniqueFactory`` instances that scenarios and
initializers register and later retrieve. Like ``ConverterRegistry`` it is a
``Registry`` whose pre-configured instances live under the ``instances``
property; unlike converters, its buildable class catalog is intentionally empty
for now — the factory still owns its own construction, and the catalog is lit up
later when the factory is decoupled into a buildable component.

Scenarios and initializers register self-describing factories (via
``register_from_factories``), retrieve them with ``get_factories`` /
``get_factories_or_raise``, filter them in-place by factory properties (e.g.
``factory.uses_adversarial`` or strategy tags), and call ``factory.create()``
with the scenario's objective target and scorer.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from pyrit.registry.object_registries.base_instance_registry import (
BaseInstanceRegistry,
)
from pyrit.registry.base import ClassRegistryEntry
from pyrit.registry.instance_registry import DefaultInstanceRegistry, InstanceRegistry
from pyrit.registry.registry import Registry

if TYPE_CHECKING:
from pyrit.registry.tag_query import TagQuery
from pyrit.scenario import AttackTechniqueFactory
from pyrit.scenario.core.attack_technique_factory import ScorerOverridePolicy
from pyrit.scenario.core.attack_technique_factory import (
AttackTechniqueFactory,
ScorerOverridePolicy,
)

logger = logging.getLogger(__name__)


class AttackTechniqueRegistry(BaseInstanceRegistry["AttackTechniqueFactory"]):
def _attack_technique_factory_type() -> type[AttackTechniqueFactory]:
"""
Return the ``AttackTechniqueFactory`` class, importing it lazily.

Used as the ``instance_type`` for the registry's ``instances`` container so a
non-factory cannot be registered, without importing the factory module (which
pulls in the executor/attack stack) at registry import time.

Returns:
type[AttackTechniqueFactory]: The ``AttackTechniqueFactory`` class.
"""
Singleton registry of reusable attack technique factories.
from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory

return AttackTechniqueFactory


@dataclass(frozen=True)
class AttackTechniqueMetadata(ClassRegistryEntry):
"""
Metadata describing a registered attack-technique class.

Placeholder for the buildable catalog, which is intentionally empty until the
factory is decoupled into a buildable component. It carries only the common
``ClassRegistryEntry`` fields today; technique-specific fields are added when
the catalog is lit up.
"""


class AttackTechniqueRegistry(Registry["AttackTechniqueFactory", AttackTechniqueMetadata]):
"""
Registry that holds reusable ``AttackTechniqueFactory`` instances.

Scenarios and initializers register self-describing
``AttackTechniqueFactory`` instances. Scenarios retrieve factories via
``get_factories()`` and call ``factory.create()`` with the scenario's
objective target and scorer.
``AttackTechniqueFactory`` instances; scenarios retrieve them via
``get_factories`` / ``get_factories_or_raise`` and call ``factory.create()``
with the scenario's objective target and scorer.

It is a ``Registry``: pre-configured factories live under the ``instances``
property (``register``, ``get``, ``get_all_instances``, ``get_by_tag``, …),
a ``DefaultInstanceRegistry``. The buildable class catalog is intentionally
empty for now — the factory still owns construction — so ``_discover``
registers no classes.
"""

def __init__(self) -> None:
"""Initialize the registry with the default scorer override policy."""
def __init__(self, *, lazy_discovery: bool = True) -> None:
"""
Initialize the registry.

Args:
lazy_discovery (bool): If True, class discovery is deferred until first
access. If False, discovery runs immediately. The buildable catalog
is empty either way; the flag is accepted for parity with other
registries.
"""
from pyrit.scenario.core.attack_technique_factory import ScorerOverridePolicy

super().__init__()
super().__init__(lazy_discovery=lazy_discovery)
self.instances: InstanceRegistry[AttackTechniqueFactory] = DefaultInstanceRegistry(
instance_type=_attack_technique_factory_type
)
self._scorer_override_policy = ScorerOverridePolicy.WARN

def _discover(self) -> None:
"""Register no classes: the factory owns construction; the catalog is lit up later."""

def _metadata_class(self) -> type[AttackTechniqueMetadata]:
"""Return ``AttackTechniqueMetadata``; unused while the buildable catalog is empty."""
return AttackTechniqueMetadata

def register_technique(
self,
*,
Expand All @@ -55,16 +118,13 @@ def register_technique(
Register an attack technique factory.

Args:
name: The registry name for this technique.
factory: The factory that produces attack techniques.
tags: Optional tags for categorisation. Accepts a ``dict[str, str]``
or a ``list[str]`` (each string becomes a key with value ``""``).
name (str): The registry name for this technique.
factory (AttackTechniqueFactory): The factory that produces attack techniques.
tags (dict[str, str] | list[str] | None): Optional tags for categorisation.
Accepts a ``dict[str, str]`` or a ``list[str]`` (each string becomes a
key with value ``""``).
"""
self.register(
factory,
name=name,
tags=tags,
)
self.instances.register(factory, name=name, tags=tags)
logger.debug(f"Registered attack technique factory: {name} ({factory.attack_class.__name__})")

def get_factories(self) -> dict[str, AttackTechniqueFactory]:
Expand All @@ -77,7 +137,7 @@ def get_factories(self) -> dict[str, AttackTechniqueFactory]:
Returns:
dict[str, AttackTechniqueFactory]: Mapping of technique name to factory.
"""
return {name: entry.instance for name, entry in self._registry_items.items()}
return {entry.name: entry.instance for entry in self.instances.get_all_instances()}

def get_factories_or_raise(self) -> dict[str, AttackTechniqueFactory]:
"""
Expand Down Expand Up @@ -131,14 +191,15 @@ def build_strategy_class_from_factories(
technique factories belong to it.

Args:
class_name: Name for the generated enum class.
factories: Technique factories to include as enum members.
aggregate_tags: Maps aggregate member names to a ``TagQuery``
that selects which techniques belong to the aggregate.
class_name (str): Name for the generated enum class.
factories (list[AttackTechniqueFactory]): Technique factories to include
as enum members.
aggregate_tags (dict[str, TagQuery]): Maps aggregate member names to a
``TagQuery`` that selects which techniques belong to the aggregate.
An ``ALL`` aggregate (expanding to all techniques) is always added.

Returns:
A ``ScenarioStrategy`` subclass with the generated members.
type: A ``ScenarioStrategy`` subclass with the generated members.
"""
from pyrit.scenario import ScenarioStrategy

Expand Down Expand Up @@ -179,16 +240,17 @@ def register_from_factories(
Per-name idempotent: existing entries are not overwritten.

Args:
factories: Self-describing factories to register. Each factory's
``name`` and ``strategy_tags`` properties are used directly.
factories (list[AttackTechniqueFactory]): Self-describing factories to
register. Each factory's ``name`` and ``strategy_tags`` properties are
used directly.
"""
for factory in factories:
if factory.name not in self:
if factory.name not in self.instances:
tags: dict[str, str] = dict.fromkeys(factory.strategy_tags, "")
self.register_technique(
name=factory.name,
factory=factory,
tags=tags,
)

logger.debug("Technique registration complete (%d total in registry)", len(self))
logger.debug("Technique registration complete (%d total in registry)", len(self.instances))
4 changes: 0 additions & 4 deletions pyrit/registry/object_registries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
For registries that store classes (type[T]), see class_registries/.
"""

from pyrit.registry.object_registries.attack_technique_registry import (
AttackTechniqueRegistry,
)
from pyrit.registry.object_registries.base_instance_registry import (
BaseInstanceRegistry,
RegistryEntry,
Expand All @@ -34,7 +31,6 @@
"RetrievableInstanceRegistry",
"RegistryEntry",
# Concrete registries
"AttackTechniqueRegistry",
"ScorerRegistry",
"TargetRegistry",
]
16 changes: 7 additions & 9 deletions pyrit/registry/object_registries/base_instance_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,17 @@
build instances by name) and hold pre-configured instances via the
``.instances`` property (a ``DefaultInstanceRegistry``). See
``ConverterRegistry`` for the target shape. This class and
``RetrievableInstanceRegistry`` remain only because ``TargetRegistry``,
``ScorerRegistry``, and ``AttackTechniqueRegistry`` still subclass them;
the whole stack is removed once those migrate.
``RetrievableInstanceRegistry`` remain only because ``TargetRegistry`` and
``ScorerRegistry`` still subclass them; the whole stack is removed once
those migrate.

This module provides ``BaseInstanceRegistry``, the shared infrastructure for
registries that store ``Identifiable`` objects (not classes): singleton
lifecycle, registration, tags, metadata, container protocol.

Subclass directly for registries that store factories or other
non-retrievable items (e.g., ``AttackTechniqueRegistry``). For registries
where callers retrieve stored objects directly, subclass
``RetrievableInstanceRegistry`` instead.
non-retrievable items. For registries where callers retrieve stored objects
directly, subclass ``RetrievableInstanceRegistry`` instead.

For registries that store classes (type[T]), see ``class_registries/``.
"""
Expand Down Expand Up @@ -59,9 +58,8 @@ class BaseInstanceRegistry(ABC, RegistryProtocol[ComponentIdentifier], Generic[T
via the ``.instances`` property (``DefaultInstanceRegistry``), which
carries this same surface (``register``/``get``/``get_by_tag``/
``add_tags``/``find_dependents_of_tag``/``list_metadata``). This class
survives only for the not-yet-migrated ``TargetRegistry``,
``ScorerRegistry``, and ``AttackTechniqueRegistry`` and is removed once
they move to ``.instances``.
survives only for the not-yet-migrated ``TargetRegistry`` and
``ScorerRegistry`` and is removed once they move to ``.instances``.

Provides singleton lifecycle, registration, tag-based lookup, metadata
filtering, and the standard container protocol (``__contains__``,
Expand Down
2 changes: 1 addition & 1 deletion pyrit/scenario/core/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def _get_attack_technique_factories(self) -> dict[str, "AttackTechniqueFactory"]
Raises:
RuntimeError: If the registry is empty (no initializer has run).
"""
from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry

registry = AttackTechniqueRegistry.get_registry_singleton()
return registry.get_factories_or_raise()
Expand Down
2 changes: 1 addition & 1 deletion pyrit/scenario/scenarios/adaptive/text_adaptive.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from pyrit.common import apply_defaults
from pyrit.models.parameter import Parameter
from pyrit.registry.object_registries.attack_technique_registry import (
from pyrit.registry.components.attack_technique_registry import (
AttackTechniqueRegistry,
)
from pyrit.registry.tag_query import TagQuery
Expand Down
2 changes: 1 addition & 1 deletion pyrit/scenario/scenarios/airt/cyber.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def _build_cyber_strategy() -> type[ScenarioStrategy]:
Returns:
type[ScenarioStrategy]: The dynamically generated strategy enum class.
"""
from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
from pyrit.registry.tag_query import TagQuery

registry = AttackTechniqueRegistry.get_registry_singleton()
Expand Down
2 changes: 1 addition & 1 deletion pyrit/scenario/scenarios/airt/leakage.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from pyrit.prompt_converter import AddImageTextConverter, FirstLetterConverter
from pyrit.prompt_normalizer import PromptConverterConfiguration
from pyrit.registry.object_registries.attack_technique_registry import (
from pyrit.registry.components.attack_technique_registry import (
AttackTechniqueRegistry,
)
from pyrit.registry.tag_query import TagQuery
Expand Down
2 changes: 1 addition & 1 deletion pyrit/scenario/scenarios/airt/rapid_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _build_rapid_response_strategy() -> type[ScenarioStrategy]:
Returns:
type[ScenarioStrategy]: The dynamically generated strategy enum class.
"""
from pyrit.registry.object_registries.attack_technique_registry import AttackTechniqueRegistry
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
from pyrit.registry.tag_query import TagQuery

registry = AttackTechniqueRegistry.get_registry_singleton()
Expand Down
2 changes: 1 addition & 1 deletion pyrit/setup/initializers/components/scenario_techniques.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
TreeOfAttacksWithPruningAttack,
)
from pyrit.models import SeedPrompt
from pyrit.registry.object_registries.attack_technique_registry import (
from pyrit.registry.components.attack_technique_registry import (
AttackTechniqueRegistry,
)
from pyrit.scenario.core.attack_technique_factory import AttackTechniqueFactory
Expand Down
Loading
Loading