Skip to content

Commit b2be470

Browse files
FIX: export partial adversarial benchmark results
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b5e066d-8e77-4ba9-8fc8-56f4745ef365
1 parent bbff19d commit b2be470

2 files changed

Lines changed: 97 additions & 10 deletions

File tree

.azuredevops/adversarial-benchmark.yml

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,21 +276,27 @@ jobs:
276276
"$artifact_dir/scenario-result.json"
277277
)"
278278
279+
export_exit_code=0
280+
if [[ -n "$result_id" ]]; then
281+
echo "export-results" > "$artifact_dir/phase.txt"
282+
set +e
283+
uv run python -m build_scripts.export_adversarial_benchmark_result \
284+
--scenario-result-id "$result_id" \
285+
--output-dir "$artifact_dir"
286+
export_exit_code=$?
287+
set -e
288+
echo "$export_exit_code" > "$artifact_dir/export-exit-code.txt"
289+
fi
290+
279291
if [[ "$benchmark_exit_code" -eq 0 ]]; then
280292
if [[ -z "$result_id" || "$run_status" != "COMPLETED" ]]; then
281293
echo "Benchmark exited successfully without a completed persisted scenario result." >&2
282294
exit 3
283295
fi
284-
285-
echo "export-results" > "$artifact_dir/phase.txt"
286-
uv run pyrit_scan \
287-
scenario-results "$result_id" \
288-
--view overview \
289-
> "$artifact_dir/overview.txt" 2>&1
290-
uv run pyrit_scan \
291-
scenario-results "$result_id" \
292-
--view attacks \
293-
> "$artifact_dir/attacks.txt" 2>&1
296+
if [[ "$export_exit_code" -ne 0 ]]; then
297+
echo "Failed to export completed benchmark result." >&2
298+
exit "$export_exit_code"
299+
fi
294300
fi
295301
296302
if [[ "$benchmark_exit_code" -eq 0 ]]; then
@@ -367,6 +373,7 @@ jobs:
367373
"last_phase": read_text("phase.txt"),
368374
"execution_exit_code": read_text("execution-exit-code.txt"),
369375
"benchmark_exit_code": read_text("benchmark-exit-code.txt"),
376+
"export_exit_code": read_text("export-exit-code.txt"),
370377
"objective_target": os.environ["OBJECTIVE_TARGET_INPUT"],
371378
"adversarial_targets": os.environ["ADVERSARIAL_TARGETS_INPUT"].split(),
372379
"technique_preset": preset,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
"""Export readable partial or completed adversarial benchmark results from SQLite."""
5+
6+
import argparse
7+
import asyncio
8+
import contextlib
9+
from pathlib import Path
10+
11+
from pyrit.cli._output import print_attacks_table
12+
from pyrit.cli._results import build_attacks_table_payload
13+
from pyrit.memory import CentralMemory
14+
from pyrit.models import ScenarioResult
15+
from pyrit.output.scenario_result.pretty import PrettyScenarioResultMemoryPrinter
16+
from pyrit.output.sink import FileSink
17+
from pyrit.setup import SQLITE, initialize_pyrit_async
18+
19+
20+
async def _load_result_async(*, scenario_result_id: str) -> ScenarioResult:
21+
"""Load one persisted scenario result, regardless of terminal state."""
22+
await initialize_pyrit_async(
23+
memory_db_type=SQLITE,
24+
load_defaults=False,
25+
env_files=[],
26+
silent=True,
27+
)
28+
results = CentralMemory.get_memory_instance().get_scenario_results(
29+
scenario_result_ids=[scenario_result_id],
30+
)
31+
if not results:
32+
raise ValueError(f"Scenario result '{scenario_result_id}' was not found in SQLite memory.")
33+
return results[0]
34+
35+
36+
async def _write_overview_async(*, result: ScenarioResult, output_dir: Path) -> None:
37+
"""Write the existing scenario overview without terminal color codes."""
38+
printer = PrettyScenarioResultMemoryPrinter(
39+
sink=FileSink(path=output_dir / "overview.txt"),
40+
enable_colors=False,
41+
)
42+
await printer.write_async(result)
43+
44+
45+
def _write_attacks(*, result: ScenarioResult, output_dir: Path) -> None:
46+
"""Write machine-readable and console-style partial attack tables."""
47+
payload = build_attacks_table_payload(
48+
result=result,
49+
scenario_result_id=str(result.id),
50+
)
51+
(output_dir / "attacks.json").write_text(payload.model_dump_json(indent=2), encoding="utf-8")
52+
with open(output_dir / "attacks.txt", "w", encoding="utf-8") as output:
53+
with contextlib.redirect_stdout(output):
54+
print_attacks_table(payload=payload)
55+
56+
57+
async def _export_async(*, scenario_result_id: str, output_dir: Path) -> None:
58+
"""Export all readable result views."""
59+
result = await _load_result_async(scenario_result_id=scenario_result_id)
60+
output_dir.mkdir(parents=True, exist_ok=True)
61+
await _write_overview_async(result=result, output_dir=output_dir)
62+
await asyncio.to_thread(_write_attacks, result=result, output_dir=output_dir)
63+
64+
65+
def main() -> None:
66+
"""Run the result exporter."""
67+
parser = argparse.ArgumentParser()
68+
parser.add_argument("--scenario-result-id", required=True)
69+
parser.add_argument("--output-dir", type=Path, required=True)
70+
args = parser.parse_args()
71+
asyncio.run(
72+
_export_async(
73+
scenario_result_id=args.scenario_result_id,
74+
output_dir=args.output_dir,
75+
)
76+
)
77+
78+
79+
if __name__ == "__main__":
80+
main()

0 commit comments

Comments
 (0)