Skip to content

FEAT: Read saved registry recipes (part 2) - #3892

Open
varunj-msft wants to merge 44 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Read-Registry-Recipes
Open

varunj-msft wants to merge 44 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/13178-Read-Registry-Recipes

Conversation

@varunj-msft

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

Copy link
Copy Markdown
Contributor

Description

Part 2 of saving Registry entries across restarts. Stacked on part 1 (#3891); part 3 (#3908) builds on it. This PR's own changes: varunj-msft/PyRIT@varunj-msft/13178-Unreadable-Document-Listing...varunj-msft/13178-Read-Registry-Recipes

Design: 4.3 What a recipe holds, 4.2 Where recipes live. Where every part fits: PR map.

This is one of the changes for saving Registry entries across restarts. It builds on #3891. Until #3891 merges, the diff here also shows its changes.

Problem. There's no file format or storage for a saved target, converter, scorer or technique.

Change.

  • pyrit/models/catalog/registry_recipe.py: the recipe models, exported from pyrit.models.catalog.
    • A target, converter or scorer recipe holds schema_version, class_module, class_name and params. A target recipe also holds auth_mode next to params, as the target create request does; any other kind with an auth_mode, or a recipe filed under the wrong kind, is rejected.
    • One RECIPE_SCHEMA_VERSION sets the format version for both models and the storage check.
    • A technique recipe holds the fields of the technique create request, without the name, with the same limits and tag rules. The library can't import the backend, so they're copied, and FEAT: Rebuild saved registry entries in dependency order (part 3) #3908 (part 3) adds a test that the two models accept and reject the same documents.
    • Unknown fields are rejected, so a file with an extra field such as credentials is reported, not silently read.
    • Also adds the failure and reference types.
  • pyrit/models/request_limits.py: adds MAX_TEXT_LENGTH for the technique description. FIX: Apply the request limits to request fields added since the last limits PR #3595 adds the same line, so the two merge cleanly; until it lands, backend/models/common.py keeps its own copy of the same value.
  • pyrit/registry/registry_recipe_storage.py: RegistryRecipeStorage, a FileDocumentStorage with one folder per registry (targets/, converters/, scorers/, techniques/) under one source.
    • On Blob the folder goes on the URL path, so a SAS token stays in the query.
    • list_recipes() uses the listing from the previous PR. It returns the parsed recipes with their versions (the SHA-256 of the bytes), a failure for each unreadable, invalid or newer-format file, and the ignored files.
    • The format version is checked first, so a file from a newer PyRIT says so instead of failing on a field it doesn't know.
  • Failure messages and file names in the report go through one helper that escapes control characters and caps the length, so a stored key like "\n\x1b[31mFAKE" or a 4,000-digit version can't break a log line. A recipe with text that isn't valid UTF-8 anywhere in it is rejected as invalid, and file names with invalid bytes are escaped in backslash form, so the report can always be sent as JSON. Files are read as utf-8-sig, and a document nested too deeply is reported as such.
  • The file name is the entry name, so only names that fit the document rule (up to 64 lowercase letters, digits and underscores, starting with a letter) can be saved. The PRs that save entries will refuse other names.

What stays the same. Nothing calls this yet; #3908 (part 3) rebuilds entries from these recipes. No API, config or behavior change.

Tests and Documentation

  • New tests/unit/registry/test_registry_recipe_storage.py: recipes and their versions, field limits and unknown fields, failures for unreadable, invalid and newer-format files, ignored names, Blob folders that keep a SAS token in the query, and report text that isn't valid UTF-8 (a recipe holding it is invalid, and a file name with invalid bytes is escaped).
  • The tests also pin the default format version for both models, a control-character key, a 4,000-digit version, and params nested 300 levels deep.
  • pytest tests/unit/registry tests/unit/models -n 6: 3,979 passed, 5 skipped, 4 xfailed
  • ruff, ruff format and ty on the changed files: passed (the pre-commit hooks can't install in my environment, so CI runs them)
  • No doc change: nothing calls this yet. JupyText: not run (no notebooks changed).

Copilot AI balanced review requested due to automatic review settings November 3, 2026 17:28

Copilot AI left a comment

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.

🟢 Approval recommended

No active review issues require changes to the registry recipe models or storage reader.

No open issues 🎉

What changed in this PR

Adds the models and storage reader needed to preserve registry entries across restarts. This second stacked PR builds on #3891’s document-listing support; entry rebuilding and startup restoration remain for later PRs.

Changes:

  • Defines versioned component and technique recipes, plus restore-report types with JSON-safe text.
  • Reads recipes from per-registry local or Azure Blob folders, preserving SAS queries and reporting invalid, unreadable, newer-format, and ignored files.
  • Adds unit coverage for parsing, versions, validation, storage failures, and report serialization without changing existing callers.
File Description
tests/​unit/​registry/​test_registry_recipe_storage.py Tests recipe loading, validation, versions, Blob paths, and report serialization.
tests/​unit/​registry/​test_file_document_storage.py Tests issue-aware local and Blob listings and safe error messages.
pyrit/​registry/​registry_recipe_storage.py Adds per-registry storage, recipe parsing, and failure reporting.
pyrit/​registry/​file_document_storage.py Adds issue-aware listings while retaining legacy listing behavior.
pyrit/​models/​request_limits.py Adds the shared technique-description text limit.
pyrit/​models/​catalog/​registry_recipe.py Defines recipe schemas, references, and restore-report models.
pyrit/​models/​catalog/​__init__.py Exposes the new models through lazy exports.

Review effort: Balanced

@varunj-msft varunj-msft changed the title FEAT: Read saved registry recipes FEAT: Read saved registry recipes (part 2) Nov 9, 2026

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.

Requesting changes. My review on #3908 has the main comment about the direction for the restore work; this PR stays as it is apart from the inline comments below. Inline comments marked Blocking need to be addressed before merge; Non-blocking ones are judgment calls.

Comment thread pyrit/models/catalog/registry_recipe.py Resolved
Comment thread pyrit/models/catalog/registry_recipe.py Resolved
Comment thread pyrit/models/catalog/registry_recipe.py Resolved
Comment thread pyrit/models/catalog/registry_recipe.py Resolved
Comment thread pyrit/models/catalog/registry_recipe.py Resolved
FileDocumentStorage now raises DocumentStorageUnavailableError when the
folder or container can't be used, and DocumentUnreadableError when one
document exists but can't be read. The message names the location, with
any SAS token hidden, and a short reason. One backend handler maps both to
503 and logs the location and reason, with the traceback at debug level,
so the per-route storage helpers are gone.

A single read, a conditional save and a delete now follow the listing's
rules: a folder, pipe or broken link named like a document is unreadable
rather than missing, and on Blob only a deleted blob counts as missing.
Blob sign-in, permission, network and missing-container errors stop the
listing instead of marking every file unreadable. Each unreadable report
keeps the real file name and the original error, and the listing's
documents are read-only.

Initializer delete removes a script that can't be read or decoded, and
registering one no longer turns a storage write failure into a 400.
Tests patch module-level aliases instead of os.
List recipes escapes the OS text it reports, such as a file name that
isn't UTF-8, and a recipe whose JSON holds an unpaired surrogate escape
is reported as invalid, so the models no longer need a JSON-only
serializer. Recipes may start with a byte-order mark. A document without
schema_version reads as version 1, and each file is checked against the
format version of the model that reads it. Validation summaries shorten
each location part and the whole message, since a location can repeat a
stored key.

Recipes and StoredRecipe are no longer frozen, since their params and
tags never were, and StoredRecipe checks that its recipe type matches its
registry. The listing's recipes are read-only, and it keeps the error
behind each recipe that couldn't be read, for the log.
… text

Recipes now share one format version, RECIPE_SCHEMA_VERSION, and a file
without one still reads as version 1. A saved target keeps the auth mode
its create request chose as its own field, so restore can replay that
request; only a target may have one.

Every failure message is escaped and shortened where it's built, so a
stored key with a new line or a terminal escape, or a long version number,
can't change a log or run on. A params value nested too deeply for pydantic
is reported as nested too deeply, and the technique recipe says why it
copies the create request's fields.

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.

Automated review completed. No blocking concerns remain. This review records source coverage; merging is handled separately.

# Conflicts:
#	pyrit/backend/middleware/error_handlers.py
#	pyrit/backend/routes/scenario_presets.py
#	pyrit/backend/routes/scenario_suites.py
#	pyrit/registry/file_document_storage.py
Its text can carry a SAS URL. The 503 handler no longer logs it as a debug traceback, and the scenario
suite service no longer attaches it to its warning; both log the storage message, which has no credentials.

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.

4 participants