Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions pyrit/converter/bin_ascii_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,12 @@ def _build_identifier(self) -> ComponentIdentifier:
Returns:
ComponentIdentifier: The identifier for this converter.
"""
return self._create_identifier(
params={
"word_selection_strategy": self._word_selection_strategy.__class__.__name__,
"word_split_separator": self._word_split_separator,
"encoding_func": self._encoding_func,
}
)
params = dict(super()._build_identifier().params)
if not params["word_selection_strategy_params"]:
# Preserve existing identities for parameterless selection strategies.
del params["word_selection_strategy_params"]
params["encoding_func"] = self._encoding_func
return self._create_identifier(params=params)

async def convert_word_async(self, word: str) -> str:
"""
Expand Down
7 changes: 7 additions & 0 deletions pyrit/converter/charswap_attack_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ def _build_identifier(self) -> ComponentIdentifier:
Returns:
ComponentIdentifier: The identifier for this converter.
"""
selection_params = dict(super()._build_identifier().params)
# Preserve the legacy identity for the default 20% selection.
if type(self._word_selection_strategy) is WordProportionSelectionStrategy and selection_params[
"word_selection_strategy_params"
] == {"proportion": 0.2}:
selection_params = {}
return self._create_identifier(
params={
**selection_params,
"max_iterations": self._max_iterations,
"seed": self._seed,
},
Expand Down
12 changes: 5 additions & 7 deletions pyrit/converter/first_letter_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ def _build_identifier(self) -> ComponentIdentifier:
Returns:
ComponentIdentifier: The identifier for this converter.
"""
return self._create_identifier(
params={
"word_selection_strategy": self._word_selection_strategy.__class__.__name__,
"word_split_separator": self._word_split_separator,
"letter_separator": self.letter_separator,
}
)
params = dict(super()._build_identifier().params)
if not params["word_selection_strategy_params"]:
del params["word_selection_strategy_params"]
params["letter_separator"] = self.letter_separator
return self._create_identifier(params=params)

async def convert_word_async(self, word: str) -> str:
"""
Expand Down
8 changes: 7 additions & 1 deletion pyrit/converter/leetspeak_converter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pyrit.converter.text_selection_strategy import WordSelectionStrategy
from pyrit.converter.text_selection_strategy import AllWordsSelectionStrategy, WordSelectionStrategy
from pyrit.converter.word_level_converter import WordLevelConverter
from pyrit.models import ComponentIdentifier

Expand Down Expand Up @@ -65,8 +65,14 @@ def _build_identifier(self) -> ComponentIdentifier:
substitutions_str = json.dumps(self._leet_substitutions, sort_keys=True)
substitutions_hash = hashlib.sha256(substitutions_str.encode("utf-8")).hexdigest()[:16]

selection_params = (
{}
if type(self._word_selection_strategy) is AllWordsSelectionStrategy
else super()._build_identifier().params
)
return self._create_identifier(
params={
**selection_params,
"deterministic": self._deterministic,
"custom_substitutions_hash": substitutions_hash,
},
Expand Down
8 changes: 4 additions & 4 deletions pyrit/converter/text_selection_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ def __init__(self, *, indices: list[int]) -> None:
Initialize the word index selection strategy.

Args:
indices (list[int]): The list of word indices to select.
indices (list[int]): The list of word indices in conversion order.

Raises:
ValueError: If an index is negative.
Expand All @@ -562,12 +562,12 @@ def __init__(self, *, indices: list[int]) -> None:

def get_identifier_params(self) -> dict[str, Any]:
"""
Return the selected indices in sorted order.
Return the selected indices in conversion order.

Returns:
dict[str, Any]: The sorted index list.
dict[str, Any]: The ordered index list.
"""
return {"indices": sorted(self._indices)}
return {"indices": list(self._indices)}

def select_words(self, *, words: list[str]) -> list[int]:
"""
Expand Down
12 changes: 5 additions & 7 deletions pyrit/converter/unicode_replacement_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@ def _build_identifier(self) -> ComponentIdentifier:
Returns:
ComponentIdentifier: The identifier for this converter.
"""
return self._create_identifier(
params={
"word_selection_strategy": self._word_selection_strategy.__class__.__name__,
"word_split_separator": self._word_split_separator,
"encode_spaces": self.encode_spaces,
}
)
params = dict(super()._build_identifier().params)
if not params["word_selection_strategy_params"]:
del params["word_selection_strategy_params"]
params["encode_spaces"] = self.encode_spaces
return self._create_identifier(params=params)

async def convert_word_async(self, word: str) -> str:
"""
Expand Down
8 changes: 7 additions & 1 deletion pyrit/converter/zalgo_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from typing import ClassVar

from pyrit.converter.text_selection_strategy import WordSelectionStrategy
from pyrit.converter.text_selection_strategy import AllWordsSelectionStrategy, WordSelectionStrategy
from pyrit.converter.word_level_converter import WordLevelConverter
from pyrit.models import ComponentIdentifier

Expand Down Expand Up @@ -50,8 +50,14 @@ def _build_identifier(self) -> ComponentIdentifier:
Returns:
ComponentIdentifier: The identifier for this converter.
"""
selection_params = (
{}
if type(self._word_selection_strategy) is AllWordsSelectionStrategy
else super()._build_identifier().params
)
return self._create_identifier(
params={
**selection_params,
"intensity": self._intensity,
},
)
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/converter/test_bin_ascii_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
import pytest

from pyrit.converter import (
AllWordsSelectionStrategy,
BinAsciiConverter,
WordIndexSelectionStrategy,
WordKeywordSelectionStrategy,
WordProportionSelectionStrategy,
WordSelectionStrategy,
)
from pyrit.registry import ConverterRegistry


class TestBinAsciiConverterHex:
Expand Down Expand Up @@ -221,3 +225,72 @@ async def test_none_separator_joins_with_encoded_space(self, encoding_func: str)
space_result = await with_space.convert_async(prompt="alpha beta", input_type="text")

assert none_result.output_text == space_result.output_text


@pytest.mark.parametrize(
("first_strategy", "second_strategy"),
[
(WordIndexSelectionStrategy(indices=[0]), WordIndexSelectionStrategy(indices=[1])),
(WordKeywordSelectionStrategy(keywords=["ab"]), WordKeywordSelectionStrategy(keywords=["cd"])),
],
)
async def test_identifier_distinguishes_word_selection_async(
*, first_strategy: WordSelectionStrategy, second_strategy: WordSelectionStrategy
) -> None:
first = BinAsciiConverter(word_selection_strategy=first_strategy)
second = BinAsciiConverter(word_selection_strategy=second_strategy)

first_result = await first.convert_async(prompt="ab cd", input_type="text")
second_result = await second.convert_async(prompt="ab cd", input_type="text")

assert first_result.output_text == "6162 cd"
assert second_result.output_text == "ab 6364"
assert first.get_identifier().hash != second.get_identifier().hash


@pytest.mark.parametrize("encoding_func", ["hex", "quoted-printable", "UUencode"])
def test_registry_accepts_distinct_word_selections(encoding_func: BinAsciiConverter.EncodingFunc) -> None:
first = BinAsciiConverter(
encoding_func=encoding_func, word_selection_strategy=WordIndexSelectionStrategy(indices=[0])
)
second = BinAsciiConverter(
encoding_func=encoding_func, word_selection_strategy=WordIndexSelectionStrategy(indices=[1])
)
instances = ConverterRegistry().instances

instances.register(first)
instances.register(second)

assert instances.get(first.get_identifier().unique_name) is first
assert instances.get(second.get_identifier().unique_name) is second


async def test_identifier_preserves_index_order_async() -> None:
first = BinAsciiConverter(word_selection_strategy=WordIndexSelectionStrategy(indices=[0, 1]))
second = BinAsciiConverter(word_selection_strategy=WordIndexSelectionStrategy(indices=[1, 0]))

first_result = await first.convert_async(prompt="ab cd", input_type="text")
second_result = await second.convert_async(prompt="ab cd", input_type="text")

assert first_result.output_text == second_result.output_text == "6162 6364"
assert first.get_identifier().hash != second.get_identifier().hash


@pytest.mark.parametrize("explicit_all_words", [False, True])
@pytest.mark.parametrize(
("encoding_func", "expected_hash"),
[
("hex", "997d88728fc485b88e30323fd1dc368292209d8ada8ed58b70e58aa8aadfe108"),
("quoted-printable", "fc9df198b3e1199c620bccc6f86bbb73477641f85c76a62b7048cbf9e16aa170"),
("UUencode", "20bc6dd2594880b93f9dab7c483ec5eee3fb8049993bb614922e59ef48a4b8e7"),
],
)
def test_identifier_preserves_default_hash(
*, encoding_func: BinAsciiConverter.EncodingFunc, expected_hash: str, explicit_all_words: bool
) -> None:
converter = BinAsciiConverter(
encoding_func=encoding_func,
word_selection_strategy=AllWordsSelectionStrategy() if explicit_all_words else None,
)

assert converter.get_identifier().hash == expected_hash
6 changes: 3 additions & 3 deletions tests/unit/converter/test_string_join_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ def test_string_join_identifier_includes_selection_parameters() -> None:
assert first.get_identifier().unique_name != second.get_identifier().unique_name


def test_string_join_identifier_normalizes_equivalent_index_sets() -> None:
def test_string_join_identifier_preserves_index_order() -> None:
first = StringJoinConverter(
word_selection_strategy=WordIndexSelectionStrategy(indices=[1, 0]),
)
second = StringJoinConverter(
word_selection_strategy=WordIndexSelectionStrategy(indices=[0, 1]),
)

assert first.get_identifier().hash == second.get_identifier().hash
assert first.get_identifier().hash != second.get_identifier().hash


def test_string_join_registry_accepts_distinct_selection_configurations() -> None:
Expand All @@ -113,7 +113,7 @@ def test_string_join_identifier_parameter_contents(*, indices: list[int], join_v
"supported_input_types": ["text"],
"supported_output_types": ["text"],
"word_selection_strategy": "WordIndexSelectionStrategy",
"word_selection_strategy_params": {"indices": sorted(indices)},
"word_selection_strategy_params": {"indices": indices},
"word_split_separator": " ",
"join_value": join_value,
}
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/converter/test_text_selection_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,11 @@ def test_select_range_converts_words_to_chars(self):
# "quick brown" starts at index 4 and ends at index 15
assert result == (4, 15)

def test_identifier_params_are_order_independent(self):
def test_identifier_params_preserve_index_order(self):
left = WordIndexSelectionStrategy(indices=[2, 0])
right = WordIndexSelectionStrategy(indices=[0, 2])
assert left.get_identifier_params() == right.get_identifier_params()
assert left.get_identifier_params() == {"indices": [0, 2]}
assert left.get_identifier_params() == {"indices": [2, 0]}
assert right.get_identifier_params() == {"indices": [0, 2]}


class TestWordKeywordSelectionStrategy:
Expand Down
Loading