fix: фаза 2 — критические фиксы
READМЕ: убрать NEXUS_REPOSITORIES, CREATED→UPDATED, go/npm/Gem→go/npm, добавить MAX_CONCURRENT_SCANS, CSV-экспорт, инструкцию по вебхукам Nexus Dockerfile: uv pip install --system . (единый источник deps — pyproject.toml) docker-compose: WEBHOOK_SECRET, SCAN_TIMEOUT_SECONDS pyproject.toml: убрать deprecated [tool.ruff].select config.py: default из DEFAULT_MAX_CONCURRENT_SCANS constants.py: убрать GUARDDOG_ERRORS_KEY (мёртвый), .gem из PACKAGE_EXTENSIONS, LLM prompt: «Python»→«software» queries.py: убрать return_total Makefile: docker-up +--build, docker-down без -v, +docker-destroy, +docker-rebuild, убран typecheck
This commit is contained in:
@@ -8,8 +8,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml ./
|
COPY pyproject.toml ./
|
||||||
RUN uv pip install --system fastapi uvicorn[standard] jinja2 httpx \
|
RUN uv pip install --system .
|
||||||
"sqlalchemy[asyncio]" aiosqlite python-multipart
|
|
||||||
|
|
||||||
RUN uv pip install --system guarddog
|
RUN uv pip install --system guarddog
|
||||||
|
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: install dev test lint format typecheck docker-build docker-up docker-down clean
|
.PHONY: install dev test lint format docker-build docker-up docker-down docker-destroy docker-rebuild docker-logs clean
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pip install -e .
|
pip install -e .
|
||||||
@@ -16,18 +16,21 @@ format:
|
|||||||
ruff format guarddog_nexus tests
|
ruff format guarddog_nexus tests
|
||||||
ruff check --fix guarddog_nexus tests
|
ruff check --fix guarddog_nexus tests
|
||||||
|
|
||||||
typecheck:
|
|
||||||
mypy guarddog_nexus
|
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|
||||||
docker-up:
|
docker-up:
|
||||||
docker compose up -d
|
docker compose up -d --build
|
||||||
|
|
||||||
docker-down:
|
docker-down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
docker-destroy:
|
||||||
docker compose down -v
|
docker compose down -v
|
||||||
|
|
||||||
|
docker-rebuild:
|
||||||
|
docker compose down && docker compose up -d --build
|
||||||
|
|
||||||
docker-logs:
|
docker-logs:
|
||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
|
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -5,8 +5,8 @@
|
|||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- **Автоматическое сканирование** по вебхукам Nexus при создании/обновлении пакетов
|
- **Автоматическое сканирование** по вебхукам Nexus при создании/обновлении пакетов
|
||||||
- **Поддержка нескольких экосистем** — PyPI, Gem, и другие форматы через Nexus
|
- **Поддержка нескольких экосистем** — PyPI, Go, npm (любой формат через прокси-репозитории Nexus)
|
||||||
- **REST API** для просмотра результатов сканирования, уязвимостей и статистики
|
- **REST API** для просмотра результатов сканирования, уязвимостей, статистики и экспорта в CSV
|
||||||
- **Веб-интерфейс** с дашбордом, таблицами сканирований и фильтрацией по уязвимостям
|
- **Веб-интерфейс** с дашбордом, таблицами сканирований и фильтрацией по уязвимостям
|
||||||
- **LLM-анализ** — автоматический разбор каждой уязвимости через OpenAI-совместимые API (опционально, настраивается)
|
- **LLM-анализ** — автоматический разбор каждой уязвимости через OpenAI-совместимые API (опционально, настраивается)
|
||||||
- **Дедупликация** по URL и SHA256 — один и тот же пакет сканируется один раз
|
- **Дедупликация** по URL и SHA256 — один и тот же пакет сканируется один раз
|
||||||
@@ -72,7 +72,6 @@ python -m guarddog_nexus.main
|
|||||||
| `NEXUS_URL` | `http://localhost:8081` | URL Sonatype Nexus |
|
| `NEXUS_URL` | `http://localhost:8081` | URL Sonatype Nexus |
|
||||||
| `NEXUS_USERNAME` | `admin` | Имя пользователя Nexus |
|
| `NEXUS_USERNAME` | `admin` | Имя пользователя Nexus |
|
||||||
| `NEXUS_PASSWORD` | _(обязательно)_ | Пароль пользователя Nexus |
|
| `NEXUS_PASSWORD` | _(обязательно)_ | Пароль пользователя Nexus |
|
||||||
| `NEXUS_REPOSITORIES` | _(пусто)_ | Список репозиториев через запятую |
|
|
||||||
| `DATABASE_PATH` | `data/guarddog.db` | Путь к SQLite-базе данных |
|
| `DATABASE_PATH` | `data/guarddog.db` | Путь к SQLite-базе данных |
|
||||||
| `HOST` | `0.0.0.0` | Хост для прослушивания |
|
| `HOST` | `0.0.0.0` | Хост для прослушивания |
|
||||||
| `PORT` | `8080` | Порт для прослушивания |
|
| `PORT` | `8080` | Порт для прослушивания |
|
||||||
@@ -85,6 +84,7 @@ python -m guarddog_nexus.main
|
|||||||
| `GUARDDOG_BINARY` | `guarddog` | Путь к бинарнику GuardDog |
|
| `GUARDDOG_BINARY` | `guarddog` | Путь к бинарнику GuardDog |
|
||||||
| `NEXUS_DOWNLOAD_TIMEOUT_SECONDS` | `120` | Таймаут загрузки пакета из Nexus |
|
| `NEXUS_DOWNLOAD_TIMEOUT_SECONDS` | `120` | Таймаут загрузки пакета из Nexus |
|
||||||
| `NEXUS_API_TIMEOUT_SECONDS` | `30` | Таймаут запросов к Nexus REST API |
|
| `NEXUS_API_TIMEOUT_SECONDS` | `30` | Таймаут запросов к Nexus REST API |
|
||||||
|
| `MAX_CONCURRENT_SCANS` | `4` | Максимум одновременных сканирований GuardDog |
|
||||||
| `LOG_SYSLOG_FACILITY` | `local0` | Syslog facility (local0–local7) |
|
| `LOG_SYSLOG_FACILITY` | `local0` | Syslog facility (local0–local7) |
|
||||||
| `LLM_ENABLED` | `0` | `1` — включить LLM-анализ уязвимостей |
|
| `LLM_ENABLED` | `0` | `1` — включить LLM-анализ уязвимостей |
|
||||||
| `LLM_API_KEY` | _(пусто)_ | API-ключ (OpenAI / Groq / Ollama / etc.) |
|
| `LLM_API_KEY` | _(пусто)_ | API-ключ (OpenAI / Groq / Ollama / etc.) |
|
||||||
@@ -102,14 +102,27 @@ python -m guarddog_nexus.main
|
|||||||
|
|
||||||
### Вебхуки
|
### Вебхуки
|
||||||
|
|
||||||
Nexus отправляет вебхуки при событиях `ASSET` и `COMPONENT`. GuardDog Nexus поддерживает:
|
GuardDog Nexus принимает вебхуки от Nexus при событии `UPDATED` (срабатывает при обновлении кэша прокси-репозитория).
|
||||||
|
|
||||||
- **CREATED** — новое событие при создании пакета
|
Для валидации вебхуков установите `WEBHOOK_SECRET` в `.env` — подпись проверяется через HMAC-SHA256.
|
||||||
- **UPDATED** — событие при обновлении пакета
|
|
||||||
|
|
||||||
Для валидации вебхуков установите `WEBHOOK_SECRET` — подпись проверяется через HMAC-SHA256.
|
> **Примечание:** Вебхуки доступны в Nexus Pro. В Nexus Repository Manager 3 Community Edition можно использовать плагин [nexus-blobstore-webhook](https://github.com/sonatype-nexus-community/nexus-blobstore-webhook) или настроить вебхуки вручную через административную панель.
|
||||||
|
|
||||||
> **Примечание:** Вебхуки доступны в Nexus Pro. В Nexus Repository Manager 3 Community Edition настройка вебхуков может потребовать дополнительных плагинов.
|
### Настройка вебхуков в Nexus
|
||||||
|
|
||||||
|
1. В административной панели Nexus перейдите в **System → Capabilities**
|
||||||
|
2. Создайте capability типа **Webhook: Repository**
|
||||||
|
3. Укажите **URL:** `http://guarddog-nexus:8080/webhooks/nexus` (замените хост если нужно)
|
||||||
|
4. Выберите тип события: **Repository → Asset → Updated**
|
||||||
|
5. В фильтре репозиториев выберите: `pypi-proxy`, `go-proxy`, `npm-proxy`
|
||||||
|
6. Если задан `WEBHOOK_SECRET`, укажите тот же секрет в поле **Secret Key**
|
||||||
|
|
||||||
|
> Для локальной разработки без реальных вебхуков можно отправлять тестовые запросы вручную:
|
||||||
|
> ```bash
|
||||||
|
> curl -X POST http://localhost:8080/webhooks/nexus \
|
||||||
|
> -H "Content-Type: application/json" \
|
||||||
|
> -d '{"action":"UPDATED","repositoryName":"pypi-proxy","asset":{"format":"pypi","name":"/packages/pkg/1.0/pkg-1.0.tar.gz","downloadUrl":"http://nexus:8081/repository/pypi-proxy/packages/pkg/1.0/pkg-1.0.tar.gz"}}'
|
||||||
|
> ```
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
@@ -160,24 +173,26 @@ guarddog-nexus/
|
|||||||
├── guarddog_nexus/ # Основной пакет
|
├── guarddog_nexus/ # Основной пакет
|
||||||
│ ├── main.py # Точка входа FastAPI
|
│ ├── main.py # Точка входа FastAPI
|
||||||
│ ├── config.py # Конфигурация из переменных окружения
|
│ ├── config.py # Конфигурация из переменных окружения
|
||||||
│ ├── database.py # Async SQLAlchemy + aiosqlite
|
|
||||||
│ ├── models.py # ORM-модели (Scan, Finding)
|
|
||||||
│ ├── logging_setup.py # JSON-логирование + syslog
|
|
||||||
│ ├── harvester.py # Пайплайн: загрузка → сканирование → сохранение
|
|
||||||
│ ├── scanner.py # Интеграция с GuardDog CLI
|
|
||||||
│ ├── nexus_client.py # HTTP-клиент для Nexus REST API
|
|
||||||
│ ├── webhooks.py # Приём вебхуков Nexus
|
|
||||||
│ ├── api/ # REST API (JSON)
|
|
||||||
│ │ ├── scans.py
|
|
||||||
│ │ ├── packages.py
|
|
||||||
│ │ └── findings.py
|
|
||||||
│ ├── web/ # Веб-интерфейс
|
|
||||||
│ │ ├── routes.py
|
|
||||||
│ │ ├── templates/ # Jinja2-шаблоны
|
|
||||||
│ │ └── static/ # CSS, JS
|
|
||||||
│ ├── constants.py # Централизованные константы
|
│ ├── constants.py # Централизованные константы
|
||||||
│ ├── queries.py # Общие SQL-запросы
|
│ ├── logging_setup.py # JSON-логирование + syslog
|
||||||
│ └── llm.py # LLM-клиент
|
│ ├── core/ # Ядро: сканер, harvester, LLM
|
||||||
|
│ │ ├── scanner.py # Интеграция с GuardDog CLI
|
||||||
|
│ │ ├── harvester.py # Пайплайн: загрузка → скан → сохранение
|
||||||
|
│ │ ├── nexus.py # HTTP-клиент + extractor'ы pypi/go/npm
|
||||||
|
│ │ └── llm.py # LLM-клиент (OpenAI-совместимый)
|
||||||
|
│ ├── db/ # База данных
|
||||||
|
│ │ ├── engine.py # Async SQLAlchemy + миграции
|
||||||
|
│ │ ├── models.py # ORM-модели (Scan, Finding)
|
||||||
|
│ │ └── queries.py # Общие SQL-запросы
|
||||||
|
│ ├── routes/ # Роутеры FastAPI
|
||||||
|
│ │ ├── webhooks.py # Приём вебхуков Nexus
|
||||||
|
│ │ ├── api_scans.py # REST API: сканирования
|
||||||
|
│ │ ├── api_packages.py # REST API: пакеты
|
||||||
|
│ │ ├── api_findings.py # REST API: уязвимости
|
||||||
|
│ │ └── web.py # Веб-интерфейс (Jinja2 + htmx)
|
||||||
|
│ └── web/ # Статика и шаблоны
|
||||||
|
│ ├── templates/ # Jinja2-шаблоны
|
||||||
|
│ └── static/ # CSS, JS
|
||||||
├── tests/ # Тесты pytest
|
├── tests/ # Тесты pytest
|
||||||
├── scripts/ # Вспомогательные скрипты
|
├── scripts/ # Вспомогательные скрипты
|
||||||
├── docker-compose.yml # Стек Docker Compose
|
├── docker-compose.yml # Стек Docker Compose
|
||||||
@@ -194,11 +209,12 @@ guarddog-nexus/
|
|||||||
| `make test` | Запуск тестов |
|
| `make test` | Запуск тестов |
|
||||||
| `make lint` | Проверка кода через Ruff |
|
| `make lint` | Проверка кода через Ruff |
|
||||||
| `make format` | Форматирование кода через Ruff |
|
| `make format` | Форматирование кода через Ruff |
|
||||||
| `make typecheck` | Проверка типов через mypy |
|
|
||||||
| `make docker-build` | Сборка Docker-образа |
|
| `make docker-build` | Сборка Docker-образа |
|
||||||
| `make docker-up` | Запуск стека Docker Compose |
|
| `make docker-up` | Пересборка и запуск стека (`up -d --build`) |
|
||||||
| `make docker-down` | Остановка стека с удалением данных |
|
| `make docker-down` | Остановка стека |
|
||||||
|
| `make docker-destroy` | Остановка стека с удалением всех данных (`-v`) |
|
||||||
| `make docker-logs` | Просмотр логов стека |
|
| `make docker-logs` | Просмотр логов стека |
|
||||||
|
| `make docker-rebuild` | Полная пересборка (down + build + up) |
|
||||||
| `make clean` | Очистка артефактов сборки |
|
| `make clean` | Очистка артефактов сборки |
|
||||||
|
|
||||||
## Безопасность
|
## Безопасность
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ services:
|
|||||||
LLM_MODEL: "${LLM_MODEL:-gpt-4o-mini}"
|
LLM_MODEL: "${LLM_MODEL:-gpt-4o-mini}"
|
||||||
LLM_TIMEOUT_SECONDS: "${LLM_TIMEOUT_SECONDS:-30}"
|
LLM_TIMEOUT_SECONDS: "${LLM_TIMEOUT_SECONDS:-30}"
|
||||||
MAX_CONCURRENT_SCANS: "${MAX_CONCURRENT_SCANS:-4}"
|
MAX_CONCURRENT_SCANS: "${MAX_CONCURRENT_SCANS:-4}"
|
||||||
|
WEBHOOK_SECRET: "${WEBHOOK_SECRET:-}"
|
||||||
|
SCAN_TIMEOUT_SECONDS: "${SCAN_TIMEOUT_SECONDS:-300}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from guarddog_nexus.constants import (
|
from guarddog_nexus.constants import (
|
||||||
|
DEFAULT_MAX_CONCURRENT_SCANS,
|
||||||
GUARDDOG_BINARY_FALLBACK,
|
GUARDDOG_BINARY_FALLBACK,
|
||||||
HTTP_TIMEOUT_API,
|
HTTP_TIMEOUT_API,
|
||||||
HTTP_TIMEOUT_DOWNLOAD,
|
HTTP_TIMEOUT_DOWNLOAD,
|
||||||
@@ -46,7 +47,9 @@ class Config:
|
|||||||
scan_timeout_seconds: int = int(os.getenv("SCAN_TIMEOUT_SECONDS", "300"))
|
scan_timeout_seconds: int = int(os.getenv("SCAN_TIMEOUT_SECONDS", "300"))
|
||||||
temp_dir: str = os.getenv("TEMP_DIR", "/tmp/guarddog-nexus")
|
temp_dir: str = os.getenv("TEMP_DIR", "/tmp/guarddog-nexus")
|
||||||
guarddog_binary: str = os.getenv("GUARDDOG_BINARY", GUARDDOG_BINARY_FALLBACK)
|
guarddog_binary: str = os.getenv("GUARDDOG_BINARY", GUARDDOG_BINARY_FALLBACK)
|
||||||
max_concurrent_scans: int = int(os.getenv("MAX_CONCURRENT_SCANS", "4"))
|
max_concurrent_scans: int = int(
|
||||||
|
os.getenv("MAX_CONCURRENT_SCANS", str(DEFAULT_MAX_CONCURRENT_SCANS))
|
||||||
|
)
|
||||||
|
|
||||||
# LLM analysis
|
# LLM analysis
|
||||||
llm_enabled: bool = os.getenv("LLM_ENABLED", "").lower() in ("1", "true", "yes")
|
llm_enabled: bool = os.getenv("LLM_ENABLED", "").lower() in ("1", "true", "yes")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ used across the codebase live here to avoid duplication and drift.
|
|||||||
# Unified list of recognised package file extensions.
|
# Unified list of recognised package file extensions.
|
||||||
# NOTE: webhooks uses this to decide whether to accept an asset;
|
# NOTE: webhooks uses this to decide whether to accept an asset;
|
||||||
# harvester uses it to decide whether to download and scan.
|
# harvester uses it to decide whether to download and scan.
|
||||||
PACKAGE_EXTENSIONS = (".tar.gz", ".tgz", ".whl", ".zip", ".gem")
|
PACKAGE_EXTENSIONS = (".tar.gz", ".tgz", ".whl", ".zip")
|
||||||
|
|
||||||
# Prefix used in PyPI-style asset paths ("/packages/name/ver/file")
|
# Prefix used in PyPI-style asset paths ("/packages/name/ver/file")
|
||||||
PYPI_PATH_PREFIX = "packages"
|
PYPI_PATH_PREFIX = "packages"
|
||||||
@@ -105,7 +105,6 @@ DEFAULT_MAX_CONCURRENT_SCANS = 4
|
|||||||
GUARDDOG_OUTPUT_KEY = "--output-format"
|
GUARDDOG_OUTPUT_KEY = "--output-format"
|
||||||
GUARDDOG_OUTPUT_FORMAT = "json"
|
GUARDDOG_OUTPUT_FORMAT = "json"
|
||||||
GUARDDOG_RESULTS_KEY = "results"
|
GUARDDOG_RESULTS_KEY = "results"
|
||||||
GUARDDOG_ERRORS_KEY = "errors"
|
|
||||||
|
|
||||||
SCAN_ERROR_TIMEOUT = "timeout"
|
SCAN_ERROR_TIMEOUT = "timeout"
|
||||||
SCAN_ERROR_BINARY_NOT_FOUND = "guarddog_not_found"
|
SCAN_ERROR_BINARY_NOT_FOUND = "guarddog_not_found"
|
||||||
@@ -156,7 +155,7 @@ LLM_DEFAULT_MODEL = "gpt-4o-mini"
|
|||||||
LLM_DEFAULT_API_BASE = "https://api.openai.com/v1"
|
LLM_DEFAULT_API_BASE = "https://api.openai.com/v1"
|
||||||
LLM_DEFAULT_TIMEOUT = 30
|
LLM_DEFAULT_TIMEOUT = 30
|
||||||
LLM_ANALYSIS_SYSTEM_PROMPT = (
|
LLM_ANALYSIS_SYSTEM_PROMPT = (
|
||||||
"You are a security analyst reviewing GuardDog findings for a Python package. "
|
"You are a security analyst reviewing GuardDog findings for a software package. "
|
||||||
"Given a finding (rule name, severity, message, code snippet, location), "
|
"Given a finding (rule name, severity, message, code snippet, location), "
|
||||||
"provide a concise security analysis in 2-3 paragraphs. "
|
"provide a concise security analysis in 2-3 paragraphs. "
|
||||||
"Assess whether this is likely a real threat or a false positive. "
|
"Assess whether this is likely a real threat or a false positive. "
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ def build_scan_list_query(
|
|||||||
sort_dir: str = "desc",
|
sort_dir: str = "desc",
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
return_total: bool = True,
|
|
||||||
):
|
):
|
||||||
"""Builds a filtered, sorted, paginated query for scans.
|
"""Builds a filtered, sorted, paginated query for scans.
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,8 @@ dev = [
|
|||||||
guarddog-nexus = "guarddog_nexus.main:main"
|
guarddog-nexus = "guarddog_nexus.main:main"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py310"
|
|
||||||
line-length = 100
|
line-length = 100
|
||||||
select = ["E", "F", "I", "W"]
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "W"]
|
select = ["E", "F", "I", "W"]
|
||||||
|
|||||||
Reference in New Issue
Block a user