Skip to content

Commit e32ee5f

Browse files
authored
Fix Dependabot Python vulnerabilities (#77)
1 parent 466a1f8 commit e32ee5f

21 files changed

Lines changed: 488 additions & 152 deletions

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
python-version: ['3.9', '3.10', '3.11']
15+
python-version: ['3.10', '3.11']
1616

1717
steps:
1818
- name: Checkout repository
@@ -49,3 +49,11 @@ jobs:
4949
run: |
5050
chmod +x ./build_package.sh
5151
./build_package.sh
52+
53+
- name: Verify built wheel
54+
run: |
55+
python -m venv .package-test-venv
56+
.package-test-venv/bin/python -m pip install --upgrade pip
57+
.package-test-venv/bin/python -m pip install dist/*.whl
58+
.package-test-venv/bin/python -m pip check
59+
.package-test-venv/bin/prompt-security-fuzzer --list-providers

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [Unreleased]
8+
9+
### Security
10+
- Upgraded LangChain, LangChain Core, python-dotenv, and related provider integrations to resolve current Dependabot advisories.
11+
12+
### Changed
13+
- Minimum Python version raised from 3.9 to 3.10.
14+
- Updated fastparquet for NumPy 2 compatibility in the upgraded runtime dependency graph.
15+
716
## [2.1.0] - 2026-02-16
817

918
### Added

CONTRIBUTING.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ By participating in this project, you agree to abide by its terms.
1313
### Prerequisites
1414

1515
Before you begin, ensure you have the following installed:
16-
- Python 3.7 or later
16+
- Python 3.10 or later
1717
- Git
1818

1919
### Setting Up Your Development Environment
@@ -29,8 +29,12 @@ cd ps-fuzz
2929
### Set up a virtual environment
3030

3131
```bash
32-
python -m venv venv
32+
# Unix or macOS
33+
python3.10 -m venv venv
3334
source venv/bin/activate # On Unix or macOS
35+
36+
# Windows
37+
py -3.10 -m venv venv
3438
venv\Scripts\activate # On Windows
3539
```
3640

@@ -128,7 +132,7 @@ from ..attack_config import AttackConfig
128132
from ..test_base import TestBase, StatusUpdate
129133
from ..attack_registry import register_test
130134
from typing import Generator
131-
from pkg_resources import resource_filename
135+
from .utils import packaged_attack_data_path
132136
import logging
133137
logger = logging.getLogger(__name__)
134138
```
@@ -208,4 +212,3 @@ Here are some helpful resources to get you started with best practices for contr
208212
- [GitHub Flow](https://guides.github.com/introduction/flow/) - An introduction to the GitHub workflow, which explains branches, pull requests, and more.
209213
- [Writing Good Commit Messages](https://chris.beams.io/posts/git-commit/) - A guide on how to write clear and concise commit messages, which are crucial for following the changes in a project.
210214
- [Python Coding Style](https://pep8.org/) - Guidelines for writing clean and understandable Python code.
211-

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Table of Contents
8383
1. Install the Fuzzer package
8484
<a id="using-pip"></a>
8585
#### Using pip install
86+
Requires Python 3.10 or newer.
8687
```zsh
8788
pip install prompt-security-fuzzer
8889
```
@@ -327,4 +328,3 @@ Turn this into a community project! We want this to be useful to everyone buildi
327328
Interested in contributing to the development of our tools? Great! For a guide on making your first contribution, please see our [Contributing Guide](https://github.com/prompt-security/ps-fuzz/blob/main/CONTRIBUTING.md#get-started-with-your-first-contribution-adding-a-new-test). This section offers a straightforward introduction to adding new tests.
328329

329330
For ideas on what tests to add, check out the issues tab in our GitHub repository. Look for issues labeled `new-test` and `good-first-issue`, which are perfect starting points for new contributors.
330-

claude.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Python Environment
44

5-
This project requires Python >= 3.9 (tested with 3.9, 3.10, 3.11).
5+
This project requires Python >= 3.10 (tested with 3.10, 3.11).
66

77
### Setup with uv
88

ps_fuzz/attacks/base64_injection.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from ..test_base import TestBase, StatusUpdate
55
from ..attack_registry import register_test
66
from ..util import contains_refusal_keywords
7-
# from .utils import evaluate_response
7+
from .utils import packaged_attack_data_path
88
from typing import Generator
99
import pandas as pd
10-
from pkg_resources import resource_filename # for loading attack data file packaged with the library
1110
import logging
1211
import base64
1312
import sys
@@ -24,8 +23,8 @@ def __init__(self, client_config: ClientConfig, attack_config: AttackConfig):
2423
)
2524

2625
def run(self) -> Generator[StatusUpdate, None, None]:
27-
dataset_filename = resource_filename('ps_fuzz', 'attack_data/prompt_injections_for_base64.parquet')
28-
data = pd.read_parquet(dataset_filename, engine='fastparquet')
26+
with packaged_attack_data_path('prompt_injections_for_base64.parquet') as dataset_filename:
27+
data = pd.read_parquet(dataset_filename, engine='fastparquet')
2928
rows_count = data.shape[0]
3029

3130
if rows_count > self.attack_config.attack_prompts_count:

ps_fuzz/attacks/custom_benchmark.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import Generator
99
import pandas as pd
1010
import os
11-
from pkg_resources import resource_filename # for loading attack data file packaged with the library
1211
import logging
1312
logger = logging.getLogger(__name__)
1413

ps_fuzz/attacks/harmful_behavior.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from ..test_base import TestBase, StatusUpdate
55
from ..attack_registry import register_test
66
# from ..util import contains_refusal_keywords
7-
from .utils import evaluate_response
7+
from .utils import evaluate_response, packaged_attack_data_path
88
from typing import Generator
99
import pandas as pd
10-
from pkg_resources import resource_filename # for loading attack data file packaged with the library
1110
import logging
1211
import sys
1312
logger = logging.getLogger(__name__)
@@ -23,9 +22,9 @@ def __init__(self, client_config: ClientConfig, attack_config: AttackConfig):
2322
)
2423

2524
def run(self) -> Generator[StatusUpdate, None, None]:
26-
dataset_filename = resource_filename('ps_fuzz', 'attack_data/harmful_behavior.csv')
27-
logger.info(f"Dataset filename: {dataset_filename}")
28-
data = pd.read_csv(dataset_filename)
25+
with packaged_attack_data_path('harmful_behavior.csv') as dataset_filename:
26+
logger.info(f"Dataset filename: {dataset_filename}")
27+
data = pd.read_csv(dataset_filename)
2928
rows_count = data.shape[0]
3029

3130
if rows_count > self.attack_config.attack_prompts_count:

ps_fuzz/attacks/rag_poisoning.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,45 +33,54 @@ def _suppress_loggers(logger_names):
3333
logging.getLogger(name).setLevel(lvl)
3434

3535
# Check dependencies availability
36-
DEPENDENCIES_AVAILABLE = True
3736
MISSING_PACKAGES = []
3837

3938
suppress_names = []
4039
try:
41-
from langchain_community.vectorstores import Chroma
40+
from langchain_chroma import Chroma
4241
suppress_names = ["chromadb"]
43-
4442
except ImportError:
45-
DEPENDENCIES_AVAILABLE = False
4643
MISSING_PACKAGES.append("chromadb")
44+
Chroma = None
45+
46+
try:
47+
from langchain_openai import OpenAIEmbeddings
48+
except ImportError:
49+
MISSING_PACKAGES.append("langchain-openai")
50+
OpenAIEmbeddings = None
4751

4852
try:
49-
from langchain_community.embeddings import OpenAIEmbeddings, OllamaEmbeddings
53+
from langchain_ollama import OllamaEmbeddings
5054
except ImportError:
51-
DEPENDENCIES_AVAILABLE = False
52-
MISSING_PACKAGES.append("langchain-community (embeddings)")
55+
MISSING_PACKAGES.append("langchain-ollama")
56+
OllamaEmbeddings = None
5357

5458
try:
5559
from langchain_core.documents import Document
5660
except ImportError:
57-
DEPENDENCIES_AVAILABLE = False
58-
MISSING_PACKAGES.append("langchain (schema)")
61+
MISSING_PACKAGES.append("langchain-core")
62+
Document = None
63+
64+
DEPENDENCIES_AVAILABLE = Chroma is not None and Document is not None
5965

6066
# Create dummy classes for when dependencies are not available
61-
if not DEPENDENCIES_AVAILABLE:
67+
if Document is None:
6268
class Document:
6369
def __init__(self, page_content, metadata):
6470
self.page_content = page_content
6571
self.metadata = metadata
66-
72+
73+
if Chroma is None:
6774
class Chroma:
6875
def __init__(self, *args, **kwargs):
6976
pass
70-
77+
78+
if OpenAIEmbeddings is None:
7179
class OpenAIEmbeddings:
7280
def __init__(self, *args, **kwargs):
7381
pass
74-
82+
83+
if OllamaEmbeddings is None:
7584
class OllamaEmbeddings:
7685
def __init__(self, *args, **kwargs):
7786
pass
@@ -126,6 +135,8 @@ def _get_embedding_function(self):
126135
model = config.embedding_model
127136

128137
if provider == 'open_ai':
138+
if 'langchain-openai' in MISSING_PACKAGES:
139+
raise ImportError("OpenAI embeddings require the langchain-openai package")
129140
# Get embedding-specific base URL if configured
130141
base_url = None
131142
if isinstance(config, dict):
@@ -141,6 +152,8 @@ def _get_embedding_function(self):
141152
return OpenAIEmbeddings(**kwargs)
142153

143154
elif provider == 'ollama':
155+
if 'langchain-ollama' in MISSING_PACKAGES:
156+
raise ImportError("Ollama embeddings require the langchain-ollama package")
144157
# Get embedding-specific base URL if configured
145158
base_url = None
146159
if isinstance(config, dict):
@@ -255,11 +268,16 @@ def _setup_poisoned_vector_database(self):
255268
with _suppress_loggers(suppress_names):
256269
self.vectorstore.add_documents([poisoned_doc])
257270

258-
# Persist the database (ChromaDB 0.4.0+ auto-persists with persist_directory)
259-
try:
260-
self.vectorstore.persist()
261-
except AttributeError:
262-
pass # ChromaDB 0.4.0+ auto-persists when using persist_directory
271+
# Chroma persists automatically when persist_directory is configured.
272+
273+
def _retrieve_relevant_documents(self, query: str):
274+
"""Use the Runnable API required by modern LangChain retrievers."""
275+
retriever = self.vectorstore.as_retriever(
276+
search_type="similarity",
277+
search_kwargs={"k": 3}
278+
)
279+
with _suppress_loggers(suppress_names):
280+
return retriever.invoke(query)
263281

264282
def _cleanup(self):
265283
"""Clean up temporary resources"""
@@ -275,7 +293,7 @@ def run(self) -> Generator[StatusUpdate, None, None]:
275293
# Preflight check: Skip if dependencies not available
276294
if not DEPENDENCIES_AVAILABLE:
277295
missing_packages_str = ", ".join(MISSING_PACKAGES) if MISSING_PACKAGES else "chromadb"
278-
error_msg = f"Required dependencies not available: {missing_packages_str}. Install with: pip install chromadb (or uv pip install chromadb) or install with RAG extras: pip install .[rag]"
296+
error_msg = f"Required dependencies not available: {missing_packages_str}. Reinstall prompt-security-fuzzer with its default dependencies."
279297
logger.warning(f"RAG poisoning attack skipped: {error_msg}")
280298
self.status.report_skipped("", error_msg)
281299
yield StatusUpdate(self.client_config, self.test_name, self.status, "Skipped", 1, 1)
@@ -311,13 +329,7 @@ def run(self) -> Generator[StatusUpdate, None, None]:
311329

312330
try:
313331
# Retrieve relevant documents
314-
retriever = self.vectorstore.as_retriever(
315-
search_type="similarity",
316-
search_kwargs={"k": 3}
317-
)
318-
# suppress telemetry during retrieval
319-
with _suppress_loggers(suppress_names):
320-
relevant_docs = retriever.get_relevant_documents(query)
332+
relevant_docs = self._retrieve_relevant_documents(query)
321333

322334
# Create context from retrieved documents
323335
context = "\n\n".join([doc.page_content for doc in relevant_docs])
@@ -394,4 +406,4 @@ def run(self) -> Generator[StatusUpdate, None, None]:
394406
yield StatusUpdate(self.client_config, self.test_name, self.status, "Error", 1, 1)
395407
finally:
396408
# Always cleanup
397-
self._cleanup()
409+
self._cleanup()

ps_fuzz/attacks/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1+
from contextlib import contextmanager
2+
from importlib.resources import as_file, files
3+
from pathlib import Path
4+
from typing import Iterator
5+
16
from ..chat_clients import ChatSession
7+
8+
9+
@contextmanager
10+
def packaged_attack_data_path(filename: str) -> Iterator[Path]:
11+
"""Yield a filesystem path for packaged attack data on every installer type."""
12+
resource = files('ps_fuzz').joinpath('attack_data', filename)
13+
with as_file(resource) as path:
14+
yield path
15+
16+
217
def evaluate_response(client,response,expected_response):
318
"""
419
Evaluate the response from the model against the expected response.

0 commit comments

Comments
 (0)