Skip to content

Commit 82fb4bf

Browse files
authored
Merge branch 'main' into users/evekazarian/split-payload-attack-strategy
2 parents d795352 + 1f962ae commit 82fb4bf

369 files changed

Lines changed: 14720 additions & 5553 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
applyTo: "pyrit/output/**"
3+
---
4+
5+
# PyRIT Output Module — Coding & Review Guidelines
6+
7+
For full architecture documentation, usage examples, and extension guides, see [doc/code/output/0_output.py](../../../doc/code/output/0_output.py).
8+
9+
This file covers the rules for **writing and reviewing** code in `pyrit/output/`.
10+
11+
## Critical Rules
12+
13+
### Output goes through the sink — never call `print()` directly
14+
15+
All rendering methods return `str`. The inherited `write_async` calls `render_async` then `_write_async(content)`. No bare `print()` calls anywhere in the output module except inside `StdoutSink`.
16+
17+
When reviewing: reject any `print()` call outside `StdoutSink`.
18+
19+
### Data fetching belongs in leaf classes only
20+
21+
Format classes (`PrettyAttackResultPrinter`, `MarkdownAttackResultPrinter`) must not import or reference `CentralMemory`. Only `*MemoryPrinter` leaf classes do data I/O.
22+
23+
When reviewing: reject any `CentralMemory` import in a non-leaf file (`pretty.py`, `markdown.py`, `json.py`).
24+
25+
### Sinks must use async I/O
26+
27+
Sink implementations must not block the event loop. Use `asyncio.to_thread()` or native async libraries for I/O operations. `FileSink` uses an `asyncio.Lock` to prevent concurrent write races.
28+
29+
When reviewing: reject synchronous `open()`, `write()`, or network calls inside a sink's `write_async`.
30+
31+
## Three-Layer Hierarchy
32+
33+
Every domain follows this structure. Do not mix responsibilities across layers.
34+
35+
| Layer | File | Responsibility | May import CentralMemory? |
36+
|-------|------|---------------|---------------------------|
37+
| **Base** | `base.py` | Abstract data-fetching methods + abstract `render_async` | No |
38+
| **Format** | `pretty.py`, `markdown.py`, `json.py` | Implements `render_async`, returns `str` | No |
39+
| **Leaf** | Same file as format (e.g., `PrettyAttackResultMemoryPrinter`) | Implements data methods via CentralMemory; forwarding `render_async` | Yes |
40+
41+
### File names = output format
42+
43+
- `pretty.py` — ANSI-colored human-readable
44+
- `markdown.py` — Markdown
45+
- `json.py` — structured JSON
46+
47+
### Memory leaf classes must work with zero args
48+
49+
```python
50+
printer = PrettyAttackResultMemoryPrinter() # defaults: StdoutSink, matching sub-printers
51+
await printer.write_async(result)
52+
```
53+
54+
Pass `sink=` to redirect output. Pass sub-printers only to override defaults.
55+
56+
### Convenience functions live in `helpers.py`
57+
58+
Every new domain printer **must** have a corresponding convenience function added to `helpers.py`. This is the primary entry point most callers use.
59+
60+
```python
61+
from pyrit.output.helpers import output_attack_async
62+
await output_attack_async(result, format="pretty")
63+
```
64+
65+
`helpers.py` resolves `format` → printer class, `sink` → Sink, and calls `write_async`.
66+
67+
When reviewing: if a new domain printer is added without a helper function, request one.

.github/instructions/scenarios.instructions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ Scenarios orchestrate multi-attack security testing campaigns. Each scenario gro
1111
All scenarios inherit from `Scenario` (ABC) and must:
1212

1313
1. **Define `VERSION`** as a class constant (increment on breaking changes)
14-
2. **Optionally declare `BASELINE_POLICY`** (defaults to `BaselinePolicy.Enabled` — a baseline `PromptSendingAttack` is prepended and callers can opt out per run via `initialize_async(include_baseline=False)`):
15-
- `BaselinePolicy.Disabled` — baseline supported but off by default (e.g. `Jailbreak`, where templates dominate the run).
16-
- `BaselinePolicy.Forbidden` — baseline is meaningless for this scenario's comparison axis (e.g. `AdversarialBenchmark`, which compares against gold-standard answers). Explicit `include_baseline=True` raises `ValueError`.
14+
2. **Optionally declare `BASELINE_ATTACK_POLICY`** (defaults to `BaselineAttackPolicy.Enabled` — a baseline `PromptSendingAttack` is prepended and callers can opt out per run via `initialize_async(include_baseline=False)`):
15+
- `BaselineAttackPolicy.Disabled` — baseline supported but off by default (e.g. `Jailbreak`, where templates dominate the run).
16+
- `BaselineAttackPolicy.Forbidden` — baseline is meaningless for this scenario's comparison axis (e.g. `AdversarialBenchmark`, which compares against gold-standard answers). Explicit `include_baseline=True` raises `ValueError`.
1717
3. **Implement three abstract methods:**
1818

1919
```python
2020
class MyScenario(Scenario):
2121
VERSION: int = 1
22-
BASELINE_POLICY: ClassVar[BaselinePolicy] = BaselinePolicy.Enabled
22+
BASELINE_ATTACK_POLICY: ClassVar[BaselineAttackPolicy] = BaselineAttackPolicy.Enabled
2323

2424
@classmethod
2525
def get_strategy_class(cls) -> type[ScenarioStrategy]:

.github/instructions/style-guide.instructions.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,26 @@ def process_items(self, *, items: list[str]) -> list[str]:
293293
return []
294294
```
295295

296+
## Deprecations
297+
298+
When deprecating a public class, function, method, parameter, or module path, use `pyrit.common.deprecation.print_deprecation_message` — never `warnings.warn` directly. It wraps `warnings.warn(..., DeprecationWarning, stacklevel=3)` with a consistent format so filtering still works.
299+
300+
Set `removed_in` to **current version + 2 minor versions** (e.g. `0.14.x``removed_in="0.16.0"`). This gives one full release cycle of warning before removal.
301+
302+
```python
303+
from pyrit.common.deprecation import print_deprecation_message
304+
305+
def old_method(self, *, foo: str) -> None:
306+
print_deprecation_message(
307+
old_item="MyClass.old_method",
308+
new_item="MyClass.new_method",
309+
removed_in="0.16.0",
310+
)
311+
...
312+
```
313+
314+
`old_item` / `new_item` accept a class/callable (qualified name is generated) or a string.
315+
296316
## Pythonic Patterns
297317

298318
### List Comprehensions

.github/workflows/docs.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,24 @@ jobs:
5555
- name: Install PyRIT with uv
5656
run: uv sync --extra all
5757

58-
# Build the book
58+
# LaTeX toolchain needed for the PDF export (jupyter-book --pdf via xelatex).
59+
# Mirrors the ReadTheDocs build so CI catches PDF-only issues (e.g. broken
60+
# image paths) instead of letting them silently break the RTD build.
61+
- name: Install LaTeX (for PDF export)
62+
run: |
63+
sudo apt-get update
64+
sudo apt-get install -y --no-install-recommends \
65+
texlive-xetex \
66+
texlive-fonts-recommended \
67+
texlive-fonts-extra \
68+
texlive-plain-generic \
69+
texlive-latex-extra \
70+
latexmk
71+
72+
# Build the book (HTML site + PDF export)
5973
- name: Build the book
6074
run: |
61-
make docs-build
75+
make docs-build-all
6276
# Upload the book's HTML as an artifact
6377
- name: Upload artifact
6478
uses: actions/upload-pages-artifact@v3

.pyrit_conf_example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ operation: op_trash_panda
117117
# Applies only to the pyrit_backend server.
118118
max_concurrent_scenario_runs: 3
119119

120+
# Custom Initializer Registration (REST API)
121+
# -------------------------------------------
122+
# When true, the REST API accepts POST /api/initializers to register custom
123+
# initializer scripts and DELETE /api/initializers/{name} to remove custom
124+
# initializers.
125+
#
126+
# ⚠️ WARNING: Enabling this allows arbitrary Python code execution on the
127+
# server via the REST API. Only enable on trusted networks.
128+
# The pyrit_backend default host is localhost, which limits exposure.
129+
# If you bind to 0.0.0.0, ensure you are on a trusted network.
130+
#
131+
# Default: false
132+
allow_custom_initializers: false
133+
120134
# Silent Mode
121135
# -----------
122136
# If true, suppresses print statements during initialization.

Makefile

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,25 @@ ty:
2020
# Build the full documentation site:
2121
# 1. Generate API reference JSON from Python source (griffe)
2222
# 2. Convert API JSON to MyST markdown pages
23-
# 3. Build the Jupyter Book site
23+
# 3. Build the Jupyter Book site (HTML only — fast, no LaTeX needed)
2424
# 4. Generate RSS feed
2525
docs-build:
2626
uv run python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json
2727
uv run python build_scripts/gen_api_md.py
2828
cd doc && uv run jupyter-book build --all --html
2929
uv run ./build_scripts/generate_rss.py
3030

31+
# Build the full documentation site including the PDF export.
32+
# Mirrors the ReadTheDocs build (.readthedocs.yaml) so CI catches PDF-only issues
33+
# such as missing images that the HTML-only build silently ignores.
34+
# Requires xelatex / latexmk on PATH (texlive-xetex + texlive-fonts-recommended +
35+
# texlive-plain-generic + latexmk on Ubuntu).
36+
docs-build-all:
37+
uv run python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json
38+
uv run python build_scripts/gen_api_md.py
39+
cd doc && uv run jupyter-book build --all --html --pdf
40+
uv run ./build_scripts/generate_rss.py
41+
3142
# Regenerate only the API reference pages (without building the full site)
3243
docs-api:
3344
uv run python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json

doc/bibliography.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ All academic papers, research blogs, and technical reports referenced throughout
55
:::{dropdown} Citation Keys
66
:class: hidden-citations
77

8-
[@aakanksha2024multilingual; @adversaai2023universal; @andriushchenko2024tense; @anthropic2024manyshot; @aqrawi2024singleturncrescendo; @bethany2024mathprompt; @bryan2025agentictaxonomy; @bullwinkel2024airtlessons; @bullwinkel2025repeng; @bullwinkel2026trigger; @chao2023pair; @chao2024jailbreakbench; @chu2023harmfulqa; @cui2024orbench; @darkbench2025; @derczynski2024garak; @ding2023wolf; @embracethered2024unicode; @embracethered2025sneakybits; @ghosh2025aegis; @haider2024phi3safety; @han2024medsafetybench; @hines2024spotlighting; @ji2023beavertails; @ji2024pkusaferlhf; @jiang2025sosbench; @jones2025computeruse; @kingma2014adam; @li2024flipattack; @li2024saladbench; @li2024wmdp; @lin2023toxicchat; @lopez2024pyrit; @lv2024codechameleon; @mazeika2023tdc; @mazeika2024harmbench; @mckee2024transparency; @mehrotra2023tap; @microsoft2024skeletonkey; @palaskar2025vlsu; @pfohl2024equitymedqa; @promptfoo2025ccp; @robustintelligence2024bypass; @roccia2024promptintel; @rottger2023xstest; @russinovich2024crescendo; @russinovich2025price; @scheuerman2025transphobia; @shayegani2025computeruse; @shen2023donotanything; @sheshadri2024lat; @stok2023ansi; @tang2025multilingual; @tedeschi2024alert; @vantaylor2024socialbias; @vidgen2023simplesafetytests; @vidgen2024ailuminate; @wang2023decodingtrust; @wang2023donotanswer; @wei2023jailbroken; @xie2024sorrybench; @yu2023gptfuzzer; @yuan2023cipherchat; @zeng2024persuasion; @zhang2024cbtbench; @zou2023gcg]
8+
[@aakanksha2024multilingual; @adversaai2023universal; @andriushchenko2024tense; @anthropic2024manyshot; @aqrawi2024singleturncrescendo; @bethany2024mathprompt; @bhardwaj2023harmfulqa; @bryan2025agentictaxonomy; @bullwinkel2025airtlessons; @bullwinkel2025repeng; @bullwinkel2026trigger; @chao2023pair; @chao2024jailbreakbench; @cui2024orbench; @darkbench2025; @derczynski2024garak; @ding2023wolf; @embracethered2024unicode; @embracethered2025sneakybits; @ghosh2025aegis; @haider2024phi3safety; @han2024medsafetybench; @hines2024spotlighting; @ji2023beavertails; @ji2024pkusaferlhf; @jiang2025sosbench; @jones2025computeruse; @kingma2014adam; @li2024saladbench; @li2024wmdp; @lin2023toxicchat; @liu2024flipattack; @lopez2024pyrit; @lv2024codechameleon; @mazeika2023tdc; @mazeika2024harmbench; @mckee2024transparency; @mehrotra2023tap; @microsoft2024skeletonkey; @palaskar2025vlsu; @pfohl2024equitymedqa; @promptfoo2025ccp; @robustintelligence2024bypass; @roccia2024promptintel; @rottger2023xstest; @russinovich2024crescendo; @russinovich2025price; @scheuerman2025transphobia; @shayegani2025computeruse; @shen2023donotanything; @sheshadri2024lat; @stok2023ansi; @tan2026comicjailbreak; @tang2025multilingual; @tedeschi2024alert; @vantaylor2024socialbias; @vidgen2023simplesafetytests; @vidgen2024ailuminate; @wang2023decodingtrust; @wang2023donotanswer; @wei2023jailbroken; @xie2024sorrybench; @yu2023gptfuzzer; @yuan2023cipherchat; @zeng2024persuasion; @zhang2024cbtbench; @zou2023gcg]
99

1010
:::

doc/code/auxiliary_attacks/0_auxiliary_attacks.ipynb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
"source": [
6565
"from pyrit.executor.attack import (\n",
6666
" AttackScoringConfig,\n",
67-
" ConsoleAttackResultPrinter,\n",
6867
" PromptSendingAttack,\n",
6968
")\n",
7069
"from pyrit.prompt_target import OpenAIChatTarget\n",
@@ -83,8 +82,7 @@
8382
"attack = PromptSendingAttack(objective_target=target, attack_scoring_config=scoring_config)\n",
8483
"result = await attack.execute_async(objective=objective) # type: ignore\n",
8584
"\n",
86-
"printer = ConsoleAttackResultPrinter()\n",
87-
"await printer.print_conversation_async(result=result) # type: ignore"
85+
"await output_attack_async(result)"
8886
]
8987
},
9088
{
@@ -180,7 +178,7 @@
180178
")\n",
181179
"\n",
182180
"result = await attack.execute_async(objective=objective) # type: ignore\n",
183-
"await printer.print_result_async(result=result) # type: ignore"
181+
"await output_attack_async(result)"
184182
]
185183
}
186184
],

doc/code/auxiliary_attacks/0_auxiliary_attacks.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,24 @@
88
# format_version: '1.3'
99
# jupytext_version: 1.17.3
1010
# ---
11-
1211
# %% [markdown]
1312
# # Auxiliary Attacks
14-
1513
# %% [markdown]
1614
# Auxiliary attacks cover a variety of techniques that do not fit into the core PyRIT functionality.
1715
#
1816
# These attack pipelines may be useful to run before orchestrating other attacks. For example, we provide an Azure Machine Learning (AML) pipeline for generating suffixes using the greedy coordinate gradient (GCG) [@zou2023gcg] algorithm.
19-
2017
# %% [markdown]
2118
# ## GCG Suffixes
22-
2319
# %% [markdown]
2420
# The [GCG demo notebook](1_gcg_azure_ml.ipynb) shows how to create an AML environment and submit a job that generates GCG suffixes, which can be appended to a base prompt to jailbreak a language model. In the example below, we compare the response generated by Phi-3-mini with and without a GCG suffix trained on that model.
2521
#
2622
# First, we send a harmful prompt to Phi-3-mini without a GCG suffix. If the environment variables `PHI3_MINI_ENDPOINT` and `PHI3_MINI_KEY` are not set in your .env file, the target will default to the model with `AZURE_ML_MANAGED_ENDPOINT` and `AZURE_ML_MANAGED_KEY`.
27-
2823
# %%
2924
from pyrit.executor.attack import (
3025
AttackScoringConfig,
31-
ConsoleAttackResultPrinter,
3226
PromptSendingAttack,
3327
)
28+
from pyrit.output import output_attack_async
3429
from pyrit.prompt_target import OpenAIChatTarget
3530
from pyrit.score import SelfAskRefusalScorer, TrueFalseInverterScorer
3631
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
@@ -47,8 +42,7 @@
4742
attack = PromptSendingAttack(objective_target=target, attack_scoring_config=scoring_config)
4843
result = await attack.execute_async(objective=objective) # type: ignore
4944

50-
printer = ConsoleAttackResultPrinter()
51-
await printer.print_conversation_async(result=result) # type: ignore
45+
await output_attack_async(result)
5246

5347
# %% [markdown]
5448
# Next, let's apply a GCG suffix trained on Phi-3-mini to the base prompt using the `SuffixAppendConverter`.
@@ -73,4 +67,4 @@
7367
)
7468

7569
result = await attack.execute_async(objective=objective) # type: ignore
76-
await printer.print_result_async(result=result) # type: ignore
70+
await output_attack_async(result)

doc/code/converters/0_converters.ipynb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@
279279
"source": [
280280
"from pyrit.executor.attack import (\n",
281281
" AttackConverterConfig,\n",
282-
" ConsoleAttackResultPrinter,\n",
283282
" PromptSendingAttack,\n",
284283
")\n",
285284
"from pyrit.prompt_converter import StringJoinConverter, VariationConverter\n",
@@ -306,8 +305,7 @@
306305
"\n",
307306
"result = await attack.execute_async(objective=objective) # type: ignore\n",
308307
"\n",
309-
"printer = ConsoleAttackResultPrinter()\n",
310-
"await printer.print_conversation_async(result=result) # type: ignore"
308+
"await output_attack_async(result)"
311309
]
312310
},
313311
{
@@ -370,7 +368,6 @@
370368
"source": [
371369
"from pyrit.executor.attack import (\n",
372370
" AttackConverterConfig,\n",
373-
" ConsoleAttackResultPrinter,\n",
374371
" PromptSendingAttack,\n",
375372
")\n",
376373
"from pyrit.prompt_converter import TranslationConverter\n",
@@ -407,8 +404,7 @@
407404
"result = await attack.execute_async(objective=objective) # type: ignore\n",
408405
"\n",
409406
"# Print the conversation showing both original and converted values\n",
410-
"printer = ConsoleAttackResultPrinter()\n",
411-
"await printer.print_conversation_async(result=result) # type: ignore"
407+
"await output_attack_async(result)"
412408
]
413409
}
414410
],

0 commit comments

Comments
 (0)