Source code for pyrit.analytics.result_analysis
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
from collections import defaultdict
from dataclasses import dataclass
from typing import DefaultDict, Optional
from pyrit.models import AttackOutcome, AttackResult
[docs]
@dataclass
class AttackStats:
success_rate: Optional[float]
total_decided: int
successes: int
failures: int
undetermined: int
def _compute_stats(successes: int, failures: int, undetermined: int) -> AttackStats:
total_decided = successes + failures
success_rate = successes / total_decided if total_decided > 0 else None
return AttackStats(
success_rate=success_rate,
total_decided=total_decided,
successes=successes,
failures=failures,
undetermined=undetermined,
)
[docs]
def analyze_results(attack_results: list[AttackResult]) -> dict:
"""
Analyze a list of AttackResult objects and return overall and grouped statistics.
Returns:
A dictionary of AttackStats objects. The overall stats are accessible with the key
"Overall", and the stats of any attack can be retrieved using "By_attack_identifier"
followed by the identifier of the attack.
Raises:
ValueError: if attack_results is empty.
TypeError: if any element is not an AttackResult.
Example:
>>> analyze_results(attack_results)
{
"Overall": AttackStats,
"By_attack_identifier": dict[str, AttackStats]
}
"""
if not attack_results:
raise ValueError("attack_results cannot be empty")
overall_counts: DefaultDict[str, int] = defaultdict(int)
by_type_counts: DefaultDict[str, DefaultDict[str, int]] = defaultdict(lambda: defaultdict(int))
for attack in attack_results:
if not isinstance(attack, AttackResult):
raise TypeError(f"Expected AttackResult, got {type(attack).__name__}: {attack!r}")
outcome = attack.outcome
attack_type = attack.attack_identifier.get("type", "unknown")
if outcome == AttackOutcome.SUCCESS:
overall_counts["successes"] += 1
by_type_counts[attack_type]["successes"] += 1
elif outcome == AttackOutcome.FAILURE:
overall_counts["failures"] += 1
by_type_counts[attack_type]["failures"] += 1
else:
overall_counts["undetermined"] += 1
by_type_counts[attack_type]["undetermined"] += 1
overall_stats = _compute_stats(
successes=overall_counts["successes"],
failures=overall_counts["failures"],
undetermined=overall_counts["undetermined"],
)
by_type_stats = {
attack_type: _compute_stats(
successes=counts["successes"],
failures=counts["failures"],
undetermined=counts["undetermined"],
)
for attack_type, counts in by_type_counts.items()
}
return {
"Overall": overall_stats,
"By_attack_identifier": by_type_stats,
}