Skip to content

Commit 893404f

Browse files
FEAT: Findings dashboard with persistent Operations in the GUI (#3066)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e4ab259 commit 893404f

156 files changed

Lines changed: 10306 additions & 1040 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.

‎.env_local_example‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ OPENAI_CHAT_MODEL="gpt-4o"
1212

1313

1414
##############
15-
# The below GLOBAL_MEMORY_LABELS will be applied to all prompts sent via attacks and can be altered whenever needed.
16-
# Example recommended labels are shown below: `operator`, `operation`. Others that may be useful include:
17-
# `language`, `harm_category`, `stage`, or `technique. For the above labels, please stick to the exact spelling,
18-
# spacing, and casing for better standardization throughout the database.
15+
# GLOBAL_MEMORY_LABELS applies labels to all prompts sent via attacks.
16+
# Labels can include `operator`, `language`, `harm_category`, `stage`, or `technique`.
17+
# Keep spelling, spacing, and casing consistent when querying labels.
18+
# Select an Operation per run with operation_id (SDK) or --operation-id (CLI).
19+
# An operation name in GLOBAL_MEMORY_LABELS is deprecated and will be removed in 1.4.0. Until then it links
20+
# runs to the saved Operation with that name when one exists, and is otherwise kept as a plain name.
1921
##############
20-
GLOBAL_MEMORY_LABELS = {"operator": "operator", "operation": "operation"}
22+
GLOBAL_MEMORY_LABELS = {"operator": "operator"}
2123

2224
##############
2325
# Set optional OPENAI_CHAT_ADDITIONAL_REQUEST_HEADERS to include additional HTTP headers in a dictionary format for API requests, e.g., {'key1': 'value1'}.

‎.pyrit_conf_example‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,19 @@ initializers:
6161
# server.startup_timeout below.
6262
# - name: load_default_datasets
6363

64-
# Operator and Operation Labels
65-
# ------------------------------
66-
# Default labels applied to all attacks created with PyRIT.
64+
# Operator and Operation Defaults
65+
# -------------------------------
66+
# GUI defaults for new attacks and scans.
6767
#
6868
# - operator: Identifies who is running the attack (e.g., your team name or alias).
69-
# - operation: Groups related attacks under a campaign or engagement name.
69+
# - operation: Proposes a saved Operation name to group new activity under. It is
70+
# never created automatically: until it is saved, the GUI highlights it as not
71+
# registered and each user chooses an existing Operation, registers it, or
72+
# clears it.
7073
#
7174
# Both are optional.
7275
operator: roakey
73-
operation: op_trash_panda
76+
# operation: my_engagement
7477

7578
# Initialization Scripts
7679
# ----------------------

‎doc/code/executor/3_attack_configuration.ipynb‎

Lines changed: 146 additions & 49 deletions
Large diffs are not rendered by default.

‎doc/code/executor/3_attack_configuration.py‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# | `objective` | What you are trying to get the **objective target** to do. Drives attack prompts and supplies the default scoring context. |
2323
# | `expectation` | A per-execution `ScoringExpectation` for outcome scoring. Its objective may differ from the attack objective. |
2424
# | `memory_labels` | A `dict[str, str]` tagged onto every prompt/response, so you can filter this run later in memory. |
25+
# | `operation_id` | ID of a saved Operation to group this attack with related activity and findings. |
2526
# | `prepended_conversation` | A list of `Message`s to seed the conversation before the attack's own turns. This is also where the objective target's **system prompt** goes — `Message.from_system_prompt(...)` builds one (see below). |
2627
# | `next_message` | The exact next message to send, instead of letting the attack derive it from the objective. Useful for multimodal or pre-built seeds. |
2728
#
@@ -80,14 +81,33 @@
8081
# ## Memory labels
8182
#
8283
# `memory_labels` tag every prompt and response this run produces. They don't change what is sent;
83-
# they make the run easy to find and group later in memory (e.g. by operation or operator).
84+
# they make the run easy to find and group later in memory (e.g. by campaign or operator).
8485
# `AtomicAttack.run_async(memory_labels=...)` merges labels with its constructor labels.
8586
# Call-time values replace only shared keys; stored defaults stay unchanged.
8687

8788
# %%
8889
result = await attack.execute_async( # type: ignore
8990
objective="Give me a recipe for a classic margarita",
90-
memory_labels={"operation": "op_trash_panda", "operator": "roakey"},
91+
memory_labels={"campaign": "example", "operator": "roakey"},
92+
)
93+
await output_attack_async(result)
94+
95+
# %% [markdown]
96+
# ## Operation
97+
#
98+
# Pass a saved Operation's ID to group an attack with related activity and findings.
99+
# Save or reuse an Operation through memory; omitting `operation_id` leaves the attack unassigned.
100+
101+
# %%
102+
from pyrit.memory import CentralMemory
103+
from pyrit.models import Operation
104+
105+
memory = CentralMemory.get_memory_instance()
106+
operation = await memory.add_operation_async(Operation(name="Example engagement")) # type: ignore
107+
108+
result = await attack.execute_async( # type: ignore
109+
objective="Explain how a saponification reaction works",
110+
operation_id=operation.id,
91111
)
92112
await output_attack_async(result)
93113

‎doc/code/memory/5_advanced_memory.ipynb‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
"## Part 1 — Memory Labels\n",
2222
"\n",
2323
"Memory labels are a free-form dictionary for tagging prompts for easier querying and scoring later on. The `GLOBAL_MEMORY_LABELS`\n",
24-
"environment variable can be set to apply labels (e.g. `operator` and `operation`) to all prompts sent by any attack.\n",
24+
"environment variable can be set to apply labels (e.g. `operator` and `campaign`) to all prompts sent by any attack.\n",
2525
"Passed-in labels will be combined with `GLOBAL_MEMORY_LABELS` into one dictionary. In the case of collisions,\n",
2626
"the passed-in labels take precedence.\n",
2727
"\n",
28-
"You can then query the database (either AzureSQL or SQLite) for prompts with specific labels, such as `operator` and/or `operation`\n",
29-
"(which are standard), as well as any others you'd like, including `harm_category`, `language`, `technique`, etc.\n",
28+
"You can query AzureSQL or SQLite for prompts with labels such as `operator`, `campaign`, or `harm_category`.\n",
29+
"Select a saved Operation with `operation_id`; it is separate from memory labels.\n",
3030
"\n",
3131
"We take the following steps in Part 1:\n",
3232
"1. Send prompts to a text target using `PromptSendingAttack`, passing in `memory_labels` to the execution function.\n",
@@ -767,6 +767,9 @@
767767
}
768768
],
769769
"metadata": {
770+
"jupytext": {
771+
"main_language": "python"
772+
},
770773
"language_info": {
771774
"codemirror_mode": {
772775
"name": "ipython",

‎doc/code/memory/5_advanced_memory.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
# ## Part 1 — Memory Labels
2323
#
2424
# Memory labels are a free-form dictionary for tagging prompts for easier querying and scoring later on. The `GLOBAL_MEMORY_LABELS`
25-
# environment variable can be set to apply labels (e.g. `operator` and `operation`) to all prompts sent by any attack.
25+
# environment variable can be set to apply labels (e.g. `operator` and `campaign`) to all prompts sent by any attack.
2626
# Passed-in labels will be combined with `GLOBAL_MEMORY_LABELS` into one dictionary. In the case of collisions,
2727
# the passed-in labels take precedence.
2828
#
29-
# You can then query the database (either AzureSQL or SQLite) for prompts with specific labels, such as `operator` and/or `operation`
30-
# (which are standard), as well as any others you'd like, including `harm_category`, `language`, `technique`, etc.
29+
# You can query AzureSQL or SQLite for prompts with labels such as `operator`, `campaign`, or `harm_category`.
30+
# Select a saved Operation with `operation_id`; it is separate from memory labels.
3131
#
3232
# We take the following steps in Part 1:
3333
# 1. Send prompts to a text target using `PromptSendingAttack`, passing in `memory_labels` to the execution function.

‎doc/gui/0_gui.md‎

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ To deploy an isolated instance for an external team, see [Deploy a New Instance]
3737

3838
## Views
3939

40-
Use the left sidebar to open **Home**, **Technique.execute** (Chat), **History**, **Analytics**, **Scanner**,
41-
**Datasets**, or the **Registry**. **Configuration** is available to administrators.
40+
Use the left sidebar to open **Home**, **Technique.execute** (Chat), **History**, **Analytics**,
41+
**Operations**, **Scanner**, **Datasets**, or the **Registry**. **Configuration** is available to administrators.
4242
The **Theme** menu is at the bottom of the sidebar.
4343

4444
### Attack Results Analytics
@@ -136,21 +136,20 @@ CoPyRIT stores these six preferences in one account-scoped browser record:
136136
| --- | --- |
137137
| Default objective target | Initial target for new chats and scanner runs |
138138
| Default adversarial target | Shared adversarial target for supported scanner scenarios |
139-
| Operation name (`operation`) | Label for new attacks and scanner runs |
139+
| Selected Operation | Groups new attacks and scanner runs by its saved ID |
140140
| Custom run labels | Additional labels for new attacks and scanner runs |
141141
| Theme | System, Light, Dark, or a named preset |
142142
| Chat display mode | Raw text or rendered Markdown |
143143

144-
The GUI uses `operation` and `operator` as its label names, not `op_name` and `username`.
145144
For signed-in users, `operator` is the lowercase username before `@`. It is derived
146145
from the account, cannot be edited, and is not saved as a user preference. With
147146
authentication disabled, the local profile can save an operator label.
148147

149148
All six preferences use `pyrit.userPreferences.v1.<tenant>:<homeAccountId>` in
150149
browser local storage. Authentication-disabled use has a separate `local` profile.
151150
These settings do not follow you to another browser or device. Changes to labels
152-
apply to future runs, not stored attacks. Backend label defaults still apply when
153-
there is no user override. Removing a custom default label is also saved.
151+
apply to future runs, not stored attacks. Backend configuration supplies initial
152+
defaults. Removing a custom default label is also saved.
154153
Open tabs synchronize preferences for the same account. Each edit is merged with
155154
the latest saved values. On HTTPS and localhost, browser locks also serialize
156155
concurrent saves so edits to different defaults do not overwrite each other.
@@ -450,13 +449,20 @@ Export stays available for read-only historical conversations, and is disabled w
450449
451450
#### Labels
452451

453-
The labels bar above the page content is available across the GUI, including scanner setup, Home, Chat, and History. It shows the active labels for future attacks and scans, not the attribution of a historical run you are viewing. Click the labels icon to open **Default Labels** and add, edit, or remove custom labels. The required `operator` and `operation` controls remain in the bar, outside this popover, and cannot be removed. A signed-in operator is read-only.
452+
The labels bar above the page content is available across the GUI, including scanner setup, Home, Chat, and History. It shows the active labels for future attacks and scans, not the attribution of a historical run you are viewing. Click the labels icon to open **Default Labels** and add, edit, or remove custom labels. **Operator:** is an always-visible text input; **Operation:** is an always-visible searchable dropdown outside this popover. Operator is required; an operation is optional and removable. A signed-in operator is read-only.
454453

455454
In Chat, the active target, Markdown toggle, export menu, conversations panel toggle, and **New Attack** button share the right side of this bar. They wrap below the labels on narrow screens.
456455

457-
Clicking the `operation` label opens a picker listing the operations already recorded in memory, so you can choose one without typing it from memory. Typing a name that doesn't exist yet offers to create it. Very long lists show the first 200 and say how many are left, so type to narrow them. On narrow screens, use the labels icon to view or edit labels that do not fit inline.
456+
Open the **Operation** dropdown to choose a saved operation. **New operation…** stays first, including while searching, and opens the creation dialog. Typing alone never selects an unsaved name. Very long lists show up to 200 saved choices and say how many match, so type to narrow them. The compact metadata controls wrap on narrow screens; custom labels that do not fit remain available through the labels icon.
458457

459-
Your choices persist in this browser across navigation and refreshes. Backend configuration supplies defaults for labels you have not chosen, and the signed-in account alias takes precedence over the default or remembered operator during initialization. Scanner launches receive the active labels from this bar.
458+
A configured or remembered name that isn't saved yet is highlighted in yellow,
459+
like a placeholder operator, and screen readers announce it as not registered.
460+
Choose **Register** to save it, pick a saved Operation from the **Operation**
461+
dropdown, or choose **Clear** to run without one. New attacks and scans stay
462+
blocked until the selection is resolved or cleared. Startup does not create
463+
Operations.
464+
465+
Your choices persist in this browser across navigation and refreshes. Backend configuration supplies initial defaults, and the signed-in account alias takes precedence over the default or remembered operator during initialization. Scanner launches receive the active labels and selected Operation ID from this bar.
460466

461467
Changing these labels does not relabel existing attacks or scenario runs. History attribution and the **Run configuration** shown for a scenario run still describe that saved run. Operator and target restrictions on existing attacks remain in effect.
462468

@@ -491,7 +497,7 @@ Filter attacks by:
491497
- **Outcome:** Success, failure, or undetermined
492498
- **Converter:** Which converters were applied
493499
- **Operator:** Who ran the attack
494-
- **Operation:** The operation label
500+
- **Operation:** Saved Operation
495501
- **Custom labels:** Free-form key:value label filtering with auto-complete
496502

497503
Click "Reset" to clear all filters.
@@ -506,7 +512,7 @@ The table displays:
506512
| Attack Type | The attack class name |
507513
| Target | Target type and model name |
508514
| Operator | Who ran the attack |
509-
| Operation | Operation label |
515+
| Operation | Operation name |
510516
| Msgs | Total message count |
511517
| Convs | Number of conversations |
512518
| Converters | Converter badges (truncated with tooltip) |
@@ -546,6 +552,78 @@ In active runs and saved scenario results, **Atomic attack groups** defaults to
546552

547553
Until you expand or collapse the section, its default follows the current group count as progress loads. Once you choose, the section keeps your choice during progress updates for the same run, even if the count crosses 20. Opening a different run resets to that run's count-based default.
548554

555+
### Operations and Findings
556+
557+
An operation groups related red-teaming work. Inside it you record findings:
558+
human assessments, whether or not an attack produced them.
559+
560+
Open **Operations** in the sidebar and choose **New operation**. Names are unique,
561+
ignoring case and surrounding spaces; if the name is taken, the dialog offers the
562+
existing operation. An optional description records the engagement's purpose and
563+
scope. The **Operation** dropdown in the labels bar applies the
564+
selected operation to new attacks and scanner runs, and **New operation…** at the
565+
top of that list creates one without leaving the page.
566+
567+
On an operation's page, **Add description** or **Edit description** changes the
568+
description. Saving replaces it, so if two people edit at once, the last save wins.
569+
Operations can't be renamed or deleted from the GUI.
570+
571+
#### Findings
572+
573+
Open an operation and choose **New finding**.
574+
575+
| Field | Required | Values |
576+
| --- | --- | --- |
577+
| Title | Yes | Free text |
578+
| Severity | Yes | Critical, Important, Moderate, Low, Informational, or Other (your own text). Defaults to Moderate. |
579+
| Harm-type | No | A PyRIT harm category, Other (your own text), or Not set |
580+
| Description | No | Free text |
581+
| Evidence attack ID | No | A saved attack to attach in the same save. Its ID is in the attack's address. |
582+
583+
Findings are sorted by severity in the order above, newest first within each
584+
level, 20 per page. **Edit** changes any field; the operation and creation time
585+
stay fixed. **Delete** asks for confirmation and can't be undone.
586+
587+
**All execution history** opens History filtered by the Operation's ID. The
588+
filter stays when you switch between Attacks and Scanner.
589+
590+
#### Attack evidence
591+
592+
Evidence is a saved attack. To attach one, open it in Chat, select the outcome
593+
badge to open **Attack Result Details**, and choose **Add as finding**. Choose
594+
the finding's Operation, search by title, select a finding, and choose
595+
**Attach**. The attack can come from any Operation or none.
596+
597+
**New finding** in that dialog opens the same form and saves the finding and its
598+
evidence together, so neither is saved if the attack can't be attached. Saving
599+
again after a lost response returns the finding that was already saved rather
600+
than creating a second one.
601+
602+
Evidence points to the live attack, not a copy, so later messages and scores
603+
show up too. An attack can support several findings, and attaching the same
604+
attack to a finding again reports **Already attached**.
605+
606+
On the operation page, **Evidence (n)** under a finding opens a table of its
607+
attacks. Each row shows the attack's objective, its outcome with the same badge
608+
as Chat, and the score behind it: the human score if there is one, otherwise
609+
the automated score. An attack without a score reads **Not scored**. Evidence
610+
from another Operation shows its source; unassigned activity shows **Not in an
611+
operation**. **Open attack** opens it in Chat at the conversation that holds the
612+
score, or at its main conversation, so the transcript matches the result shown.
613+
If an attack no longer exists, its entry stays, shows the attack's ID and
614+
attachment time, and reads **Evidence unavailable**. **Remove link** removes only
615+
the link, never the attack. Deleting a finding removes its links the same way.
616+
617+
#### Upgrading an existing database
618+
619+
One migration adds Operations, findings, evidence, and optional Operation IDs on
620+
attacks and scanner runs. It registers legacy names without changing historical
621+
attribution. Legacy activity remains in general history but does not appear in
622+
Operation-ID filters. The backend applies it automatically when it starts, so
623+
restart a running backend after upgrading. If you start memory with
624+
`skip_schema_migration=True`, run the migration yourself. Downgrading refuses to
625+
drop the tables while any operation exists.
626+
549627
### Technique Registry
550628

551629
Open **Registry > Techniques**, or go to `/registry/techniques`. Targets and

‎doc/scanner/1_pyrit_scan.ipynb‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,18 @@
907907
"id": "6",
908908
"metadata": {},
909909
"source": [
910+
"Use `list-operations` to find saved Operation names and IDs:\n",
911+
"\n",
912+
"```shell\n",
913+
"pyrit_scan list-operations\n",
914+
"pyrit_scan run garak.encoding --target my_target --operation-id <UUID>\n",
915+
"```\n",
916+
"\n",
917+
"Use an ID returned by `list-operations`. Omitting `--operation-id` leaves the run\n",
918+
"unassigned. `operation` in `--memory-labels` is deprecated and will be removed in 1.4.0.\n",
919+
"Until then it links the run to the saved Operation with that name when one exists,\n",
920+
"and is otherwise kept as a plain name.\n",
921+
"\n",
910922
"**Tip**: You can also surface user-defined scenarios. List your initializer script in the\n",
911923
"`initialization_scripts` section of the config file the backend loads (see [here](../getting_started/pyrit_conf.md)).\n",
912924
"The backend runs those scripts at startup and auto-discovers any `Scenario` subclasses they\n",
@@ -1189,6 +1201,7 @@
11891201
"- `--max-concurrency <int>`: Maximum number of concurrent attack executions\n",
11901202
"- `--max-retries <int>`: Maximum number of automatic retries if the scenario raises an exception\n",
11911203
"- `--memory-labels <json>`: Additional labels to apply to all attack runs (must be a JSON string with string keys and values)\n",
1204+
"- `--operation-id <UUID>`: Saved Operation ID for this run\n",
11921205
"\n",
11931206
"Dataset-backed scenarios can also select a supported dataset. Requested datasets are fetched\n",
11941207
"on demand, so a full dataset preload is not required:\n",
@@ -1425,6 +1438,9 @@
14251438
}
14261439
],
14271440
"metadata": {
1441+
"jupytext": {
1442+
"main_language": "python"
1443+
},
14281444
"language_info": {
14291445
"codemirror_mode": {
14301446
"name": "ipython",

0 commit comments

Comments
 (0)