@@ -85,33 +85,50 @@ class EncodingStrategy(ScenarioStrategy):
8585 Strategies for encoding attacks.
8686
8787 Each enum member represents an encoding scheme that will be tested against the target model.
88- The ALL aggregate expands to include all encoding strategies.
88+ The ``ALL`` aggregate expands to every encoding scheme (exhaustive run). The ``DEFAULT``
89+ aggregate expands to a small curated subset that spans distinct encoding families, giving a
90+ fast, representative default run.
8991
9092 Note: EncodingStrategy does not support composition. Each encoding must be applied individually.
93+ The strategy axis here is the encoding scheme (not an attack technique), and every encoding runs
94+ as a single-turn ``PromptSendingAttack``, so SINGLE_TURN/MULTI_TURN aggregates are not applicable.
9195 """
9296
93- # Aggregate member
97+ # Aggregate members
9498 ALL = ("all" , {"all" })
99+ DEFAULT = ("default" , {"default" })
95100
96- # Individual encoding strategies (matching the atomic attack names)
101+ # Individual encoding strategies (each value matches the encoding name used for display grouping).
102+ # Members tagged ``default`` form the curated DEFAULT aggregate: one base-N encoding (Base16),
103+ # one substitution cipher (ROT13), and one symbolic alphabet (MorseCode).
97104 Base64 = ("base64" , set [str ]())
98105 Base2048 = ("base2048" , set [str ]())
99- Base16 = ("base16" , set [ str ]() )
106+ Base16 = ("base16" , { "default" } )
100107 Base32 = ("base32" , set [str ]())
101108 ASCII85 = ("ascii85" , set [str ]())
102109 Hex = ("hex" , set [str ]())
103110 QuotedPrintable = ("quoted_printable" , set [str ]())
104111 UUencode = ("uuencode" , set [str ]())
105- ROT13 = ("rot13" , set [ str ]() )
112+ ROT13 = ("rot13" , { "default" } )
106113 Braille = ("braille" , set [str ]())
107114 Atbash = ("atbash" , set [str ]())
108- MorseCode = ("morse_code" , set [ str ]() )
115+ MorseCode = ("morse_code" , { "default" } )
109116 NATO = ("nato" , set [str ]())
110117 Ecoji = ("ecoji" , set [str ]())
111118 Zalgo = ("zalgo" , set [str ]())
112119 LeetSpeak = ("leet_speak" , set [str ]())
113120 AsciiSmuggler = ("ascii_smuggler" , set [str ]())
114121
122+ @classmethod
123+ def get_aggregate_tags (cls ) -> set [str ]:
124+ """
125+ Get the set of tags that represent aggregate categories.
126+
127+ Returns:
128+ set[str]: The base ``"all"`` aggregate plus the scenario-specific ``"default"`` aggregate.
129+ """
130+ return super ().get_aggregate_tags () | {"default" }
131+
115132
116133logger = logging .getLogger (__name__ )
117134
@@ -133,7 +150,7 @@ class Encoding(Scenario):
133150 By default, this uses the same dataset as Garak: slur terms and web XSS payloads.
134151 """
135152
136- VERSION : int = 1
153+ VERSION : int = 2
137154
138155 @apply_defaults
139156 def __init__ (
@@ -165,7 +182,7 @@ def __init__(
165182 super ().__init__ (
166183 version = self .VERSION ,
167184 strategy_class = EncodingStrategy ,
168- default_strategy = EncodingStrategy .ALL ,
185+ default_strategy = EncodingStrategy .DEFAULT ,
169186 default_dataset_config = CompoundDatasetAttackConfiguration (
170187 configurations = [
171188 EncodingDatasetConfiguration (dataset_names = ["garak_slur_terms_en" ], max_dataset_size = 3 ),
@@ -231,45 +248,57 @@ def _get_converter_attacks(self) -> list[AtomicAttack]:
231248 Returns:
232249 list[AtomicAttack]: List of all atomic attacks to execute.
233250 """
234- # Map of all available converters with their encoding names
235- all_converters_with_encodings : list [tuple [list [PromptConverter ], str ]] = [
236- ([Base64Converter ()], "base64" ),
237- ([Base64Converter (encoding_func = "urlsafe_b64encode" )], "base64" ),
238- ([Base64Converter (encoding_func = "standard_b64encode" )], "base64" ),
239- ([Base64Converter (encoding_func = "b2a_base64" )], "base64" ),
240- ([Base2048Converter ()], "base2048" ),
241- ([Base64Converter (encoding_func = "b16encode" )], "base16" ),
242- ([Base64Converter (encoding_func = "b32encode" )], "base32" ),
243- ([Base64Converter (encoding_func = "a85encode" )], "ascii85" ),
244- ([Base64Converter (encoding_func = "b85encode" )], "ascii85" ),
245- ([BinAsciiConverter (encoding_func = "hex" )], "hex" ),
246- ([BinAsciiConverter (encoding_func = "quoted-printable" )], "quoted_printable" ),
247- ([BinAsciiConverter (encoding_func = "UUencode" )], "uuencode" ),
248- ([ROT13Converter ()], "rot13" ),
249- ([BrailleConverter ()], "braille" ),
250- ([AtbashConverter ()], "atbash" ),
251- ([MorseConverter ()], "morse_code" ),
252- ([NatoConverter ()], "nato" ),
253- ([EcojiConverter ()], "ecoji" ),
254- ([ZalgoConverter ()], "zalgo" ),
255- ([LeetspeakConverter ()], "leet_speak" ),
256- ([AsciiSmugglerConverter ()], "ascii_smuggler" ),
251+ # Map of all available converters with their encoding name and a unique variant slug.
252+ # ``encoding_name`` drives strategy selection and user-facing grouping (display_group);
253+ # ``variant_slug`` is unique per row so that atomic-attack names stay unique even when one
254+ # encoding name maps to multiple converter variants (e.g. base64, ascii85).
255+ # NOTE: some base64 variants are near-duplicates (default == standard_b64encode; b2a only
256+ # appends a trailing newline). They are retained here to keep the exhaustive ALL run stable
257+ # behind the VERSION gate; trimming them is a separate cleanup.
258+ all_converters_with_encodings : list [tuple [list [PromptConverter ], str , str ]] = [
259+ ([Base64Converter ()], "base64" , "base64" ),
260+ ([Base64Converter (encoding_func = "urlsafe_b64encode" )], "base64" , "base64_urlsafe" ),
261+ ([Base64Converter (encoding_func = "standard_b64encode" )], "base64" , "base64_standard" ),
262+ ([Base64Converter (encoding_func = "b2a_base64" )], "base64" , "base64_b2a" ),
263+ ([Base2048Converter ()], "base2048" , "base2048" ),
264+ ([Base64Converter (encoding_func = "b16encode" )], "base16" , "base16" ),
265+ ([Base64Converter (encoding_func = "b32encode" )], "base32" , "base32" ),
266+ ([Base64Converter (encoding_func = "a85encode" )], "ascii85" , "ascii85_a85" ),
267+ ([Base64Converter (encoding_func = "b85encode" )], "ascii85" , "ascii85_b85" ),
268+ ([BinAsciiConverter (encoding_func = "hex" )], "hex" , "hex" ),
269+ ([BinAsciiConverter (encoding_func = "quoted-printable" )], "quoted_printable" , "quoted_printable" ),
270+ ([BinAsciiConverter (encoding_func = "UUencode" )], "uuencode" , "uuencode" ),
271+ ([ROT13Converter ()], "rot13" , "rot13" ),
272+ ([BrailleConverter ()], "braille" , "braille" ),
273+ ([AtbashConverter ()], "atbash" , "atbash" ),
274+ ([MorseConverter ()], "morse_code" , "morse_code" ),
275+ ([NatoConverter ()], "nato" , "nato" ),
276+ ([EcojiConverter ()], "ecoji" , "ecoji" ),
277+ ([ZalgoConverter ()], "zalgo" , "zalgo" ),
278+ ([LeetspeakConverter ()], "leet_speak" , "leet_speak" ),
279+ ([AsciiSmugglerConverter ()], "ascii_smuggler" , "ascii_smuggler" ),
257280 ]
258281
259282 # Filter to only include selected strategies
260283 selected_encoding_names = {s .value for s in self ._scenario_strategies }
261284 converters_with_encodings = [
262- (conv , name ) for conv , name in all_converters_with_encodings if name in selected_encoding_names
285+ (conv , name , variant_slug )
286+ for conv , name , variant_slug in all_converters_with_encodings
287+ if name in selected_encoding_names
263288 ]
264289
265290 atomic_attacks = []
266- for conv , name in converters_with_encodings :
267- atomic_attacks .extend (self ._get_prompt_attacks (converters = conv , encoding_name = name ))
291+ for conv , name , variant_slug in converters_with_encodings :
292+ atomic_attacks .extend (
293+ self ._get_prompt_attacks (converters = conv , encoding_name = name , variant_slug = variant_slug )
294+ )
268295 return atomic_attacks
269296
270- def _get_prompt_attacks (self , * , converters : list [PromptConverter ], encoding_name : str ) -> list [AtomicAttack ]:
297+ def _get_prompt_attacks (
298+ self , * , converters : list [PromptConverter ], encoding_name : str , variant_slug : str
299+ ) -> list [AtomicAttack ]:
271300 """
272- Create atomic attacks for a specific encoding scheme .
301+ Create atomic attacks for a specific encoding converter variant .
273302
274303 For each seed prompt (the text to be decoded), creates atomic attacks that:
275304 1. Encode the seed prompt using the specified converter(s)
@@ -279,31 +308,42 @@ def _get_prompt_attacks(self, *, converters: list[PromptConverter], encoding_nam
279308
280309 Args:
281310 converters (list[PromptConverter]): The list of converters to apply to the seed prompts.
282- encoding_name (str): Human-readable name of the encoding scheme (e.g., "Base64", "ROT13").
311+ encoding_name (str): Human-readable name of the encoding scheme (e.g., "base64", "rot13").
312+ Used as the ``display_group`` so all variants of an encoding aggregate together in output.
313+ variant_slug (str): Unique slug for this converter variant, used to build a unique
314+ ``atomic_attack_name`` per converter variant and prompt config.
283315
284316 Returns:
285- list[AtomicAttack]: List of atomic attacks for this encoding scheme .
317+ list[AtomicAttack]: List of atomic attacks for this encoding converter variant .
286318
287319 Raises:
288320 ValueError: If scenario is not properly initialized.
289321 """
290- converter_configs = [
291- AttackConverterConfig (
292- request_converters = PromptConverterConfiguration .from_converters (converters = converters )
322+ # (config_name_suffix, converter_config). The bare "raw" config encodes only; each
323+ # decode-template config additionally asks the model to decode.
324+ converter_configs : list [tuple [str , AttackConverterConfig ]] = [
325+ (
326+ "raw" ,
327+ AttackConverterConfig (
328+ request_converters = PromptConverterConfiguration .from_converters (converters = converters )
329+ ),
293330 )
294331 ]
295332
296- for decode_type in self ._encoding_templates :
333+ for decode_index , decode_type in enumerate ( self ._encoding_templates ) :
297334 converters_ = converters [:] + [AskToDecodeConverter (template = decode_type , encoding_name = encoding_name )]
298335
299336 converter_configs .append (
300- AttackConverterConfig (
301- request_converters = PromptConverterConfiguration .from_converters (converters = converters_ )
337+ (
338+ f"decode{ decode_index } " ,
339+ AttackConverterConfig (
340+ request_converters = PromptConverterConfiguration .from_converters (converters = converters_ )
341+ ),
302342 )
303343 )
304344
305345 atomic_attacks = []
306- for attack_converter_config in converter_configs :
346+ for config_suffix , attack_converter_config in converter_configs :
307347 # objective_target is guaranteed to be non-None by parent class validation
308348 if self ._objective_target is None :
309349 raise ValueError (
@@ -316,7 +356,8 @@ def _get_prompt_attacks(self, *, converters: list[PromptConverter], encoding_nam
316356 )
317357 atomic_attacks .append (
318358 AtomicAttack (
319- atomic_attack_name = encoding_name ,
359+ atomic_attack_name = f"{ variant_slug } _{ config_suffix } " ,
360+ display_group = encoding_name ,
320361 attack_technique = AttackTechnique (attack = attack ),
321362 seed_groups = self ._resolved_seed_groups or [],
322363 )
0 commit comments