-
Notifications
You must be signed in to change notification settings - Fork 806
FEAT Plug-in mechanism for private scenarios #2131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ValbuenaVC
wants to merge
49
commits into
microsoft:main
Choose a base branch
from
ValbuenaVC:vvalbuena-microsoft-plugin-mechanism
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
9f409ab
feat(setup): add plug-in loading to initialize_pyrit_async
c7a7528
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC e7d63cf
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC eca5928
Potential fix for pull request finding
ValbuenaVC 0b11b6c
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC 8781038
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC e6159e5
test(setup): verify plug-in scenarios are discovered end-to-end
f8e761d
fix(setup): restore overwritten registry entries on plug-in rollback
ed3fc0d
refactor(setup): extract plug-in wheels with safe_extract_zip
fb822e0
perf(setup): run blocking plug-in extraction off the event loop
95de68c
feat(setup): add granular PluginLoadError subclasses
c108578
fix(setup): use a unique temp dir for plug-in wheel extraction
b551008
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC c028f8b
refactor(setup): rename plugin_fail_open to plugin_accept_load_failures
67f8a85
Merge commit 'b55100804be2f4fae26d619cbd5b072e104a94ce' into vvalbuen…
067ffba
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC 2ae6c55
feat(setup): configure plug-ins in .pyrit_conf with multi-plug-in sup…
b21f856
Merge commit '067ffba6461c6e89590a3d369a3d5bcc408508a3' into vvalbuen…
21ebf36
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC 466679a
Merge commit '21ebf36815e93891ef55221912fee2345bd311c9' into vvalbuen…
88bd76a
feat(registry): auto-register external plug-in Scenario subclasses
b177f49
feat(setup): tolerate plug-in version drift and re-extract stale wheels
1538e6e
test(setup): cover plug-in auto-registration, discovery, instantiate,…
bbd3da8
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC 9b440cd
refactor(exceptions): centralize plug-in load errors
87f1156
feat(exceptions): define source plug-in failure stages
97e5976
feat(setup): normalize source and wheel plug-in specs
d092a10
refactor(setup): activate plug-ins through privileged initializer
38f62fa
refactor(setup): isolate wheel plug-in preparation
6010b12
feat(setup): prepare source-file plug-ins
c5d0507
feat(setup): prepare source-package plug-ins
8ead0c5
feat(setup): discover source plug-in scenarios
9f0c0c8
feat(setup): discover plug-in attack techniques
153b2c0
feat(setup): register plug-in components transactionally
f94cea0
feat(scenario): expose applicable plug-in techniques
11e40cb
test(setup): exercise source plug-ins through scanner
ddf27ac
docs(setup): explain private scenario plug-in usage
34daddf
refactor(setup): validate plug-in scenarios during activation
d8377e7
test(setup): make mock plug-in scenario self-contained
7bcb8d3
Merge origin/main into plug-in mechanism
f7ea23d
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC 26267c7
refactor(scenario): expose all registered RapidResponse techniques
3fbdd04
refactor(registry): drop plug-in contributed and external-subclass APIs
a9764ac
refactor(setup): point plug-ins at a PyRITInitializer, drop discovery…
d288506
docs: retarget plug-in config and docs to the source-initializer model
19bdbf9
test(setup): rewrite plug-in tests around the initializer path
7bc560b
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC 5b3e996
docs: address review comments; drop version and internal-repo references
a7800f3
Merge branch 'main' into vvalbuena-microsoft-plugin-mechanism
ValbuenaVC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| # Private Scenarios and Attack Techniques | ||
|
|
||
| PyRIT plug-ins let an operator activate private scenarios and attack techniques from a | ||
| **stock PyRIT installation** — without forking PyRIT or passing per-run CLI flags. | ||
|
|
||
| ## Plug-in or `--initialization-scripts`? | ||
|
|
||
| A plug-in is a thin layer over the existing initializer path: | ||
|
|
||
| - `--initialization-scripts ./my_init.py` runs a custom `PyRITInitializer` from a loose | ||
| file. It works for a self-contained script but is per-run and breaks for a packaged | ||
| initializer that imports from its own package. | ||
| - A `plugins:` entry in `.pyrit_conf` names a private initializer once, runs it | ||
| **first** (before other initializers and before catalog warming), and anchors the | ||
| package root on `sys.path` so a packaged initializer with intra-package imports loads | ||
| correctly. | ||
|
|
||
| Use a plug-in when a private, packaged initializer (for example a team's internal | ||
| red-teaming package) needs to behave like a built-in inside the standard backend, GUI, | ||
| and `pyrit_scan` catalog. | ||
|
|
||
| ## What a plug-in is | ||
|
|
||
| A plug-in is a config entry that points at a concrete `PyRITInitializer` reachable by a | ||
| dotted path from a source root: | ||
|
|
||
| ```yaml | ||
| # .pyrit_conf | ||
| plugins: | ||
| - name: my_redteam | ||
| source: /repos/my-redteam | ||
| initializer: my_redteam.setup.MyInitializer | ||
| ``` | ||
| - `name` — an operator label used in logs and errors. | ||
| - `source` — the folder placed on `sys.path` so your package can be imported (see below). | ||
| - `initializer` — a dotted `module.Class` path to a concrete `PyRITInitializer`. | ||
|
|
||
| ### What `source` should point at (and why it matters) | ||
|
|
||
| `source` should be the folder that **contains** your package — the directory you would be | ||
| sitting in for `import my_redteam` to succeed at a Python prompt. PyRIT adds that folder | ||
| to Python's import search path (`sys.path`) before importing your initializer. | ||
|
|
||
| You need this because a real private package doesn't live next to PyRIT; its modules | ||
| import from *each other* (for example `from my_redteam.datasets import load`). If Python | ||
| can't find the package root, those imports fail and the plug-in won't load. In plain | ||
| terms: point `source` at the folder above your package, not at the package folder itself | ||
| and not at a single file buried inside it. If you point at the wrong place, loading fails | ||
| closed with an import error naming what could not be found. | ||
|
|
||
| `ConfigurationLoader` runs the plug-in as a privileged initializer, always **first** — | ||
| before your other initializers and before anything reads the scenario/technique catalog. | ||
| You do not (and cannot) add it to `initializers:`: it isn't a registered initializer name, | ||
| so listing it there fails with an "initializer not found" error. The framework constructs | ||
| and runs it for you. | ||
|
|
||
| ## The initializer owns registration | ||
|
|
||
| PyRIT discovers nothing on its own. The plug-in's initializer registers everything it | ||
| wants discoverable, at whatever level of abstraction fits: | ||
|
|
||
| - **Attack techniques** — | ||
| `AttackTechniqueRegistry.get_registry_singleton().register_from_factories([...])`; | ||
| selectable via `--techniques`. | ||
| - **Scenarios** — | ||
| `ScenarioRegistry.get_registry_singleton().register_class(MyScenario, name="my_redteam.violence")`; | ||
| runnable via `pyrit_scan my_redteam.violence`. | ||
| - **Datasets** — register providers and load them into memory so private seeds stay in | ||
| the operator's database and are never published. | ||
| - **Default targets** — `set_default_value(...)`. | ||
|
|
||
| ### Deciding what to keep private | ||
|
|
||
| "Private" is rarely all-or-nothing. If you build a custom scenario or technique, you have | ||
| to decide whether to contribute it publicly, keep it in your own tracked repo, or keep it | ||
| fully private — and often only *part* of it is sensitive. A plug-in lets you keep exactly | ||
| the sensitive layer private while everything else stays public and works out of the box: | ||
|
|
||
| - sometimes only the **dataset** (the prompts/objectives) is sensitive; | ||
| - sometimes it's a niche **technique**; | ||
| - sometimes an entire **scenario** should not be exposed at all. | ||
|
|
||
| Because the initializer registers each level independently, you can publish the generic | ||
| parts and register only the sensitive parts from your private package. | ||
|
|
||
| ### Example initializer | ||
|
|
||
| ```python | ||
| from pyrit.executor.attack import PromptSendingAttack | ||
| from pyrit.registry import AttackTechniqueRegistry, ScenarioRegistry | ||
| from pyrit.scenario.core import AttackTechniqueFactory | ||
| from pyrit.setup.pyrit_initializer import PyRITInitializer | ||
| from my_redteam.scenarios import Violence | ||
| class MyInitializer(PyRITInitializer): | ||
| """Register a private technique and a private scenario.""" | ||
| async def initialize_async(self) -> None: | ||
| AttackTechniqueRegistry.get_registry_singleton().register_from_factories( | ||
| [AttackTechniqueFactory(name="operation_foobar", attack_class=PromptSendingAttack)] | ||
| ) | ||
| ScenarioRegistry.get_registry_singleton().register_class(Violence, name="my_redteam.violence") | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```powershell | ||
| # A private technique through a public scenario | ||
| pyrit_scan airt.rapid_response --target openai_chat --techniques operation_foobar | ||
| # A private scenario | ||
| pyrit_scan my_redteam.violence --target openai_chat | ||
| ``` | ||
|
|
||
| ## Behavior and limits | ||
|
|
||
| - The plug-in initializer runs **first**, so lazy catalog/metadata consumers see a | ||
| complete registry. | ||
| - Loading executes third-party Python with backend permissions; whoever can write the | ||
| config or the source can run code on the host. Treat the config as sensitive. | ||
| - Dependencies must already be installed in the backend environment. | ||
| - The plug-in path is **fail-closed** and supports **one** plug-in. A failed load aborts | ||
| initialization — fix the config or source and restart. | ||
| - Plug-ins activate only at process/backend startup. Restart after changing the config | ||
| or the source; there is no hot reload. | ||
|
|
||
| See [Plug-In Troubleshooting](./troubleshooting/plugins.md) for common failures. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Plug-In Troubleshooting | ||
|
|
||
| ## The plug-in did not activate | ||
|
|
||
| Restart the backend after changing `.pyrit_conf` or the source: | ||
|
|
||
| ```powershell | ||
| pyrit_scan --stop-server | ||
| pyrit_scan --start-server --config-file /path/to/.pyrit_conf | ||
| ``` | ||
|
|
||
| `pyrit_scan` is a thin client. An already-running backend keeps the plug-in loaded from | ||
| its startup config. | ||
|
|
||
| ## Configuration is rejected | ||
|
|
||
| A plug-in is one entry with three fields: | ||
|
|
||
| ```yaml | ||
| plugins: | ||
| - name: my_redteam | ||
| source: /repos/my-redteam | ||
| initializer: my_redteam.setup.MyInitializer | ||
| ``` | ||
| Check that: | ||
| - `name` is a valid lowercase snake_case registry name; | ||
| - `source` is present (relative paths resolve against the config file); | ||
| - `initializer` is a dotted `module.Class` path; | ||
| - only one plug-in is configured. | ||
|
|
||
| ## Source path does not exist | ||
|
|
||
| The loader fails closed with a "source path does not exist" error. Point `source` at the | ||
| directory that contains your package (the parent of the top-level package) so | ||
| `import <your_package>` resolves once that directory is on `sys.path`. | ||
|
|
||
| ## Initializer cannot be imported | ||
|
|
||
| - Confirm `initializer` names a real `module.Class` importable from `source`. | ||
| - Install the plug-in's dependencies into the backend Python environment. | ||
| - A packaged initializer must import from its own package (for example | ||
| `from my_redteam... import ...`); pointing `source` at the package root makes that work. | ||
|
|
||
| ## Target is not a PyRITInitializer | ||
|
|
||
| `initializer` must resolve to a concrete subclass of `PyRITInitializer`. A plug-in | ||
| contributes an initializer, not loose scenario or technique objects. | ||
|
|
||
| ## Nothing shows up in the catalog | ||
|
|
||
| PyRIT does not discover components — the initializer must register them. For scanner | ||
| discovery, the initializer must call | ||
| `ScenarioRegistry.get_registry_singleton().register_class(...)` for scenarios and | ||
| `AttackTechniqueRegistry.get_registry_singleton().register_from_factories(...)` for | ||
| techniques. Datasets must be registered as providers and loaded into memory. | ||
|
|
||
| ## Partial state after a failed load | ||
|
|
||
| Plug-in loading is fail-closed. A failed load aborts initialization; fix the reported | ||
| stage and restart the process. Continuing in the same process after a failed | ||
| initialization is not supported. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stale after the pivot - wheels are gone, config only takes source + initializer. This ships to every pyrit_scan --help, so worth fixing: drop "or wheel artifacts."