Compare commits

...

101 Commits
v6.4.1 ... main

Author SHA1 Message Date
jialong.wang
6d871e638b feat: update version 2025-11-05 15:26:38 +08:00
xbingW
ad2aeb4cf9 Update version.json 2025-08-14 14:45:49 +08:00
xbingW
8247d11dda Update version.json 2025-08-08 10:55:18 +08:00
xbingW
63588b8f5e Update version.json 2025-08-07 20:13:00 +08:00
xbingW
bf8d64c6b2 Update version.json 2025-07-24 20:25:36 +08:00
xbingW
0b8b253efc Create version.json 2025-07-10 19:31:10 +08:00
xbingW
24e2bac1bd Update README.md 2025-05-27 11:45:01 +08:00
safe1ine
8113dbb171 Update README.md 2025-04-21 16:08:27 +08:00
xbingW
66c4e60255 feat: desc 2025-04-10 19:33:42 +08:00
xbingW
eeae48affb feat: tools 2025-04-10 18:30:29 +08:00
xbingW
75a5346e6f feat: tools 2025-04-10 18:28:54 +08:00
xbingW
b6f694a376 feat: tools 2025-04-10 18:27:39 +08:00
xbingW
9cab30cef1 feat: defaults 2025-04-10 18:14:57 +08:00
xbingW
8fd59844ea feat: log level 2025-04-10 17:35:54 +08:00
xbingW
681e7f95f2 feat: mcp go 2025-04-10 17:30:17 +08:00
xbingW
84c2db9ee3 fix: int64 2025-04-10 17:23:01 +08:00
xbingW
c845231a1d feat: quick start 2025-04-10 17:23:01 +08:00
safe1ine
91a26b8f93 Update README.md 2025-04-10 10:32:42 +08:00
xbingW
24770cf5b4 fix: unit 2025-04-09 20:20:34 +08:00
xbingW
ff4d6616fb feat: attack events 2025-04-09 20:15:49 +08:00
xbingW
a13d269c28 feat: path 2025-04-09 19:25:11 +08:00
xbingW
19db896a9a feat: log 2025-04-09 19:16:41 +08:00
xbingW
01c38c26d0 feat: whitelist 2025-04-09 19:16:05 +08:00
xbingW
14fb45f648 feat: blacklist 2025-04-09 19:03:30 +08:00
xbingW
f371d588a4 feat: blacklist 2025-04-09 18:40:34 +08:00
xbingW
1317b3ed5a fix: rule 2025-04-09 18:16:54 +08:00
xbingW
7fdf63310d fix: tml 2025-04-09 18:15:56 +08:00
xbingW
1578ea6027 fix: tools 2025-04-09 18:13:24 +08:00
xbingW
ec3dc96765 feat: mcp go 2025-04-09 18:11:50 +08:00
姚凯
48e828cc03 fix: check slce response data status code 2025-04-08 10:27:49 +08:00
safe1ine
78e71aae23 Merge pull request #1179 from dhsifss/feat/mcp
fix: get_api response
2025-04-07 18:49:47 +08:00
姚凯
857e0c46ef fix: get_slce_api 2025-04-07 18:46:55 +08:00
姚凯
8cee76ecbe fix: remove register classmethod 2025-04-07 18:00:09 +08:00
Changzhi Li
6abcb6b69f fix requests.get 2025-04-07 09:34:48 +00:00
Changzhi Li
075bd524a9 fix names 2025-04-07 09:22:26 +00:00
Changzhi Li
5a96c2d4d0 remove bad spacve 2025-04-07 09:10:30 +00:00
Changzhi Li
8de97ff483 add mcp get_attack_event tool 2025-04-07 07:41:42 +00:00
xbingW
b7449480b3 Update docker-compose.yaml 2025-04-03 19:34:19 +08:00
姚凯
38f00302bf fix: log format 2025-04-03 19:12:14 +08:00
姚凯
850fd440ff refactor: tools 2025-04-03 18:08:14 +08:00
姚凯
b243e2b51d refactor: tools 2025-04-03 17:17:36 +08:00
xbingW
34fc8fa8e5 fix: ci 2025-04-03 15:24:53 +08:00
safe1ine
6ceeae4d2b Merge pull request #1174 from dhsifss/feat/mcp
feat: add doc and rename
2025-04-03 15:11:30 +08:00
xbingW
abd0427273 feat: rename 2025-04-03 15:04:16 +08:00
xbingW
82f0c5d52e fix: arm 2025-04-03 15:01:50 +08:00
xbingW
c7fa1efe5d feat: build image 2025-04-03 14:49:39 +08:00
xbingW
be0571a67f fix: ci 2025-04-03 14:46:26 +08:00
xbingW
a79d932801 fix: ci 2025-04-03 14:45:09 +08:00
xbingW
8157ef050e fix: ci 2025-04-03 14:44:25 +08:00
xbingW
04d1891891 fix: ci 2025-04-03 14:43:50 +08:00
xbingW
1d7eeeba36 fix: ci 2025-04-03 14:40:03 +08:00
xbingW
52f6e857df fix: ci 2025-04-03 14:38:21 +08:00
xbingW
255fd4173d fix: ci 2025-04-03 14:34:19 +08:00
姚凯
1a80a5ac07 feat: add doc and rename 2025-04-03 14:33:17 +08:00
xbingW
6324dea166 feat: add ci 2025-04-03 14:31:16 +08:00
safe1ine
695c438ec3 Merge pull request #1173 from dhsifss/feat/mcp
feat: init slmcp
2025-04-03 10:29:57 +08:00
姚凯
bb0b1187a0 feat: init slmcp 2025-04-02 23:12:41 +08:00
xbingW
ba84cc5380 Update manage.py 2025-03-25 12:10:12 +08:00
xiaobing.wang
e7f6a66083 feat: 1.0.4 2025-02-14 14:49:31 +08:00
xiaobing.wang
9086cf52d4 feat: 1.0.7 2025-02-14 14:40:58 +08:00
xiaobing.wang
b853673100 feat: use submodule 2025-01-22 12:11:27 +08:00
xiaobing.wang
381f2cba28 feat: 1.0.6 2025-01-20 19:15:45 +08:00
xiaobing.wang
37d37728ca feat: 1.0.5 2025-01-20 18:58:40 +08:00
xiaobing.wang
a1f151eed6 feat: build 2025-01-20 18:47:58 +08:00
xiaobing.wang
0e88a09fcf feat: 1.0.3 2025-01-20 18:36:28 +08:00
xiaobing.wang
644943fac1 feat: 1.0.3 2025-01-20 18:26:52 +08:00
xiaobing.wang
41888dcff7 fix: tonumber 2025-01-20 18:23:10 +08:00
xiaobing.wang
c0dfa51925 feat: 7.6.0 2025-01-20 18:23:10 +08:00
safe1ine
8d66f96228 Merge pull request #1091 from eltociear/patch-1
docs: update README.md
2025-01-17 09:53:17 +08:00
xiaobing.wang
b7a2e86c5e chore: xnet 2025-01-02 18:34:23 +08:00
xiaobing.wang
078ef2a3ce feat: add more check 2025-01-02 16:41:23 +08:00
xiaobing.wang
49deca7ce5 feat: 7.4.0 2025-01-02 16:33:44 +08:00
姚凯
53a955b439 fix: environment 2024-12-19 19:49:20 +08:00
姚凯
d4f31efdea fix: environment 2024-12-19 14:58:47 +08:00
姚凯
c3f064fa4c fix: insufficient-disk-capacity argument 2024-12-09 18:24:34 +08:00
姚凯
233ac9547c fix: clean image when uninstall 2024-12-09 10:54:54 +08:00
姚凯
5d73a333d0 feat: add restart 2024-12-06 18:05:13 +08:00
姚凯
e443cd47c0 fix: add more info 2024-12-06 16:18:48 +08:00
姚凯
2f61ebaf85 feat: add repair 2024-12-06 14:37:41 +08:00
姚凯
9fe9ff3ef1 fix: typo 2024-12-05 19:19:36 +08:00
姚凯
a75123c86d feat: add uninstall 2024-12-05 19:19:36 +08:00
xiaobing.wang
599a6903a9 feat: 7.3.0 2024-12-05 18:50:55 +08:00
xiaobing.wang
bf87488eff feat: 7.3.0 2024-12-05 18:37:14 +08:00
姚凯
c2c3fe32fb fix: remove captcha_output 2024-12-05 17:51:24 +08:00
姚凯
a18cc16b33 fix: add more log 2024-12-05 16:23:07 +08:00
xbingW
3efdd3fff5 Update manage.py 2024-12-05 15:33:25 +08:00
xbingW
9d156c65ba Merge pull request #1100 from dhsifss/main
feat: add upgrade
2024-12-05 14:38:40 +08:00
姚凯
8f575dc232 feat: add upgrade 2024-11-27 10:25:49 +08:00
xiaobing.wang
e44e88c136 feat: 7.2.0 2024-11-15 00:38:59 +08:00
safe1ine
3f8187ae6c Create manage.py
new installer
2024-11-14 17:37:26 +08:00
xiaobing.wang
de7307fbda feat: add release 2024-11-12 22:55:55 +08:00
xiaobing.wang
2685813f4d fix: chaos 2024-10-31 18:30:19 +08:00
xbingW
78912d064e Merge pull request #1095 from dhsifss/main
feat: support arm
2024-10-31 17:22:21 +08:00
姚凯
e1e801cec8 feat: support arm 2024-10-31 17:21:36 +08:00
Ikko Eltociear Ashimine
71b14ddcb0 docs: update README.md
minor fix
2024-10-26 17:48:22 +09:00
xbingW
f851033a7a Merge pull request #1085 from dhsifss/main
refactor: remove bridge
2024-10-17 19:29:37 +08:00
姚凯
82a2e4473a refactor: remove bridge 2024-10-11 18:13:20 +08:00
xiaobing.wang
c6f5467000 feat: 6.10.2 2024-09-27 14:38:47 +08:00
xiaobing.wang
2c7f0f94c5 feat: 6.9.0 2024-09-12 19:09:15 +08:00
safe1ine
e2672c93e2 Update README.md 2024-08-26 21:06:46 +08:00
xiaobing.wang
4e033ed5f3 feat: update traefik sdk 2024-08-09 11:37:49 +08:00
62 changed files with 4195 additions and 1023 deletions

41
.github/workflows/slmcp-docker.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: MCP Docker
on:
push:
branches:
- main
tags:
- "v*"
paths:
- "mcp_server/**"
- ".github/workflows/slmcp-docker.yml"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERIO_USERNAME }}
password: ${{ secrets.DOCKERIO_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./mcp_server
push: true
platforms: linux/amd64,linux/arm64
tags: |
chaitin/safeline-mcp:latest
chaitin/safeline-mcp:${{ github.ref_name }}
cache-from: type=registry,ref=chaitin/safeline-mcp:buildcache
cache-to: type=registry,ref=chaitin/safeline-mcp:buildcache,mode=max

3
.gitignore vendored
View File

@@ -5,3 +5,6 @@
*.tar.gz
build.sh
compose.yml
__pycache__
.cursor
.vscode

3
.gitmodules vendored
View File

@@ -3,3 +3,6 @@
url = https://github.com/chaitin/blazehttp
[submodule "sdk/traefik-safeline"]
path = sdk/traefik-safeline
url = https://github.com/chaitin/traefik-safeline

View File

@@ -7,9 +7,9 @@
</h4>
<p align="center">
<a target="_blank" href="https://waf.chaitin.com/">🏠 Website</a> &nbsp; | &nbsp;
<a target="_blank" href="https://docs.waf.chaitin.com/">📖 Docs</a> &nbsp; | &nbsp;
<a target="_blank" href="https://demo.waf.chaitin.com:9443/">🔍 Live Demo</a> &nbsp; | &nbsp;
<a target="_blank" href="https://ly.safepoint.cloud/laA8asp">🏠 Website</a> &nbsp; | &nbsp;
<a target="_blank" href="https://ly.safepoint.cloud/w2AeHhb">📖 Docs</a> &nbsp; | &nbsp;
<a target="_blank" href="https://ly.safepoint.cloud/hSMd4SH">🔍 Live Demo</a> &nbsp; | &nbsp;
<a target="_blank" href="https://discord.gg/SVnZGzHFvn">🙋‍♂️ Discord</a> &nbsp; | &nbsp;
<a target="_blank" href="/README_CN.md">中文版</a>
</p>
@@ -26,7 +26,7 @@ A web application firewall helps protect web apps by filtering and monitoring HT
By deploying a WAF in front of a web application, a shield is placed between the web application and the Internet. While a proxy server protects a client machines identity by using an intermediary, a WAF is a type of reverse-proxy, protecting the server from exposure by having clients pass through the WAF before reaching the server.
A WAF protects your web apps by filtering, monitoring, and blocking any malicious HTTP/S traffic traveling to the web application, and prevents any unauthorized data from leaving the app. It does this by adhering to a set of policies that help determine what traffic is malicious and what traffic is safe. Just as a proxy server acts as an intermediary to protect the identity of a client, a WAF operates in similar fashion but acting as an reverse proxy intermediary that protects the web app server from a potentially malicious client.
A WAF protects your web apps by filtering, monitoring, and blocking any malicious HTTP/S traffic traveling to the web application, and prevents any unauthorized data from leaving the app. It does this by adhering to a set of policies that help determine what traffic is malicious and what traffic is safe. Just as a proxy server acts as an intermediary to protect the identity of a client, a WAF operates in similar fashion but acting as a reverse proxy intermediary that protects the web app server from a potentially malicious client.
its core capabilities include:
@@ -52,8 +52,8 @@ List of the main features as follows:
- It defenses for all of web attacks, such as `SQL injection`, `XSS`, `code injection`, `os command injection`, `CRLF injection`, `XXE`, `SSRF`, `path traversal` and so on.
- **`Rate Limiting`**
- Defend your web apps against `DoS attacks`, `bruteforce attempts`, `traffic surges`, and other types of abuse by throttling traffic that exceeds defined limits.
- **`Captcha Challenge`**
- CAPTCHA challenges to protect your website from `bot attacks`, humen users will be allowed, crawlers and bots will be blocked.
- **`Anti-Bot Challenge`**
- Anti-Bot challenges to protect your website from `bot attacks`, humen users will be allowed, crawlers and bots will be blocked.
- **`Authentication Challenge`**
- When authentication challenge turned on, visitors need to enter the password, otherwise they will be blocked.
- **`Dynamic Protection`**
@@ -65,7 +65,7 @@ List of the main features as follows:
| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- |
| **`Block Web Attacks`** | <img src="./images/skeleton.png" width=270 /> | <img src="./images/blocked-for-attack-detected.png" width=270 /> |
| **`Rate Limiting`** | <img src="./images/skeleton.png" width=270 /> | <img src="./images/blocked-for-access-too-fast.png" width=270 /> |
| **`Captcha Challenge`** | <img src="./images/captcha-1.gif" width=270 /> | <img src="./images/captcha-2.gif" width=270 /> |
| **`Anti-Bot Challenge`** | <img src="./images/captcha-1.gif" width=270 /> | <img src="./images/captcha-2.gif" width=270 /> |
| **`Auth Challenge`** | <img src="./images/auth-1.gif" width=270 /> | <img src="./images/auth-2.gif" width=270 /> |
| **`HTML Dynamic Protection`** | <img src="./images/dynamic-html-1.png" width=270 /> | <img src="./images/dynamic-html-2.png" width=270 /> |
| **`JS Dynamic Protection`** | <img src="./images/dynamic-js-1.png" width=270 /> | <img src="./images/dynamic-js-2.png" width=270 /> |
@@ -77,11 +77,11 @@ List of the main features as follows:
#### 📦 Installing
Information on how to install SafeLine can be found in the [Install Guide](https://docs.waf.chaitin.com/en/tutorials/install)
Information on how to install SafeLine can be found in the [Install Guide](https://docs.waf.chaitin.com/en/GetStarted/Deploy)
#### ⚙️ Protecting Web Apps
to see [Configuration](https://docs.waf.chaitin.com/en/tutorials/Configuration)
to see [Configuration](https://docs.waf.chaitin.com/en/GetStarted/AddApplication)
## 📋 More Informations

View File

@@ -14,7 +14,7 @@ services:
postgres:
container_name: safeline-pg
restart: always
image: ${IMAGE_PREFIX}/safeline-postgres:15.2
image: ${IMAGE_PREFIX}/safeline-postgres${ARCH_SUFFIX}:15.2
volumes:
- ${SAFELINE_DIR}/resources/postgres/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
@@ -30,11 +30,13 @@ services:
mgt:
container_name: safeline-mgt
restart: always
image: ${IMAGE_PREFIX}/safeline-mgt-g:${IMAGE_TAG:?image tag required}
image: ${IMAGE_PREFIX}/safeline-mgt${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG:?image tag required}
volumes:
- /etc/localtime:/etc/localtime:ro
- ${SAFELINE_DIR}/resources/mgt:/app/data
- ${SAFELINE_DIR}/logs/nginx:/app/log/nginx:z
- ${SAFELINE_DIR}/resources/sock:/app/sock
- /var/run:/app/run
ports:
- ${MGT_PORT:-9443}:1443
healthcheck:
@@ -45,6 +47,7 @@ services:
- postgres
- fvm
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
@@ -54,7 +57,7 @@ services:
detect:
container_name: safeline-detector
restart: always
image: ${IMAGE_PREFIX}/safeline-detector-g:${IMAGE_TAG}
image: ${IMAGE_PREFIX}/safeline-detector${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
volumes:
- ${SAFELINE_DIR}/resources/detector:/resources/detector
- ${SAFELINE_DIR}/logs/detector:/logs/detector
@@ -64,48 +67,32 @@ services:
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.5
mario:
container_name: safeline-mario
restart: always
image: ${IMAGE_PREFIX}/safeline-mario-g:${IMAGE_TAG}
volumes:
- ${SAFELINE_DIR}/resources/mario:/resources/mario
- ${SAFELINE_DIR}/logs/mario:/logs/mario
- /etc/localtime:/etc/localtime:ro
environment:
- LOG_DIR=/logs/mario
- GOGC=100
- DATABASE_URL=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce
logging:
options:
max-size: "100m"
max-file: "5"
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.6
tengine:
container_name: safeline-tengine
restart: always
image: ${IMAGE_PREFIX}/safeline-tengine-g:${IMAGE_TAG}
image: ${IMAGE_PREFIX}/safeline-tengine${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/resolv.conf:/etc/resolv.conf:ro
- ${SAFELINE_DIR}/resources/nginx:/etc/nginx
- ${SAFELINE_DIR}/resources/detector:/resources/detector
- ${SAFELINE_DIR}/resources/chaos:/resources/chaos
- ${SAFELINE_DIR}/logs/nginx:/var/log/nginx:z
- ${SAFELINE_DIR}/resources/cache:/usr/local/nginx/cache
- ${SAFELINE_DIR}/resources/sock:/app/sock
environment:
- TCD_MGT_API=https://${SUBNET_PREFIX}.4:1443/api/open/publish/server
- TCD_SNSERVER=${SUBNET_PREFIX}.5:8000
# deprecated
- SNSERVER_ADDR=${SUBNET_PREFIX}.5:8000
- CHAOS_ADDR=${SUBNET_PREFIX}.10
ulimits:
nofile: 131072
network_mode: host
luigi:
container_name: safeline-luigi
restart: always
image: ${IMAGE_PREFIX}/safeline-luigi-g:${IMAGE_TAG}
image: ${IMAGE_PREFIX}/safeline-luigi${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
environment:
- MGT_IP=${SUBNET_PREFIX}.4
- LUIGI_PG=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable
@@ -113,6 +100,7 @@ services:
- /etc/localtime:/etc/localtime:ro
- ${SAFELINE_DIR}/resources/luigi:/app/data
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
@@ -125,49 +113,31 @@ services:
fvm:
container_name: safeline-fvm
restart: always
image: ${IMAGE_PREFIX}/safeline-fvm-g:${IMAGE_TAG}
image: ${IMAGE_PREFIX}/safeline-fvm${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
volumes:
- /etc/localtime:/etc/localtime:ro
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.8
bridge:
container_name: safeline-bridge
restart: always
image: ${IMAGE_PREFIX}/safeline-bridge-g:${IMAGE_TAG}
command:
- /app/bridge
- serve
- -n
- unix
- -a
- /app/run/safeline.sock
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run:/app/run
logging:
options:
max-size: "100m"
max-file: "5"
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.9
depends_on:
- mgt
chaos:
container_name: safeline-chaos
restart: always
image: ${IMAGE_PREFIX}/safeline-chaos-g:${IMAGE_TAG}
image: ${IMAGE_PREFIX}/safeline-chaos${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
environment:
- DB_ADDR=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable
volumes:
- ${SAFELINE_DIR}/resources/sock:/app/sock
- ${SAFELINE_DIR}/resources/chaos:/app/chaos
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.10
ipv4_address: ${SUBNET_PREFIX}.10

View File

@@ -14,7 +14,7 @@ require (
)
require (
golang.org/x/net v0.25.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)

28
mcp_server/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM golang:1.24-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o mcp-server .
FROM alpine:latest
WORKDIR /app
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/mcp-server .
COPY --from=builder /app/config.yaml .
ENV TZ=Asia/Shanghai
EXPOSE 5678
CMD ["./mcp-server"]

256
mcp_server/README.md Normal file
View File

@@ -0,0 +1,256 @@
# SafeLine MCP Server
SafeLine MCP Server is an implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) that provides complete management and control capabilities for SafeLine WAF.
[![Docker](https://img.shields.io/badge/Docker-Supported-2496ED?style=flat-square&logo=docker&logoColor=white)](docker-compose.yml)
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod)
## Use Cases
- Automated management and control of SafeLine WAF instances
- WAF configuration and policy management through API
- Building AI-based security protection tools and applications
## Prerequisites
1. Install [Docker](https://www.docker.com/) (if running in container)
2. Configure SafeLine API Token (obtained from SafeLine console)
## Features
- Complete MCP (Management Control Protocol) server implementation
- Support for SafeLine WAF instance management and control
- Flexible configuration system supporting file configuration and environment variables
- Docker containerization support
- Secure API communication
## Quick Start
### Environment Variables
| Environment Variable | Description | Default Value | Required |
|---------|------|--------|-----|
| LISTEN_PORT | Service listening port | 5678 | No |
| LISTEN_ADDRESS | Service listening address | 0.0.0.0 | No |
| SAFELINE_SECRET | SSE server secret | - | No |
| SAFELINE_ADDRESS | SafeLine API address | - | Yes |
| SAFELINE_API_TOKEN | SafeLine API authentication token | - | Yes |
### Using Docker
#### Method 1: Using docker run
```bash
docker run -d \
--name safeline-mcp \
-p 5678:5678 \
-e SAFELINE_API_TOKEN="your_api_token" \
-e SAFELINE_ADDRESS="https://your.safeline.com" \
-e LISTEN_PORT=5678 \
-e LISTEN_ADDRESS="0.0.0.0" \
chaitin/safeline-mcp:latest
```
#### Method 2: Using docker-compose
```bash
# 1. Clone repository
git clone https://github.com/chaitin/safeline-mcp.git
cd safeline-mcp
# 2. Edit docker-compose.yml to configure environment variables
# Example docker-compose.yml:
# version: '3'
# services:
# mcp:
# image: chaitin/safeline-mcp:latest
# container_name: safeline-mcp
# ports:
# - "5678:5678"
# environment:
# - SAFELINE_API_TOKEN=your_api_token
# - SAFELINE_ADDRESS=https://your.safeline.com
# - LISTEN_PORT=5678
# - LISTEN_ADDRESS=0.0.0.0
# 3. Start service
docker compose -f docker-compose.yml up -d
```
#### Method 3: Using Go
```bash
# 1. Clone repository
git clone https://github.com/chaitin/SafeLine.git
cd safeline-mcp
# 2. Install dependencies
go mod download
# 3. Configure config.yaml
cp config.yaml.example config.yaml
# Edit config.yaml with necessary configurations
# 4. Run service
go run main.go
```
For more API details, please refer to the [API Documentation](https://demo.waf.chaitin.com:9443/swagger/index.html).
## Tools
### Application Management
- **create_application**
### Rule Management
- **create_blacklist_rule**
- **create_whitelist_rule**
### Analyze
- **get_attack_events**
## Development Guide
The Go API in this project is currently under development, and APIs may change. If you have specific requirements, please submit an Issue for discussion.
### Directory Structure
```
internal/
├── api/ # API implementation
│ ├── app/ # Application-related APIs
│ │ └── create_application.go
│ └── rule/ # Rule-related APIs
│ └── create_rule.go
└── tools/ # MCP tool implementation
├── app/ # Application-related tools
│ └── create_application.go
└── rule/ # Rule-related tools
└── create_rule.go
```
### Adding New Tools
1. **Create Tool File**
- Create corresponding directory and file under `internal/tools`
- File name should match tool name
- Use separate file for each tool
- Example: `internal/tools/app/create_application.go`
2. **Tool Implementation Template**
```go
package app
type ToolName struct{}
type ToolParams struct {
// Parameter definitions
Param1 string `json:"param1" desc:"parameter description" required:"true"`
Param2 int `json:"param2" desc:"parameter description" required:"false"`
}
type ToolResult struct {
Field1 string `json:"field1"`
}
func (t *ToolName) Name() string {
return "tool_name"
}
func (t *ToolName) Description() string {
return "tool description"
}
func (t *ToolName) Validate(params ToolParams) error {
// Parameter validation logic
return nil
}
func (t *ToolName) Execute(ctx context.Context, params ToolParams) (result ToolResult, err error) {
// Tool execution logic
return result, nil
}
```
3. **[Optional]Create API Implementation**
If you need to use some APIs that have not been implemented yet, you need to create corresponding files in the api directory for implementation
- Create same directory structure under `internal/api`
- File name should match tool func
- Example: `internal/api/app/create_application.go`
**API Implementation Template**
```go
package app
type RequestType struct {
// Request parameter definitions
Param1 string `json:"param1"`
Param2 int `json:"param2"`
}
func APIName(ctx context.Context, req *RequestType) (ResultType, error) {
if req == nil {
return nil, errors.New("request is required")
}
var resp api.Response[ResultType]
err := api.Service().Post(ctx, "/api/path", req, &resp)
if err != nil {
return nil, errors.Wrap(err, "failed to execute")
}
if resp.Err != nil {
return nil, errors.New(resp.Msg)
}
return resp.Data, nil
}
```
4. **Tool Registration (init.go)**
The tool registration file `internal/tools/init.go` is used to centrally manage all tool registrations
- Register all tools uniformly in the `init()` function
- Use the `AppendTool()` method for registration
- Example:
```go
// Register create application tool
AppendTool(&app.CreateApp{})
// Register create blacklist rule tool
AppendTool(&rule.CreateBlacklistRule{})
```
### Development Standards
1. **Naming Conventions**
- Use lowercase letters and underscores for tool names
- File names should match tool names
2. **Directory Organization**
- Divide directories by functional modules (e.g., app, rule, etc.)
- Maintain consistent structure between tools and api directories
- Keep related functionality in the same directory
3. **Code Standards**
- Follow Go standard code conventions
- Add necessary parameter validation
- Use unified error handling approach
- Add appropriate logging
4. **Documentation Requirements**
- Provide clear functional description in tool Description
- Add detailed description for parameters
- Update API toolkit documentation in README
### Example
Refer to the implementation of the `create_application` tool:
- Tool implementation: `internal/tools/app/create_application.go`
- API implementation: `internal/api/app/create_application.go`

27
mcp_server/config.yaml Normal file
View File

@@ -0,0 +1,27 @@
# Server Configuration
server:
name: "SafeLine MCP Server"
version: "1.0.0"
# Can be overridden by environment variable LISTEN_PORT
port: 5678
# Can be overridden by environment variable LISTEN_ADDRESS
host: "0.0.0.0"
# Can be overridden by environment variable SAFELINE_SECRET
secret: "" # Secret for SSE server
# Logger Configuration
logger:
level: "info" # Log level: debug, info, warn, error
file_path: "" # Log file path
console: true # Whether to output to console
caller: false # Whether to record caller information
development: true # Whether to use development mode
# API Configuration
api:
# Can be overridden by environment variable SAFELINE_ADDRESS
base_url: "" # API service address
# Can be overridden by environment variable SAFELINE_API_TOKEN
token: "" # Authentication token
timeout: 30 # Timeout in seconds
debug: false # Whether to enable debug mode
insecure_skip_verify: true # Whether to skip certificate verification

View File

@@ -0,0 +1,15 @@
version: '3.8'
services:
mcp_server:
image: chaitin/safeline-mcp:latest
container_name: mcp_server
restart: always
ports:
- "5678:5678"
environment:
- SAFELINE_SECRET=your_secret_key # optional, if you want to use secret key to authenticate
- SAFELINE_ADDRESS=https://your_safeline_ip:9443 # required, your SafeLine WAF address
- SAFELINE_API_TOKEN=your_safeline_api_token # required, your SafeLine WAF api token
- LISTEN_PORT=5678 # optional, default is 5678
- LISTEN_ADDRESS=0.0.0.0 # optional, default is 0.0.0.0

16
mcp_server/go.mod Normal file
View File

@@ -0,0 +1,16 @@
module github.com/chaitin/SafeLine/mcp_server
go 1.24.1
require (
github.com/mark3labs/mcp-go v0.18.0
go.uber.org/zap v1.27.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/google/uuid v1.6.0 // indirect
github.com/mcuadros/go-defaults v1.2.0
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
)

28
mcp_server/go.sum Normal file
View File

@@ -0,0 +1,28 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,47 @@
package analyze
import (
"context"
"fmt"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
)
type GetEventListRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
IP string `json:"ip"`
Start int64 `json:"start"`
End int64 `json:"end"`
}
type GetEventListResponse struct {
Nodes []Event `json:"nodes"`
Total int64 `json:"total"`
}
type Event struct {
ID uint `json:"id"`
IP string `json:"ip"`
Protocol int `json:"protocol"`
Host string `json:"host"`
DstPort uint64 `json:"dst_port"`
UpdatedAt int64 `json:"updated_at"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
DenyCount int64 `json:"deny_count"`
PassCount int64 `json:"pass_count"`
Finished bool `json:"finished"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}
func GetEventList(ctx context.Context, req *GetEventListRequest) (*GetEventListResponse, error) {
var resp api.Response[GetEventListResponse]
err := api.Service().Get(ctx, fmt.Sprintf("/api/open/events?page=%d&page_size=%d&ip=%s&start=%d&end=%d", req.Page, req.PageSize, req.IP, req.Start, req.End), &resp)
if err != nil {
return nil, err
}
return &resp.Data, nil
}

View File

@@ -0,0 +1,34 @@
package app
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
)
type CreateAppRequest struct {
ServerNames []string `json:"server_names"`
Ports []string `json:"ports"`
Upstreams []string `json:"upstreams"`
Comment string `json:"comment"`
}
// CreateApp Create new website or app
func CreateApp(ctx context.Context, req *CreateAppRequest) (int64, error) {
if req == nil {
return 0, errors.New("request is required")
}
var resp api.Response[int64]
err := api.Service().Post(ctx, "/api/open/site", req, &resp)
if err != nil {
return 0, errors.Wrap(err, "failed to create app")
}
if resp.Err != nil {
return 0, errors.New(resp.Msg)
}
return resp.Data, nil
}

View File

@@ -0,0 +1,157 @@
package api
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
// Client API client
type Client struct {
baseURL string
httpClient *http.Client
headers map[string]string
}
// ClientOption Client configuration options
type ClientOption func(*Client)
// WithTimeout Set timeout duration
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.httpClient.Timeout = timeout
}
}
// WithHeader Set request header
func WithHeader(key, value string) ClientOption {
return func(c *Client) {
c.headers[key] = value
}
}
// WithBaseURL Set base URL
func WithBaseURL(baseURL string) ClientOption {
return func(c *Client) {
c.baseURL = baseURL
}
}
// WithInsecureSkipVerify Set whether to skip certificate verification
func WithInsecureSkipVerify(skip bool) ClientOption {
return func(c *Client) {
if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = skip
}
}
}
// NewClient Create new API client
func NewClient(opts ...ClientOption) *Client {
transport := &http.Transport{
TLSClientConfig: &tls.Config{},
}
c := &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
},
headers: make(map[string]string),
}
for _, opt := range opts {
opt(c)
}
return c
}
// Request Send request
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
reqURL := fmt.Sprintf("%s%s", c.baseURL, path)
var bodyReader io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return errors.Wrap(err, "marshal request body failed")
}
bodyReader = bytes.NewReader(bodyBytes)
}
logger.With("url", reqURL).Debug("request url")
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
if err != nil {
return errors.Wrap(err, "create request failed")
}
// Set common headers
req.Header.Set("Content-Type", "application/json")
for k, v := range c.headers {
req.Header.Set(k, v)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "send request failed")
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "read response body failed")
}
// Check status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New(fmt.Sprintf("request failed with status %d: %s", resp.StatusCode, string(respBody)))
}
// Parse response
if result != nil {
if err := json.Unmarshal(respBody, result); err == nil {
return nil
}
var respData map[string]interface{}
if err := json.Unmarshal(respBody, &respData); err != nil {
return errors.Wrap(err, "unmarshal response failed")
}
if respData["err"] != nil || respData["msg"] != nil {
return errors.New(respData["msg"].(string))
}
}
return nil
}
// Get Send GET request
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
return c.Request(ctx, http.MethodGet, path, nil, result)
}
// Post Send POST request
func (c *Client) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.Request(ctx, http.MethodPost, path, body, result)
}
// Put Send PUT request
func (c *Client) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.Request(ctx, http.MethodPut, path, body, result)
}
// Delete Send DELETE request
func (c *Client) Delete(ctx context.Context, path string, result interface{}) error {
return c.Request(ctx, http.MethodDelete, path, nil, result)
}

View File

@@ -0,0 +1,11 @@
package api
// Response Common API response structure
type Response[T any] struct {
// Response data
Data T `json:"data"`
// Error message
Err any `json:"err"`
// Prompt message
Msg string `json:"msg"`
}

View File

@@ -0,0 +1,35 @@
package rule
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
)
type CreateRuleRequest struct {
Name string `json:"name"`
IP []string `json:"ip"`
IsEnabled bool `json:"is_enabled"`
Pattern [][]api.Pattern `json:"pattern"`
Action int `json:"action"`
}
// CreateRule Create new rule
func CreateRule(ctx context.Context, req *CreateRuleRequest) (int64, error) {
if req == nil {
return 0, errors.New("request is required")
}
var resp api.Response[int64]
err := api.Service().Post(ctx, "/api/open/policy", req, &resp)
if err != nil {
return 0, errors.Wrap(err, "failed to create policy rule")
}
if resp.Err != nil {
return 0, errors.New(resp.Msg)
}
return resp.Data, nil
}

View File

@@ -0,0 +1,100 @@
package api
import (
"context"
"sync"
"time"
"github.com/chaitin/SafeLine/mcp_server/internal/config"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
// APIClient API client implementation
type APIClient struct {
client *Client
config *config.APIConfig
}
var (
instance *APIClient
once sync.Once
)
// Init Initialize API service
func Init(cfg *config.APIConfig) error {
var err error
once.Do(func() {
instance, err = newAPIClient(cfg)
if err != nil {
logger.With("error", err).Error("failed to initialize API service")
return
}
logger.Info("API service initialized successfully")
})
return err
}
// Service Get API service instance
func Service() *APIClient {
if instance == nil {
logger.Error("API service not initialized")
panic("API service not initialized")
}
return instance
}
// newAPIClient Create new API client
func newAPIClient(config *config.APIConfig) (*APIClient, error) {
if config == nil {
return nil, errors.New("config is required")
}
if config.BaseURL == "" {
return nil, errors.New("base_url is required")
}
timeout := 30
if config.Timeout > 0 {
timeout = config.Timeout
}
opts := []ClientOption{
WithBaseURL(config.BaseURL),
WithTimeout(time.Duration(timeout) * time.Second),
WithHeader("User-Agent", "SafeLine-MCP/1.0"),
WithInsecureSkipVerify(config.InsecureSkipVerify),
}
// If token is configured, add authentication header
if config.Token != "" {
opts = append(opts, WithHeader("X-SLCE-API-TOKEN", config.Token))
}
client := NewClient(opts...)
return &APIClient{
client: client,
config: config,
}, nil
}
// Post Send POST request
func (c *APIClient) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.client.Request(ctx, "POST", path, body, result)
}
// Get Send GET request
func (c *APIClient) Get(ctx context.Context, path string, result interface{}) error {
return c.client.Request(ctx, "GET", path, nil, result)
}
// Put Send PUT request
func (c *APIClient) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.client.Request(ctx, "PUT", path, body, result)
}
// Delete Send DELETE request
func (c *APIClient) Delete(ctx context.Context, path string, result interface{}) error {
return c.client.Request(ctx, "DELETE", path, nil, result)
}

View File

@@ -0,0 +1,50 @@
package api
type PolicyRuleAction int
const (
PolicyRuleActionAllow PolicyRuleAction = iota
PolicyRuleActionDeny
PolicyRuleActionMax
)
type Key = string
const (
KeySrcIP Key = "src_ip"
KeyURI Key = "uri"
KeyURINoQuery Key = "uri_no_query"
KeyHost Key = "host"
KeyMethod Key = "method"
KeyReqHeader Key = "req_header"
KeyReqBody Key = "req_body"
KeyGetParam Key = "get_param"
KeyPostParam Key = "post_param"
)
type Op = string
const (
OpEq Op = "eq" // equal
OpNotEq Op = "not_eq" // not equal
OpMatch Op = "match" // match
OpCIDR Op = "cidr" // cidr
OpHas Op = "has" // has
OpNotHas Op = "not_has" // not has
OpPrefix Op = "prefix" // prefix
OpRe Op = "re" // regex
OpIn Op = "in" // in
OpNotIn Op = "not_in" // not in
OpNotCIDR Op = "not_cidr" // not cidr
OpExist Op = "exist" // exist
OpNotExist Op = "not_exist" // not exist
OpGeoEq Op = "geo_eq" // geo equal
OpGeoNotEq Op = "geo_not_eq" // geo not equal
)
type Pattern struct {
K Key `json:"k"`
Op Op `json:"op"`
V []string `json:"v"`
SubK string `json:"sub_k"`
}

View File

@@ -0,0 +1,118 @@
package config
import (
"os"
"strconv"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"gopkg.in/yaml.v3"
)
// Config Global configuration structure
type Config struct {
Server *ServerConfig `yaml:"server"`
Logger *LoggerConfig `yaml:"logger"`
API *APIConfig `yaml:"api"`
}
// APIConfig API configuration
type APIConfig struct {
// API base URL
BaseURL string `yaml:"base_url"`
// API token
Token string `yaml:"token"`
// API timeout
Timeout int `yaml:"timeout"`
// API debug mode
Debug bool `yaml:"debug"`
// API insecure skip verify
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
}
// ServerConfig Server configuration
type ServerConfig struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Port int `yaml:"port"`
Host string `yaml:"host"`
Secret string `yaml:"secret"`
}
// LoggerConfig Logger configuration
type LoggerConfig struct {
Level string `yaml:"level"`
FilePath string `yaml:"file_path"`
Console bool `yaml:"console"`
Caller bool `yaml:"caller"`
Development bool `yaml:"development"`
}
var config *Config
// getEnvString Get string value from environment variable, return default value if not exists
func getEnvString(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
// getEnvInt Get integer value from environment variable, return default value if not exists or cannot be parsed
func getEnvInt(key string, defaultValue int) int {
if value, exists := os.LookupEnv(key); exists {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// Load Load configuration file
func Load(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return errors.Wrap(err, "read config file failed")
}
config = &Config{}
if err := yaml.Unmarshal(data, config); err != nil {
return errors.Wrap(err, "unmarshal config failed")
}
// Override configuration from environment variables
if config.Server != nil {
config.Server.Host = getEnvString("LISTEN_ADDRESS", config.Server.Host)
config.Server.Port = getEnvInt("LISTEN_PORT", config.Server.Port)
}
if config.API != nil {
config.API.BaseURL = getEnvString("SAFELINE_ADDRESS", config.API.BaseURL)
config.API.Token = getEnvString("SAFELINE_API_TOKEN", config.API.Token)
}
return nil
}
// GetServer Get server configuration
func GetServer() *ServerConfig {
if config == nil {
return nil
}
return config.Server
}
// GetLogger Get logger configuration
func GetLogger() *LoggerConfig {
if config == nil {
return nil
}
return config.Logger
}
// GetAPI Get API configuration
func GetAPI() *APIConfig {
if config == nil {
return nil
}
return config.API
}

View File

@@ -0,0 +1,45 @@
package analyze
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api/analyze"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type GetAttackEventsParams struct {
IP string `json:"ip" desc:"ip" required:"false"`
Page int `json:"page" desc:"page" required:"false" default:"1"`
PageSize int `json:"page_size" desc:"page size" required:"false" default:"10"`
Start int64 `json:"start" desc:"start unix timestamp in milliseconds" required:"false"`
End int64 `json:"end" desc:"end unix timestamp in milliseconds" required:"false"`
}
type GetAttackEvents struct{}
func (t *GetAttackEvents) Name() string {
return "get_attack_events"
}
func (t *GetAttackEvents) Description() string {
return "get attack events"
}
func (t *GetAttackEvents) Validate(params GetAttackEventsParams) error {
return nil
}
func (t *GetAttackEvents) Execute(ctx context.Context, params GetAttackEventsParams) (analyze.GetEventListResponse, error) {
resp, err := analyze.GetEventList(ctx, &analyze.GetEventListRequest{
IP: params.IP,
PageSize: params.PageSize,
Page: params.Page,
Start: params.Start,
End: params.End,
})
if err != nil {
return analyze.GetEventListResponse{}, err
}
logger.With("total", resp.Total).Info("get attack events")
return *resp, nil
}

View File

@@ -0,0 +1,41 @@
package app
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api/app"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CreateApp struct{}
type CreateAppParams struct {
ServerNames []string `json:"server_names" desc:"domain list" required:"true"`
Ports []string `json:"ports" desc:"port list" required:"true"`
Upstreams []string `json:"upstreams" desc:"upstream list" required:"true"`
}
func (t *CreateApp) Name() string {
return "create_http_application"
}
func (t *CreateApp) Description() string {
return "create a new website or app"
}
func (t *CreateApp) Validate(params CreateAppParams) error {
return nil
}
func (t *CreateApp) Execute(ctx context.Context, params CreateAppParams) (int64, error) {
id, err := app.CreateApp(ctx, &app.CreateAppRequest{
ServerNames: params.ServerNames,
Ports: params.Ports,
Upstreams: params.Upstreams,
})
if err != nil {
return 0, err
}
logger.Info("create app success", logger.Int64("id", id))
return id, nil
}

View File

@@ -0,0 +1,46 @@
package tools
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CalculateSum struct{}
func (t *CalculateSum) Name() string {
return "calculate_sum"
}
func (t *CalculateSum) Description() string {
return "Add two numbers together"
}
type MyToolInput struct {
A int `json:"a" desc:"number a" required:"true"`
B int `json:"b" desc:"number b" required:"true"`
}
type MyToolOutput struct {
C int `json:"c"`
}
func (t *CalculateSum) Validate(params MyToolInput) error {
return nil
}
func (t *CalculateSum) Execute(ctx context.Context, params MyToolInput) (MyToolOutput, error) {
logger.With("a", params.A).
With("b", params.B).
Debug("Executing calculation")
result := MyToolOutput{
C: params.A + params.B,
}
logger.With("result", result.C).
Debug("Calculation completed")
return result, errors.New("test error")
}

View File

@@ -0,0 +1,19 @@
package tools
import (
"github.com/chaitin/SafeLine/mcp_server/internal/tools/analyze"
"github.com/chaitin/SafeLine/mcp_server/internal/tools/app"
"github.com/chaitin/SafeLine/mcp_server/internal/tools/rule"
)
func init() {
// app
AppendTool(&app.CreateApp{})
// rule
AppendTool(&rule.CreateBlacklistRule{})
AppendTool(&rule.CreateWhitelistRule{})
// analyze
AppendTool(&analyze.GetAttackEvents{})
}

View File

@@ -0,0 +1,66 @@
package rule
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CreateBlacklistRule struct{}
type CreateBlacklistRuleParams struct {
Name string `json:"name" desc:"name" required:"true"`
IP []string `json:"ip" desc:"ip" required:"false"`
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
}
func (t *CreateBlacklistRule) Name() string {
return "create_blacklist_rule"
}
func (t *CreateBlacklistRule) Description() string {
return "create a new blacklist rule"
}
func (t *CreateBlacklistRule) Validate(params CreateBlacklistRuleParams) error {
return nil
}
func (t *CreateBlacklistRule) Execute(ctx context.Context, params CreateBlacklistRuleParams) (int64, error) {
var pattern [][]api.Pattern
if len(params.IP) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeySrcIP,
Op: api.OpEq,
V: params.IP,
SubK: "",
},
})
}
if len(params.URINoQuery) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeyURINoQuery,
Op: api.OpEq,
V: params.URINoQuery,
SubK: "",
},
})
}
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
Name: params.Name,
IP: params.IP,
IsEnabled: true,
Action: int(api.PolicyRuleActionDeny),
Pattern: pattern,
})
if err != nil {
return 0, err
}
logger.With("id", id).Info("create blacklist rule success")
return id, nil
}

View File

@@ -0,0 +1,66 @@
package rule
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CreateWhitelistRule struct{}
type CreateWhitelistRuleParams struct {
Name string `json:"name" desc:"name" required:"true"`
IP []string `json:"ip" desc:"ip" required:"false"`
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
}
func (t *CreateWhitelistRule) Name() string {
return "create_whitelist_rule"
}
func (t *CreateWhitelistRule) Description() string {
return "create a new whitelist rule"
}
func (t *CreateWhitelistRule) Validate(params CreateWhitelistRuleParams) error {
return nil
}
func (t *CreateWhitelistRule) Execute(ctx context.Context, params CreateWhitelistRuleParams) (int64, error) {
var pattern [][]api.Pattern
if len(params.IP) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeySrcIP,
Op: api.OpEq,
V: params.IP,
SubK: "",
},
})
}
if len(params.URINoQuery) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeyURINoQuery,
Op: api.OpEq,
V: params.URINoQuery,
SubK: "",
},
})
}
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
Name: params.Name,
IP: params.IP,
IsEnabled: true,
Action: int(api.PolicyRuleActionAllow),
Pattern: pattern,
})
if err != nil {
return 0, err
}
logger.With("id", id).Info("create whitelist rule success")
return id, nil
}

View File

@@ -0,0 +1,41 @@
package tools
import (
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
)
// By deferring the concretization of generic types to the Register method,
// we avoid type inference issues.
// Each Tool is wrapped in a toolWrapper that knows its concrete type,
// allowing correct passing of generic parameters during registration.
type ToolWrapper interface {
Register(s *mcp.MCPServer) error
}
var (
tools = []ToolWrapper{}
)
func AppendTool[T any, R any](tool ...mcp.Tool[T, R]) {
for _, t := range tool {
tools = append(tools, &toolWrapper[T, R]{tool: t})
}
}
func Tools() []ToolWrapper {
return tools
}
type toolWrapper[T any, R any] struct {
tool mcp.Tool[T, R]
}
func (w *toolWrapper[T, R]) Register(s *mcp.MCPServer) error {
logger.Info("Registering tool",
logger.String("name", w.tool.Name()),
logger.String("description", w.tool.Description()),
)
return mcp.RegisterTool(s, w.tool)
}

62
mcp_server/main.go Normal file
View File

@@ -0,0 +1,62 @@
package main
import (
"flag"
"fmt"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/internal/config"
"github.com/chaitin/SafeLine/mcp_server/internal/tools"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
)
func main() {
configPath := flag.String("config", "config.yaml", "path to config file")
flag.Parse()
if err := config.Load(*configPath); err != nil {
panic(fmt.Errorf("failed to load config: %v", err))
}
logConfig := config.GetLogger()
if err := logger.Init(&logger.Config{
Level: logConfig.Level,
FilePath: logConfig.FilePath,
Console: logConfig.Console,
Caller: logConfig.Caller,
Development: logConfig.Development,
}); err != nil {
panic(fmt.Errorf("failed to init logger: %v", err))
}
logger.With("base_url", config.GetAPI().BaseURL).Info("Initializing API service...")
if err := api.Init(config.GetAPI()); err != nil {
panic(fmt.Errorf("failed to init API service: %v", err))
}
logger.Info("Starting MCP Server...")
serverConfig := config.GetServer()
s := mcp.NewMCPServer(
serverConfig.Name,
serverConfig.Version,
serverConfig.Secret,
)
logger.Info("Registering tools...")
for _, tool := range tools.Tools() {
if err := tool.Register(s); err != nil {
logger.With("error", err).
Error("Failed to register tool")
panic(err)
}
}
addr := fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port)
logger.With("addr", addr).Info("Starting server")
if err := s.Start(addr); err != nil {
logger.With("error", err).
Error("Server failed to start")
panic(err)
}
}

View File

@@ -0,0 +1,72 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
)
// Config Global configuration structure
type Config struct {
Server ServerConfig `yaml:"server"`
Logger LoggerConfig `yaml:"logger"`
}
// ServerConfig Server configuration
type ServerConfig struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Port int `yaml:"port"`
Host string `yaml:"host"`
Secret string `yaml:"secret"`
}
// LoggerConfig Logger configuration
type LoggerConfig struct {
Level string `yaml:"level"`
FilePath string `yaml:"file_path"`
Console bool `yaml:"console"`
Caller bool `yaml:"caller"`
Development bool `yaml:"development"`
}
var (
globalConfig *Config
)
// Load Load configuration from file
func Load(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return err
}
config := &Config{}
if err := yaml.Unmarshal(data, config); err != nil {
return err
}
globalConfig = config
return nil
}
// Get Get global configuration
func Get() *Config {
return globalConfig
}
// GetServer Get server configuration
func GetServer() ServerConfig {
if globalConfig == nil {
return ServerConfig{}
}
return globalConfig.Server
}
// GetLogger Get logger configuration
func GetLogger() LoggerConfig {
if globalConfig == nil {
return LoggerConfig{}
}
return globalConfig.Logger
}

View File

@@ -0,0 +1,136 @@
package errors
import (
"errors"
"fmt"
"runtime"
"strings"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
var (
// Common errors
ErrInternal = New("internal error")
ErrInvalidParam = New("invalid parameter")
ErrNotFound = New("resource not found")
ErrUnauthorized = New("unauthorized")
ErrForbidden = New("forbidden")
ErrTimeout = New("timeout")
)
// Error Custom error structure
type Error struct {
err error
stack []string
msg string
location string
}
// Error Implement error interface
func (e *Error) Error() string {
if e.msg != "" {
return fmt.Sprintf("%s: %v (at %s)", e.msg, e.err, e.location)
}
return fmt.Sprintf("%v (at %s)", e.err, e.location)
}
// Unwrap Return original error
func (e *Error) Unwrap() error {
if e.err == nil {
return nil
}
if wrapped, ok := e.err.(*Error); ok {
return wrapped.Unwrap()
}
return e.err
}
// Stack Return error stack
func (e *Error) Stack() []string {
return e.stack
}
// Location Return error location
func (e *Error) Location() string {
return e.location
}
// getCallerLocation Get caller location
func getCallerLocation(skip int) string {
_, file, line, ok := runtime.Caller(skip)
if !ok {
return "unknown"
}
return fmt.Sprintf("%s:%d", file, line)
}
// WrapL Wrap error and print log
func WrapL(err error, msg string) error {
if err == nil {
return nil
}
// Get stack trace information
var stack []string
for i := 1; i < 32; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fn := runtime.FuncForPC(pc)
if fn == nil {
break
}
name := fn.Name()
if strings.Contains(name, "runtime.") {
break
}
stack = append(stack, fmt.Sprintf("%s:%d", file, line))
}
wrappedErr := &Error{
err: err,
stack: stack,
msg: msg,
location: getCallerLocation(2),
}
// Print error information and stack using logger
logger.With("error", err).
With("location", wrappedErr.location).
With("stack", strings.Join(stack, "\n")).
Error(msg)
return wrappedErr
}
// Is Check error type
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As Type assertion
func As(err error, target interface{}) bool {
return errors.As(err, target)
}
// Wrap Wrap error without printing log
func Wrap(err error, msg string) error {
if err == nil {
return nil
}
return &Error{
err: err,
msg: msg,
location: getCallerLocation(2),
}
}
// New Create new error
func New(text string) error {
return &Error{
err: errors.New(text),
location: getCallerLocation(2),
}
}

View File

@@ -0,0 +1,49 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Field 日志字段
type Field = zapcore.Field
// String 创建字符串字段
func String(key string, val string) Field {
return zap.String(key, val)
}
// Int 创建整数字段
func Int(key string, val int) Field {
return zap.Int(key, val)
}
// Int64 创建 int64 字段
func Int64(key string, val int64) Field {
return zap.Int64(key, val)
}
// Float64 创建浮点数字段
func Float64(key string, val float64) Field {
return zap.Float64(key, val)
}
// Bool 创建布尔字段
func Bool(key string, val bool) Field {
return zap.Bool(key, val)
}
// Err 创建错误字段
func Err(err error) Field {
return zap.Error(err)
}
// Any 创建任意类型字段
func Any(key string, val interface{}) Field {
return zap.Any(key, val)
}
// Duration 创建时间段字段
func Duration(key string, val float64) Field {
return zap.Float64(key, val)
}

View File

@@ -0,0 +1,205 @@
package logger
import (
"os"
"path/filepath"
"sync"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Logger 封装 zap.Logger
type Logger struct {
zl *zap.Logger
}
var (
defaultLogger *Logger
once sync.Once
)
// Config 日志配置
type Config struct {
// 日志级别
Level string `json:"level" yaml:"level"`
// 日志文件路径
FilePath string `json:"file_path" yaml:"file_path"`
// 是否输出到控制台
Console bool `json:"console" yaml:"console"`
// 是否记录调用者信息
Caller bool `json:"caller" yaml:"caller"`
// 是否使用开发模式(更详细的日志)
Development bool `json:"development" yaml:"development"`
}
// 默认配置
var defaultConfig = Config{
Level: "info",
FilePath: "logs/mcp.log",
Console: true,
Caller: true,
Development: false,
}
// Init 初始化日志系统
func Init(cfg *Config) error {
var err error
once.Do(func() {
if cfg == nil {
cfg = &defaultConfig
}
// 确保日志目录存在
if cfg.FilePath != "" {
dir := filepath.Dir(cfg.FilePath)
if err = os.MkdirAll(dir, 0755); err != nil {
return
}
}
// 配置编码器
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
},
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
// 设置日志级别
var level zapcore.Level
if err = level.UnmarshalText([]byte(cfg.Level)); err != nil {
level = zapcore.InfoLevel
}
// 创建Core
var cores []zapcore.Core
// 文件输出
if cfg.FilePath != "" {
fileWriter, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return
}
cores = append(cores, zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(fileWriter),
level,
))
}
// 控制台输出
if cfg.Console {
cores = append(cores, zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig),
zapcore.AddSync(os.Stdout),
level,
))
}
// 创建Logger
core := zapcore.NewTee(cores...)
zl := zap.New(core)
// 是否记录调用者信息
if cfg.Caller {
zl = zl.WithOptions(zap.AddCaller())
}
// 是否使用开发模式
if cfg.Development {
zl = zl.WithOptions(zap.Development())
}
defaultLogger = &Logger{zl: zl}
})
return err
}
// GetLogger 获取日志实例
func GetLogger() *Logger {
if defaultLogger == nil {
Init(nil)
}
return defaultLogger
}
// With 创建带有字段的新Logger
func (l *Logger) With(key string, value interface{}) *Logger {
return &Logger{zl: l.zl.With(Any(key, value))}
}
// Debug level
func (l *Logger) Debug(msg string, fields ...Field) {
l.zl.Debug(msg, fields...)
}
// Info level
func (l *Logger) Info(msg string, fields ...Field) {
l.zl.Info(msg, fields...)
}
// Warn level
func (l *Logger) Warn(msg string, fields ...Field) {
l.zl.Warn(msg, fields...)
}
// Error level
func (l *Logger) Error(msg string, fields ...Field) {
l.zl.Error(msg, fields...)
}
// Fatal level
func (l *Logger) Fatal(msg string, fields ...Field) {
l.zl.Fatal(msg, fields...)
}
// 全局函数
// With 创建带有字段的新Logger
func With(key string, value interface{}) *Logger {
l := GetLogger()
// 为链式调用创建新的logger实例并添加caller skip
return &Logger{zl: l.zl.WithOptions(zap.AddCallerSkip(1)).With(Any(key, value))}
}
// Debug level
func Debug(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...)
}
// Info level
func Info(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...)
}
// Warn level
func Warn(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...)
}
// Error level
func Error(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...)
}
// Fatal level
func Fatal(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...)
}

148
mcp_server/pkg/mcp/mcp.go Normal file
View File

@@ -0,0 +1,148 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/mcuadros/go-defaults"
)
type Tool[T any, R any] interface {
Name() string
Description() string
Execute(ctx context.Context, params T) (R, error)
Validate(params T) error
}
type SSEServer struct {
sse *server.SSEServer
secret string
}
func (s *SSEServer) Start(addr string) error {
srv := &http.Server{
Addr: addr,
Handler: s,
}
return srv.ListenAndServe()
}
func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.secret == "" {
s.sse.ServeHTTP(w, r)
return
}
messagePath := s.sse.CompleteMessagePath()
if messagePath != "" && r.URL.Path == messagePath {
secret := r.Header.Get("Secret")
if secret != s.secret {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
s.sse.ServeHTTP(w, r)
}
type MCPServer struct {
server *server.MCPServer
sse *SSEServer
}
func NewMCPServer(name, version string, secret string) *MCPServer {
s := server.NewMCPServer(
name,
version,
server.WithLogging(),
)
return &MCPServer{
server: s,
sse: &SSEServer{sse: server.NewSSEServer(s), secret: secret},
}
}
func (s *MCPServer) Start(addr string) error {
return s.sse.Start(addr)
}
func handleToolCall[T any, R any](ctx context.Context, request mcp.CallToolRequest, tool Tool[T, R]) (result *mcp.CallToolResult, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
var raw []byte
raw, err = json.Marshal(request.Params.Arguments)
if err != nil {
return nil, errors.Wrap(err, "marshal arguments failed")
}
var params T
defaults.SetDefaults(&params)
if err = json.Unmarshal(raw, &params); err != nil {
return nil, errors.Wrap(err, "unmarshal parameters failed")
}
if err = tool.Validate(params); err != nil {
return nil, err
}
var execResult R
execResult, err = tool.Execute(ctx, params)
if err != nil {
return nil, err
}
v := any(execResult)
switch v := v.(type) {
case string:
return mcp.NewToolResultText(v), nil
case []byte:
return mcp.NewToolResultText(string(v)), nil
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return mcp.NewToolResultText(json.Number(fmt.Sprint(v)).String()), nil
case bool:
return mcp.NewToolResultText(strconv.FormatBool(v)), nil
default:
bytes, err := json.Marshal(v)
if err != nil {
return nil, errors.New("invalid result type")
}
return mcp.NewToolResultText(string(bytes)), nil
}
}
func RegisterTool[T any, R any](s *MCPServer, tool Tool[T, R]) error {
var v T
opts, err := SchemaToOptions(v)
if err != nil {
return err
}
opts = append(opts, mcp.WithDescription(tool.Description()))
t := mcp.NewTool(tool.Name(),
opts...,
)
s.server.AddTool(t, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
result, err := handleToolCall(ctx, request, tool)
if err != nil {
logger.With("error", err).Error("handle tool call failed")
if wrapped, ok := err.(*errors.Error); ok {
return nil, wrapped.Unwrap()
}
return nil, err
}
return result, nil
})
return nil
}

View File

@@ -0,0 +1,169 @@
package mcp
import (
"encoding/json"
"reflect"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
)
// SchemaToOptions Convert struct to MCP ToolOption list
func SchemaToOptions(schema any) ([]mcp.ToolOption, error) {
t := reflect.TypeOf(schema)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
var options []mcp.ToolOption
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
continue
}
desc := field.Tag.Get("desc")
required := field.Tag.Get("required") == "true"
enumTag := field.Tag.Get("enum")
defaultTag := field.Tag.Get("default")
minTag := field.Tag.Get("min")
maxTag := field.Tag.Get("max")
opts := []mcp.PropertyOption{}
if desc != "" {
opts = append(opts, mcp.Description(desc))
}
if required {
opts = append(opts, mcp.Required())
}
if enumTag != "" && field.Type.Kind() == reflect.String {
enumValues := strings.Split(enumTag, ",")
opts = append(opts, mcp.Enum(enumValues...))
}
switch field.Type.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
if defaultTag != "" {
if defaultValue, err := strconv.Atoi(defaultTag); err == nil {
opts = append(opts, mcp.DefaultNumber(float64(defaultValue)))
}
}
if minTag != "" {
if minValue, err := strconv.Atoi(minTag); err == nil {
opts = append(opts, mcp.Min(float64(minValue)))
}
}
if maxTag != "" {
if maxValue, err := strconv.Atoi(maxTag); err == nil {
opts = append(opts, mcp.Max(float64(maxValue)))
}
}
options = append(options, mcp.WithNumber(jsonTag, opts...))
case reflect.Bool:
if defaultTag != "" {
if defaultValue, err := strconv.ParseBool(defaultTag); err == nil {
opts = append(opts, mcp.DefaultBool(defaultValue))
}
}
options = append(options, mcp.WithBoolean(jsonTag, opts...))
case reflect.String:
if defaultTag != "" {
opts = append(opts, mcp.DefaultString(defaultTag))
}
options = append(options, mcp.WithString(jsonTag, opts...))
case reflect.Struct:
subSchema := reflect.New(field.Type).Interface()
subOptions, err := SchemaToOptions(subSchema)
if err != nil {
return nil, err
}
// Create a temporary Tool to get JSON Schema of sub-struct
tempTool := mcp.NewTool("temp", subOptions...)
tempJSON, _ := tempTool.MarshalJSON()
var tempMap map[string]any
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
continue
}
// Extract properties from temporary Tool
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
if properties, ok := inputSchema["properties"].(map[string]any); ok {
// Check if there are required fields
if required, ok := inputSchema["required"].([]any); ok {
// Add required field information to corresponding properties
for _, req := range required {
if reqStr, ok := req.(string); ok {
if prop, ok := properties[reqStr].(map[string]any); ok {
prop["required"] = true
}
}
}
}
opts = append(opts, mcp.Properties(properties))
}
}
options = append(options, mcp.WithObject(jsonTag, opts...))
case reflect.Slice:
elemType := field.Type.Elem()
var items map[string]any
switch elemType.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
items = map[string]any{
"type": "number",
}
case reflect.Bool:
items = map[string]any{
"type": "boolean",
}
case reflect.String:
items = map[string]any{
"type": "string",
}
case reflect.Struct:
subSchema := reflect.New(elemType).Interface()
subOptions, err := SchemaToOptions(subSchema)
if err != nil {
return nil, err
}
// Create a temporary Tool to get JSON Schema of sub-struct
tempTool := mcp.NewTool("temp", subOptions...)
tempJSON, _ := tempTool.MarshalJSON()
var tempMap map[string]any
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
continue
}
// Extract properties from temporary Tool
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
if properties, ok := inputSchema["properties"].(map[string]any); ok {
// Check if there are required fields
if required, ok := inputSchema["required"].([]any); ok {
// Add required field information to corresponding properties
for _, req := range required {
if reqStr, ok := req.(string); ok {
if prop, ok := properties[reqStr].(map[string]any); ok {
prop["required"] = true
}
}
}
}
items = map[string]any{
"type": "object",
"properties": properties,
}
}
}
}
opts = append(opts, mcp.Items(items))
options = append(options, mcp.WithArray(jsonTag, opts...))
}
}
return options, nil
}

View File

@@ -0,0 +1,227 @@
package mcp
import (
"reflect"
"testing"
"github.com/mark3labs/mcp-go/mcp"
)
func TestSchemaToOptions(t *testing.T) {
tests := []struct {
name string
args any
want mcp.Tool
}{
{
name: "test number",
args: struct {
A int `json:"a" desc:"number a" required:"true"`
}{},
want: mcp.NewTool("test number",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
),
},
{
name: "test number int64",
args: struct {
A int64 `json:"a" desc:"number a" required:"true"`
}{},
want: mcp.NewTool("test number int64",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
),
},
{
name: "test number float64",
args: struct {
A float64 `json:"a" desc:"number a" required:"true"`
}{},
want: mcp.NewTool("test number float64",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
),
},
{
name: "test number default",
args: struct {
A int `json:"a" desc:"number a" required:"true" default:"10"`
}{},
want: mcp.NewTool("test number default",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.DefaultNumber(10)),
),
},
{
name: "test number min max",
args: struct {
A int `json:"a" desc:"number a" required:"true" min:"10" max:"20"`
}{},
want: mcp.NewTool("test number min max",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.Min(10), mcp.Max(20)),
),
},
{
name: "test number optional",
args: struct {
A int `json:"a" desc:"number a"`
}{},
want: mcp.NewTool("test number optional",
mcp.WithNumber("a", mcp.Description("number a")),
),
},
{
name: "test boolean",
args: struct {
A bool `json:"a" desc:"boolean a" required:"true"`
}{},
want: mcp.NewTool("test boolean",
mcp.WithBoolean("a", mcp.Required(), mcp.Description("boolean a")),
),
},
{
name: "test string",
args: struct {
A string `json:"a" desc:"string a" required:"true"`
}{},
want: mcp.NewTool("test string",
mcp.WithString("a", mcp.Required(), mcp.Description("string a")),
),
},
{
name: "test string default",
args: struct {
A string `json:"a" desc:"string a" required:"true" default:"hello"`
}{},
want: mcp.NewTool("test string default",
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.DefaultString("hello")),
),
},
{
name: "test string enum",
args: struct {
A string `json:"a" desc:"string a" required:"true" enum:"1,2,3"`
}{},
want: mcp.NewTool("test string enum",
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.Enum("1", "2", "3")),
),
},
{
name: "test object",
args: struct {
A struct {
B int `json:"b" desc:"number b" required:"true"`
} `json:"a" desc:"object a" required:"true"`
}{},
want: mcp.NewTool("test object",
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
mcp.Properties(map[string]any{
"b": map[string]any{
"type": "number",
"description": "number b",
"required": true,
},
}),
),
),
},
{
name: "test object optional",
args: struct {
A struct {
B int `json:"b" desc:"number b" required:"true"`
C int `json:"c" desc:"number c"`
} `json:"a" desc:"object a" required:"true"`
}{},
want: mcp.NewTool("test object optional",
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
mcp.Properties(map[string]any{
"b": map[string]any{
"type": "number",
"description": "number b",
"required": true,
},
"c": map[string]any{
"type": "number",
"description": "number c",
},
}),
),
),
},
{
name: "test nested object",
args: struct {
A struct {
B struct {
C int `json:"c" desc:"number c" required:"true"`
} `json:"b" desc:"object b" required:"true"`
} `json:"a" desc:"object a" required:"true"`
}{},
want: mcp.NewTool("test nested object",
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
mcp.Properties(map[string]any{
"b": map[string]any{
"type": "object",
"description": "object b",
"required": true,
"properties": map[string]any{
"c": map[string]any{
"type": "number",
"description": "number c",
"required": true,
},
},
},
}),
),
),
},
{
name: "test array",
args: struct {
A []int `json:"a" desc:"array a" required:"true"`
}{},
want: mcp.NewTool("test array",
mcp.WithArray("a", mcp.Required(), mcp.Description("array a"),
mcp.Items(map[string]any{
"type": "number",
}),
),
),
},
{
name: "test array of object",
args: struct {
A []struct {
B int `json:"b" desc:"number b" required:"true"`
} `json:"a" desc:"array of object a" required:"true"`
}{},
want: mcp.NewTool("test array of object",
mcp.WithArray("a", mcp.Required(), mcp.Description("array of object a"),
mcp.Items(map[string]any{
"type": "object",
"properties": map[string]any{
"b": map[string]any{
"type": "number",
"description": "number b",
"required": true,
},
},
}),
),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SchemaToOptions(tt.args)
if err != nil {
t.Errorf("SchemaToOptions() error = %v", err)
return
}
s1, _ := mcp.NewTool(tt.name, got...).MarshalJSON()
s2, _ := tt.want.MarshalJSON()
if !reflect.DeepEqual(s1, s2) {
t.Errorf("\n got %v\n want %v", string(s1), string(s2))
}
})
}
}

1567
scripts/manage.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,42 @@
[Ingress-nginx](https://kubernetes.github.io/ingress-nginx/) plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI.
## Usage
## Safeline Prepare
The detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin.
1.Navigate to the configuration directory of the SafeLine detection engine:
```shell
cd /data/safeline/resources/detector/
```
2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:
```yaml
bind_addr: 0.0.0.0
listen_port: 8000
```
These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.
3.Next, map the containers port 8000 to the host machine. First, navigate to the SafeLine installation directory:
```shell
cd /data/safeline
```
4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:
```yaml
...
detect:
ports:
- 8000:8000
...
```
5.Save the changes and restart SafeLine with the following commands:
```shell
docker-compose down
docker-compose up -d
```
This will apply the changes and activate the new configuration.
## Plugin Usage
### Step 1: Install the plugin
@@ -67,7 +102,7 @@ env:
configMapKeyRef:
name: safeline
key: port
...
...
```

View File

@@ -0,0 +1,20 @@
package = "ingress-nginx-safeline"
version = "1.0.4-1"
source = {
url = "git://github.com/chaitin/ingress-nginx-safeline.git"
}
description = {
summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/ingress-nginx-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["safeline.main"] = "lib/safeline/main.lua"
}
}

View File

@@ -1,14 +1,49 @@
# Kong Safeline Plugin
Kong plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI.
## Installation
## Safeline Prepare
The detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin.
1.Navigate to the configuration directory of the SafeLine detection engine:
```shell
cd /data/safeline/resources/detector/
```
2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:
```yaml
bind_addr: 0.0.0.0
listen_port: 8000
```
These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.
3.Next, map the containers port 8000 to the host machine. First, navigate to the SafeLine installation directory:
```shell
cd /data/safeline
```
4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:
```yaml
...
detect:
ports:
- 8000:8000
...
```
5.Save the changes and restart SafeLine with the following commands:
```shell
docker-compose down
docker-compose up -d
```
This will apply the changes and activate the new configuration.
## Plugin Installation
To install the plugin, run the following command in your Kong server:
```shell
$ luarocks install kong-safeline
```
## Configuration
## Plugin Configuration
You can add the plugin to your API by making the following request:
```shell

View File

@@ -0,0 +1,31 @@
package = "kong-safeline"
version = "1.0.3-1"
source = {
url = "file://kong-safeline-1.0.3.tar.gz",
}
build = {
type = "script",
rockspec = {
build = {
"git clone https://github.com/chaitin/SafeLine.git",
"cp -r sdk/kong .",
"rm -rf SafeLine"
}
}
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}

View File

@@ -0,0 +1,31 @@
package = "kong-safeline"
version = "1.0.4-1"
source = {
url = "git://github.com/chaitin/SafeLine.git",
}
build = {
type = "script",
rockspec = {
build = {
"git clone https://github.com/chaitin/SafeLine.git",
"cp -r sdk/kong .",
"rm -rf SafeLine"
}
}
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}

View File

@@ -0,0 +1,31 @@
package = "kong-safeline"
version = "1.0.5-1"
source = {
url = "git://github.com/chaitin/SafeLine.git",
}
build = {
type = "script",
rockspec = {
build = {
"git clone https://github.com/chaitin/SafeLine.git",
"cp -r sdk/kong .",
"rm -rf SafeLine"
}
}
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}

View File

@@ -0,0 +1,21 @@
package = "kong-safeline"
version = "1.0.6-1"
source = {
url = "git://github.com/xbingW/kong-safeline.git",
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}

View File

@@ -0,0 +1,21 @@
package = "kong-safeline"
version = "1.0.7-1"
source = {
url = "git://github.com/chaitin/kong-safeline.git",
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}

View File

@@ -34,11 +34,11 @@ function SafelineHandler:access(conf)
if not ok then
kong.log.err("failed to detector req: ", err)
end
if result then
if result and result.status then
if result.action == t1k_constants.ACTION_BLOCKED then
local msg = fmt(blocked_message, result.status, result.event_id)
kong.log.debug("blocked by safeline: ",msg)
return kong.response.exit(result.status, msg)
return kong.response.exit(tonumber(result.status), msg)
end
end
end

1
sdk/traefik-safeline Submodule

Submodule sdk/traefik-safeline added at bc2258ccc3

View File

@@ -1,9 +0,0 @@
displayName: Chaitin Safeline WAF
type: middleware
import: github.com/xbingW/traefik-safeline
summary: 'Traefik plugin to proxy requests to safeline waf.t serves as a reverse proxy access to protect your website from network attacks that including OWASP attacks, zero-day attacks, web crawlers, vulnerability scanning, vulnerability exploit, http flood and so on.'
testData:
addr: safeline-detector:8000

View File

@@ -1,62 +0,0 @@
# Traefik Plugin Safeline
This plugin is a middleware for Traefik that can be used to detect and block malicious requests which base on the [Safeline](https://waf.chaitin.com/) engine.
## Usage
For a plugin to be active for a given Traefik instance, it must be declared in the static configuration.
Plugins are parsed and loaded exclusively during startup, which allows Traefik to check the integrity of the code and catch errors early on.
If an error occurs during loading, the plugin is disabled.
For security reasons, it is not possible to start a new plugin or modify an existing one while Traefik is running.
Once loaded, middleware plugins behave exactly like statically compiled middlewares.
Their instantiation and behavior are driven by the dynamic configuration.
Plugin dependencies must be [vendored](https://golang.org/ref/mod#vendoring) for each plugin.
Vendored packages should be included in the plugin's GitHub repository. ([Go modules](https://blog.golang.org/using-go-modules) are not supported.)
### Configuration
For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).
The following declaration (given here in YAML) defines a plugin:
```yaml
# Static configuration
experimental:
plugins:
safeline:
moduleName: github.com/xbingW/traefik-safeline
version: v1.0.0
```
Here is an example of a file provider dynamic configuration (given here in YAML), where the interesting part is the `http.middlewares` section:
```yaml
# Dynamic configuration
http:
routers:
my-router:
rule: host(`demo.localhost`)
service: service-foo
entryPoints:
- web
middlewares:
- chaitin
services:
service-foo:
loadBalancer:
servers:
- url: http://127.0.0.1:5000
middlewares:
chaitin:
plugin:
safeline:
addr: safeline-detector.safeline:8000
```

View File

@@ -1,5 +0,0 @@
module github.com/xbingW/traefik-safeline
go 1.17
require github.com/xbingW/t1k v1.2.1

View File

@@ -1,75 +0,0 @@
package traefik_safeline
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"github.com/xbingW/t1k"
)
// Package example a example plugin.
// Config the plugin configuration.
type Config struct {
// Addr is the address for the detector
Addr string `yaml:"addr"`
// Get ip from header, if not set, get ip from remote addr
IpHeader string `yaml:"ipHeader"`
// When ip_header has multiple ip, use this to get the ip
//
//for example, X-Forwarded-For: ip1, ip2, ip3
// when ip_last_index is 0, the client ip is ip3
// when ip_last_index is 1, the client ip is ip2
// when ip_last_index is 2, the client ip is ip1
IPRightIndex uint `yaml:"ipRightIndex"`
}
// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
return &Config{
Addr: "",
IpHeader: "",
IPRightIndex: 0,
}
}
// Safeline a plugin.
type Safeline struct {
next http.Handler
name string
config *Config
logger *log.Logger
}
// New created a new plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
return &Safeline{
next: next,
name: name,
config: config,
logger: log.New(os.Stdout, "safeline", log.LstdFlags),
}, nil
}
func (s *Safeline) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
d := t1k.NewDetector(t1k.Config{
Addr: s.config.Addr,
IpHeader: s.config.IpHeader,
IPRightIndex: s.config.IPRightIndex,
})
resp, err := d.DetectorRequest(req)
if err != nil {
s.logger.Printf("Failed to detect request: %v", err)
}
if resp != nil && !resp.Allowed() {
rw.WriteHeader(resp.StatusCode())
if err := json.NewEncoder(rw).Encode(resp.BlockMessage()); err != nil {
s.logger.Printf("Failed to encode block message: %v", err)
}
return
}
s.next.ServeHTTP(rw, req)
}

View File

@@ -1,4 +0,0 @@
# t1k-go
## Getting started

View File

@@ -1,173 +0,0 @@
package t1k
import (
"bufio"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/xbingW/t1k/pkg/datetime"
"github.com/xbingW/t1k/pkg/rand"
"github.com/xbingW/t1k/pkg/t1k"
)
type Detector struct {
cfg Config
}
type Config struct {
Addr string `json:"addr"`
// Get ip from header, if not set, get ip from remote addr
IpHeader string `json:"ip_header"`
// When ip_header has multiple ip, use this to get ip from right
//
//for example, X-Forwarded-For: ip1, ip2, ip3
// when ip_last_index is 0, the client ip is ip3
// when ip_last_index is 1, the client ip is ip2
// when ip_last_index is 2, the client ip is ip1
IPRightIndex uint `json:"ip_right_index"`
}
func NewDetector(cfg Config) *Detector {
return &Detector{
cfg: cfg,
}
}
func (d *Detector) GetConn() (net.Conn, error) {
_, _, err := net.SplitHostPort(d.cfg.Addr)
if err != nil {
return nil, fmt.Errorf("detector add %s is invalid because %v", d.cfg.Addr, err)
}
return net.Dial("tcp", d.cfg.Addr)
}
func (d *Detector) DetectorRequestStr(req string) (*t1k.DetectorResponse, error) {
httpReq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(req)))
if err != nil {
return nil, fmt.Errorf("read request failed: %v", err)
}
return d.DetectorRequest(httpReq)
}
func (d *Detector) DetectorRequest(req *http.Request) (*t1k.DetectorResponse, error) {
extra, err := d.GenerateExtra(req)
if err != nil {
return nil, fmt.Errorf("generate extra failed: %v", err)
}
dc := t1k.NewHttpDetector(req, extra)
conn, err := d.GetConn()
if err != nil {
return nil, fmt.Errorf("get conn failed: %v", err)
}
defer conn.Close()
return dc.DetectRequest(conn)
}
func (d *Detector) DetectorResponseStr(req string, resp string) (*t1k.DetectorResponse, error) {
httpReq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(req)))
if err != nil {
return nil, fmt.Errorf("read request failed: %v", err)
}
httpResp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(resp)), httpReq)
if err != nil {
return nil, fmt.Errorf("read response failed: %v", err)
}
extra, err := d.GenerateExtra(httpReq)
if err != nil {
return nil, fmt.Errorf("generate extra failed: %v", err)
}
conn, err := d.GetConn()
if err != nil {
return nil, fmt.Errorf("get conn failed: %v", err)
}
defer conn.Close()
return t1k.NewHttpDetector(httpReq, extra).SetResponse(httpResp).DetectResponse(conn)
}
func (d *Detector) DetectorResponse(req *http.Request, resp *http.Response) (*t1k.DetectorResponse, error) {
extra, err := d.GenerateExtra(req)
if err != nil {
return nil, fmt.Errorf("generate extra failed: %v", err)
}
conn, err := d.GetConn()
if err != nil {
return nil, fmt.Errorf("get conn failed: %v", err)
}
defer conn.Close()
return t1k.NewHttpDetector(req, extra).SetResponse(resp).DetectResponse(conn)
}
func (d *Detector) GenerateExtra(req *http.Request) (*t1k.HttpExtra, error) {
clientHost, err := d.getClientIP(req)
if err != nil {
return nil, err
}
serverHost, serverPort := req.Host, "80"
if hasPort(req.Host) {
serverHost, serverPort, err = net.SplitHostPort(req.Host)
if err != nil {
return nil, err
}
}
return &t1k.HttpExtra{
UpstreamAddr: "",
RemoteAddr: clientHost,
RemotePort: d.getClientPort(req),
LocalAddr: serverHost,
LocalPort: serverPort,
ServerName: "",
Schema: req.URL.Scheme,
ProxyName: "",
UUID: rand.String(32),
HasRspIfOK: "y",
HasRspIfBlock: "n",
ReqBeginTime: strconv.FormatInt(datetime.Now(), 10),
ReqEndTime: "",
RspBeginTime: strconv.FormatInt(datetime.Now(), 10),
RepEndTime: "",
}, nil
}
func (d *Detector) getClientIP(req *http.Request) (string, error) {
if d.cfg.IpHeader != "" {
ips := req.Header.Get(d.cfg.IpHeader)
if ips != "" {
ipList := reverseStrSlice(strings.Split(ips, ","))
if len(ipList) > int(d.cfg.IPRightIndex) {
return strings.TrimSpace(ipList[d.cfg.IPRightIndex]), nil
}
return ipList[0], nil
}
}
if !hasPort(req.RemoteAddr) {
return req.RemoteAddr, nil
}
clientHost, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return "", err
}
return clientHost, nil
}
func (d *Detector) getClientPort(req *http.Request) string {
_, clientPort, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return ""
}
return clientPort
}
// has port check if host has port
func hasPort(host string) bool {
return strings.LastIndex(host, ":") > strings.LastIndex(host, "]")
}
func reverseStrSlice(arr []string) []string {
if len(arr) == 0 {
return arr
}
return append(reverseStrSlice(arr[1:]), arr[0])
}

View File

@@ -1,7 +0,0 @@
package datetime
import "time"
func Now() int64 {
return time.Now().UnixNano() / 1e3
}

View File

@@ -1,15 +0,0 @@
package rand
import (
"math/rand"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789"
func String(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

View File

@@ -1,163 +0,0 @@
package t1k
import (
"errors"
"io"
"log"
"net/http"
"regexp"
"strconv"
)
type ResultFlag string
const (
ResultFlagAllowed ResultFlag = "."
ResultFlagBlocked ResultFlag = "?"
)
func (d ResultFlag) Byte() byte {
return d[0]
}
type DetectorResponse struct {
Head byte
Body []byte
Delay []byte
ExtraHeader []byte
ExtraBody []byte
Context []byte
Cookie []byte
WebLog []byte
BotQuery []byte
BotBody []byte
Forward []byte
}
func (r *DetectorResponse) Allowed() bool {
return r.Head == ResultFlagAllowed.Byte()
}
func (r *DetectorResponse) StatusCode() int {
str := string(r.Body)
if str == "" {
return http.StatusForbidden
}
code, err := strconv.Atoi(str)
if err != nil {
log.Printf("t1k convert status code failed: %v", err)
return http.StatusForbidden
}
return code
}
func (r *DetectorResponse) BlockMessage() map[string]interface{} {
return map[string]interface{}{
"status": r.StatusCode(),
"success": false,
"message": "blocked by Chaitin SafeLine Web Application Firewall",
"event_id": r.EventID(),
}
}
func (r *DetectorResponse) EventID() string {
extra := string(r.ExtraBody)
if extra == "" {
return ""
}
// <!-- event_id: e1impksyjq0gl92le6odi0fnobi270cj -->
re, err := regexp.Compile(`<\!--\s*event_id:\s*([a-zA-Z0-9]+)\s*-->\s*`)
if err != nil {
log.Printf("t1k compile regexp failed: %v", err)
return ""
}
matches := re.FindStringSubmatch(extra)
if len(matches) < 2 {
log.Printf("t1k regexp not match event id: %s", extra)
return ""
}
return matches[1]
}
type HttpDetector struct {
extra *HttpExtra
req Request
resp Response
}
func NewHttpDetector(req *http.Request, extra *HttpExtra) *HttpDetector {
return &HttpDetector{
req: NewHttpRequest(req, extra),
extra: extra,
}
}
func (d *HttpDetector) SetResponse(resp *http.Response) *HttpDetector {
d.resp = NewHttpResponse(d.req, resp, d.extra)
return d
}
func (d *HttpDetector) DetectRequest(socket io.ReadWriter) (*DetectorResponse, error) {
raw, err := d.req.Serialize()
if err != nil {
return nil, err
}
_, err = socket.Write(raw)
if err != nil {
return nil, err
}
return d.ReadResponse(socket)
}
func (d *HttpDetector) DetectResponse(socket io.ReadWriter) (*DetectorResponse, error) {
raw, err := d.resp.Serialize()
if err != nil {
return nil, err
}
_, err = socket.Write(raw)
if err != nil {
return nil, err
}
return d.ReadResponse(socket)
}
func (d *HttpDetector) ReadResponse(r io.Reader) (*DetectorResponse, error) {
res := &DetectorResponse{}
for {
p, err := ReadPacket(r)
if err != nil {
return nil, err
}
switch p.Tag().Strip() {
case TAG_HEADER:
if len(p.PayLoad()) != 1 {
return nil, errors.New("len(T1K_HEADER) != 1")
}
res.Head = p.PayLoad()[0]
case TAG_DELAY:
res.Delay = p.PayLoad()
case TAG_BODY:
res.Body = p.PayLoad()
case TAG_EXTRA_HEADER:
res.ExtraHeader = p.PayLoad()
case TAG_EXTRA_BODY:
res.ExtraBody = p.PayLoad()
case TAG_CONTEXT:
res.Context = p.PayLoad()
case TAG_COOKIE:
res.Cookie = p.PayLoad()
case TAG_WEB_LOG:
res.WebLog = p.PayLoad()
case TAG_BOT_QUERY:
res.BotQuery = p.PayLoad()
case TAG_BOT_BODY:
res.BotBody = p.PayLoad()
case TAG_FORWARD:
res.Forward = p.PayLoad()
}
if p.Last() {
break
}
}
return res, nil
}

View File

@@ -1,75 +0,0 @@
package t1k
import "fmt"
type HttpExtra struct {
UpstreamAddr string
RemoteAddr string
RemotePort string
LocalAddr string
LocalPort string
ServerName string
Schema string
ProxyName string
UUID string
HasRspIfOK string
HasRspIfBlock string
ReqBeginTime string
ReqEndTime string
RspBeginTime string
RepEndTime string
}
func (h *HttpExtra) ReqSerialize() ([]byte, error) {
format := "UpstreamAddr:%s\n" +
"RemotePort:%s\n" +
"LocalPort:%s\n" +
"RemoteAddr:%s\n" +
"LocalAddr:%s\n" +
"ServerName:%s\n" +
"Schema:%s\n" +
"ProxyName:%s\n" +
"UUID:%s\n" +
"HasRspIfOK:%s\n" +
"HasRspIfBlock:%s\n" +
"ReqBeginTime:%s\n" +
"ReqEndTime:%s\n"
return []byte(fmt.Sprintf(
format,
h.UpstreamAddr,
h.RemotePort,
h.LocalPort,
h.RemoteAddr,
h.LocalAddr,
h.ServerName,
h.Schema,
h.ProxyName,
h.UUID,
h.HasRspIfOK,
h.HasRspIfBlock,
h.ReqBeginTime,
h.ReqEndTime,
)), nil
}
func (h *HttpExtra) RspSerialize() ([]byte, error) {
format := "Scheme:%s\n" +
"ProxyName:%s\n" +
"RemoteAddr:%s\n" +
"RemotePort:%s\n" +
"LocalAddr:%s\n" +
"LocalPort:%s\n" +
"UUID:%s\n" +
"RspBeginTime:%s\n"
return []byte(fmt.Sprintf(
format,
h.Schema,
h.ProxyName,
h.RemoteAddr,
h.RemotePort,
h.LocalAddr,
h.LocalPort,
h.UUID,
h.RspBeginTime,
)), nil
}

View File

@@ -1,69 +0,0 @@
package t1k
import (
"bytes"
"encoding/binary"
"io"
)
type Packet interface {
Serialize() []byte
Last() bool
Tag() Tag
PayLoad() []byte
}
type HttpPacket struct {
tag Tag
payload []byte
}
func (p *HttpPacket) Last() bool {
return p.tag.Last()
}
func (p *HttpPacket) Tag() Tag {
return p.tag
}
func (p *HttpPacket) PayLoad() []byte {
return p.payload
}
func NewHttpPacket(tag Tag, payload []byte) Packet {
return &HttpPacket{
tag: tag,
payload: payload,
}
}
func (p *HttpPacket) SizeBytes() []byte {
sizeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(sizeBytes, uint32(len(p.payload)))
return sizeBytes
}
func (p *HttpPacket) Serialize() []byte {
var buf bytes.Buffer
buf.WriteByte(byte(p.tag))
buf.Write(p.SizeBytes())
buf.Write(p.payload)
return buf.Bytes()
}
func ReadPacket(r io.Reader) (Packet, error) {
tag := make([]byte, 1)
if _, err := io.ReadFull(r, tag); err != nil {
return nil, err
}
sizeBytes := make([]byte, 4)
if _, err := io.ReadFull(r, sizeBytes); err != nil {
return nil, err
}
size := binary.LittleEndian.Uint32(sizeBytes)
payload := make([]byte, size)
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
return NewHttpPacket(Tag(tag[0]), payload), nil
}

View File

@@ -1,122 +0,0 @@
package t1k
import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"strings"
)
type Request interface {
Header() ([]byte, error)
Body() ([]byte, error)
Extra() ([]byte, error)
Serialize() ([]byte, error)
}
type HttpRequest struct {
extra *HttpExtra
req *http.Request
}
func NewHttpRequest(req *http.Request, extra *HttpExtra) *HttpRequest {
return &HttpRequest{
req: req,
extra: extra,
}
}
func NewHttpRequestRead(req string) *HttpRequest {
httpReq, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(req)))
return &HttpRequest{
req: httpReq,
}
}
func (r *HttpRequest) Header() ([]byte, error) {
var buf bytes.Buffer
proto := r.req.Proto
startLine := fmt.Sprintf("%s %s %s\r\n", r.req.Method, r.req.URL.RequestURI(), proto)
_, err := buf.Write([]byte(startLine))
if err != nil {
return nil, err
}
_, err = buf.Write([]byte(fmt.Sprintf("Host: %s\r\n", r.req.Host)))
if err != nil {
return nil, err
}
err = r.req.Header.Write(&buf)
if err != nil {
return nil, err
}
_, err = buf.Write([]byte("\r\n"))
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (r *HttpRequest) Body() ([]byte, error) {
var buf bytes.Buffer
_, err := buf.ReadFrom(r.req.Body)
if err != nil {
return nil, err
}
r.req.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
return buf.Bytes(), nil
}
func (r *HttpRequest) Extra() ([]byte, error) {
return r.extra.ReqSerialize()
}
func (r *HttpRequest) Version() []byte {
return []byte("Proto:3\n")
}
func (r *HttpRequest) Serialize() ([]byte, error) {
var buf bytes.Buffer
{
raw, err := r.Header()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_HEADER|MASK_FIRST, raw)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
raw, err := r.Body()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_BODY, raw)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
raw, err := r.Extra()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_EXTRA, raw)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
packet := NewHttpPacket(TAG_VERSION|MASK_LAST, r.Version())
_, err := buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

View File

@@ -1,127 +0,0 @@
package t1k
import (
"bytes"
"fmt"
"io"
"net/http"
)
type Response interface {
RequestHeader() ([]byte, error)
RspHeader() ([]byte, error)
Body() ([]byte, error)
Extra() ([]byte, error)
Serialize() ([]byte, error)
}
type HttpResponse struct {
req Request
extra *HttpExtra
rsp *http.Response
}
func NewHttpResponse(req Request, rsp *http.Response, extra *HttpExtra) *HttpResponse {
return &HttpResponse{
req: req,
rsp: rsp,
extra: extra,
}
}
func (r *HttpResponse) RequestHeader() ([]byte, error) {
return r.req.Header()
}
func (r *HttpResponse) RspHeader() ([]byte, error) {
var buf bytes.Buffer
statusLine := fmt.Sprintf("HTTP/1.1 %s\n", r.rsp.Status)
_, err := buf.Write([]byte(statusLine))
if err != nil {
return nil, err
}
err = r.rsp.Header.Write(&buf)
if err != nil {
return nil, err
}
_, err = buf.Write([]byte("\r\n"))
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (r *HttpResponse) Body() ([]byte, error) {
data, err := io.ReadAll(r.rsp.Body)
if err != nil {
return nil, err
}
r.rsp.Body = io.NopCloser(bytes.NewReader(data))
return data, nil
}
func (r *HttpResponse) Extra() ([]byte, error) {
return r.extra.RspSerialize()
}
func (r *HttpResponse) Version() []byte {
return []byte("Proto:3\n")
}
func (r *HttpResponse) Serialize() ([]byte, error) {
var buf bytes.Buffer
{
header, err := r.RequestHeader()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_HEADER|MASK_FIRST, header)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
header, err := r.RspHeader()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_RSP_HEADER, header)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
body, err := r.Body()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_RSP_BODY, body)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
extra, err := r.Extra()
if err != nil {
return nil, err
}
packet := NewHttpPacket(TAG_RSP_EXTRA, extra)
_, err = buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
{
version := r.Version()
packet := NewHttpPacket(TAG_VERSION|MASK_LAST, version)
_, err := buf.Write(packet.Serialize())
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

View File

@@ -1,45 +0,0 @@
package t1k
type Tag byte
const (
TAG_HEADER Tag = 0x01
TAG_BODY Tag = 0x02
TAG_EXTRA Tag = 0x03
TAG_RSP_HEADER Tag = 0x11
TAG_RSP_BODY Tag = 0x12
TAG_RSP_EXTRA Tag = 0x13
TAG_VERSION Tag = 0x20
TAG_ALOG Tag = 0x21
TAG_STAT Tag = 0x22
TAG_EXTRA_HEADER Tag = 0x23
TAG_EXTRA_BODY Tag = 0x24
TAG_CONTEXT Tag = 0x25
TAG_COOKIE Tag = 0x26
TAG_WEB_LOG Tag = 0x27
TAG_USER_DATA Tag = 0x28
TAG_BOT_QUERY Tag = 0x29
TAG_DELAY Tag = 0x2b
TAG_FORWARD Tag = 0x2c
TAG_BOT_BODY Tag = 0x30
MASK_TAG Tag = 0x3f
MASK_FIRST Tag = 0x40
MASK_LAST Tag = 0x80
)
func (t Tag) Last() bool {
return t&MASK_LAST != 0
}
func (t Tag) First() bool {
return t&MASK_FIRST != 0
}
func (t Tag) Strip() Tag {
return t & MASK_TAG
}
func (t Tag) Byte() byte {
return byte(t)
}

View File

@@ -1,6 +0,0 @@
# github.com/xbingW/t1k v1.2.1
## explicit; go 1.17
github.com/xbingW/t1k
github.com/xbingW/t1k/pkg/datetime
github.com/xbingW/t1k/pkg/rand
github.com/xbingW/t1k/pkg/t1k

5
version.json Normal file
View File

@@ -0,0 +1,5 @@
{
"latest_version": "v9.2.7",
"rec_version": "v9.2.7",
"lts_version": "v9.1.0-lts"
}