Skip to content

Commit 14a3bc1

Browse files
Sarayu-codeSarayu Poddutooriromanlutz
authored
FEAT analytics by attack type (#1041)
Co-authored-by: Sarayu Poddutoori <you@example.com> Co-authored-by: Roman Lutz <romanlutz13@gmail.com>
1 parent d25ce41 commit 14a3bc1

File tree

5 files changed

+223
-186
lines changed

5 files changed

+223
-186
lines changed

doc/code/memory/11_harm_categories.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@
378378
"for i, attack_result in enumerate(multiple_groups):\n",
379379
" print(f\"Attack {i+1}: {attack_result.objective}...\")\n",
380380
" print(f\"Conversation ID: {attack_result.conversation_id}\")\n",
381-
"print()\n"
381+
"print()"
382382
]
383383
}
384384
],

pyrit/analytics/analyze_results.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

pyrit/analytics/result_analysis.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
from collections import defaultdict
5+
from dataclasses import dataclass
6+
from typing import DefaultDict, Optional
7+
8+
from pyrit.models import AttackOutcome, AttackResult
9+
10+
11+
@dataclass
12+
class AttackStats:
13+
success_rate: Optional[float]
14+
total_decided: int
15+
successes: int
16+
failures: int
17+
undetermined: int
18+
19+
20+
def _compute_stats(successes: int, failures: int, undetermined: int) -> AttackStats:
21+
total_decided = successes + failures
22+
success_rate = successes / total_decided if total_decided > 0 else None
23+
return AttackStats(
24+
success_rate=success_rate,
25+
total_decided=total_decided,
26+
successes=successes,
27+
failures=failures,
28+
undetermined=undetermined,
29+
)
30+
31+
32+
def analyze_results(attack_results: list[AttackResult]) -> dict:
33+
"""
34+
Analyze a list of AttackResult objects and return overall and grouped statistics.
35+
36+
Returns:
37+
{
38+
"Overall": AttackStats,
39+
"By_attack_identifier": dict[str, AttackStats]
40+
}
41+
42+
Raises:
43+
ValueError: if attack_results is empty.
44+
TypeError: if any element is not an AttackResult.
45+
"""
46+
if not attack_results:
47+
raise ValueError("attack_results cannot be empty")
48+
49+
overall_counts: DefaultDict[str, int] = defaultdict(int)
50+
by_type_counts: DefaultDict[str, DefaultDict[str, int]] = defaultdict(lambda: defaultdict(int))
51+
52+
for attack in attack_results:
53+
if not isinstance(attack, AttackResult):
54+
raise TypeError(f"Expected AttackResult, got {type(attack).__name__}: {attack!r}")
55+
56+
outcome = attack.outcome
57+
attack_type = attack.attack_identifier.get("type", "unknown")
58+
59+
if outcome == AttackOutcome.SUCCESS:
60+
overall_counts["successes"] += 1
61+
by_type_counts[attack_type]["successes"] += 1
62+
elif outcome == AttackOutcome.FAILURE:
63+
overall_counts["failures"] += 1
64+
by_type_counts[attack_type]["failures"] += 1
65+
else:
66+
overall_counts["undetermined"] += 1
67+
by_type_counts[attack_type]["undetermined"] += 1
68+
69+
overall_stats = _compute_stats(
70+
successes=overall_counts["successes"],
71+
failures=overall_counts["failures"],
72+
undetermined=overall_counts["undetermined"],
73+
)
74+
75+
by_type_stats = {
76+
attack_type: _compute_stats(
77+
successes=counts["successes"],
78+
failures=counts["failures"],
79+
undetermined=counts["undetermined"],
80+
)
81+
for attack_type, counts in by_type_counts.items()
82+
}
83+
84+
return {
85+
"Overall": overall_stats,
86+
"By_attack_identifier": by_type_stats,
87+
}

tests/unit/analytics/test_analyze_results.py

Lines changed: 0 additions & 128 deletions
This file was deleted.

0 commit comments

Comments
 (0)