unh0lymos3s/Pinkeye
GitHub: unh0lymos3s/Pinkeye
一个以 LLM 为核心的网络安全测试与红队工具 AI 编排平台,通过统一控制平面串联多种安全工具并自动执行侦察与测试任务。
Stars: 1 | Forks: 0
# Pinkeye
阅读以下说明,这只是我设计的一个包装器,代码由 Claude 编写。AI 生成的垃圾内容从此处开始:
有关完整的架构和设计,请参阅 [`IMPLEMENTATION_REPORT.md`](IMPLEMENTATION_REPORT.md)。
## 布局
| 路径 | 内容 |
|------|------|
| `control-plane/` | FastAPI 服务:scope guard、审计日志、仓库、查询/KPI/关联/报告、RBAC、作业队列、worker |
| `agent-runtime/` | LLM 提供商 + 规划循环、工具注册表、沙箱运行器、工具 + 标准化器 |
| `web/` | Next.js UI:网络地图、仪表板、查询 |
| `deploy/` | docker-compose + Dockerfiles |
| `graph/` | Neo4j schema(约束/索引) |
## 前置条件
- **Docker + Docker Compose**(最快的路径 — 启动整个技术栈)。
- 对于本地(非 Docker)开发:**Python 3.11+**、**Node 20+**,以及可访问的 **Postgres 16** 和
**Neo4j 5** 实例。
## 快速开始
应用栈在容器中运行,agent 直接与 **Ollama Cloud** 通信
(`https://ollama.com/v1`)— 无需主机 Ollama,也无需 GPU。将你的 Ollama API 密钥(来自
https://ollama.com/settings/keys)放入 `deploy/.env` 中,然后启动技术栈:
```
echo 'OLLAMA_API_KEY=' > deploy/.env # gitignored; compose loads it automatically
cd deploy
docker compose up --build
```
Compose 会读取 `deploy/.env` 并将密钥注入 `api`/`worker`(它们已预先配置为
`EYE_LLM_PROVIDER=openai`、`EYE_LLM_BASE_URL=https://ollama.com/v1`、
`EYE_LLM_MODEL=minimax-m3:cloud`)。这将启动五个服务:
| 服务 | URL / 端口 | 用途 |
|---------|-----------|---------|
| `web` | http://localhost:3000 | UI:网络地图、仪表板、查询、`/agent` 聊天 |
| `api` | http://localhost:9000 (`/docs` 为 OpenAPI) | 控制平面 |
| `worker` | — | 消耗作业队列(使用 `--scale worker=N` 进行扩容) |
| `neo4j` | http://localhost:7474 (bolt 7687) | 知识图谱 |
| `postgres` | 5432 | 持久化存储 |
API 在启动时会自动运行数据库迁移并应用 Neo4j schema。**然后播种 CVE
数据库**,以便 `cve_lookup` 和 `/cve` 正常工作:
```
docker compose exec api python -m app.cve_seed
```
打开 http://localhost:3000,创建一个限定在实验室网络范围内的 engagement,并启动扫描(或者使用
`/agent` 聊天进行由 LLM 规划的运行)。任何 Ollama Cloud 模型均可工作 — 在
`api`+`worker` 服务上设置 `EYE_LLM_MODEL`(例如 `gemma4:31b`、`gpt-oss:120b`)。如果想要
**完全在本地主机上运行且不使用云**,请指向主机/容器化的 Ollama(`EYE_LLM_PROVIDER=ollama`,`EYE_LLM_BASE_URL` 指向它),
并使用本地拉取的模型(如 `llama3.1`) — 参见“连接 LLM 提供商”部分。
## 构建与部署
### 构建镜像
```
# from repo root
docker compose -f deploy/docker-compose.yml build # all images
docker build -f deploy/api.Dockerfile -t eye-api . # API + worker (same image)
docker build -f deploy/web.Dockerfile -t eye-web ./web # UI
```
API 镜像打包了两个 Python 包(`control-plane` + `agent-runtime`),因此 API 可以在进程内运行
orchestrator,并且 worker 可以运行相同的代码路径。
### 部署清单(超出本地实验室范围)
1. **设置真实的密钥/配置**(参见下表):强 `EYE_SCOPE_SIGNING_KEY`、数据库
凭据,以及用于开启身份验证的 `EYE_API_KEYS`(否则 API 将处于开放的开发模式
管理员状态)。
2. **运行迁移**(幂等;也会在 API 启动时自动运行):
docker compose exec api python -m app.db.migrate
3. **播种/刷新 CVE 数据库**(内置的入门集,或 NVD JSON 导出):
docker compose exec api python -m app.cve_seed # bundled set
docker compose exec api python -m app.cve_seed /path/nvd.json
4. **挂载密钥**以用于可选集成(参见“启用可选集成”)。
5. **扩展 worker** 以提高吞吐量:`docker compose up -d --scale worker=4`。
6. **针对真实目标强化沙箱**:设置 `EYE_SANDBOX_RUNTIME=runsc` (gVisor) 并接入
`EYE_EGRESS_ENFORCER`。请注意,Docker socket 挂载会授予 daemon 访问权限 — 对于共享主机,请替换为代理的
runner。
### 配置(环境变量)
| 变量 | 默认值 | 用途 |
|----------|---------|---------|
| `EYE_NEO4J_URI` | `bolt://localhost:7687` | Neo4j bolt endpoint |
| `EYE_NEO4J_USER` / `EYE_NEO4J_PASSWORD` | `neo4j` / `eye-dev-password` | Neo4j 身份验证 |
| `EYE_POSTGRES_DSN` | `postgresql://eye:eye@localhost:5432/eye` | Postgres DSN |
| `EYE_SCOPE_SIGNING_KEY` | `dev-insecure-signing-key` | **用于签名范围的 HMAC 密钥 — 请务必设置此项** |
| `EYE_API_KEYS` | _(未设置 → 开放开发模式)_ | RBAC 密钥:`key:tenant:role,...` (role = viewer\|operator\|admin) |
| `EYE_LLM_PROVIDER` | `ollama` | `claude` \| `openai` \| `ollama` (默认指向位于 `localhost:11434` 的本地 Ollama) |
| `EYE_LLM_MODEL` | 提供商默认值 | 模型 ID;`EYE_LLM__MODEL` 可按角色覆盖 |
| `EYE_LLM_BASE_URL` | — | `openai`/`ollama` 适配器的 base URL(Ollama、vLLM/LM Studio 或 LiteLLM 代理) |
| `EYE_LLM_API_KEY` | — | `openai` 适配器的 API 密钥;回退到 `OPENAI_API_KEY`。Claude 读取 `ANTHROPIC_API_KEY`;Ollama 不需要 |
| `EYE_LLM_CONFIG` | — | JSON 路由文件的路径,每次运行都会重新读取 — 编辑它即可热切换模型/回退而无需重启 |
| `EYE_LLM_FALLBACK_MODELS` | — | 逗号分隔的 `provider:model`(或纯 `model`)列表,在主模型拒绝时按顺序尝试 |
| `EYE_UPLOAD_ROOT` | `/eye-uploads` | 解压 SAST 上传文件的位置;必须在主机 + api/worker + 任何 MCP SAST 同级组件上保持相同的绝对路径 |
| `EYE_SANDBOX_RUNTIME` | daemon 默认值 (`runc`) | 例如用于 gVisor 隔离的 `runsc` |
| `EYE_EGRESS_ENFORCER` | — | 应用每个作业 egress 防火墙规则的外部命令 |
| `EYE_SECRETS_DIR` | `/run/secrets` | 扫描机密文件的目录 |
| `EYE_MCP_SERVERS` | — | 将工具映射到 MCP server 后端的内联 JSON(见下文) |
| `EYE_MCP_CONFIG` | — | 具有与 `EYE_MCP_SERVERS` 相同结构的 JSON 文件路径 |
机密(通过 `EYE_SECRETS_DIR` 文件或环境变量):`VT_API_KEY` (VirusTotal)、`MSF_RPC_PASSWORD`、
`MSF_RPC_HOST`、`MSF_RPC_PORT` (Metasploit RPC)。
### MCP 工具后端(可选)
注册的工具可以通过外部 **MCP server**(semgrep、trivy、nmap、ZAP、nuclei、
VirusTotal、Metasploit 等)执行,而不是在本地沙箱中。这是按工具选择的:没有配置时,每个
工具都像以前一样在沙箱中运行。至关重要的是,harness 仅充当 MCP **client**,并且
由 MCP 支持的工具会被*包装*,因此 **scope guard、offensive-flag 门控和审计仍会优先执行**
— 模型永远不会获得原始的 MCP 访问权限,并且范围外的目标永远不会到达服务器。
```
# Run the SAST slot via Snyk Code — pooled in its own hardened container (verified live) — trivy via
# its plugin, nmap via a community one. Snyk needs SNYK_TOKEN (free-tier works); it's forwarded by
# name into the sibling. Build the Snyk MCP image first: `docker compose build mcp-snyk`.
export SNYK_TOKEN=
export EYE_MCP_SERVERS='{
"semgrep": {"pooled": true, "image": "eye-mcp-snyk:latest", "tool": "snyk_code_scan", "target_arg": "path", "env": {"SNYK_TOKEN": "'"$SNYK_TOKEN"'"}, "mounts": ["'"$PWD"'/deploy/samples:/samples:ro"]},
"trivy": {"command": "trivy", "args": ["mcp", "-t", "stdio"], "tool": "scan_filesystem", "target_arg": "path"},
"nmap": {"command": "npx", "args": ["-y", "nmap-mcp-server"], "tool": "run_nmap_scan", "target_arg": "target"}
}'
```
`pooled: true` 在一个**预热的、锁定的容器**(`docker run --rm -i`、cap-drop-all
/ read-only / no-new-privileges、无 Docker socket)中运行服务器,进程范围的池跨调用复用该容器,并在空闲 `EYE_MCP_IDLE_TTL`(默认 300s)后将其
驱逐 — 这比每次调用生成进程更轻量,**并且**与
api 进程隔离。将其用于只读分析器;将 offensive/network 工具保留在用完即弃的
单次运行沙箱中。更简单的每次调用生成形式(`command`/`args`,无 `pooled`)如
`trivy`/`nmap` 所示。默认的 `target_mode: "value"` 将经过范围检查的目标作为纯
字符串传递(`snyk_code_scan` 的源路径、nmap 的主机、ZAP 的 URL);`target_mode: "path_list"`
也可用。有关可用性表和完整规范字段,请参见 [`list.md`](list.md#mcp-integration)。
### 连接 LLM 提供商
模型层是模型无关的(`agent-runtime/runtime/llm/`):三个环境变量用于选择
提供商、模型以及(在需要时)endpoint/密钥。在运行 agent 的任何进程上设置它们
— 对于本地开发是 `uvicorn` API 和 `worker`,或者是 Compose 中的 `api` 和 `worker` 服务
(**两者**都要设置 — 它们中的任何一个都可以启动 agent 运行)。每次运行都会重新进行选择,因此更改后的
环境变量/配置将在下次运行时生效。
**Ollama Cloud — 默认提供商。** Compose 将 `api`/`worker` 指向通过 OpenAI 兼容
endpoint 的 Ollama 云,因此无需主机 Ollama 也无需 GPU。提供来自
https://ollama.com/settings/keys 的 API 密钥(在 `deploy/.env` 中作为 `OLLAMA_API_KEY`,或通过 export 导出):
```
export EYE_LLM_PROVIDER=openai
export EYE_LLM_MODEL=minimax-m3:cloud # any cloud model: gemma4:31b, gpt-oss:120b, deepseek-v3.1:671b, …
export EYE_LLM_BASE_URL=https://ollama.com/v1
export EYE_LLM_API_KEY=... # your Ollama key (compose reads OLLAMA_API_KEY from deploy/.env)
```
**本地/自托管 Ollama(完全在本地主机上,无云)— 内置默认值。** 如果没有设置
`EYE_LLM_*`,harness 将使用位于 `http://localhost:11434/v1` 的本地 Ollama(模型为
`minimax-m3:cloud`),因此纯粹的本地运行根本不需要 LLM 环境变量。自行运行 Ollama 并指向它;
从容器中使用 `http://host.docker.internal:11434/v1`(需要主机可从 Docker 访问 —
将 Ollama 绑定到 `0.0.0.0` 并允许 Docker 子网通过任何主机防火墙),或者将 Ollama 作为
同一网络上的 compose 服务运行:
```
ollama serve &
ollama pull llama3.1
# The provider/base_url already default to local Ollama; set these only to override the model/endpoint.
export EYE_LLM_PROVIDER=ollama EYE_LLM_MODEL=llama3.1 EYE_LLM_BASE_URL=http://localhost:11434/v1
```
**任何 OpenAI 兼容的 API** — OpenAI、Groq、Together、OpenRouter、vLLM、LM Studio 或
**LiteLLM 代理** — 只需将 `base_url` 指向它并提供密钥:
```
export EYE_LLM_PROVIDER=openai
export EYE_LLM_MODEL=gpt-4o # whatever id the endpoint serves
export EYE_LLM_BASE_URL=https://api.groq.com/openai/v1 # omit for OpenAI itself
export EYE_LLM_API_KEY=sk-... # or set OPENAI_API_KEY
```
**Anthropic Claude:**
```
export EYE_LLM_PROVIDER=claude
export EYE_LLM_MODEL=claude-fable-5
export ANTHROPIC_API_KEY=sk-ant-...
```
在启动运行之前验证 runtime 是否能看到提供商:
```
python -c "from runtime.llm.config import get_provider; print(type(get_provider()).__name__)"
```
**热切换(无需重启)。** `get_provider()` 在每次运行开始时都会全新执行,因此
`EYE_LLM_CONFIG` JSON 文件每次都会被重新读取 — 编辑它,下次运行即会应用
更改。LiteLLM *proxy* 也可以通过
OpenAI 兼容适配器作为 `EYE_LLM_BASE_URL` 目标运行(无进程内依赖)。
```
{ "provider": "openai", "model": "minimax-m3:cloud",
"base_url": "https://ollama.com/v1",
"fallbacks": ["openai:gpt-oss:120b", "claude:claude-fable-5"] }
```
**拒绝。** 经过安全调优的本地模型有时会拒绝已授权的侦测步骤,
返回道歉文本且没有工具调用 — 这通常看起来与“已完成”完全相同,从而静默结束运行。当配置了任何
`fallbacks` 时,提供商将被包装,以便在检测到拒绝时,它会 (1) 重新声明 engagement 的签名
授权并重试同一模型一次,然后 (2) 遍历回退链,直到
有模型配合,并在每次转换时向转录发出 `refusal` 事件。
**模型选择。** 经过重度 RLHF 的聊天模型通常会过度拒绝安全相关的措辞
对于已授权的 engagement,请优先选择适合安全操作的本地模型,并将更强的
托管模型放在回退链的最后作为后盾。
## 本地开发(不使用 Docker)
```
# Control plane + agent runtime (editable installs into one venv)
python -m venv .venv && source .venv/bin/activate
pip install -e './control-plane[dev]' -e './agent-runtime[dev]'
# Point at running Postgres/Neo4j, then migrate + seed
export EYE_POSTGRES_DSN=postgresql://eye:eye@localhost:5432/eye
python -m app.db.migrate
python -m app.cve_seed
# Run the API and (separately) a worker
uvicorn app.main:app --reload --port 9000
python -m app.worker
# Web UI
cd web && npm install && npm run dev # http://localhost:3000
```
### 运行测试
```
cd control-plane && pytest # 31 tests: scope guard, query, scoring, correlation, tenancy, hardening
cd agent-runtime && pytest # 36 tests: normalizers, agent loop, tools, exploitation/creds gating
cd web && npm run build # type-check + production build
```
## 用法
### 创建 engagement(定义已签名的范围)
```
curl -X POST localhost:9000/engagements -H 'content-type: application/json' -d '{
"name": "lab",
"allowed_cidrs": ["10.0.0.0/24"],
"allowed_domains": ["lab.example.com"],
"allowed_artifacts": ["/repos/app"],
"max_intensity": "normal"
}'
```
**仅**在那些已获得授权的 engagement 中添加 `"allow_exploit": true` 和/或
`"allow_credential_attacks": true` — 它们会被固化到 scope 签名中。
### 启动运行
```
# deterministic single tool
curl -X POST localhost:9000/engagements//runs -H 'content-type: application/json' \
-d '{"target":"10.0.0.5","tool":"nmap","intensity":"light"}'
# LLM agent plans multi-step recon/scan across all authorized tools
curl -X POST localhost:9000/engagements//runs -H 'content-type: application/json' \
-d '{"target":"10.0.0.5","mode":"agent"}'
# authenticated DAST with ZAP (credential resolved from a secret named "app-token")
curl -X POST localhost:9000/engagements//runs -H 'content-type: application/json' \
-d '{"target":"https://lab.example.com","tool":"zap",
"auth":{"header_name":"Authorization","value_ref":"app-token"}}'
```
如果设置了 `EYE_API_KEYS`,请传入 `-H "X-API-Key: "`;写入需要 `operator` 角色,读取需要 `viewer` 角色。
### SAST:扫描上传的代码库
**SAST** 标签页(`/sast`)允许操作员上传代码库 — `.zip` / `.tar` / `.tar.gz` 或
单个源文件 — 而不是预挂载仓库路径。harness 会将其解压,**通过将解压后的目录添加到 engagement 的签名 scope**(`allowed_artifacts`,重新签名)来对其进行**授权**,
然后使用所选的分析器在其上运行 `sast` 专家,并实时流式传输:**Snyk Code**(连接 Snyk MCP 后端时的
`semgrep` 插槽,否则为 Semgrep)、**gitleaks**(机密)和
**trivy**(依赖项 CVE)。上传即代表授权决定 — 它受操作员控制、可审计,
并且永远只能添加本地源路径;它永远不会授予网络或 offensive 权限。
```
# Raw file as the request body; the server extracts it and returns the path a SAST run targets.
curl -X POST "localhost:9000/engagements//sast/upload?filename=app.zip" \
--data-binary @app.zip -H 'content-type: application/octet-stream'
# -> {"path":"/eye-uploads//","file_count":123,"kind":"zip", ...}
# Then scan it (agent `sast` profile over the enabled analyzers):
curl -X POST localhost:9000/engagements//runs -H 'content-type: application/json' \
-d '{"target":"/eye-uploads//","mode":"agent","profile":"sast",
"enabled_tools":["semgrep","gitleaks","trivy"]}'
```
### 使用 LLM agent 运行 engagement(端到端)
Agent 模式(`"mode":"agent"`)将规划交给配置的 LLM:它提出工具调用,
harness 针对签名的 scope 验证每个调用,在沙箱中执行它,并反馈
简短摘要。步骤:
```
# 1. Put your Ollama key in deploy/.env and bring up the stack — the provider is already wired
# (Ollama Cloud direct). Skip to step 3 once it's running.
echo 'OLLAMA_API_KEY=' > deploy/.env
docker compose -f deploy/docker-compose.yml up --build -d
# --- OR run the agent locally instead of in Compose (see "Connecting an LLM provider"): ---
# export EYE_LLM_PROVIDER=openai EYE_LLM_MODEL=minimax-m3:cloud EYE_LLM_BASE_URL=https://ollama.com/v1 EYE_LLM_API_KEY=...
# export EYE_LLM_FALLBACK_MODELS="claude:claude-fable-5" # optional backstop, needs ANTHROPIC_API_KEY
# uvicorn app.main:app --port 9000 &
# python -m app.worker &
# 3. Create an engagement (defines the signed scope the agent can never exceed):
EID=$(curl -s -X POST localhost:9000/engagements -H 'content-type: application/json' \
-d '{"name":"lab","allowed_cidrs":["10.0.0.0/24"],"max_intensity":"normal"}' | jq -r .id)
# 4. Launch an agent run. `objective` is free-text guidance (never authorization):
RID=$(curl -s -X POST localhost:9000/engagements/$EID/runs -H 'content-type: application/json' \
-d '{"target":"10.0.0.5","mode":"agent","objective":"map exposed services and known CVEs"}' | jq -r .id)
# 5. Watch it think and act live (SSE), or replay the full transcript:
curl -N localhost:9000/runs/$RID/events # streaming: plan, thinking, tool_call, finding, refusal…
curl -s localhost:9000/runs/$RID/transcript # full JSON transcript
```
或者使用 Web UI:打开 **`/agent`** (http://localhost:3000/agent),选择 engagement,输入
目标,并观察相同的事件流 — 工具调用、发现以及任何 `refusal`/回退
转换都会内联呈现。拒绝已授权步骤的模型将作为 `refusal` 事件(以及
`stop_reason: "model refused"`)显现,而不是静默完成。
### 关键 API 端点
| 方法 / 路径 | 用途 |
|---------------|---------|
| `POST /engagements`, `GET /engagements`, `GET /engagements/{id}` | 管理 engagement |
| `POST /engagements/{id}/runs`, `GET /runs/{id}` | 启动 / 检查运行 |
| `POST /engagements/{id}/sast/upload?filename=` | 上传代码库 (zip/tar/file) 以进行静态分析;解压 + 授权路径 |
| `GET /runs/{id}/events` (SSE), `GET /runs/{id}/transcript` | 实时 agent 事件流 / 完整转录 |
| `GET /map`, `GET /engagements/{id}/graph` | 网络地图(受限) |
| `POST /engagements/{id}/graph/query` | 对图谱进行只读 Cypher 查询 |
| `GET /engagements/{id}/findings` | 筛选后的发现(严重性/类别/cve/状态/文本) |
| `GET /engagements/{id}/entities` | 主机/服务实体搜索 |
| `GET /engagements/{id}/metrics` | 仪表板 KPI |
| `GET /engagements/{id}/chains` | 关联的攻击链 |
| `POST /engagements/{id}/validate` | 将相互印证的发现提升为已确认 |
| `GET /engagements/{id}/report` | Markdown 报告 (CVSS + ATT&CK) |
| `GET /cve?product=&version=` | 离线 CVE 查找 |
## 启用可选集成
- **VirusTotal (`virustotal` 工具):** 提供作为机密/环境变量的 `VT_API_KEY`。
- **利用 / 后渗透 (`exploit`, `post_exploit`):** 运行 `msfrpcd` 并设置
`MSF_RPC_PASSWORD`(+ `MSF_RPC_HOST`/`MSF_RPC_PORT`);在 engagement 上设置 `"allow_exploit": true`。
利用操作默认为非破坏性的 `check`。
- **凭据攻击 (`credential_attack`):** 设置 `"allow_credential_attacks": true`;hydra 运行时会进行硬性限制
(≤4 个线程,遇到第一个即停止),以避免锁定/DoS。
## 验证端到端(推荐的冒烟测试)
在沙箱网络内**故意搭建一个易受攻击的目标**(Web 使用 OWASP Juice Shop,
网络使用 Metasploitable/DVWA),并**仅**针对它运行:
1. 确认 scope guard:针对范围外 IP 的扫描会被硬拒绝并记录在案。
2. 运行 nmap,然后进行 agent 运行;确认主机/端口/发现作为连接的节点出现在图谱
和仪表板 KPI 中。
3. 确认已知植入的漏洞显示为标准化、去重的发现,并附带 CVSS + ATT&CK 映射。
4. 生成报告(`GET /engagements/{id}/report`)并重新运行,以确认去重(`times_seen`)和
重放完整性。
标签:AI智能体, AI风险缓解, AV绕过, FastAPI, Neo4j, 二进制模式, 插件系统, 测试用例, 版权保护, 网络侦查, 请求拦截, 逆向工具