diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e10cf61..640616f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,6 @@ jobs: strategy: matrix: python-version: - - "3.12" - "3.14" steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef764b6..f4320af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: rev: v3.21.2 hooks: - id: pyupgrade - args: [--py312-plus] + args: [--py314-plus] - repo: https://github.com/psf/black rev: 26.3.1 @@ -64,11 +64,7 @@ repos: rev: v2.6.0 hooks: - id: pycln - - - repo: https://github.com/isidentical/teyit - rev: 0.4.3 - hooks: - - id: teyit + language_version: python3.14 - repo: https://github.com/python-poetry/poetry rev: '2.4.1' diff --git a/Dockerfile b/Dockerfile index feeebcc..231b9f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM python:3.12-slim AS builder +FROM python:3.14-slim AS builder WORKDIR /app @@ -26,7 +26,7 @@ RUN pip install --upgrade pip setuptools wheel RUN pip install --no-cache-dir -r requirements.txt # Runtime stage -FROM python:3.12-slim +FROM python:3.14-slim # Set environment variables ENV PYTHONDONTWRITEBYTECODE=1 @@ -35,7 +35,7 @@ ENV PYTHONUNBUFFERED=1 WORKDIR /app # Copy only the necessary files from the builder stage -COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder /usr/local/lib/python3.14/site-packages /usr/local/lib/python3.14/site-packages COPY --from=builder /usr/local/bin /usr/local/bin # Copy application code diff --git a/agentic_security/attack_rules/models.py b/agentic_security/attack_rules/models.py index 9c949c3..413f9b9 100644 --- a/agentic_security/attack_rules/models.py +++ b/agentic_security/attack_rules/models.py @@ -9,7 +9,7 @@ class AttackRuleSeverity(Enum): HIGH = "high" @classmethod - def from_string(cls, value: str) -> "AttackRuleSeverity": + def from_string(cls, value: str) -> AttackRuleSeverity: try: return cls(value.lower()) except ValueError: @@ -28,7 +28,7 @@ class AttackRule: metadata: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls, data: dict[str, Any]) -> "AttackRule": + def from_dict(cls, data: dict[str, Any]) -> AttackRule: severity = AttackRuleSeverity.from_string(data.get("severity", "medium")) return cls( name=data["name"], diff --git a/agentic_security/cache_config.py b/agentic_security/cache_config.py index 765f526..98b5de9 100644 --- a/agentic_security/cache_config.py +++ b/agentic_security/cache_config.py @@ -1,7 +1,5 @@ """Utilities to keep cache-to-disk storage in a writable, predictable location.""" -from __future__ import annotations - import os from pathlib import Path diff --git a/agentic_security/fuzz_chain/chain.py b/agentic_security/fuzz_chain/chain.py index 1d3bcd0..9095531 100644 --- a/agentic_security/fuzz_chain/chain.py +++ b/agentic_security/fuzz_chain/chain.py @@ -1,4 +1,3 @@ -from __future__ import annotations import logging from typing import Any, Protocol diff --git a/agentic_security/http_spec.py b/agentic_security/http_spec.py index 483369b..d22147d 100644 --- a/agentic_security/http_spec.py +++ b/agentic_security/http_spec.py @@ -155,7 +155,7 @@ class LLMSpec(BaseModel): try: body_json = json.loads(self.body) return body_json.get("model", "unknown") - except (json.JSONDecodeError, TypeError): + except json.JSONDecodeError, TypeError: return "unknown" @property diff --git a/agentic_security/primitives/models.py b/agentic_security/primitives/models.py index eb04cfa..7a5be43 100644 --- a/agentic_security/primitives/models.py +++ b/agentic_security/primitives/models.py @@ -28,7 +28,7 @@ class Scan(BaseModel): # Set and managed by the backend secrets: dict[str, str] = Field(default_factory=dict) - def with_secrets(self, secrets) -> "Scan": + def with_secrets(self, secrets) -> Scan: match secrets: case dict(): self.secrets.update(secrets) diff --git a/agentic_security/probe_actor/fuzzer.py b/agentic_security/probe_actor/fuzzer.py index 60b2135..9b6dd1a 100644 --- a/agentic_security/probe_actor/fuzzer.py +++ b/agentic_security/probe_actor/fuzzer.py @@ -66,7 +66,7 @@ MAX_INJECTION_ATTEMPTS = settings_var("fuzzer.max_injection_attempts", 20) async def generate_prompts( prompts: list[str] | AsyncGenerator, -) -> AsyncGenerator[str, None]: +) -> AsyncGenerator[str]: """ Asynchronously generates and yields individual prompts. @@ -227,7 +227,7 @@ async def scan_module( optimize: bool = False, stop_event: asyncio.Event | None = None, token_counter: dict[str, int] | None = None, -) -> AsyncGenerator[dict[str, Any], None]: +) -> AsyncGenerator[dict[str, Any]]: """ Scan a single module. @@ -390,7 +390,7 @@ async def perform_single_shot_scan( stop_event: asyncio.Event | None = None, secrets: dict[str, str] | None = None, inline_datasets: list[dict[str, Any]] | None = None, -) -> AsyncGenerator[str, None]: +) -> AsyncGenerator[str]: """ Perform a standard security scan using a given request factory. @@ -491,7 +491,7 @@ async def perform_many_shot_scan( probe_frequency: float = 0.2, max_ctx_length: int = 10_000, secrets: dict[str, str] | None = None, -) -> AsyncGenerator[str, None]: +) -> AsyncGenerator[str]: """ Perform a multi-step security scan with probe injection. diff --git a/agentic_security/refusal_classifier/hybrid_classifier.py b/agentic_security/refusal_classifier/hybrid_classifier.py index 9021f4c..38075e6 100644 --- a/agentic_security/refusal_classifier/hybrid_classifier.py +++ b/agentic_security/refusal_classifier/hybrid_classifier.py @@ -86,7 +86,7 @@ class HybridRefusalClassifier: detector: RefusalDetector, weight: float = 1.0, name: str | None = None, - ) -> "HybridRefusalClassifier": + ) -> HybridRefusalClassifier: """Add a detection method with specified weight. Args: diff --git a/agentic_security/refusal_classifier/pii_detector.py b/agentic_security/refusal_classifier/pii_detector.py index 3d76010..4a39c89 100644 --- a/agentic_security/refusal_classifier/pii_detector.py +++ b/agentic_security/refusal_classifier/pii_detector.py @@ -4,8 +4,6 @@ Provides a small, dependency-free detector for responses that may contain sensitive personal or credential material. """ -from __future__ import annotations - import re from dataclasses import dataclass from re import Pattern diff --git a/agentic_security/refusal_classifier/registry.py b/agentic_security/refusal_classifier/registry.py index b425946..dad2aa0 100644 --- a/agentic_security/refusal_classifier/registry.py +++ b/agentic_security/refusal_classifier/registry.py @@ -27,8 +27,6 @@ Example configuration:: threshold = 3 """ -from __future__ import annotations - import importlib from collections import OrderedDict from collections.abc import Callable, Mapping diff --git a/agentic_security/refusal_classifier/sandbox_escape_detector.py b/agentic_security/refusal_classifier/sandbox_escape_detector.py index 55a89db..bb2df9a 100644 --- a/agentic_security/refusal_classifier/sandbox_escape_detector.py +++ b/agentic_security/refusal_classifier/sandbox_escape_detector.py @@ -5,8 +5,6 @@ to break out of Docker/Kubernetes container isolation (capability probing, mount inspection, kernel/exploit reconnaissance). """ -from __future__ import annotations - import re from dataclasses import dataclass from re import Pattern diff --git a/agentic_security/routes/static.py b/agentic_security/routes/static.py index 9cc6313..d549572 100644 --- a/agentic_security/routes/static.py +++ b/agentic_security/routes/static.py @@ -70,7 +70,7 @@ def get_static_file(filepath: Path, content_type: str | None = None) -> FileResp @router.get("/", response_class=HTMLResponse) async def root(request: Request) -> Response: """Serve the main index.html template.""" - return templates.TemplateResponse("index.html", {"request": request}) + return templates.TemplateResponse(request, "index.html") @router.get("/main.js") diff --git a/poetry.lock b/poetry.lock index 854127b..34a64e0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -148,7 +148,6 @@ attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" propcache = ">=0.2.0" -typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} yarl = ">=1.17.0,<2.0" [package.extras] @@ -168,7 +167,6 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "annotated-doc" @@ -239,7 +237,6 @@ files = [ [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.26.1)"] @@ -987,26 +984,27 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastapi" -version = "0.122.0" +version = "0.141.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67"}, - {file = "fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b"}, + {file = "fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3"}, + {file = "fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1"}, ] [package.dependencies] annotated-doc = ">=0.0.2" -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.51.0" +pydantic = ">=2.9.0" +starlette = ">=0.46.0" typing-extensions = ">=4.8.0" +typing-inspection = ">=0.4.2" [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.32)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.32)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.32)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "fastjsonschema" @@ -3878,7 +3876,6 @@ files = [ [package.dependencies] pytest = ">=8.2,<10" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] @@ -3960,14 +3957,14 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pyt [[package]] name = "python-multipart" -version = "0.0.27" +version = "0.0.31" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645"}, - {file = "python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602"}, + {file = "python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28"}, + {file = "python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680"}, ] [[package]] @@ -4266,7 +4263,6 @@ files = [ [package.dependencies] attrs = ">=22.2.0" rpds-py = ">=0.7.0" -typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" @@ -4761,22 +4757,21 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "starlette" -version = "0.50.0" +version = "1.6.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca"}, - {file = "starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"}, + {file = "starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"}, + {file = "starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b"}, ] [package.dependencies] anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} [package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "tabulate" @@ -5399,5 +5394,5 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" -python-versions = ">=3.12,<4.0" -content-hash = "dc2edc6c72835e82a8954273da5ae9e1c231686ff73886a9f962410a384b3f1f" +python-versions = ">=3.14,<4.0" +content-hash = "c49467950cab4b9cc0f103ef570770421926fa223ecc155620d9863a9677407b" diff --git a/pyproject.toml b/pyproject.toml index 2f0891b..4784516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [{ name = "Alexander Miasoiedov", email = "msoedov@gmail.com" }] maintainers = [{ name = "Alexander Miasoiedov", email = "msoedov@gmail.com" }] license = "Apache-2.0" readme = "Readme.md" -requires-python = ">=3.12,<4.0" +requires-python = ">=3.14,<4.0" keywords = [ "LLM vulnerability scanner", "llm security", @@ -32,7 +32,7 @@ agentic_security = "agentic_security.__main__:main" packages = [{ include = "agentic_security", from = "." }] [tool.poetry.dependencies] -fastapi = "^0.122.0" +fastapi = "^0.141.1" uvicorn = "^0.38.0" fire = "0.7.1" loguru = "^0.7.3" @@ -48,7 +48,7 @@ scikit-optimize = "^0.10.2" scikit-learn = "^1.7.2" numpy = ">=1.24.3,<3.0.0" jinja2 = "^3.1.4" -python-multipart = "^0.0.27" +python-multipart = "^0.0.31" tomli = "^2.3.0" rich = "^14.2.0" gTTS = "^2.5.4"