From b1f257f75ad9b05539902fff6fcfa973d4ac0f0a Mon Sep 17 00:00:00 2001 From: yonatanzunger <30514250+yonatanzunger@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:11:06 -0700 Subject: [PATCH 1/4] Add UTR39 confusability converter --- pyproject.toml | 1 + pyrit/prompt_converter/__init__.py | 4 +++ .../unicode_confusable_converter.py | 36 +++++++++++++++++++ tests/test_prompt_converter.py | 6 ++++ 4 files changed, 47 insertions(+) create mode 100644 pyrit/prompt_converter/unicode_confusable_converter.py diff --git a/pyproject.toml b/pyproject.toml index a88a8e0b9..251a4e086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "azure-identity>=1.12.0", "azure-ai-ml==1.13.0", "azure-storage-blob>=12.19.0", + "confusables>=1.2.0", "duckdb==0.10.0", "duckdb-engine==0.11.2", "jsonpickle>=3.0.2", diff --git a/pyrit/prompt_converter/__init__.py b/pyrit/prompt_converter/__init__.py index 94a43899b..8f7399c44 100644 --- a/pyrit/prompt_converter/__init__.py +++ b/pyrit/prompt_converter/__init__.py @@ -9,9 +9,11 @@ from pyrit.prompt_converter.rot13_converter import ROT13Converter from pyrit.prompt_converter.string_join_converter import StringJoinConverter from pyrit.prompt_converter.translation_converter import TranslationConverter +from pyrit.prompt_converter.unicode_confusable_converter import UnicodeConfusableConverter from pyrit.prompt_converter.unicode_sub_converter import UnicodeSubstitutionConverter from pyrit.prompt_converter.variation_converter import VariationConverter + __all__ = [ "AsciiArtConverter", "Base64Converter", @@ -19,7 +21,9 @@ "PromptConverter", "ROT13Converter", "StringJoinConverter", + "StringJoinConverter", "TranslationConverter", + "UnicodeConfusableConverter", "UnicodeSubstitutionConverter", "VariationConverter", ] diff --git a/pyrit/prompt_converter/unicode_confusable_converter.py b/pyrit/prompt_converter/unicode_confusable_converter.py new file mode 100644 index 000000000..d910fddd1 --- /dev/null +++ b/pyrit/prompt_converter/unicode_confusable_converter.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import random +from pyrit.prompt_converter import PromptConverter +from confusables import confusable_characters + + +class UnicodeConfusableConverter(PromptConverter): + def __init__(self, deterministic: bool = False): + """Set up a converter. The 'deterministic' argument is for unittesting only.""" + self.deterministic = deterministic + + def convert(self, prompts: list[str]) -> list[str]: + """ + Converts the given prompts into things that look similar, but are actually different, + using Unicode confusables -- e.g., replacing a Latin 'a' with a Cyrillic 'а'. + + This is sort of running UTR-39 in reverse, *introducing* confusables rather than + removing them. (https://www.unicode.org/reports/tr39/tr39-1.html) + + Args: + prompts (list[str]): The prompts to be converted. + + Returns: + list[str]: The converted representations of the prompts. + """ + return ["".join(self._confusable(c) for c in prompt) for prompt in prompts] + + def _confusable(self, char: str) -> str: + """Pick a confusable character for the given character.""" + if char == " ": + return char + + choices = confusable_characters(char) + return choices[-1] if len(choices) == 1 or self.deterministic else random.choice(choices) diff --git a/tests/test_prompt_converter.py b/tests/test_prompt_converter.py index f7991be7c..b2f99cc43 100644 --- a/tests/test_prompt_converter.py +++ b/tests/test_prompt_converter.py @@ -5,6 +5,7 @@ Base64Converter, NoOpConverter, UnicodeSubstitutionConverter, + UnicodeConfusableConverter, StringJoinConverter, ROT13Converter, AsciiArtConverter, @@ -106,3 +107,8 @@ def test_translator_converter_languages_validation_throws(languages): prompt_target = MockPromptTarget() with pytest.raises(ValueError): TranslationConverter(converter_target=prompt_target, languages=languages) + + +def test_unicode_confusable_converter() -> None: + converter = UnicodeConfusableConverter(deterministic=True) + assert converter.convert(["lorem ipsum dolor sit amet"]) == ["ïỎ𐒴ḕ𝗠 ïṗṡ𝘶𝗠 𝑫ỎïỎ𐒴 ṡï𝚝 ḁ𝗠ḕ𝚝"] From f8dc5c9e79852555a29e40c2a38941806a673177 Mon Sep 17 00:00:00 2001 From: yonatanzunger <30514250+yonatanzunger@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:01:12 -0700 Subject: [PATCH 2/4] Remove redundant line --- pyrit/prompt_converter/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyrit/prompt_converter/__init__.py b/pyrit/prompt_converter/__init__.py index 8f7399c44..e6ec88189 100644 --- a/pyrit/prompt_converter/__init__.py +++ b/pyrit/prompt_converter/__init__.py @@ -21,7 +21,6 @@ "PromptConverter", "ROT13Converter", "StringJoinConverter", - "StringJoinConverter", "TranslationConverter", "UnicodeConfusableConverter", "UnicodeSubstitutionConverter", From c9e067deae179144107d04440496ee3b0aacec5f Mon Sep 17 00:00:00 2001 From: yonatanzunger <30514250+yonatanzunger@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:20:40 -0700 Subject: [PATCH 3/4] Fixes per PR comments. --- pyproject.toml | 2 +- pyrit/prompt_converter/unicode_confusable_converter.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 251a4e086..08cbe83d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "azure-identity>=1.12.0", "azure-ai-ml==1.13.0", "azure-storage-blob>=12.19.0", - "confusables>=1.2.0", + "confusables==1.2.0", "duckdb==0.10.0", "duckdb-engine==0.11.2", "jsonpickle>=3.0.2", diff --git a/pyrit/prompt_converter/unicode_confusable_converter.py b/pyrit/prompt_converter/unicode_confusable_converter.py index d910fddd1..e9704f256 100644 --- a/pyrit/prompt_converter/unicode_confusable_converter.py +++ b/pyrit/prompt_converter/unicode_confusable_converter.py @@ -27,10 +27,16 @@ def convert(self, prompts: list[str]) -> list[str]: """ return ["".join(self._confusable(c) for c in prompt) for prompt in prompts] + def is_one_to_one_converter(self) -> bool: + return True + def _confusable(self, char: str) -> str: """Pick a confusable character for the given character.""" if char == " ": return char - choices = confusable_characters(char) - return choices[-1] if len(choices) == 1 or self.deterministic else random.choice(choices) + confusable_options = confusable_characters(char) + if len(confusable_options) < 2 or self.deterministic: + return confusable_options[-1] + else: + return random.choice(confusable_options) From 95bba7072f2fe71cb117655b03b599cfacf2c563 Mon Sep 17 00:00:00 2001 From: yonatanzunger <30514250+yonatanzunger@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:32:01 -0700 Subject: [PATCH 4/4] Actually this is a bit more robust in case confusables ever returns an empty set. --- pyrit/prompt_converter/unicode_confusable_converter.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyrit/prompt_converter/unicode_confusable_converter.py b/pyrit/prompt_converter/unicode_confusable_converter.py index e9704f256..e01b59939 100644 --- a/pyrit/prompt_converter/unicode_confusable_converter.py +++ b/pyrit/prompt_converter/unicode_confusable_converter.py @@ -32,11 +32,10 @@ def is_one_to_one_converter(self) -> bool: def _confusable(self, char: str) -> str: """Pick a confusable character for the given character.""" - if char == " ": - return char - confusable_options = confusable_characters(char) - if len(confusable_options) < 2 or self.deterministic: + if not confusable_options or char == " ": + return char + elif self.deterministic or len(confusable_options) == 1: return confusable_options[-1] else: return random.choice(confusable_options)