Skip to content

FEAT: Save techniques (part 14) - #4066

Open
varunj-msft wants to merge 389 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Save-Techniques
Open

varunj-msft wants to merge 389 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Save-Techniques

Conversation

@varunj-msft

@varunj-msft varunj-msft commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

Part 14 of saving Registry entries across restarts. Stacked on part 13 (#4052); part 15 (#4067) builds on it. This PR's own changes: varunj-msft/PyRIT@varunj-msft/13178-Save-Converters-From-Registry...varunj-msft/13178-Save-Techniques

Design: 4.1 What gets saved, 4.3 What a recipe holds. Where every part fits: PR map.

This is one of the changes for saving Registry entries across restarts. It builds on #4052, which saves converters from the Registry. Until the earlier PRs merge, the diff here also shows their changes.

A technique made with New technique, or through POST /api/techniques, was gone after the backend restarted. It now saves by default and comes back at every start, like saved targets and converters.

  • POST /api/techniques takes "persist": true. A saved technique's name follows the saved-name rule (lowercase letters, digits and underscores, starting with a letter, up to 64 characters), or the request gets a 422. Its recipe is the create request without the name, plus who saved it.
  • A technique that uses a Temporary converter or adversarial target can't be saved, since those are gone after a restart (typed 400, /errors/not-saveable). Its attack type and attack parameters get the checks restore runs, so a type that can't be saved, a credential, or a Temporary entry named in the parameters is refused the same way. A name another technique has, or a tag that matches a technique's name or another spelling of a tag in use, is a 409 before anything is written, and storage that can't be written is a 503.
  • Restore rebuilds a saved technique with the call the API uses, after the converters and targets it names, and keeps who saved it out of the attack's settings. Techniques the startup scripts register are marked From configuration.
  • A scan's launch script that registers a technique under a saved technique's name or one of its tags stops the scan, even when the script catches the refusal, so the saved technique can't run in its place.
  • GET /api/techniques gives each technique an origin: saved, configuration or temporary.
  • The technique editor saves by default. When a converter or adversarial target it uses is Temporary, or the backend refuses those exact settings, the button reads Create as Temporary and a warning says why. A taken name shows on the name field.
  • The Techniques table labels each technique Saved, From configuration or Temporary. The backend README and the GUI doc cover saving techniques.

What stays the same: a technique created without persist is Temporary, as before. Settings changed for one run in the Scanner (POST /api/techniques/{name}/edit) are never saved. Startup techniques work as before; they're only labeled From configuration.

Screenshots: New technique saves by default, the saved technique in the Techniques table, and a technique that uses a Temporary converter offered as Temporary with the reason.
techniques_1_new_technique_saves_by_default
techniques_2_saved_technique_row
techniques_3_temporary_converter_offers_temporary

Not covered:

# Conflicts:
#	pyrit/backend/services/runtime_lifecycle.py
#	tests/unit/backend/test_runtime_lifecycle.py
# Conflicts:
#	pyrit/setup/initializers/tools.py
#	tests/unit/backend/test_scenario_run_service.py
#	tests/unit/backend/test_scenario_suite_execution_service.py
#	tests/unit/registry/test_attack_technique_registry.py
#	tests/unit/registry/test_instance_registry.py
#	tests/unit/registry/test_registry_recipe_restore.py
create_named_instance checks the name before it builds, and the technique
registry checks names case-insensitively before it registers. Both raised the
duplicate-name error without recording the clash, so a launch script that
caught either one could still leave a saved entry in place of its own. The name
check now records the clash itself, as register already did, and the technique
check records it too.
# Conflicts:
#	tests/unit/registry/test_registry_recipe_restore.py
# Conflicts:
#	pyrit/backend/mappers/target_mappers.py
#	pyrit/backend/models/targets.py
#	pyrit/backend/routes/targets.py
#	pyrit/backend/services/target_service.py
#	tests/unit/backend/test_api_routes.py
#	tests/unit/backend/test_mappers.py
#	tests/unit/backend/test_runtime_lifecycle.py
#	tests/unit/backend/test_target_service.py
#	tests/unit/registry/test_instance_registry.py
# Conflicts:
#	frontend/src/components/Config/CreateTargetDialog.tsx
#	pyrit/backend/routes/targets.py
#	pyrit/backend/services/target_service.py
#	tests/unit/backend/test_target_service.py
#	tests/unit/registry/test_registry_recipe_rules.py
# Conflicts:
#	pyrit/backend/mappers/target_mappers.py
#	pyrit/backend/routes/targets.py
#	tests/unit/backend/test_api_routes.py
#	tests/unit/backend/test_mappers.py
#	tests/unit/registry/test_registry_recipe_restore.py
# Conflicts:
#	tests/unit/registry/test_registry_recipe_rules.py
# Conflicts:
#	pyrit/backend/mappers/target_mappers.py
#	pyrit/backend/models/scorers.py
#	pyrit/backend/routes/converters.py
#	pyrit/backend/routes/targets.py
#	pyrit/backend/services/converter_service.py
#	pyrit/backend/services/scorer_service.py
#	pyrit/backend/services/target_service.py
#	pyrit/registry/instance_registry.py
#	tests/unit/backend/test_api_routes.py
#	tests/unit/backend/test_converter_service.py
#	tests/unit/backend/test_mappers.py
#	tests/unit/backend/test_runtime_lifecycle.py
#	tests/unit/backend/test_scorer_service.py
#	tests/unit/backend/test_target_service.py
#	tests/unit/registry/test_instance_registry.py
main now declares that TextTarget takes no credentials, with an empty
supported_auth_modes. The saved-entry check read that as supporting no mode at
all, so it refused saved TextTargets and left the saveability check for the
target types with no mode to report. A class that declares no auth modes is now
checked as the default api_key mode, which it has no key for.
# Conflicts:
#	pyrit/registry/registry_recipe_rules.py
A reference left to a configured default can be a list, such as a composite
scorer's scorers. Each entry in the list is now named in the saved recipe, in
order, and the list is refused if any of them is Temporary or not registered,
instead of the whole list being refused as one unregistered entry.
# Conflicts:
#	frontend/src/components/Registry/CreateConverterDialog.tsx
# Conflicts:
#	frontend/src/components/Config/CreateTargetDialog.tsx
A suite launch runs its launch scripts during preflight and sends its scans without them, so the clash is
raised there. Map it to 409 with its message, as a single launch does, and drop the per-entry message that
a clash can no longer reach.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants