44from unittest .mock import AsyncMock , MagicMock , patch
55
66import pytest
7+ from unit .mocks import MockPromptTarget
78
89from pyrit .analytics import ApproximateTextMatching , ExactTextMatching
10+ from pyrit .converter import AskToDecodeConverter , Base64Converter
11+ from pyrit .executor .attack import AttackConverterConfig , AttackScoringConfig , PromptSendingAttack
912from pyrit .memory .central_memory import CentralMemory
1013from pyrit .memory .memory_interface import MemoryInterface
11- from pyrit .models import Message , MessagePiece
14+ from pyrit .models import AttackOutcome , Message , MessagePiece
15+ from pyrit .prompt_normalizer import ConverterConfiguration
1216from pyrit .score import DecodingScorer
1317
1418
@@ -18,8 +22,8 @@ def sample_message_pieces():
1822 user_piece = MessagePiece (
1923 role = "user" ,
2024 original_value = "secret" ,
21- converted_value = "encoded_secret " ,
22- prompt_metadata = {"decoded_text" : "decoded_secret " },
25+ converted_value = "c2VjcmV0 " ,
26+ prompt_metadata = {"decoded_text" : "hidden payload " },
2327 )
2428
2529 assistant_piece = MessagePiece (
@@ -31,6 +35,24 @@ def sample_message_pieces():
3135 return user_piece , assistant_piece
3236
3337
38+ class _EchoTarget (MockPromptTarget ):
39+ """Answers every request with ``reply``, where ``{received}`` is the text the target received."""
40+
41+ def __init__ (self , * , reply : str ) -> None :
42+ super ().__init__ ()
43+ self ._reply = reply
44+
45+ async def _send_prompt_to_target_async (self , * , normalized_conversation : list [Message ]) -> list [Message ]:
46+ request = normalized_conversation [- 1 ].message_pieces [0 ]
47+ return [
48+ MessagePiece (
49+ role = "assistant" ,
50+ original_value = self ._reply .format (received = request .converted_value ),
51+ conversation_id = request .conversation_id ,
52+ ).to_message ()
53+ ]
54+
55+
3456class TestDecodingScorer :
3557 async def test_decoding_scorer_original_value_match (self , patch_central_database , sample_message_pieces ):
3658 user_piece , assistant_piece = sample_message_pieces
@@ -50,9 +72,9 @@ async def test_decoding_scorer_original_value_match(self, patch_central_database
5072 "text_matcher" : "<class 'pyrit.analytics.text_matching.ExactTextMatching'>"
5173 }
5274
53- async def test_decoding_scorer_converted_value_match (self , patch_central_database , sample_message_pieces ):
75+ async def test_decoding_scorer_encoded_echo_does_not_match (self , patch_central_database , sample_message_pieces ):
5476 user_piece , assistant_piece = sample_message_pieces
55- assistant_piece .converted_value = "Response with encoded_secret in it "
77+ assistant_piece .converted_value = "You sent the Base64 string c2VjcmV0 "
5678
5779 memory = MagicMock (MemoryInterface )
5880 user_message = Message (message_pieces = [user_piece ])
@@ -63,11 +85,11 @@ async def test_decoding_scorer_converted_value_match(self, patch_central_databas
6385 score = await scorer ._score_piece_async (assistant_piece )
6486
6587 assert len (score ) == 1
66- assert score [0 ].get_value () is True
88+ assert score [0 ].get_value () is False
6789
6890 async def test_decoding_scorer_metadata_match (self , patch_central_database , sample_message_pieces ):
6991 user_piece , assistant_piece = sample_message_pieces
70- assistant_piece .converted_value = "Response with decoded_secret in it"
92+ assistant_piece .converted_value = "Response with hidden payload in it"
7193
7294 memory = MagicMock (MemoryInterface )
7395 user_message = Message (message_pieces = [user_piece ])
@@ -217,3 +239,32 @@ def test_decoding_identifier_stable_for_equivalent_matchers(self, patch_central_
217239
218240 assert first .get_identifier ().hash == second .get_identifier ().hash
219241 assert first .get_identifier ().eval_hash == second .get_identifier ().eval_hash
242+
243+ @pytest .mark .parametrize (
244+ "decode_template,reply,expected_outcome" ,
245+ [
246+ pytest .param (None , "You sent: {received}" , AttackOutcome .FAILURE , id = "raw-echo" ),
247+ pytest .param (
248+ AskToDecodeConverter .garak_templates [0 ], "You sent: {received}" , AttackOutcome .FAILURE , id = "decode-echo"
249+ ),
250+ pytest .param (None , "Decoded: secret" , AttackOutcome .SUCCESS , id = "plaintext" ),
251+ pytest .param (None , "I refuse" , AttackOutcome .FAILURE , id = "refusal" ),
252+ ],
253+ )
254+ async def test_encoding_attack_counts_only_decoded_replies (
255+ self , patch_central_database , decode_template , reply , expected_outcome
256+ ):
257+ converters = [Base64Converter ()]
258+ if decode_template :
259+ converters .append (AskToDecodeConverter (template = decode_template , encoding_name = "base64" ))
260+ attack = PromptSendingAttack (
261+ objective_target = _EchoTarget (reply = reply ),
262+ attack_converter_config = AttackConverterConfig (
263+ request_converters = ConverterConfiguration .from_converters (converters = converters )
264+ ),
265+ attack_scoring_config = AttackScoringConfig (objective_scorer = DecodingScorer ()),
266+ )
267+
268+ result = await attack .execute_async (objective = "secret" )
269+
270+ assert result .outcome == expected_outcome
0 commit comments