Skip to content

Commit d0b29a3

Browse files
author
test
committed
MAINT: Standardize garak.encoding defaults and fix atomic-attack name collisions
Re-migrated onto current main (post technique rename microsoft#2153 / technique organization microsoft#2155). garak.encoding was already seam-migrated on main to EncodingTechnique(ScenarioTechnique); this layers the remaining #8380 standardization on top. Add an EncodingTechnique.DEFAULT aggregate (curated 8-scheme spread across base-N, cipher, and symbolic families) with a get_aggregate_tags() override, make it the default technique, and bump VERSION to 2. Follows the hand-written enum DEFAULT pattern used by scam/web_injection (not the factory default_technique_names path, which applies only to the core catalog). Fix atomic-attack name collisions by naming each converter variant f"{variant_slug}_{config_suffix}" and grouping variants under display_group=encoding_name so per-encoding results aggregate. Trim near-duplicate base64 variants (standard_b64encode is byte-identical to the default; b2a_base64 only appends a newline). Keep the default per-dataset cap at main's max_dataset_size=3 (no timing change).
1 parent 40cecc7 commit d0b29a3

4 files changed

Lines changed: 209 additions & 61 deletions

File tree

doc/scanner/garak.ipynb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
"QuotedPrintable, UUencode, ROT13, Braille, Atbash, MorseCode, NATO, Ecoji, Zalgo, LeetSpeak,\n",
9292
"AsciiSmuggler\n",
9393
"\n",
94+
"**Aggregate techniques:** `ALL` (every encoding, exhaustive) and `DEFAULT` (a curated subset —\n",
95+
"Base64, Base16, Base32, Hex, ROT13, Atbash, MorseCode, NATO — spanning encoding families for a\n",
96+
"fast, representative default run). `DEFAULT` is used when no techniques are specified.\n",
97+
"\n",
9498
"> **Note:** Technique composition is NOT supported for Encoding — each encoding is tested\n",
9599
"> independently."
96100
]

doc/scanner/garak.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
# QuotedPrintable, UUencode, ROT13, Braille, Atbash, MorseCode, NATO, Ecoji, Zalgo, LeetSpeak,
5050
# AsciiSmuggler
5151
#
52+
# **Aggregate techniques:** `ALL` (every encoding, exhaustive) and `DEFAULT` (a curated subset —
53+
# Base64, Base16, Base32, Hex, ROT13, Atbash, MorseCode, NATO — spanning encoding families for a
54+
# fast, representative default run). `DEFAULT` is used when no techniques are specified.
55+
#
5256
# > **Note:** Technique composition is NOT supported for Encoding — each encoding is tested
5357
# > independently.
5458

pyrit/scenario/scenarios/garak/encoding.py

Lines changed: 95 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -77,33 +77,50 @@ class EncodingTechnique(ScenarioTechnique):
7777
Techniques for encoding attacks.
7878
7979
Each enum member represents an encoding scheme that will be tested against the target model.
80-
The ALL aggregate expands to include all encoding techniques.
80+
The ``ALL`` aggregate expands to every encoding scheme (exhaustive run). The ``DEFAULT``
81+
aggregate expands to a small curated subset that spans distinct encoding families, giving a
82+
fast, representative default run.
8183
8284
Note: EncodingTechnique does not support composition. Each encoding must be applied individually.
8385
"""
8486

85-
# Aggregate member
87+
# Aggregate members
8688
ALL = ("all", {"all"})
87-
88-
# Individual encoding techniques (matching the atomic attack names)
89-
Base64 = ("base64", set[str]())
89+
DEFAULT = ("default", {"default"})
90+
91+
# Individual encoding techniques (matching the atomic attack names). Members tagged ``default``
92+
# form the curated DEFAULT aggregate: a representative spread across encoding families so a
93+
# default scan is broad enough to be meaningful without running every scheme — base-N (Base64,
94+
# Base16, Base32, Hex), substitution ciphers (ROT13, Atbash), and symbolic alphabets
95+
# (MorseCode, NATO).
96+
Base64 = ("base64", {"default"})
9097
Base2048 = ("base2048", set[str]())
91-
Base16 = ("base16", set[str]())
92-
Base32 = ("base32", set[str]())
98+
Base16 = ("base16", {"default"})
99+
Base32 = ("base32", {"default"})
93100
ASCII85 = ("ascii85", set[str]())
94-
Hex = ("hex", set[str]())
101+
Hex = ("hex", {"default"})
95102
QuotedPrintable = ("quoted_printable", set[str]())
96103
UUencode = ("uuencode", set[str]())
97-
ROT13 = ("rot13", set[str]())
104+
ROT13 = ("rot13", {"default"})
98105
Braille = ("braille", set[str]())
99-
Atbash = ("atbash", set[str]())
100-
MorseCode = ("morse_code", set[str]())
101-
NATO = ("nato", set[str]())
106+
Atbash = ("atbash", {"default"})
107+
MorseCode = ("morse_code", {"default"})
108+
NATO = ("nato", {"default"})
102109
Ecoji = ("ecoji", set[str]())
103110
Zalgo = ("zalgo", set[str]())
104111
LeetSpeak = ("leet_speak", set[str]())
105112
AsciiSmuggler = ("ascii_smuggler", set[str]())
106113

114+
@classmethod
115+
def get_aggregate_tags(cls) -> set[str]:
116+
"""
117+
Get the set of tags that represent aggregate categories.
118+
119+
Returns:
120+
set[str]: The base ``"all"`` aggregate plus the scenario-specific ``"default"`` aggregate.
121+
"""
122+
return super().get_aggregate_tags() | {"default"}
123+
107124

108125
logger = logging.getLogger(__name__)
109126

@@ -125,7 +142,7 @@ class Encoding(Scenario):
125142
By default, this uses the same dataset as Garak: slur terms and web XSS payloads.
126143
"""
127144

128-
VERSION: int = 1
145+
VERSION: int = 2
129146

130147
@apply_defaults
131148
def __init__(
@@ -154,7 +171,7 @@ def __init__(
154171
super().__init__(
155172
version=self.VERSION,
156173
technique_class=EncodingTechnique,
157-
default_technique=EncodingTechnique.ALL,
174+
default_technique=EncodingTechnique.DEFAULT,
158175
default_dataset_config=CompoundDatasetAttackConfiguration(
159176
configurations=[
160177
EncodingDatasetConfiguration(dataset_names=["garak_slur_terms_en"], max_dataset_size=3),
@@ -195,49 +212,63 @@ def _get_converter_attacks(self, *, seed_groups: list[SeedAttackGroup]) -> list[
195212
Returns:
196213
list[AtomicAttack]: List of all atomic attacks to execute.
197214
"""
198-
# Map of all available converters with their encoding names
199-
all_converters_with_encodings: list[tuple[list[PromptConverter], str]] = [
200-
([Base64Converter()], "base64"),
201-
([Base64Converter(encoding_func="urlsafe_b64encode")], "base64"),
202-
([Base64Converter(encoding_func="standard_b64encode")], "base64"),
203-
([Base64Converter(encoding_func="b2a_base64")], "base64"),
204-
([Base2048Converter()], "base2048"),
205-
([Base64Converter(encoding_func="b16encode")], "base16"),
206-
([Base64Converter(encoding_func="b32encode")], "base32"),
207-
([Base64Converter(encoding_func="a85encode")], "ascii85"),
208-
([Base64Converter(encoding_func="b85encode")], "ascii85"),
209-
([BinAsciiConverter(encoding_func="hex")], "hex"),
210-
([BinAsciiConverter(encoding_func="quoted-printable")], "quoted_printable"),
211-
([BinAsciiConverter(encoding_func="UUencode")], "uuencode"),
212-
([ROT13Converter()], "rot13"),
213-
([BrailleConverter()], "braille"),
214-
([AtbashConverter()], "atbash"),
215-
([MorseConverter()], "morse_code"),
216-
([NatoConverter()], "nato"),
217-
([EcojiConverter()], "ecoji"),
218-
([ZalgoConverter()], "zalgo"),
219-
([LeetspeakConverter()], "leet_speak"),
220-
([AsciiSmugglerConverter()], "ascii_smuggler"),
215+
# Map of all available converters with their encoding name and a unique variant slug.
216+
# ``encoding_name`` drives technique selection and user-facing grouping (display_group);
217+
# ``variant_slug`` is unique per row so atomic-attack names stay unique even when one
218+
# encoding name maps to multiple converter variants (e.g. base64, ascii85).
219+
# NOTE: near-duplicate base64 variants were trimmed alongside the VERSION bump
220+
# (``standard_b64encode`` is byte-identical to the default ``b64encode``; ``b2a_base64``
221+
# only appends a trailing newline). We keep the default encoding plus the url-safe alphabet,
222+
# which is a genuinely distinct representation.
223+
all_converters_with_encodings: list[tuple[list[PromptConverter], str, str]] = [
224+
([Base64Converter()], "base64", "base64"),
225+
([Base64Converter(encoding_func="urlsafe_b64encode")], "base64", "base64_urlsafe"),
226+
([Base2048Converter()], "base2048", "base2048"),
227+
([Base64Converter(encoding_func="b16encode")], "base16", "base16"),
228+
([Base64Converter(encoding_func="b32encode")], "base32", "base32"),
229+
([Base64Converter(encoding_func="a85encode")], "ascii85", "ascii85_a85"),
230+
([Base64Converter(encoding_func="b85encode")], "ascii85", "ascii85_b85"),
231+
([BinAsciiConverter(encoding_func="hex")], "hex", "hex"),
232+
([BinAsciiConverter(encoding_func="quoted-printable")], "quoted_printable", "quoted_printable"),
233+
([BinAsciiConverter(encoding_func="UUencode")], "uuencode", "uuencode"),
234+
([ROT13Converter()], "rot13", "rot13"),
235+
([BrailleConverter()], "braille", "braille"),
236+
([AtbashConverter()], "atbash", "atbash"),
237+
([MorseConverter()], "morse_code", "morse_code"),
238+
([NatoConverter()], "nato", "nato"),
239+
([EcojiConverter()], "ecoji", "ecoji"),
240+
([ZalgoConverter()], "zalgo", "zalgo"),
241+
([LeetspeakConverter()], "leet_speak", "leet_speak"),
242+
([AsciiSmugglerConverter()], "ascii_smuggler", "ascii_smuggler"),
221243
]
222244

223245
# Filter to only include selected techniques
224246
selected_encoding_names = {s.value for s in self._scenario_techniques}
225247
converters_with_encodings = [
226-
(conv, name) for conv, name in all_converters_with_encodings if name in selected_encoding_names
248+
(conv, name, variant_slug)
249+
for conv, name, variant_slug in all_converters_with_encodings
250+
if name in selected_encoding_names
227251
]
228252

229253
atomic_attacks = []
230-
for conv, name in converters_with_encodings:
254+
for conv, name, variant_slug in converters_with_encodings:
231255
atomic_attacks.extend(
232-
self._get_prompt_attacks(converters=conv, encoding_name=name, seed_groups=seed_groups)
256+
self._get_prompt_attacks(
257+
converters=conv, encoding_name=name, variant_slug=variant_slug, seed_groups=seed_groups
258+
)
233259
)
234260
return atomic_attacks
235261

236262
def _get_prompt_attacks(
237-
self, *, converters: list[PromptConverter], encoding_name: str, seed_groups: list[SeedAttackGroup]
263+
self,
264+
*,
265+
converters: list[PromptConverter],
266+
encoding_name: str,
267+
variant_slug: str,
268+
seed_groups: list[SeedAttackGroup],
238269
) -> list[AtomicAttack]:
239270
"""
240-
Create atomic attacks for a specific encoding scheme.
271+
Create atomic attacks for a specific encoding converter variant.
241272
242273
For each seed prompt (the text to be decoded), creates atomic attacks that:
243274
1. Encode the seed prompt using the specified converter(s)
@@ -247,32 +278,43 @@ def _get_prompt_attacks(
247278
248279
Args:
249280
converters (list[PromptConverter]): The list of converters to apply to the seed prompts.
250-
encoding_name (str): Human-readable name of the encoding scheme (e.g., "Base64", "ROT13").
281+
encoding_name (str): Human-readable name of the encoding scheme (e.g., "base64", "rot13").
282+
Used as the ``display_group`` so all variants of an encoding aggregate together in output.
283+
variant_slug (str): Unique slug for this converter variant, used to build a unique
284+
``atomic_attack_name`` per converter variant and prompt config.
251285
seed_groups (list[SeedAttackGroup]): Seed groups the attacks draw from.
252286
253287
Returns:
254-
list[AtomicAttack]: List of atomic attacks for this encoding scheme.
288+
list[AtomicAttack]: List of atomic attacks for this encoding converter variant.
255289
256290
Raises:
257291
ValueError: If scenario is not properly initialized.
258292
"""
259-
converter_configs = [
260-
AttackConverterConfig(
261-
request_converters=PromptConverterConfiguration.from_converters(converters=converters)
293+
# (config_name_suffix, converter_config). The bare "raw" config encodes only; each
294+
# decode-template config additionally asks the model to decode.
295+
converter_configs: list[tuple[str, AttackConverterConfig]] = [
296+
(
297+
"raw",
298+
AttackConverterConfig(
299+
request_converters=PromptConverterConfiguration.from_converters(converters=converters)
300+
),
262301
)
263302
]
264303

265-
for decode_type in self._encoding_templates:
304+
for decode_index, decode_type in enumerate(self._encoding_templates):
266305
converters_ = converters[:] + [AskToDecodeConverter(template=decode_type, encoding_name=encoding_name)]
267306

268307
converter_configs.append(
269-
AttackConverterConfig(
270-
request_converters=PromptConverterConfiguration.from_converters(converters=converters_)
308+
(
309+
f"decode{decode_index}",
310+
AttackConverterConfig(
311+
request_converters=PromptConverterConfiguration.from_converters(converters=converters_)
312+
),
271313
)
272314
)
273315

274316
atomic_attacks = []
275-
for attack_converter_config in converter_configs:
317+
for config_suffix, attack_converter_config in converter_configs:
276318
# objective_target is guaranteed to be non-None by parent class validation
277319
if self._objective_target is None:
278320
raise ValueError(
@@ -285,7 +327,8 @@ def _get_prompt_attacks(
285327
)
286328
atomic_attacks.append(
287329
AtomicAttack(
288-
atomic_attack_name=encoding_name,
330+
atomic_attack_name=f"{variant_slug}_{config_suffix}",
331+
display_group=encoding_name,
289332
attack_technique=AttackTechnique(attack=attack),
290333
seed_groups=seed_groups,
291334
)

0 commit comments

Comments
 (0)