diff --git a/doc/bibliography.md b/doc/bibliography.md index c1d391ff17..dc1eec5762 100644 --- a/doc/bibliography.md +++ b/doc/bibliography.md @@ -5,6 +5,6 @@ All academic papers, research blogs, and technical reports referenced throughout :::{dropdown} Citation Keys :class: hidden-citations -[@aakanksha2024multilingual; @adversaai2023universal; @andriushchenko2024tense; @anthropic2024manyshot; @aqrawi2024singleturncrescendo; @atr2026; @bethany2024mathprompt; @bhardwaj2023harmfulqa; @bhardwaj2024homer; @boucher2023trojan; @brahman2024coconot; @bryan2025agentictaxonomy; @bullwinkel2025airtlessons; @bullwinkel2025repeng; @bullwinkel2026trigger; @chao2023pair; @chao2024jailbreakbench; @choi2026xlsafetybench; @cui2024orbench; @darkbench2025; @derczynski2024garak; @ding2023wolf; @embracethered2024unicode; @embracethered2025sneakybits; @gehman2020realtoxicityprompts; @ghosh2025aegis; @ghosh2025ailuminate; @gong2025figstep; @gupta2024walledeval; @haider2024phi3safety; @han2024medsafetybench; @han2024wildguard; @hiddenlayer2025policypuppetry; @hines2024spotlighting; @inie2025summon; @ji2023beavertails; @ji2024pkusaferlhf; @jiang2025sosbench; @jones2025computeruse; @kingma2014adam; @li2024drattack; @li2024mossbench; @li2024saladbench; @li2024wmdp; @lin2023toxicchat; @liu2024flipattack; @liu2024mmsafetybench; @lopez2024pyrit; @luo2024jailbreakv; @lv2024codechameleon; @mazeika2023tdc; @mazeika2024harmbench; @mckee2024transparency; @mehrotra2023tap; @microsoft2024skeletonkey; @odin2024; @palaskar2025vlsu; @pfohl2024equitymedqa; @promptfoo2025ccp; @robustintelligence2024bypass; @roccia2024promptintel; @rottger2023xstest; @rottger2025msts; @russinovich2024crescendo; @russinovich2025cca; @russinovich2025price; @scheuerman2025transphobia; @shaikh2022second; @shayegani2025computeruse; @shen2023donotanything; @sheshadri2024lat; @souly2024strongreject; @stok2023ansi; @tan2026comicjailbreak; @tang2025multilingual; @tedeschi2024alert; @vantaylor2024socialbias; @vidgen2023simplesafetytests; @wang2023decodingtrust; @wang2023donotanswer; @wang2025siuo; @wang2026visualleakbench; @wei2023jailbroken; @xie2024sorrybench; @yu2023gptfuzzer; @yuan2023cipherchat; @zeng2024persuasion; @zeng2024shieldgemma; @zhang2024cbtbench; @ziems2022mic; @zong2024vlguard; @zou2023gcg] +[@aakanksha2024multilingual; @adversaai2023universal; @andriushchenko2024tense; @anthropic2024manyshot; @aqrawi2024singleturncrescendo; @atr2026; @bethany2024mathprompt; @bhardwaj2023harmfulqa; @bhardwaj2024homer; @boucher2023trojan; @brahman2024coconot; @bryan2025agentictaxonomy; @bullwinkel2025airtlessons; @bullwinkel2025repeng; @bullwinkel2026trigger; @chao2023pair; @chao2024jailbreakbench; @choi2026xlsafetybench; @cui2024orbench; @darkbench2025; @deniz2026turkishpromptinjection; @derczynski2024garak; @ding2023wolf; @embracethered2024unicode; @embracethered2025sneakybits; @gehman2020realtoxicityprompts; @ghosh2025aegis; @ghosh2025ailuminate; @gong2025figstep; @gupta2024walledeval; @haider2024phi3safety; @han2024medsafetybench; @han2024wildguard; @hiddenlayer2025policypuppetry; @hines2024spotlighting; @inie2025summon; @ji2023beavertails; @ji2024pkusaferlhf; @jiang2025sosbench; @jones2025computeruse; @kingma2014adam; @li2024drattack; @li2024mossbench; @li2024saladbench; @li2024wmdp; @lin2023toxicchat; @liu2024flipattack; @liu2024mmsafetybench; @lopez2024pyrit; @luo2024jailbreakv; @lv2024codechameleon; @mazeika2023tdc; @mazeika2024harmbench; @mckee2024transparency; @mehrotra2023tap; @microsoft2024skeletonkey; @odin2024; @palaskar2025vlsu; @pfohl2024equitymedqa; @promptfoo2025ccp; @robustintelligence2024bypass; @roccia2024promptintel; @rottger2023xstest; @rottger2025msts; @russinovich2024crescendo; @russinovich2025cca; @russinovich2025price; @scheuerman2025transphobia; @shaikh2022second; @shayegani2025computeruse; @shen2023donotanything; @sheshadri2024lat; @souly2024strongreject; @stok2023ansi; @tan2026comicjailbreak; @tang2025multilingual; @tedeschi2024alert; @vantaylor2024socialbias; @vidgen2023simplesafetytests; @wang2023decodingtrust; @wang2023donotanswer; @wang2025siuo; @wang2026visualleakbench; @wei2023jailbroken; @xie2024sorrybench; @yu2023gptfuzzer; @yuan2023cipherchat; @zeng2024persuasion; @zeng2024shieldgemma; @zhang2024cbtbench; @ziems2022mic; @zong2024vlguard; @zou2023gcg] ::: diff --git a/doc/references.bib b/doc/references.bib index c1397ec3b5..941de2fff6 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -39,6 +39,15 @@ @article{tedeschi2024alert url = {https://arxiv.org/abs/2404.08676}, } +@misc{deniz2026turkishpromptinjection, + title = {Turkish Prompt-Injection Dataset}, + author = {Enes Deniz}, + year = {2026}, + howpublished = {Hugging Face}, + url = {https://huggingface.co/datasets/3nesdeniz/turkish-prompt-injection-1k}, + note = {CC BY 4.0}, +} + @article{derczynski2024garak, title = {garak: A Framework for Security Probing Large Language Models}, author = {Leon Derczynski and Erick Galinkin and Jeffrey Martin and Subho Majumdar and Nanna Inie}, diff --git a/pyrit/datasets/seed_datasets/remote/__init__.py b/pyrit/datasets/seed_datasets/remote/__init__.py index 986d24a844..a6e731b482 100644 --- a/pyrit/datasets/seed_datasets/remote/__init__.py +++ b/pyrit/datasets/seed_datasets/remote/__init__.py @@ -17,12 +17,20 @@ ATRVariationType, _AgentThreatRulesDataset, ) -from pyrit.datasets.seed_datasets.remote.aya_redteaming_dataset import _AyaRedteamingDataset -from pyrit.datasets.seed_datasets.remote.babelscape_alert_dataset import _BabelscapeAlertDataset +from pyrit.datasets.seed_datasets.remote.aya_redteaming_dataset import ( + _AyaRedteamingDataset, +) +from pyrit.datasets.seed_datasets.remote.babelscape_alert_dataset import ( + _BabelscapeAlertDataset, +) from pyrit.datasets.seed_datasets.remote.beaver_tails_dataset import _BeaverTailsDataset -from pyrit.datasets.seed_datasets.remote.categorical_harmful_qa_dataset import _CategoricalHarmfulQADataset +from pyrit.datasets.seed_datasets.remote.categorical_harmful_qa_dataset import ( + _CategoricalHarmfulQADataset, +) from pyrit.datasets.seed_datasets.remote.cbt_bench_dataset import _CBTBenchDataset -from pyrit.datasets.seed_datasets.remote.ccp_sensitive_prompts_dataset import _CCPSensitivePromptsDataset +from pyrit.datasets.seed_datasets.remote.ccp_sensitive_prompts_dataset import ( + _CCPSensitivePromptsDataset, +) from pyrit.datasets.seed_datasets.remote.coconot_dataset import ( CoCoNotCategory, CoCoNotSplit, @@ -41,9 +49,17 @@ _DecodingTrustToxicityDataset, ) from pyrit.datasets.seed_datasets.remote.equitymedqa_dataset import _EquityMedQADataset -from pyrit.datasets.seed_datasets.remote.figstep_dataset import FigStepCategory, FigStepVariant, _FigStepDataset -from pyrit.datasets.seed_datasets.remote.forbidden_questions_dataset import _ForbiddenQuestionsDataset -from pyrit.datasets.seed_datasets.remote.garak_audio_dataset import _GarakAudioAchillesHeelDataset +from pyrit.datasets.seed_datasets.remote.figstep_dataset import ( + FigStepCategory, + FigStepVariant, + _FigStepDataset, +) +from pyrit.datasets.seed_datasets.remote.forbidden_questions_dataset import ( + _ForbiddenQuestionsDataset, +) +from pyrit.datasets.seed_datasets.remote.garak_audio_dataset import ( + _GarakAudioAchillesHeelDataset, +) from pyrit.datasets.seed_datasets.remote.garak_package_hallucination_dataset import ( _GarakCratesDataset, _GarakDartDataset, @@ -58,27 +74,51 @@ _GarakTmSystemPromptDataset, ) from pyrit.datasets.seed_datasets.remote.harmbench_dataset import _HarmBenchDataset -from pyrit.datasets.seed_datasets.remote.harmbench_multimodal_dataset import _HarmBenchMultimodalDataset +from pyrit.datasets.seed_datasets.remote.harmbench_multimodal_dataset import ( + _HarmBenchMultimodalDataset, +) from pyrit.datasets.seed_datasets.remote.harmful_qa_dataset import _HarmfulQADataset -from pyrit.datasets.seed_datasets.remote.hixstest_dataset import HiXSTestLanguage, _HiXSTestDataset -from pyrit.datasets.seed_datasets.remote.jailbreakv_28k_dataset import _JailbreakV28KDataset -from pyrit.datasets.seed_datasets.remote.jailbreakv_redteam_2k_dataset import _JailbreakVRedteam2KDataset -from pyrit.datasets.seed_datasets.remote.jbb_behaviors_dataset import _JBBBehaviorsDataset -from pyrit.datasets.seed_datasets.remote.librai_do_not_answer_dataset import _LibrAIDoNotAnswerDataset +from pyrit.datasets.seed_datasets.remote.hixstest_dataset import ( + HiXSTestLanguage, + _HiXSTestDataset, +) +from pyrit.datasets.seed_datasets.remote.jailbreakv_28k_dataset import ( + _JailbreakV28KDataset, +) +from pyrit.datasets.seed_datasets.remote.jailbreakv_redteam_2k_dataset import ( + _JailbreakVRedteam2KDataset, +) +from pyrit.datasets.seed_datasets.remote.jbb_behaviors_dataset import ( + _JBBBehaviorsDataset, +) +from pyrit.datasets.seed_datasets.remote.librai_do_not_answer_dataset import ( + _LibrAIDoNotAnswerDataset, +) from pyrit.datasets.seed_datasets.remote.llm_latent_adversarial_training_dataset import ( _LLMLatentAdversarialTrainingDataset, ) -from pyrit.datasets.seed_datasets.remote.medsafetybench_dataset import _MedSafetyBenchDataset -from pyrit.datasets.seed_datasets.remote.mlcommons_ailuminate_dataset import _MLCommonsAILuminateDataset +from pyrit.datasets.seed_datasets.remote.medsafetybench_dataset import ( + _MedSafetyBenchDataset, +) +from pyrit.datasets.seed_datasets.remote.mlcommons_ailuminate_dataset import ( + _MLCommonsAILuminateDataset, +) from pyrit.datasets.seed_datasets.remote.mm_safetybench_dataset import ( MMSafetyBenchCategory, MMSafetyBenchVariant, _MMSafetyBenchDataset, ) -from pyrit.datasets.seed_datasets.remote.moral_integrity_corpus_dataset import _MICDataset -from pyrit.datasets.seed_datasets.remote.mossbench_dataset import MossBenchOversensitivityType, _MossBenchDataset +from pyrit.datasets.seed_datasets.remote.moral_integrity_corpus_dataset import ( + _MICDataset, +) +from pyrit.datasets.seed_datasets.remote.mossbench_dataset import ( + MossBenchOversensitivityType, + _MossBenchDataset, +) from pyrit.datasets.seed_datasets.remote.msts_dataset import _MSTSDataset -from pyrit.datasets.seed_datasets.remote.multilingual_vulnerability_dataset import _MultilingualVulnerabilityDataset +from pyrit.datasets.seed_datasets.remote.multilingual_vulnerability_dataset import ( + _MultilingualVulnerabilityDataset, +) from pyrit.datasets.seed_datasets.remote.odin_dataset import ( ODINSecurityBoundary, ODINSeverity, @@ -90,24 +130,47 @@ _ORBenchHardDataset, _ORBenchToxicDataset, ) -from pyrit.datasets.seed_datasets.remote.pku_safe_rlhf_dataset import _PKUSafeRLHFDataset +from pyrit.datasets.seed_datasets.remote.pku_safe_rlhf_dataset import ( + _PKUSafeRLHFDataset, +) from pyrit.datasets.seed_datasets.remote.promptintel_dataset import ( PromptIntelCategory, PromptIntelSeverity, _PromptIntelDataset, ) -from pyrit.datasets.seed_datasets.remote.red_team_social_bias_dataset import _RedTeamSocialBiasDataset -from pyrit.datasets.seed_datasets.remote.remote_dataset_loader import _RemoteDatasetLoader +from pyrit.datasets.seed_datasets.remote.red_team_social_bias_dataset import ( + _RedTeamSocialBiasDataset, +) +from pyrit.datasets.seed_datasets.remote.remote_dataset_loader import ( + _RemoteDatasetLoader, +) from pyrit.datasets.seed_datasets.remote.salad_bench_dataset import _SaladBenchDataset -from pyrit.datasets.seed_datasets.remote.sgxstest_dataset import SGXSTestLabel, _SGXSTestDataset -from pyrit.datasets.seed_datasets.remote.simple_safety_tests_dataset import _SimpleSafetyTestsDataset +from pyrit.datasets.seed_datasets.remote.sgxstest_dataset import ( + SGXSTestLabel, + _SGXSTestDataset, +) +from pyrit.datasets.seed_datasets.remote.simple_safety_tests_dataset import ( + _SimpleSafetyTestsDataset, +) from pyrit.datasets.seed_datasets.remote.siuo_dataset import SIUOCategory, _SIUODataset from pyrit.datasets.seed_datasets.remote.sorry_bench_dataset import _SorryBenchDataset from pyrit.datasets.seed_datasets.remote.sosbench_dataset import _SOSBenchDataset -from pyrit.datasets.seed_datasets.remote.strong_reject_dataset import _StrongRejectDataset -from pyrit.datasets.seed_datasets.remote.tdc23_redteaming_dataset import _TDC23RedteamingDataset +from pyrit.datasets.seed_datasets.remote.strong_reject_dataset import ( + _StrongRejectDataset, +) +from pyrit.datasets.seed_datasets.remote.tdc23_redteaming_dataset import ( + _TDC23RedteamingDataset, +) from pyrit.datasets.seed_datasets.remote.toxic_chat_dataset import _ToxicChatDataset -from pyrit.datasets.seed_datasets.remote.transphobia_awareness_dataset import _TransphobiaAwarenessDataset +from pyrit.datasets.seed_datasets.remote.transphobia_awareness_dataset import ( + _TransphobiaAwarenessDataset, +) +from pyrit.datasets.seed_datasets.remote.turkish_prompt_injection_dataset import ( + TurkishPromptInjectionFamily, + TurkishPromptInjectionLabel, + TurkishPromptInjectionSplit, + _TurkishPromptInjectionDataset, +) from pyrit.datasets.seed_datasets.remote.visual_leak_bench_dataset import ( VisualLeakBenchCategory, VisualLeakBenchPIIType, @@ -119,7 +182,9 @@ VLGuardSubset, _VLGuardDataset, ) -from pyrit.datasets.seed_datasets.remote.vlsu_multimodal_dataset import _VLSUMultimodalDataset +from pyrit.datasets.seed_datasets.remote.vlsu_multimodal_dataset import ( + _VLSUMultimodalDataset, +) from pyrit.datasets.seed_datasets.remote.wildguardmix_dataset import ( WildGuardMixAdversarial, WildGuardMixPromptHarmLabel, @@ -155,6 +220,9 @@ "PromptIntelSeverity", "SGXSTestLabel", "SIUOCategory", + "TurkishPromptInjectionFamily", + "TurkishPromptInjectionLabel", + "TurkishPromptInjectionSplit", "VLGuardCategory", "VLGuardSubcategory", "VLGuardSubset", @@ -227,6 +295,7 @@ "_TDC23RedteamingDataset", "_ToxicChatDataset", "_TransphobiaAwarenessDataset", + "_TurkishPromptInjectionDataset", "_VLGuardDataset", "_VLSUMultimodalDataset", "_VisualLeakBenchDataset", diff --git a/pyrit/datasets/seed_datasets/remote/turkish_prompt_injection_dataset.py b/pyrit/datasets/seed_datasets/remote/turkish_prompt_injection_dataset.py new file mode 100644 index 0000000000..4ae4211419 --- /dev/null +++ b/pyrit/datasets/seed_datasets/remote/turkish_prompt_injection_dataset.py @@ -0,0 +1,241 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import logging +from enum import Enum +from typing import Any + +from typing_extensions import override + +from pyrit.datasets.seed_datasets.remote.remote_dataset_loader import ( + _RemoteDatasetLoader, +) +from pyrit.models import Modality, SeedDataset, SeedPrompt, SeedUnion + +logger = logging.getLogger(__name__) + + +class TurkishPromptInjectionLabel(Enum): + """Filter records by the published security decision label.""" + + BENIGN = 0 + ATTACK = 1 + ALL = "all" + + +class TurkishPromptInjectionSplit(Enum): + """Select a published split or combine the complete dataset.""" + + TRAIN = "train" + VALIDATION = "validation" + TEST = "test" + ALL = "all" + + +class TurkishPromptInjectionFamily(Enum): + """Turkish-native prompt-injection families represented in the dataset.""" + + INSTRUCTION_OVERRIDE_EXTRACTION = "tr_instruction_override_extraction" + JAILBREAK_PERSONA = "tr_jailbreak_persona" + OBFUSCATION_EXFILTRATION = "tr_obfuscation_exfiltration" + AGENTIC_TOOL_ABUSE = "tr_agentic_toolabuse" + + +class _TurkishPromptInjectionDataset(_RemoteDatasetLoader): + """ + Loader for the Turkish Prompt-Injection dataset. + + A Turkish-native prompt-injection dataset with paired benign and attack + examples for LLM guardrail and detector evaluation. Attack records cover + instruction override and system-prompt extraction, jailbreak personas, + obfuscation and data exfiltration, and agentic tool abuse, with Turkish + morphological variation. Benign hard negatives share surface vocabulary + with the attacks while being fully legitimate, so false-positive behavior + can be measured directly. Templates are isolated across the train, + validation, and test splits. + + Attack records are returned by default for red-team use. Set ``label`` to + ``ALL`` to load the full benign/attack surface or ``BENIGN`` to load only + the hard negatives. + + Reference: [@deniz2026turkishpromptinjection]. + License: CC BY 4.0. + + The dataset's labels describe prompt-injection decisions rather than content + harms, so they are retained in seed metadata and ``harm_categories`` is + intentionally empty. + """ + + HF_DATASET_NAME: str = "3nesdeniz/turkish-prompt-injection-1k" + HF_DATASET_REVISION: str = "1cbd1152d9732f40148fbd5bb7cf0f58ddfe84c6" + DATASET_VERSION: str = "1.0.0" + harm_categories: list[str] = [] + modalities: tuple[Modality, ...] = (Modality.TEXT,) + size: str = "medium" # 1,000 records + tags: frozenset[str] = frozenset({"safety", "prompt_injection", "turkish", "multilingual", "synthetic"}) + + def __init__( + self, + *, + label: TurkishPromptInjectionLabel = TurkishPromptInjectionLabel.ATTACK, + split: TurkishPromptInjectionSplit = TurkishPromptInjectionSplit.ALL, + attack_families: list[TurkishPromptInjectionFamily] | None = None, + ) -> None: + """ + Initialize the Turkish Prompt-Injection loader. + + Args: + label: Security decision label to load. Defaults to attack records. + split: Published split to load, or all splits. Defaults to all. + attack_families: Optional non-empty list of attack families. Applies + to attack records only; benign records are not family-scoped. + + Raises: + ValueError: If an enum value is invalid or a filter list is empty. + """ + self._validate_enum(label, TurkishPromptInjectionLabel, "label") + self._validate_enum(split, TurkishPromptInjectionSplit, "split") + + if attack_families is not None: + if not attack_families: + raise ValueError("`attack_families` must be a non-empty list (pass None to include all families)") + self._validate_enums(attack_families, TurkishPromptInjectionFamily, "attack_family") + + self._label = label + self._split = split + self._attack_families = {family.value for family in attack_families} if attack_families else None + + @property + @override + def dataset_name(self) -> str: + """The registered dataset name.""" + return "turkish_prompt_injection" + + @classmethod + def _validate_row(cls, *, row: dict[str, Any]) -> None: + """ + Validate the published schema and label-dependent invariants. + + The split is implied by the fetched parquet file rather than stored per + row, so it is not part of the row schema. + + Raises: + ValueError: If the row violates the published dataset schema. + """ + required_keys = { + "id", + "text", + "label", + "class", + "attack_family", + "technique", + "severity", + "language", + } + missing = required_keys - row.keys() + if missing: + raise ValueError(f"Missing keys in Turkish Prompt-Injection entry: {', '.join(sorted(missing))}") + + if not isinstance(row["text"], str) or not row["text"].strip(): + raise ValueError(f"Turkish Prompt-Injection entry has an invalid `text` value: {row['text']!r}") + + row_label = row["label"] + if type(row_label) is not int or row_label not in {0, 1}: + raise ValueError(f"Invalid label in Turkish Prompt-Injection entry: {row_label!r}") + + if row["language"] != "tr": + raise ValueError(f"Turkish Prompt-Injection entry has non-Turkish language: {row['language']!r}") + + valid_families = {family.value for family in TurkishPromptInjectionFamily} + expected_class = "injection" if row_label == 1 else "benign" + if row["class"] != expected_class: + raise ValueError( + f"Turkish Prompt-Injection entry has class={row['class']!r}; expected {expected_class!r} " + f"for label {row_label}" + ) + + if row_label == 1: + if row["attack_family"] not in valid_families: + raise ValueError(f"Invalid attack family in Turkish Prompt-Injection entry: {row['attack_family']!r}") + elif row["attack_family"] != "benign": + raise ValueError( + f"Benign Turkish Prompt-Injection entry must have attack_family 'benign', " + f"got {row['attack_family']!r}" + ) + + @override + async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset: + """ + Fetch the selected records from Hugging Face. + + Args: + cache: Whether Hugging Face may reuse its local cache. + + Returns: + SeedDataset: Literal prompts with label, family, and split metadata. + + Raises: + ValueError: If a row is malformed or the requested filters produce + no records. + """ + split_names = ( + ["train", "validation", "test"] if self._split is TurkishPromptInjectionSplit.ALL else [self._split.value] + ) + + rows: list[dict[str, Any]] = [] + for split_name in split_names: + split_rows = await self._fetch_from_huggingface_async( + dataset_name=self.HF_DATASET_NAME, + config="default", + split=split_name, + cache=cache, + revision=self.HF_DATASET_REVISION, + ) + for split_row in split_rows: + row = dict(split_row) + self._validate_row(row=row) + row["split"] = split_name + rows.append(row) + + source_url = f"https://huggingface.co/datasets/{self.HF_DATASET_NAME}" + authors = ["Enes Deniz"] + groups = ["AltaySec"] + seeds: list[SeedUnion] = [] + + for row in rows: + row_label = row["label"] + if self._label is not TurkishPromptInjectionLabel.ALL and row_label != self._label.value: + continue + + attack_family = row["attack_family"] + if self._attack_families and row_label == 1 and attack_family not in self._attack_families: + continue + + if row_label == 1: + description = f"Turkish prompt-injection case for {attack_family.replace('tr_', '').replace('_', ' ')}." + else: + description = "Paired legitimate Turkish request that shares surface vocabulary with an attack." + + metadata = {key: row[key] for key in sorted(row) if key != "text"} + metadata["dataset_version"] = self.DATASET_VERSION + metadata["hf_revision"] = self.HF_DATASET_REVISION + seeds.append( + SeedPrompt( + value=str(row["text"]), + name=str(row["id"]), + data_type="text", + dataset_name=self.dataset_name, + harm_categories=[], + description=description, + source=source_url, + authors=authors, + groups=groups, + metadata=metadata, + ) + ) + + if not seeds: + raise ValueError("SeedDataset cannot be empty. Check your filter criteria.") + + logger.info(f"Loaded {len(seeds)} records from {self.dataset_name}") + return SeedDataset(seeds=seeds, dataset_name=self.dataset_name) diff --git a/tests/unit/datasets/test_turkish_prompt_injection_dataset.py b/tests/unit/datasets/test_turkish_prompt_injection_dataset.py new file mode 100644 index 0000000000..8314ee2ce4 --- /dev/null +++ b/tests/unit/datasets/test_turkish_prompt_injection_dataset.py @@ -0,0 +1,176 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from pyrit.datasets.seed_datasets.remote import ( + TurkishPromptInjectionFamily, + TurkishPromptInjectionLabel, + TurkishPromptInjectionSplit, + _TurkishPromptInjectionDataset, +) +from pyrit.models import SeedDataset, SeedPrompt + +FETCH_TARGET = "_fetch_from_huggingface_async" + + +def _row( + *, + identifier: int, + label: int, + split: str, + attack_family: str = "tr_instruction_override_extraction", + technique: str = "override", + severity: str = "high", +) -> dict[str, Any]: + is_attack = label == 1 + return { + "id": identifier, + "text": f"{'Saldiri' if is_attack else 'Mesru'} ornegi {identifier} icin yeterince uzun bir Turkce metin.", + "label": label, + "class": "injection" if is_attack else "benign", + "attack_family": attack_family if is_attack else "benign", + "technique": technique if is_attack else "hard_negative", + "severity": severity if is_attack else "none", + "language": "tr", + "split": split, + } + + +@pytest.fixture +def mock_rows_by_split() -> dict[str, list[dict[str, Any]]]: + return { + "train": [ + _row(identifier=1, label=1, split="train", attack_family="tr_instruction_override_extraction"), + _row(identifier=2, label=0, split="train"), + _row(identifier=3, label=1, split="train", attack_family="tr_jailbreak_persona"), + ], + "validation": [ + _row(identifier=4, label=1, split="validation", attack_family="tr_obfuscation_exfiltration"), + _row(identifier=5, label=0, split="validation"), + ], + "test": [ + _row(identifier=6, label=1, split="test", attack_family="tr_agentic_toolabuse"), + _row(identifier=7, label=0, split="test"), + ], + } + + +def _fetch_side_effect(rows_by_split: dict[str, list[dict[str, Any]]]): + def _inner(*, dataset_name: str, config: str, split: str, cache: bool, revision: str): + return list(rows_by_split.get(split, [])) + + return _inner + + +@pytest.mark.asyncio +async def test_default_loads_attacks_from_all_splits(mock_rows_by_split): + loader = _TurkishPromptInjectionDataset() + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(mock_rows_by_split))): + dataset = await loader.fetch_dataset_async() + + assert isinstance(dataset, SeedDataset) + assert all(isinstance(seed, SeedPrompt) for seed in dataset.seeds) + # 4 attack rows across all splits, no benign + assert len(dataset.seeds) == 4 + assert all(seed.metadata["label"] == 1 for seed in dataset.seeds) + assert all(seed.metadata["language"] == "tr" for seed in dataset.seeds) + + +@pytest.mark.asyncio +async def test_label_all_loads_both_classes(mock_rows_by_split): + loader = _TurkishPromptInjectionDataset(label=TurkishPromptInjectionLabel.ALL) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(mock_rows_by_split))): + dataset = await loader.fetch_dataset_async() + + labels = sorted(seed.metadata["label"] for seed in dataset.seeds) + assert labels == [0, 0, 0, 1, 1, 1, 1] + + +@pytest.mark.asyncio +async def test_benign_filter_loads_hard_negatives(mock_rows_by_split): + loader = _TurkishPromptInjectionDataset(label=TurkishPromptInjectionLabel.BENIGN) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(mock_rows_by_split))): + dataset = await loader.fetch_dataset_async() + + assert len(dataset.seeds) == 3 + assert all(seed.metadata["label"] == 0 for seed in dataset.seeds) + assert all(seed.metadata["attack_family"] == "benign" for seed in dataset.seeds) + + +@pytest.mark.asyncio +async def test_family_filter_scopes_attacks(mock_rows_by_split): + loader = _TurkishPromptInjectionDataset(attack_families=[TurkishPromptInjectionFamily.JAILBREAK_PERSONA]) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(mock_rows_by_split))): + dataset = await loader.fetch_dataset_async() + + assert len(dataset.seeds) == 1 + assert dataset.seeds[0].metadata["attack_family"] == "tr_jailbreak_persona" + + +@pytest.mark.asyncio +async def test_single_split(mock_rows_by_split): + loader = _TurkishPromptInjectionDataset( + label=TurkishPromptInjectionLabel.ALL, split=TurkishPromptInjectionSplit.TEST + ) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(mock_rows_by_split))): + dataset = await loader.fetch_dataset_async() + + assert len(dataset.seeds) == 2 + assert all(seed.metadata["split"] == "test" for seed in dataset.seeds) + + +def test_invalid_enum_type_raises(): + with pytest.raises(ValueError): + _TurkishPromptInjectionDataset(label="attack") # type: ignore[arg-type] + + +def test_empty_family_list_raises(): + with pytest.raises(ValueError): + _TurkishPromptInjectionDataset(attack_families=[]) + + +@pytest.mark.asyncio +async def test_validation_rejects_non_turkish_language(mock_rows_by_split): + bad = {"train": [_row(identifier=1, label=1, split="train")]} + bad["train"][0]["language"] = "en" + loader = _TurkishPromptInjectionDataset(split=TurkishPromptInjectionSplit.TRAIN) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(bad))): + with pytest.raises(ValueError, match="non-Turkish"): + await loader.fetch_dataset_async() + + +@pytest.mark.asyncio +async def test_validation_rejects_bad_label(mock_rows_by_split): + bad = {"train": [_row(identifier=1, label=1, split="train")]} + bad["train"][0]["label"] = 2 + loader = _TurkishPromptInjectionDataset(split=TurkishPromptInjectionSplit.TRAIN) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(bad))): + with pytest.raises(ValueError, match="Invalid label"): + await loader.fetch_dataset_async() + + +@pytest.mark.asyncio +async def test_validation_rejects_bool_label(mock_rows_by_split): + # True == 1, but type(True) is bool, not int — must be rejected. + bad = {"train": [_row(identifier=1, label=1, split="train")]} + bad["train"][0]["label"] = True + loader = _TurkishPromptInjectionDataset(split=TurkishPromptInjectionSplit.TRAIN) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(bad))): + with pytest.raises(ValueError, match="Invalid label"): + await loader.fetch_dataset_async() + + +@pytest.mark.asyncio +async def test_empty_after_filter_raises(mock_rows_by_split): + # Only benign rows exist for this split, but we ask for attacks. + rows = {"validation": [_row(identifier=5, label=0, split="validation")]} + loader = _TurkishPromptInjectionDataset( + label=TurkishPromptInjectionLabel.ATTACK, split=TurkishPromptInjectionSplit.VALIDATION + ) + with patch.object(loader, FETCH_TARGET, new=AsyncMock(side_effect=_fetch_side_effect(rows))): + with pytest.raises(ValueError, match="cannot be empty"): + await loader.fetch_dataset_async()