pyproject.toml
Rama main · réplica de lectura del repositorio original.
[project]
name = "soc-ai"
version = "1.6.0"
description = "Self-hosted, privacy-respecting LLM-powered triage assistant for Security Onion"
readme = "README.md"
requires-python = ">=3.12,<3.13"
license = { text = "Apache-2.0" }
authors = [{ name = "soc-ai contributors" }]
keywords = ["security-onion", "soc", "llm", "siem", "mcp", "triage"]
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.12",
"Topic :: Security",
"Framework :: FastAPI",
"Typing :: Typed",
]
dependencies = [
# web framework
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"sse-starlette>=2.1.0",
"python-multipart>=0.0.18",
# data validation / config
"pydantic>=2.9.0",
"pydantic-settings>=2.6.0",
# agent + MCP
# pydantic-ai went 1.0 well before this floor mattered; the resolved
# client is 1.91.x. Pin the major so a fresh resolve can't silently
# cross into a future breaking 2.x.
# The slim distribution with only the OpenAI extra: the agent talks to
# LiteLLM and the Oracle through the OpenAI-compatible client and imports
# nothing else from the provider set. The full `pydantic-ai` meta-package
# pulls every provider SDK it supports, plus Temporal and Logfire, into
# the image and the lockfile: ~80 packages nothing here imports, and where
# the advisory-flagged transitive pins (pyasn1, google-auth, authlib)
# came from.
"pydantic-ai-slim[openai]>=1.0,<2",
# mcp 2.x is a split package (mcp + mcp-types) that removes
# `mcp.server.fastmcp`: the module is a stub that raises ModuleNotFoundError
# because FastMCP became `mcp.server.mcpserver.MCPServer`. That is the one
# import soc_ai.mcp_server makes, so cap the major here as well; a bare
# `uv lock --upgrade` otherwise takes the MCP server down with no diff
# in this file to review.
"mcp>=1.1.0,<2",
# HTTP / search clients
"httpx>=0.27.0",
"geoip2>=4.8.0",
# MUST track the SO grid's ES major. SO ships Elasticsearch 8.x; a 9.x
# client refuses to talk to an 8.x cluster (compatible-with header
# rejection) and 9.x also drops the `body=` search/index kwarg this
# client still uses. Cap below 9 until the grid itself moves to ES 9.
"elasticsearch[async]>=8.16.0,<9",
# OQL grammar
"lark>=1.2.0",
# YAML parsing
"pyyaml>=6.0.2",
# logging
"structlog>=24.4.0",
"sqlalchemy[asyncio]>=2.0.36",
"aiosqlite>=0.20",
"alembic>=1.14",
"bcrypt>=4.2",
"jinja2>=3.1",
"python-ulid>=2.7",
"dpkt>=1.9.8",
# Fernet encryption for secret config overrides at rest (Danger Zone).
"cryptography>=43",
# Semver comparison for the About page's opt-in GitHub update check
# (soc_ai.webui.updates). Ubiquitous + already resolved transitively; declared
# here because updates.py imports it directly.
"packaging>=24.0",
]
[project.optional-dependencies]
# The PostgreSQL store (SOC_AI_DATABASE_URL). SQLite needs nothing extra.
# The container image installs it, so the compose `postgres` profile works.
postgres = ["asyncpg>=0.30"]
# The tier 3 estate model (soc_ai/hunting/estate_model): peer groups by
# clustering and an estate outlier score per host. The container image
# installs it. The app imports it only when the estate model runs, and the
# job logs one line and does nothing when the extra is absent. The major is
# capped like the other wide dependencies here: a fresh resolve must not cross
# into a breaking 2.x of scikit-learn or a 3.x of numpy with no diff to review.
ml = ["scikit-learn>=1.5,<2", "numpy>=2.0,<3"]
[project.urls]
Homepage = "https://github.com/nuk3s/soc-ai"
Repository = "https://github.com/nuk3s/soc-ai"
Issues = "https://github.com/nuk3s/soc-ai/issues"
Changelog = "https://github.com/nuk3s/soc-ai/blob/main/CHANGELOG.md"
[dependency-groups]
dev = [
"pytest>=8.3.0",
"pytest-asyncio>=0.24.0",
"pytest-cov>=6.0.0",
"respx>=0.21.0",
"ruff>=0.8.0",
"mypy>=1.13.0",
"playwright>=1.59.0",
"pytest-playwright>=0.7.2",
"pre-commit>=3.8.0",
"pytest-xdist>=3.6",
]
docs = [
# Documentation site (Material for MkDocs). Build/serve with:
# uv run --group docs mkdocs serve
# uv run --group docs mkdocs build --strict
"mkdocs-material>=9.5",
]
[project.scripts]
soc-ai = "soc_ai.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["soc_ai"]
[tool.ruff]
line-length = 100
target-version = "py312"
src = ["soc_ai", "tests"]
[tool.ruff.lint]
select = [
"E", "F", "W", # pycodestyle / pyflakes
"I", # isort
"B", # bugbear
"UP", # pyupgrade
"S", # bandit (security)
"ASYNC", # async-correctness
"RUF", # ruff-specific
"PL", # pylint
"PT", # pytest-style
"SIM", # simplify
"TCH", # type-checking imports
]
ignore = [
"PLR0913", # too many arguments - security tools have wide signatures
"PLR0911", # too many return statements
"S101", # asserts ok in tests (per-file rule below covers tests)
"TC001", # type-checking blocks for app imports - causes runtime gaps
"TC002", # type-checking blocks for third-party imports - same
"TC003", # type-checking blocks for stdlib imports - same
"PLR2004", # magic-value-comparison - status codes etc are self-documenting
"B008", # Depends() in defaults is FastAPI's documented idiom
"BLE001", # bare except is sometimes the right call (top-level SSE wrapper)
]
[tool.ruff.lint.pylint]
max-branches = 20
[tool.ruff.lint.per-file-ignores]
# Tests favour readability over strict style: broad pytest.raises, compound
# asserts, and en-dashes in comments/strings are fine in test code.
"tests/**" = ["S", "PLR2004", "ASYNC230", "PLR0915", "PLC0415", "RUF001", "RUF003", "PT011", "PT018"]
"scripts/**" = ["S", "PLR2004", "PLC0415", "E501"]
# `×` is intentional in the report headings (cross-tab tables) and
# in meta_analysis size estimates (".. × 5K tokens").
"soc_ai/eval/report.py" = ["RUF001", "RUF003"]
"soc_ai/eval/meta_analysis.py" = ["RUF002", "RUF003"]
# `×` is the intentional "fired N× times" count multiplier in the
# detection-tuning reason/summary strings + docs (same as the webui count badge).
"soc_ai/webui/detection_tuning.py" = ["RUF001", "RUF002", "RUF003"]
"soc_ai/tools/rule_tuning.py" = ["RUF001", "RUF002", "RUF003"]
"soc_ai/tools/tuning_heuristic.py" = ["RUF001", "RUF002", "RUF003"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_configs = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
files = ["soc_ai"]
# Third-party packages without bundled type stubs. PyYAML ships no inline
# types and we don't vendor types-PyYAML; the YAML we load (blocklists,
# synth loader) is validated downstream, so treating it as untyped here is
# safe and keeps strict mode on everywhere else.
[[tool.mypy.overrides]]
module = ["yaml"]
ignore_missing_imports = true
# dpkt ships no py.typed and no stub package; it is a pure-Python decode
# library with no C surface — treating it as untyped is safe.
[[tool.mypy.overrides]]
module = ["dpkt"]
ignore_missing_imports = true
# scikit-learn ships no py.typed and no stub package. numpy and scipy ship
# types, but all three come from the optional `ml` extra, and a checkout
# without the extra must still type-check. The estate model converts every
# value it returns to a builtin type, so an untyped import loses nothing.
[[tool.mypy.overrides]]
module = ["sklearn.*", "numpy", "numpy.*", "scipy.*"]
ignore_missing_imports = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
# `--ignore=tests/browser` keeps the default coverage-gated run from collecting
# the Playwright E2E smoke: it needs the seeded demo stack + chromium and would
# tank coverage. It runs in a DEDICATED CI job (`browser-smoke`) via `-m browser`.
# `--ignore=tests/scale` does the same for the scale harness. It runs a
# 2,000-host estate for minutes, in its own CI job (`scale`) via `-m scale`.
addopts = "-ra --strict-markers --strict-config --cov=soc_ai --cov-report=term-missing --cov-fail-under=80 --ignore=tests/browser --ignore=tests/scale"
testpaths = ["tests"]
markers = [
"slow: marks tests as slow",
"integration: requires a live SO grid (skipped in CI)",
"browser: marks browser/E2E smoke tests (own CI job; needs the demo stack + chromium)",
"scale: marks the scale harness budget tests (own CI job; a synthetic estate of 2,000 hosts)",
"postgres: store tests that also run on PostgreSQL when SOC_AI_TEST_DATABASE_URL is set",
"sqlite_only: reads SQLite internals; the PostgreSQL run skips it",
]
[tool.coverage.run]
source = ["soc_ai"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"\\.\\.\\.",
]