Repository navigation
FEAT: Rebuild saved registry entries in dependency order (part 3) - #3908
varunj-msft wants to merge 91 commits into
Conversation
…API input External construction now rejects a parameter whose type includes Path unless the calling service passes the name in allowed_path_params after making the value safe. The converter service allows its upload parameters once they are stored, and the target service allows the server-set upload folder and the GitHubCopilotTarget working directory. The scorer-only check is removed, and the "Valid parameters" list leaves out path parameters callers cannot set.
The target catalog now applies the path rule to required parameters too, so a target that needs a disallowed path is left out instead of listed without the field, matching the scorer catalog. allowed_path_params is typed frozenset[str] so a bare string cannot turn the allow-list into a substring match.
The target catalog still lists a target whose required path parameter is its server-set upload folder when the server has one, and leaves it out otherwise.
# Conflicts: # tests/unit/backend/test_converter_service.py
… 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.
Restore still reads, checks, and orders the saved entries, but no longer builds them itself: restore_async takes the builder, so the backend can build each entry through the API's own create. The copies of the technique build, the reserved technique names, and the upload-folder refusal are gone. A name another entry holds is a name clash; a reserved name, or a technique name a scenario can't use, is an invalid recipe. Each failure message is escaped and shortened where it's built, and the tests use small test classes and a builder that raises instead of OpenAI's error text.
…ction - restore_async reads storage in a worker thread when it isn't given what read() returned, so a slow store doesn't hold the event loop. - Each restore failure is logged as an error: the saved entry is lost, and a caller that fails closed won't become ready. - redact_urls stays linear in the text's length. A long run of scheme characters such as "a.a.a..." made it quadratic (21 s for 160 KB). - redact_urls treats everything before the last "@" as the user name and password, so an unescaped password holding "/", "?" or "#", or a URL right after "_", no longer leaks. - Tests: the planning-bug and unprintable-error cases go through public paths, a duplicate technique-admission test is removed, and reading without building is checked by its result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Treating everything before the last "@" as a user name and password rewrote a URL such as https://graph.example/users/alex@contoso.com/mail into one that names the wrong host. Operators need the real URL (microsoft#4175: don't redact what operators need), and the case it covered, an unescaped "/", "?" or "#" in a password, is a malformed URL. The linear-time match, which also finds a URL right after "_", stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Richard Lundeen (richlundeen)
left a comment
There was a problem hiding this comment.
Approving. To finish this without another review round, I pushed two small commits:
f59306e:- When
restore_asyncisn't givensaved, it reads storage in a worker thread (the open P2). - Each restore failure is logged as an error.
redact_urlsnow takes time in proportion to the text's length. Before, text like"a." * 80_000took 21 s, and a long value in a saved file can reach it through a build error.- It also finds a URL right after
_, soendpoint_https://user:secret@hostno longer leaks. - Tests: two cases now go through public paths, and a duplicate technique-admission test and an assertion that couldn't fail are gone.
- When
f1ff143: keeps an@in a URL's path. The first commit had treated everything before it as a user name and password, which rewrote such URLs to name the wrong host (#4175: don't redact what operators need).
I'm withdrawing the main point of my Nov 9 review. When a saved entry can't be restored, the backend should not become ready. #4175 now states this as "Fail loudly rather than run partially." This PR already leaves that decision to its caller and reports every failure in one pass, so nothing changes here. The change is in #3909.
Non-blocking, for a later part: the server-path check here duplicates the registry's own check on API input, and it is stricter for converters: it refuses the data-URI and blob-URL uploads that the converter API accepts. When converter saving lands (#4051), it should use the same check as restore so the two can't disagree.
When you merge this into #3909, registry_recipe_restore.py may conflict with #3909's held-name code. Failing closed removes that code anyway.
# 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
…y the other storage tests do
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.
Description
This is one of the changes for saving Registry entries across restarts. It builds on #3892. Until #3891 and #3892 merge, the diff here also shows their changes; this PR's own code is
pyrit/registry/registry_recipe_restore.pyandRegistryRestoreReport, with their tests.Problem. Nothing turns saved recipes back into registry entries.
Change.
RegistryRecipeRestorer:read()reads every folder without building anything, so a caller on an event loop can run it in a worker thread.restore_async(build_async=...)then works out the order and builds; called without whatread()returned, it reads in a worker thread itself:Parameter.referenceand the technique fields. A name resolves to a saved entry first (even a broken one), then to a startup entry. Missing and failed dependencies and loops are reported with the entry each one waits on.name_clash; a reserved name, or a technique name a scenario can't use, isinvalid_recipe; a request the create refuses keeps the create's reason; anything else isbuild_error. Taken names raiseRegistryNameTakenErrorand technique admission problems raiseTechniqueAdmissionError, so nothing parses error messages.RegistryRestoreReport: restored entries, failures (kind, name, reason, message, version, dependency) and ignored files.Not in this PR.
Tests and Documentation
tests/unit/registry/test_registry_recipe_restore.py(39 tests), building through a small test builder: order across the four kinds (including converters in either of a technique's lists), loops, missing and failed dependencies, clashes with startup entries,read()building nothing andrestore_asyncusing what it returned without reading again, or reading in a worker thread and building on the calling one when it wasn't given it, a startup entry found by its exact name, stored credentials, server paths, a class found by name after it moved within PyRIT, a real module undertmp_paththat restore never imports, unknown classes and attack types, technique recipes matching the create request, the reason each kind of failure gets, one build error that doesn't stop the rest, failure text (URL credentials removed, control characters escaped, long text capped), and logging each failure once without URL credentials.tests/unit/common/test_azure_storage.py: URL credentials are removed from any text.pytest tests/unit/registry tests/unit/models tests/unit/common -n 6: 4,663 passed, 5 skipped, 4 xfailed