FEAT: Improve adversarial benchmark dataset and scoring - #2551
Open
hannahwestra25 wants to merge 42 commits into
Open
hannahwestra25 wants to merge 42 commits into
hannahwestra25 wants to merge 42 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
This reverts commit cd131ef.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Use task-achievement evaluation so benchmark objectives outside Azure Content Safety categories are scored correctly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Balance capped objective selection across harm categories and classify Azure cyber policy rejections as blocked scorer responses so benchmark attacks finish undetermined instead of erroring. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Treat blocked adversarial-chat responses as completed attack failures, including simulated-conversation preparation, while preserving unrelated error propagation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
hannahwestra25
added a commit
to hannahwestra25/PyRIT
that referenced
this pull request
Sep 16, 2026
- New build_scripts/import_adversarial_benchmark_snapshot.py upserts an externally-produced technique-metrics.json (e.g. downloaded from an Azure DevOps pipeline artifact) into the committed benchmark metrics store, reusing the same upsert_benchmark_metrics() as the local exporter. - Supports --default-objective-target/--default-objective-scorer/ --default-dataset to backfill identity fields on rows produced by an exporter version that predates them, without overwriting a field a row already has. - Ingested the 6 real technique-metrics rows from Azure DevOps build 16150 (a preview run of PR microsoft#2551's not-yet-merged dataset/pipeline updates), backfilling objective_target=openai_chat and dataset=adversarial_benchmark_v1 per the run's actual pipeline parameters; objective_scorer is <unknown> since it wasn't captured by that pipeline version. - Documented the provenance of these rows and the new ingestion path in the dashboard docs, and regenerated the benchmark leaderboard HTML fragment. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add stable objective identity fields, committed-store reuse, exporter upserts, and the rerunnable benchmark wrapper while preserving this branch's exact-result cache and balanced sampling behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Run the benchmark exporter with committed-store updates enabled and cover the pipeline command contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Match the persisted identifier shape, query both planned and inner attack hashes, and recover scorer identity for scoreless terminal refusals. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Gate committed-store updates behind an opt-in parameter and use a secret GitHub token to publish changes on an automation branch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4b6deb5-a62a-403d-9ed9-2eb369b5174d
Adds a one-line diagnostic to the 'Publish benchmark store update as a PR' step that prints only the character count of GITHUB_PAT (never the value) before the existing not-configured guard. To be reverted once diagnosed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Azure DevOps macro substitution ($(var)) rewrites a literal $(GITHUB_PAT)
reference anywhere it textually appears in a task's script body, not just
in the env: mapping. The guard's "unresolved macro" fallback compared
$GITHUB_PAT against that same literal text written directly in the
script, so once the secret was actually configured, ADO substituted the
real secret into both sides of the comparison -- making it always match
and incorrectly report "GITHUB_PAT is not configured" even when it was
correctly set (confirmed live: a genuine 93-char PAT still tripped the
guard).
Build the comparison sentinel from separate bash-only parts
(dollar='$'; unresolved_macro="${dollar}(GITHUB_PAT)") so ADO's
compile-time scanner never sees a literal $(GITHUB_PAT) substring to
rewrite in the script; it's reassembled only once bash runs. Verified
locally (bash -n, and running the extracted guard with bash) that this
now correctly falls through when GITHUB_PAT holds a real value, and
still reports "not configured" both when the variable is empty and
when its macro is genuinely left unresolved.
Also removes the temporary debug diagnostic added while investigating
this, and updates the pipeline unit test to assert the corrected guard
construction plus the absence of the vulnerable literal comparison.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: PyRIT Benchmark Bot <pyrit-benchmark-bot@users.noreply.github.com>
Remove dashboard, leaderboard-store, and PR-publication changes from the adversarial benchmark PR while retaining scenario pipeline improvements and persisted-result compatibility fixes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
hannahwestra25
marked this pull request as ready for review
September 22, 2026 15:46
Resolve conflicts in the content-filter marker set and attack parameters: - Keep main's combined bio_policy/cyber_policy marker documentation and drop the duplicate cyber_policy entry the textual merge produced. - Keep both main's ScoringExpectation import and this branch's AdversarialChatResponseBlockedException import. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Provider blocks on the adversarial chat are now UNDETERMINED instead of FAILURE, so they are neither reused from the cross-run cache nor counted as completed on resume. Genuine model refusals still flow through the scorer as measured failures because AdversarialChatResponseBlockedException is only raised for a provider content-filter block on the adversarial response. Replace the stringly typed "adversarial_chat_blocked" metadata key with a typed AttackPreparationFailure/Kind contract in the attack core, so red_teaming and simulated_conversation share a contract instead of importing a constant from each other. PromptSendingAttack now attaches the same signal on its preparation-failure path, which previously carried no metadata at all. Replace the four TAP-specific scenario parameters with a generic, typed technique_args parameter dispatched through AttackTechniqueFactory.with_attack_kwargs, keeping the scenario technique-agnostic. The ADO pipeline keeps its operator-facing TAP knobs and quick/full profiles but translates them into the generic flag. Also: remove the inert _build_initial_scenario_metadata override, degrade gracefully when a restored cache artifact fails analytics lookup, apply RAISE_IF_DEFAULT_SCORER_BLOCKS on the registry-default scorer branch without mutating the shared singleton, drop duplicate cyber_policy test entries, and make the pipeline tests verify profile-variable wiring structurally. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolves a conflict in simulated_conversation.py where main's SeedPrompt normalization (#2698) renamed the next-message system prompt parameter while this branch added typed preparation-failure handling around the same call. The resolution keeps both: the guarded call now uses the resolved next_message_system_prompt SeedPrompt and retains the AttackPreparationFailure short-circuit and blocked-response handling. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
_use_cached was written in __init__ and again in _build_atomic_attacks_async, then read once immediately after the second write. _is_cache_reuse_enabled() is the real accessor and was already called directly elsewhere, so the field only provided a way to observe a stale value that ignored the runtime parameter before the build ran. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolves an import-list conflict in adversarial.py: main added ScenarioRunSizeEstimateCondition/Status/Factor while this branch added ScorerEvaluationIdentifier. Both sides are kept. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
varunj-msft
reviewed
Sep 23, 2026
Distinguish adversarial-model refusals from provider blocks, narrow the resume gate so deterministic UNDETERMINED rows converge, and make the default-scorer block policy reach the scorers that actually call an LLM. - Add AdversarialChatRefusedException as a subclass of AdversarialChatResponseBlockedException. An SDK `refusal` field and a provider content filter both surface as response_error == "blocked", so the manager now branches on the structured refusal to report the adversarial model's own decision separately from a deployment filter. - Add AttackPreparationFailureKind.ADVERSARIAL_CHAT_REFUSED and from_exception() so producers map the raised failure to a kind instead of hardcoding ADVERSARIAL_CHAT_BLOCKED. - Carry the typed AttackPreparationFailure through SimulatedConversationResult and PromptSendingAttackParameters rather than a bare reason string, so the kind survives to AttackResult.metadata. - Narrow the resume gate: only ERROR rows and rows carrying a preparation failure stay pending. An UNDETERMINED row that reached the objective target recorded the best verdict its configuration allows, so counting it as incomplete re-sent it on every resume without ever converging. - Add Scorer.with_scorer_block_policy, mirroring get_chat_target, and override it on the composite, inverter, and threshold wrappers. The previous isinstance(scorer, MessageScorer) guard bailed at the root of the registered scale_and_refusal default, so the block policy never reached the SelfAsk leaves. Each level copies only when something changed, leaving the shared registry instance unmutated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Three hooks failed on the merge commit, all from changes in this branch:
- check-no-rest-roles: AdversarialChatRefusedException used a Sphinx
:class: role. PyRIT renders docstrings with MyST, so the role would
appear as literal text. Use plain double backticks; gen_api_md links
known symbols at render time.
- ty redundant-condition-strict: AttackPreparationFailureKind has two
members, so ty narrowed the second identity check to always-true and
the trailing fallback to unreachable. Replace the if-chain with a
module-level lookup table, which also makes a future member without a
description fail loudly instead of inheriting generic text.
- ty unsound-return-statement: params.get("use_cached") is Any, so the
ternary inferred bool | (Any & ~None), assignable to but not a subtype
of the declared bool return. Coerce the runtime branch explicitly.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolves conflicts with #2494, which added benchmark-owned adversarial guidance and touched the same regions of AdversarialBenchmark. - VERSION: both branches independently bumped 4 -> 5 for different reasons. Main's v5 (shared benchmark guidance) is already published, and this branch's scoring change is additive on top, so the merged scenario is neither v5 and becomes v6 with both rationale lines retained. Keeping 5 would let runs with different scoring resume and reuse each other's results. - Main's note that the behavioral cache stays keyed across scenario versions is dropped: this branch deliberately made cache reuse version-gated, so that sentence no longer describes the code. - _build_atomic_attacks_async: main wraps each factory with the guidance prefix, this branch resolves generic technique_args overrides. Composed so overrides are resolved first and the prefix is applied to the resulting factories. Applying the prefix only to registry factories would silently drop guidance from overridden techniques and make their ASR non-comparable, which is the whole point of the benchmark. - Test target mocks keep TargetIdentifier (EVAL_ROOT for objective-target eval hashing, needed by this branch's cache identity) while taking main's added objective-scorer identifier mock. Adds a regression test proving an overridden factory still receives the guidance prefix; verified it fails when the prefix is applied before overrides are merged in. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolves conflicts with #2738 (Seed Expectation, scorer phase 7), which replaced the scorer condition API in the same files that gained with_scorer_block_policy. Scorer conflicts (message_scorer, float_scale_threshold, true_false_inverter, true_false_composite): kept with_scorer_block_policy and adopted main's _get_child_scorers()/_get_condition_type(), dropping matched_conditions, required_conditions and the wrapper _validate_expectation overrides. Those were pre-existing code from #2400 that #2738 deliberately removed, not part of this branch. Keeping them was not optional: Scorer.__init_subclass__ now raises TypeError when a subclass declares matched_conditions or required_conditions, so retaining them would break import pyrit.score. Executor conflicts (red_teaming, prompt_sending): import-only; unioned the attack_preparation imports with main's score_attack_response_async. Verified with_scorer_block_policy still propagates through a three-level composite, leaves shared registry scorers unmutated, and returns self when the policy is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Improves the adversarial benchmark end to end: a balanced 120-objective dataset (
adversarial_benchmark_v1), better objective scoring, and Role Play / Crescendo / TAP technique defaults.The ADO pipeline gains
quickandfullprofiles and reuses compatible results from a same-branch SQLite artifact, so a re-run only pays for objectives it has not already measured.Attacks that never reached the objective target are no longer recorded as measured failures. When the adversarial model produces no attacker turn, the objective target is never probed, so there is no verdict to record. Those attacks now return
AttackOutcome.UNDETERMINEDand carry a typed marker (pyrit/executor/attack/core/attack_preparation.py) shared byRedTeamingAttack,PromptSendingAttack, andgenerate_simulated_conversation_async. That keeps them out of the result cache and out of resume-completion.The typed marker exists so the producers and consumers of that signal agree on one contract without depending on each other's modules, and so the failure reason is guaranteed non-empty — previously the consumer read a magic metadata key imported from
red_teamingand could end up with aNonereason that silently fell through.Provider blocks vs. adversarial-model refusals
An SDK-reported
refusaland a provider content filter both surface asresponse_error == "blocked", so they were previously indistinguishable.AdversarialChatRefusedExceptionnow subclassesAdversarialChatResponseBlockedException, and_raise_for_adversarial_errorbranches on the message piece's structured refusal. The two are recorded as distinct failure kinds (ADVERSARIAL_CHAT_BLOCKEDvsADVERSARIAL_CHAT_REFUSED).Both stay
UNDETERMINED: in either case nothing was sent to the objective target, so scoring them as a defensive win would put an unsent attack in the ASR denominator. They are recorded separately so the distinction is auditable. A plain-text refusal from the adversarial model is unaffected — it carries no error, is still sent to the target, and remains a genuine measuredFAILURE.Notable behavior change
Scenario._get_completed_objective_hashes_for_attacknow leaves a row pending only when it is anERRORor carries a preparation-failure marker. This applies to all scenarios, not just this benchmark.UNDETERMINEDalone is deliberately not enough to re-run a row. A row that reached the objective target and endedUNDETERMINED— for example because no objective scorer was configured, or the scorer abstained — recorded the best verdict its configuration allows. Re-running it would re-send the objective on every resume without ever converging. Rows that reached a real verdict are unaffected.Cache reuse break for TAP consumers
TreeOfAttacksWithPruningAttacknow includes its search configuration (tree_width,tree_depth,branching_factor,on_topic_checking_enabled,desired_response_prefix) in its identifier. These parameters change what the attack actually does, so results produced under different settings were previously indistinguishable and could be reused for one another.This changes TAP's eval hash for every consumer, not just this benchmark — including
FoundryScenario, where TAP is one of the available techniques. Existing TAP results will no longer match for cache reuse and will be re-run once.FoundryScenario.VERSIONis deliberately left at1. A changed eval hash already fails safe: it produces a cache miss, so stale results are ignored rather than incorrectly reused. Bumping the scenario version would additionally invalidate reuse for every other Foundry technique whose identifier did not change, turning a narrow and correct invalidation into a broad one.Other changes
--technique-argsparameter taking<technique>.<argument>=<value>entries, dispatched throughAttackTechniqueFactory.with_attack_kwargs. This replaces the TAP-specifictap_tree_width/tap_tree_depth/tap_branching_factor/tap_batch_sizeparameters introduced earlier in this same PR, so the scenario no longer hardcodes knowledge of one technique. Those parameters never shipped, so this is not a breaking change. Unknown technique names, unknown kwargs, and malformed entries all fail with a clear message. The pipeline keeps its operator-facingtapTreeWidth-style inputs and its quick/full tuning; they are now marshalled into--technique-args.RAISE_IF_DEFAULT_SCORER_BLOCKSnow also applies on the registry-default-scorer branch. The registered default (scale_and_refusal) is aTrueFalseCompositeScorerwrapping aFloatScaleThresholdScorerand aTrueFalseInverterScorer, so a policy applied only at the root never reached theSelfAsk*leaves that actually decide whether a blocked scoring response raises.Scorer.with_scorer_block_policy— mirroring the existingget_chat_targetdelegation convention — is overridden by those three wrappers so each level forwards to its own children and copies only when something changed, leaving the shared registry instance unmutated.Tests and Documentation
Unit tests cover each behavior above rather than its implementation:
UNDETERMINEDand round-trip their typed marker through memory. A blocked result with no outcome reason still yields a usable reason.UNDETERMINEDrows as complete.--technique-argsparsing, including a non-TAP technique, unregistered technique names, unknown kwargs, malformed entries, and conflicting repeats.scale_and_refusalshape and asserts the policy lands on both LLM-backed leaves while the shared instance and its leaves stay unmutated. Each wrapper also has a focused delegation test, including that it returns itself when already compliant. The fallback scorer path is covered separately.The pipeline test now verifies structurally that every benchmark variable consumed by a run step's
env:block is emitted by the profile-resolution step, and that every declared env var is actually read. It no longer restates literal profile values or template strings, so profile drift is caught without the test needing an edit for every tuning change.No documentation or notebook changes, and no documented public API surface changes, so JupyText was not run.