This document belongs to Cloud WAF — Enterprise Web Protection Management Platform
CLI tool:zcloud· 5 modules: guard / sys / analytics / cli_release / auth
Full API index: /api/openapi.json · Sitemap: /sitemap.xml · AI Quick Read: /llms.txt
API Documentation
Production-grade REST API documentation · 57 endpoints · 80+ chart-key data interfaces · Dual-channel authentication
Audience: integration engineers, SREs, SaaS integrators, AI agents
Reading order: §0 Conventions → §1 Authentication → jump to your business module
Table of Contents
Must Read
- §0 Overall Conventions — protocol / envelope / status codes / pagination / three audiences
- §1 Authentication (read first) — Bearer Session + API Key dual channel / scope narrowing
Public Endpoints (no auth)
- §2 CLI Release — version · install.sh · download · checksums
- §3 User Authentication — login · logout
Business Endpoints (dual-channel auth)
- §4 System Management
- 4.1 User Management —
/api/sys/users - 4.2 API Key Management —
/api/sys/apikeys - 4.3 Permission Tree —
/api/sys/permissions/tree
- 4.1 User Management —
- §5 Guard Resource Management
- 5.1 Domains —
/api/guard/domains - 5.2 Certificates —
/api/guard/certs - 5.3 Policies —
/api/guard/policies - 5.4 CC Rules —
/api/guard/policies/{id}/cc/rules - 5.5 ACL Rules —
/api/guard/policies/{id}/acl/rules - 5.6 BW List —
/api/guard/bwlist - 5.7 IP Forward —
/api/guard/forwards - 5.8 DNS Scheduling —
/api/guard/schedules - 5.9 WAF Rules —
/api/guard/waf/rules
- 5.1 Domains —
Plan Catalog
- §7 Plan Catalog — Read-only external: list · describe; admin ops excluded
Node Operations
- §8 Node Install / Upgrade — artifacts · commands · upgrades · jobs · tokens + installer-side script/package/env/report
Analytics (74 chart-keys · single-shape contract)
- §6.0 Common Calling Contract — 5-field contract / render_hint 8-word whitelist
- §6.1 Glossary · §6.2 Cross-page Batch · §6.3 Single-chart GET entry
- §6.4 Overview page · 8 chart-keys
- §6.5 Access page · 10 chart-keys
- §6.6 Protect page · 12 chart-keys (WAF / CC / DDoS)
- §6.7 AI page · 6 chart-keys
- §6.8 Bot page · 10 chart-keys
- §6.9 Alert page · 5 chart-keys
- §6.10 Health · 7 chart-keys
- §6.11 Ops · 8 chart-keys
- §6.12 Closure · 4 chart-keys
- §6.13 Cache · 4 chart-keys
- §6.14 Raw Logs — non-chart endpoint
- §6.15 Reports — non-chart endpoint
- §6.16 CLI Mapping — 30 cobra command mapping
Reference
- §A Common Query Parameters — window / stime / etime / site_id / domain_id / target_user_id / compare / top / order
- §B Visualization Recommendations — B.1 render_hint lookup · B.2 data shape lookup
- §C Related Documentation
Full OpenAPI: /api/openapi.json · AI Quick Read: /llms.txt · Error Codes: /docs/errors · CLI: /docs/cli · Permissions: /docs/permissions
§0 Overall Conventions
The Cloud WAF backend is a Gin-based RESTful service.
0.1 Basic Protocol
| Item | Value |
|---|---|
| Protocol | HTTPS (recommended) / HTTP |
| Data format | Requests and responses are application/json (exceptions: POST /api/guard/certs is still JSON with PEM as string fields; export/download endpoints return text/csv, application/pdf, etc.) |
| Charset | UTF-8 |
| Path prefix | All business APIs live under /api/ |
| Time format | Unix milliseconds (int64), not ISO strings |
| i18n | Accept-Language: zh-CN or en-US localizes error messages and permission names |
0.2 Unified Response Envelope
Every JSON response follows this three-field shape:
{
"code": 0,
"message": "ok",
"data": { /* business payload, type depends on the endpoint */ }
}
| Field | Type | Meaning |
|---|---|---|
code |
number | 0 = success; non-zero = business error code |
message |
string | Error description (honors Accept-Language) |
data |
any | Payload; list endpoints use { list, total, page, size } |
Exceptions: file-export endpoints (
POST /api/analytics/overview/export,GET /api/analytics/reports/:id/download,POST /api/analytics/logs/export) return raw binary or CSV/JSON streams, not wrapped in the envelope.
0.3 HTTP Status Codes
| Code | When emitted |
|---|---|
| 200 | Business success (still inspect code) |
| 201 | Resource created |
| 400 | Bad request (missing param, invalid format, out-of-range) |
| 401 | Not logged in / session expired / API Key revoked |
| 403 | Authenticated but insufficient permission / cross-OEM forbidden (see Permission Matrix) |
| 404 | Resource not found or out of visible scope |
| 429 | Rate-limited (default 100 RPS per Key) |
| 5xx | Server error |
0.4 Pagination Convention
All list endpoints use page + size (not page_size):
| Field | Type | Default | Range |
|---|---|---|---|
page |
int | 1 |
≥ 1 |
size |
int | 20 |
1 - 100 |
Response shape:
{
"code": 0,
"data": {
"list": [ /* ... */ ],
"total": 42,
"page": 1,
"size": 20
}
}
0.5 Cross-Module Design Markers (D*)
A few D* markers in this document come from cross-module design decisions. They warn integrators not to use fields that do not exist or have different semantics:
| Marker | Meaning |
|---|---|
| D4 | percentile / p50 / p95 / p99 are not exposed as common fields; only time windows ≤ 24h trigger real-time ES percentile calculation |
| D7 | Report template names are a closed enum; templates outside this enum are not callable |
| D8 | Cache-value fields use total_cache_*; single-field names such as cache_count / cache_bytes / cache_hit do not exist |
| D10 | Alert/risk closure uses process_uid / process_time / status / level; old field names handle_user / handle_time / risk_score / alert_status are not supported |
0.6 Three Audiences
| Audience | Entry | Prefer |
|---|---|---|
| Human integrators | This doc + Quickstart | curl / Postman with single endpoints |
| Scripts / CI / third-party systems | This doc + API Key Management | API Key + narrowed scopes |
| Machines / AI agents | /api/openapi.json / /llms.txt / /llms-full.txt |
OpenAPI v3 schema |
§1 Authentication (read first)
Cloud WAF supports two authentication channels. Pick exactly one per request:
| Scenario | Header | Best for | Notes |
|---|---|---|---|
| Human login / Web Console / interactive CLI login | Authorization: Bearer <token> |
People | Token is issued by POST /api/auth/login; expires; best for short-lived sessions |
| Scripts / CI / third-party integrations | Authorization: ApiKey zck_<prefix>.<secret> |
Machine calls | The plaintext API Key is returned only once at issuance; best for long-running automation |
Public endpoints (no auth) — only 5:
GET /api/cli/versionGET /api/cli/install.shGET /api/cli/download/{filename}GET /api/cli/checksums.txtPOST /api/auth/login
Every other endpoint must include one of the headers above. Examples in this doc default to Bearer; switch to API Key by replacing the header with Authorization: ApiKey zck_<prefix>.<secret> and ensure the key scopes cover the required permission.
1.1 API Key Permission Rule
effective_perms = user.RBAC ∩ key.scope
An API Key can never exceed the issuing user's current RBAC; scope can only narrow, not expand. After authentication, middleware injects the same user_id / role_id context as the session channel, so downstream RBAC/OEM isolation works identically.
Security guarantees:
- The plaintext API Key is returned exactly once in the create response; only
key_hash(bcrypt digest) is stored - If the user is locked or deleted, all API Keys under that user become invalid automatically
- If
scopesis non-empty, the required permission must match one of them exactly; an empty array means full inheritance of the issuing user's RBAC - Cross-OEM use is blocked by middleware: a 403 is returned when
api_keys.oem_idmismatches the request hostname - Optional IP allowlist
allowed_ip_cidrs: if non-empty, only requests from matching CIDRs are accepted
Full API Key management endpoints are documented in §4.2.
§2 CLI Release (public endpoints)
Used for zcloud CLI self-update and one-line install. No authentication required.
GET /api/cli/version — Query latest CLI version
Purpose: clients self-check on startup; the install script /api/cli/install.sh calls this internally to decide which binary to download.
Authentication: none (public)
Input parameters: none
Output fields:
| Field | Type | Description |
|---|---|---|
data.version |
string | Like v0.1.0-31, aligned with git tag |
data.binaries[] |
array | Download URLs for the four os/arch combinations |
data.binaries[].os |
string | linux / darwin |
data.binaries[].arch |
string | amd64 / arm64 |
data.binaries[].download_url |
string | Append to your service URL to download |
Visualization recommendation: plain text (version badge); not suitable for charts. Frontend may use this on a "System Settings - CLI version" page as a KPI card.
Example response:
{
"code": 0,
"message": "ok",
"data": {
"version": "v0.1.0-31",
"binaries": [
{ "os": "linux", "arch": "amd64", "download_url": "/api/cli/download/linux-amd64" },
{ "os": "linux", "arch": "arm64", "download_url": "/api/cli/download/linux-arm64" },
{ "os": "darwin", "arch": "amd64", "download_url": "/api/cli/download/darwin-amd64" },
{ "os": "darwin", "arch": "arm64", "download_url": "/api/cli/download/darwin-arm64" }
]
}
}
GET /api/cli/install.sh — One-line installer script
Purpose: install CLI on Linux/macOS in one command. Returns text/x-shellscript, suitable for curl ... | sh.
Authentication: none (public)
Input parameters: none
Output fields: a raw shell script (text); not wrapped in JSON envelope.
Visualization recommendation: not a chart; render as a code snippet.
Example:
curl -fsSL https://waf.example.com/api/cli/install.sh | sh
GET /api/cli/download/{filename} — Download a specific binary
Purpose: fetch the zcloud binary (signed) for a specific platform. filename matches the last segment of download_url returned by /api/cli/version (e.g. linux-amd64).
Authentication: none (public)
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
filename |
path | yes | one of linux-amd64 / linux-arm64 / darwin-amd64 / darwin-arm64 |
Output fields: binary stream (application/octet-stream).
Visualization recommendation: not a chart.
GET /api/cli/checksums.txt — Binary checksums
Purpose: SHA256 integrity check for /api/cli/download/*. The install script fetches this before downloading the binary.
Authentication: none (public)
Input parameters: none
Output fields: plain text/plain with one <sha256> <filename> per line.
Visualization recommendation: not a chart.
§3 User Authentication
POST /api/auth/login — Login
Purpose: exchange username/password for a session token. Used by Web Console, interactive CLI login, mobile clients.
Authentication: none (public)
Input parameters (request body):
| Field | Type | Required | Description |
|---|---|---|---|
username |
string | yes | Login name |
password |
string | yes | Password is base64-encoded by the frontend before sending; the backend decodes then bcrypt-compares (legacy compatibility) |
Output fields:
| Field | Type | Description |
|---|---|---|
data.token |
string | 32-char session token; use as Authorization: Bearer <token> |
data.user_id |
string | Unique user ID |
data.user_name |
string | Login name |
data.nick_name |
string | Display name |
data.need_change_password |
bool | true means first-login forced password change required |
Visualization recommendation: login response is not charted; if need_change_password=true, the frontend should redirect to the change-password page.
Example request:
curl -X POST https://waf.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"'$(echo -n 'your_password' | base64)'"}'
Example response:
{
"code": 0,
"message": "ok",
"data": {
"token": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "u-admin",
"user_name": "admin",
"nick_name": "System Administrator",
"need_change_password": false
}
}
Common pitfalls:
- Sending plaintext password → backend base64-decode fails, returns generic "wrong username or password" without locating the real cause
- Reusing token across multiple endpoints → logout on one may affect others; recommend per-device login
POST /api/auth/logout — Logout
Purpose: actively invalidate the current Bearer token; subsequent requests with that token return 401.
Authentication: Bearer <token> (API Key has no logout concept; revoke via DELETE /api/sys/api-keys/:id)
Input parameters: none
Output fields: data is null.
Visualization recommendation: not a chart.
Example request:
curl -X POST https://waf.example.com/api/auth/logout \
-H "Authorization: Bearer $TOKEN"
§4 System Management
4.1 User Management
Use case: CRUD users in the "System Settings - Users" page. All user endpoints are OEM-scoped; cross-OEM operations return 403.
GET /api/sys/users — User list (paged)
Purpose: render the user table on the "Users" page with keyword search and pagination.
Authentication: sys.user.list
Input parameters:
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
page |
int | no | 1 |
Page number (1-based) |
size |
int | no | 20 |
Per-page count, 1-100 |
keyword |
string | no | admin |
Fuzzy search on username / nick_name |
Output fields (data.list[]):
| Field | Type | Description |
|---|---|---|
user_id |
string | Unique user ID |
user_name |
string | Login name |
nick_name |
string | Display name |
email |
string | |
mobile |
string | Phone |
locked |
int | 0 = normal, non-zero = locked |
role_ids |
int64[] | Role ID array (multi-role) |
roles[] |
array | Role brief {role_id, name, level} |
ctime |
int64 | Creation time, Unix ms |
Visualization recommendation: table. locked column uses badges (green/red); roles rendered as chips.
Example request:
curl -H "Authorization: Bearer $TOKEN" \
"https://waf.example.com/api/sys/users?page=1&size=20&keyword=admin"
Example response:
{
"code": 0,
"message": "ok",
"data": {
"list": [
{
"user_id": "u-001",
"user_name": "admin",
"nick_name": "System Administrator",
"email": "admin@example.com",
"mobile": "",
"locked": 0,
"role_ids": [1],
"roles": [{ "role_id": 1, "name": "Super Administrator", "level": 1 }],
"ctime": 1714521600000
}
],
"total": 42,
"page": 1,
"size": 20
}
}
Common pitfalls:
- Treating
role_idsas a single value → users may have multiple roles; always handle as an array - Using
levelas a permission threshold →levelis a display field (business hierarchy); userole_idas the permission threshold (see pitfall_role_level_vs_role_id)
POST /api/sys/users — Create user
Purpose: backend for the "Add User" form on the user management page.
Authentication: sys.user.create
Input parameters (request body):
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
user_name |
string | yes | u1 |
2-255 chars |
password |
string | yes | InitPassw0rd! |
6-72 chars (bcrypt limit) |
nick_name |
string | no | Operator A |
≤ 100 chars |
email |
string | no | u1@x.com |
RFC email format |
mobile |
string | no | 13800138000 |
≤ 20 chars |
comment |
string | no | On-call colleague |
Note |
Output fields: returns the new user object with the same shape as a list item.
Visualization recommendation: not a chart; one-shot write. Frontend should refresh the list after success.
Example request:
curl -X POST https://waf.example.com/api/sys/users \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"user_name":"u1","password":"InitPassw0rd!","nick_name":"Operator A","email":"u1@example.com"}'
DELETE /api/sys/users/{id} — Delete user
Purpose: backend for the "Delete" button on user management. Deletion cascades to sessions, API Keys, and role bindings.
Authentication: sys.user.delete
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
id |
path | yes | User user_id |
Output fields: data is null.
Visualization recommendation: not a chart.
PUT /api/sys/users/{id}/password — Reset password
Purpose: an admin resets a user's password. The user is forced to change the password on next login.
Authentication: sys.user.resetpwd
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
id |
path | yes | User user_id |
password |
string | yes | New password (plaintext, 6-72 chars; backend bcrypts) |
Output fields: data is null.
Visualization recommendation: not a chart.
Example request:
curl -X PUT https://waf.example.com/api/sys/users/u-001/password \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"password":"NewPassw0rd!"}'
Common pitfalls:
- Expecting frontend base64 encoding here → this endpoint accepts plaintext directly (login is base64 only for legacy reasons)
4.2 API Key Management
Use case: issue/revoke machine credentials in the "System Settings - API Keys" page. Pair with
/api/sys/api-keys/:id/logs|stats|audit-actionsfor call auditing.
POST /api/sys/api-keys — Issue a new API Key
Purpose: issue a machine credential for scripts/CI/third-party systems. The plaintext api_key field is returned exactly once; the frontend must let the user copy and store it immediately.
Authentication: sys.apikey.create
Input parameters (request body):
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
name |
string | yes | prod-integration |
≤ 100 chars; for audit identification |
scopes |
string[] | no | ["guard.domain.list"] |
Permission full key list; empty = full inheritance |
expires_in_days |
int | no | 90 |
Default 90, max 365 |
allowed_ip_cidrs |
string[] | no | ["203.0.113.0/24"] |
E14 IP allowlist CIDRs; empty means no IP restriction |
Output fields:
| Field | Type | Description |
|---|---|---|
data.key_id |
string | Unique API Key ID (used for revoke / log lookup) |
data.name |
string | Same as request |
data.api_key |
string | Full plaintext prefix.secret, returned exactly once |
data.prefix |
string | Like zck_abc12345, safe to log |
data.last4 |
string | Last 4 chars of secret, frontend uses to identify "the one I just created" |
data.expires_at |
int64 | Expiration time, Unix ms |
Error codes:
1052scope exceeds the user's RBAC boundary1054expires_in_days > 3651055namemissing
Visualization recommendation: issuance is a one-shot write; frontend should use a modal with a one-time copy button + masked display (industry pattern: GitHub/Stripe).
Example request:
curl -X POST https://waf.example.com/api/sys/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"prod-integration","scopes":["guard.domain.list"],"expires_in_days":90}'
Example response:
{
"code": 0,
"data": {
"key_id": "8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0",
"name": "prod-integration",
"api_key": "zck_abc12345.A1b2C3d4E5f6G7h8I9j0K1L2M3n4O5p6Q7r8S9t0",
"prefix": "zck_abc12345",
"last4": "5t0",
"expires_at": 1732982400000
}
}
Common pitfalls:
- Storing the
api_keyfield in your DB → store onlykey_id+prefix; hand the plaintext to the operator once - Treating empty
scopesas no permission → empty actually means full inheritance of current RBAC; pass an explicit list to narrow
GET /api/sys/api-keys — List API Keys
Purpose: render the API Keys list page with status for the current user / OEM.
Authentication: sys.apikey.list
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
page |
int | no | Default 1 |
size |
int | no | Default 20, max 100 |
Output fields (data.list[]):
| Field | Type | Description |
|---|---|---|
key_id |
string | Unique API Key ID |
name |
string | Name |
prefix |
string | zck_xxx prefix |
last4 |
string | Last 4 of secret |
user_id |
string | Owner |
oem_id |
string | OEM scope boundary |
scopes |
string[] | Narrowed permission list |
allowed_ip_cidrs |
string[] | E14 IP allowlist |
status |
int | 1 = active, 2 = revoked |
expires_at |
int64 | Expiration time |
last_used_at |
int64 | Last call time; 0 if never used |
last_used_ip |
string | Last call IP; "" if never used |
ctime |
int64 | Issuance time |
Visible scope:
- Regular users (
role_id ≥ 10): only their own - Platform users (
role_id < 10: super-admin / ops / auditor): all keys in this OEM
Visualization recommendation: table. status rendered as a badge (green=active / gray=revoked); highlight rows where expires_at is < 7d. Combined with /stats, you can plot a "Top 5 Keys by call volume" bar chart.
Example request:
curl -H "Authorization: Bearer $TOKEN" \
"https://waf.example.com/api/sys/api-keys?page=1&size=20"
DELETE /api/sys/api-keys/{id} — Revoke an API Key
Purpose: soft-revoke (status → revoked); audit trail is preserved. Middleware rejects subsequent requests where status != active.
Authentication: sys.apikey.delete
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
id |
path | yes | API Key key_id |
Output fields: data is null.
Idempotency: re-revoking returns 200; the frontend can call repeatedly without errors.
Operable scope:
- Regular users: only their own
- Platform users: any key in this OEM (for emergency stop)
Error code: 1051 API Key not found or out of visible scope.
Visualization recommendation: not a chart; trigger from a "Revoke" button with a confirmation dialog.
Example request (using API Key as caller):
curl -X DELETE https://waf.example.com/api/sys/api-keys/8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0 \
-H "Authorization: ApiKey zck_abc12345.A1b2C3d4..."
GET /api/sys/api-keys/{id}/logs — Call audit log for a Key (E12)
Purpose: audit the call history of an API Key. Returns events from the audit table api_key_audit_logs.
Authentication: sys.apikey.logs
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
id |
path | yes | API Key key_id |
event |
string | no | call (default, call audit) / manage (management actions) |
page |
int | no | Default 1 |
size |
int | no | Default 20, max 100 |
Output fields (per record):
| Field | Type | Description |
|---|---|---|
id |
int | Auto-increment primary key |
event_type |
string | call / manage |
key_id |
string | Associated API Key ID |
user_id |
string | Subject (call = key holder; manage = operator) |
auth_mode |
string | apikey / session; the channel the request came through |
action |
string | call = <METHOD> <PATH>; manage = create / revoke / renew / revoke-all |
status_code |
int | HTTP response status (call type only) |
biz_code |
int | Business code (0 = success; call type only) |
client_ip |
string | Client IP |
user_agent |
string | UA (≤ 255 chars, truncated) |
extra |
string | JSON string with renew / batch action structured extensions |
ctime |
int | Unix ms |
Visualization recommendation:
- Recommended chart: table + time-series line chart (call count aggregated by ctime)
- Derived metrics: success-rate stacked bar (split by status_code 2xx/4xx/5xx)
Visible scope:
- Regular users: only keys they issued
- Platform users: any key in this OEM (super-admin: all)
Error code: 1051 API Key not found or out of visible scope.
GET /api/sys/api-keys/{id}/stats — Aggregated statistics for a Key
Purpose: on the API Key detail page, show aggregate KPIs (total calls, success rate, QPS, top endpoints). Based only on event_type=call records.
Authentication: sys.apikey.stats
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
id |
path | yes | API Key key_id |
since |
string/int | no | Aggregation window start; supports 24h / 7d / 30m relative or pure integer ms timestamp; empty = all history |
Output fields:
| Field | Type | Description |
|---|---|---|
total_calls |
int | Total calls in window |
success |
int | Count of 200 ≤ status_code < 400 |
client_err |
int | Count of 400 ≤ status_code < 500 |
server_err |
int | Count of status_code ≥ 500 |
top_endpoints |
array | Top 5 actions by count ({action, count}, descending) |
last_1h_qps |
float | Last 1h QPS (count / 3600) |
Visualization recommendation:
- Recommended chart: KPI card grid (4 cards: total_calls / success rate / QPS / errors) + horizontal bar chart (top_endpoints)
- Derived metrics: pie chart (success / client_err / server_err)
Error codes: 400 since parse failed; 1051 API Key not found or out of visible scope.
Example request:
curl -H "Authorization: Bearer $TOKEN" \
"https://waf.example.com/api/sys/api-keys/8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0/stats?since=24h"
GET /api/sys/api-keys/audit-actions — Management action audit log (E13)
Purpose: cross-Key audit view; returns the management actions (create / revoke / renew / revoke-all) audit stream.
Authentication: sys.apikey.audit
Input parameters: page / size, standard pagination.
Output fields: same as GET /api/sys/api-keys/{id}/logs; event_type is always manage.
Visible scope:
- Regular users: only records where they are the operator (
user_id = currentUserID) - Platform users: all in this OEM (super-admin: all)
Visualization recommendation:
- Recommended chart: table sorted by ctime desc
- Derived metrics: bar chart aggregated by action (create / revoke / renew / revoke-all)
4.3 Permission Tree
GET /api/sys/permissions/tree — Permission tree
Purpose: returns the full permission tree (module / resource / action three layers + i18n names) for the current OEM. The frontend "Role Permissions" page renders the checkbox tree from this; the API Key scope picker also uses the same tree.
Authentication: authenticated session (no specific permission required)
Input parameters: optionally append Accept-Language: en-US to localize permission names.
Output fields (excerpt, data[]):
| Field | Type | Description |
|---|---|---|
module |
string | Module, e.g. guard / sys / analytics |
resources[] |
array | Resources under this module |
resources[].prefix |
string | Resource prefix, e.g. guard.domain |
resources[].name |
string | Resource i18n display name |
resources[].actions[] |
array | Actions under this resource |
resources[].actions[].key |
string | Short action key, e.g. list / create |
resources[].actions[].name |
string | Action i18n display name |
resources[].actions[].full_key |
string | Full permission key, e.g. guard.domain.list |
Visualization recommendation:
- Recommended chart: three-level tree (n-tree / el-tree), module → resource → action
- Reuse scenarios: API Key scope picker, role-permission checkbox tree
Example response (excerpt):
{
"code": 0,
"data": [
{
"module": "guard",
"resources": [
{
"prefix": "guard.domain",
"name": "Domains",
"actions": [
{ "key": "list", "name": "List domains", "full_key": "guard.domain.list" },
{ "key": "view", "name": "View domain", "full_key": "guard.domain.view" },
{ "key": "create", "name": "Create domain", "full_key": "guard.domain.create" }
]
}
]
}
]
}
§5 Guard Resource Management
📦 Guard Resource Management · 30 endpoints · used to configure protection objects (domains/certs/policies/CC&ACL rules/bwlist/forwards/schedules/WAF rules); the "configuration plane" of WAF protection.
Full schemas in/api/openapi.json. This section gives the most critical fields, enums, and common pitfalls for integration.
5.1 Domains /api/guard/domains
The domain is the core resource of Guard — every protection policy, certificate binding, and analytics aggregation anchors on domain_id.
GET /api/guard/domains — Domain list
Purpose: render the domain table on the "Protection - Domains" page; supports filtering by audit_status and keyword search.
Authentication: guard.domain.list
Input parameters:
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
page |
int | no | 1 |
Page number |
size |
int | no | 20 |
Per-page count, 1-100 (not page_size) |
keyword |
string | no | api.example |
Fuzzy search on domain / asset_name |
audit_status |
int | no | 4 |
1 = unaudited, 2 = auditing, 3 = rejected, 4 = approved |
Output fields (data.list[] = DomainVO):
| Field | Type | Description |
|---|---|---|
domain_id |
string | Unique domain ID |
domain |
string | The domain itself, e.g. api.example.com |
asset_name |
string | Asset note / alias |
user_id |
string | Owner UUID (kept for legacy platform compatibility) |
user_name |
string | Owner display name (P1.3 added, sourced from cloud sys.users) |
policy_id |
string | Bound policy ID |
cname |
string | CNAME assigned by the backend |
auto_cert |
bool | Whether auto-issuance of certificates is enabled |
mode |
int32 | Onboarding mode (1 = reverse-proxy, etc.; see ops doc) |
audit_status |
int32 | 1=unaudited 2=auditing 3=rejected 4=approved |
switches |
map<string,int32> | Protection switches; keys from waf/cc/acl/bot/cache; 1=on 0=off |
ctime / utime |
int64 | Creation / update time |
Visualization recommendation:
- Recommended chart: table (primary) + top KPI cards (total / approved / pending) +
switchesrendered as a chip grid for switch states - Derived metrics: pie chart (4-way split of
audit_status)
Example request:
curl -H "Authorization: ApiKey $ZCLOUD_API_KEY" \
"https://waf.example.com/api/guard/domains?page=1&size=20&audit_status=4"
Example response:
{
"code": 0,
"data": {
"list": [
{
"domain_id": "d_8a3b1c",
"domain": "api.example.com",
"asset_name": "Production API gateway",
"user_id": "u_abc",
"policy_id": "p_default",
"cname": "api.example.com.cname.zcloud.io",
"auto_cert": false,
"mode": 1,
"audit_status": 4,
"switches": { "waf": 1, "cc": 1, "acl": 1, "bot": 0, "cache": 1 },
"ctime": 1714521600000,
"utime": 1714608000000
}
],
"total": 8,
"page": 1,
"size": 20
}
}
Common pitfalls:
- Paginating with
page_size→ the parameter issize;page_sizeis silently ignored - Expecting
protection_enabled/current_cert_id/origin_addr→ these fields don't exist; protection switches live in theswitchesmap; cert binding is queried via/api/guard/certs/:id/domainsreverse lookup
POST /api/guard/domains — Create domain
Purpose: backend for the "Add Domain" form, creating a new protected domain.
Authentication: guard.domain.create
Input parameters (request body DomainCreateReq):
| Field | Type | Required | Description |
|---|---|---|---|
domain |
string | yes | The domain itself, e.g. api.example.com |
asset_name |
string | no | Asset note |
policy_id |
string | no | Bind a policy; default if not given |
Important:
domainis the only required field. Certificate binding goes throughPOST /api/guard/certs/:id/bind; do not set it via create-domain. Fieldsorigin_addr/port/cert_iddo not exist.
Output fields: returns DomainVO (same as a list item) with the assigned domain_id and cname.
Visualization recommendation: not a chart. After success, jump to the domain detail page or refresh the list.
GET /api/guard/domains/{id} — Domain detail
Purpose: fetch a single domain when entering the detail page.
Authentication: guard.domain.view
Input parameters: path id = domain_id.
Output fields: DomainVO, identical to a list item.
Visualization recommendation: form display. switches rendered as a switch group; audit_status as a badge.
PUT /api/guard/domains/{id} — Update domain
Purpose: edit mutable fields like asset_name, policy_id, auto_cert.
Authentication: guard.domain.edit
Input parameters (request body DomainUpdateReq):
| Field | Type | Required | Description |
|---|---|---|---|
asset_name |
string | no | Asset note |
policy_id |
string | no | Switch policy |
auto_cert |
bool | no | Toggle auto-issuance |
mode |
int32 | no | Onboarding mode |
Output fields: returns the updated DomainVO.
Visualization recommendation: not a chart.
DELETE /api/guard/domains/{id} — Delete domain
Purpose: remove the domain from the protection list. Deletion cascades to settings, cert bindings, analytics snapshots.
Authentication: guard.domain.delete
Input parameters: path id = domain_id.
Output fields: data is null.
Visualization recommendation: not a chart. Confirm twice in the UI; warn that "analytics and bindings will be cleaned up".
GET /api/guard/domains/{id}/settings — Get domain settings
Purpose: render the "Advanced Settings" tab on the domain detail page; shows current effective settings (protection module switches, cache policy, CC limits).
Authentication: guard.domain.view
Input parameters: path id = domain_id.
Output fields:
| Field | Type | Description |
|---|---|---|
data.settings |
map<string,string> | keys taken from the backend settings.* dictionary, typically waf/cc/acl/bot/cache; values are stringified config JSON |
Visualization recommendation: form display; one config card per key.
PUT /api/guard/domains/{id}/settings — Update domain settings
Purpose: modify the settings map for a domain.
Authentication: guard.domain.edit
Input parameters (request body DomainSettingsUpdateReq):
| Field | Type | Required | Description |
|---|---|---|---|
settings |
map<string,string> | yes | Keys must come from settings.* returned by GET; unknown keys rejected |
Output fields: data is null; the caller should issue a GET to fetch the new state.
Visualization recommendation: not a chart.
Common pitfalls:
- Passing top-level boolean fields → settings values are stringified JSON, not bool
- Inventing non-existent keys → backend whitelists; unknown keys return 400
5.2 Certificates /api/guard/certs
Certificate flow is a two-step process: "upload PEM → bind to domain". Content-Type is application/json, not multipart/form-data.
GET /api/guard/certs — Cert list
Purpose: table on the "Protection - Certificates" page.
Authentication: guard.cert.list
Input parameters: page / size / keyword (search name/common_name).
Output fields (data.list[] = CertVO):
| Field | Type | Description |
|---|---|---|
id |
uint64 | Unique cert ID |
name |
string | Custom name |
user_name |
string | Owner display name (P1.3 replaces user_id, sourced from cloud sys.users) |
certificate_type |
int32 | Cert type enum |
common_name |
string | Cert CN |
issuer |
string | Issuer |
expired_at |
int64 | Expiry, Unix ms |
auto_cert |
bool | Auto-renewal |
ctime / utime |
int64 | Creation / update time |
Field
bound_domainsdoes not exist; query bound domains viaGET /api/guard/certs/{id}/domains.
Visualization recommendation:
- Recommended chart: table + near-expiry highlight (
expired_at - now < 30dcolored row) - Derived metrics: KPI cards (total / expiring in 30d / expired); pie chart (auto_cert true/false)
POST /api/guard/certs — Upload certificate
Purpose: upload one PEM cert. The most common integration pitfall is using multipart/form-data; please use JSON.
Authentication: guard.cert.create
Security note: direct integrations still pass cert / key as JSON PEM strings. Do not put private keys in AI chats, tickets, logs, or telemetry. When operating through the Aegeon Cloud conversational assistant, prefer its secure certificate attachment flow: the browser uploads the cert/key to Aegeon and the chat only contains an attachment reference; the private key is not stored in the AI message.
Input parameters (request body CertUploadReq, application/json):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Cert name |
cert |
string | yes | PEM text (with -----BEGIN CERTIFICATE----- markers) |
key |
string | yes | Private key PEM text |
sign_cert |
string | no | SM2 signing cert PEM (optional) |
sign_key |
string | no | SM2 signing private key PEM (optional) |
Output fields: returns the new CertVO.
Visualization recommendation: not a chart. The frontend upload component should support both "paste PEM" and "read local file".
Example request:
curl -X POST https://waf.example.com/api/guard/certs \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"prod-2026","cert":"-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----","key":"-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----"}'
Common pitfalls:
- Using
multipart/form-datato upload files → backend doesn't support it; returns 400 - Passing file paths in
cert/key→ must be the PEM text content itself - Pasting private keys into AI chat or logs → use a secure attachment / secret-handling path and let the server decrypt only when calling this API
GET /api/guard/certs/{id} — Cert detail
Purpose: detail page; includes full PEM text.
Authentication: guard.cert.view
Input parameters: path id.
Output fields: CertDetailVO = CertVO + cert + sign_cert (PEM text, useful for download).
Visualization recommendation: form display. Add a "Download cert" button (frontend assembles PEM into a download).
PUT /api/guard/certs/{id} — Update cert
Purpose: replace PEM directly (no need to delete-then-create).
Authentication: guard.cert.edit
Input parameters (request body CertUpdateReq, same fields as upload but all optional): name / cert / key / sign_cert / sign_key.
Output fields: returns the updated CertVO.
DELETE /api/guard/certs/{id} — Delete cert
Authentication: guard.cert.delete
Input parameters: path id.
Output fields: data is null.
Side effect: deletion auto-unbinds the cert from all bound domains.
GET /api/guard/certs/{id}/domains — List domains bound to a cert
Purpose: on the cert detail page, show "which domains is this cert protecting".
Authentication: guard.cert.view
Input parameters: path id.
Output fields (data[] = CertDomainVO[]):
| Field | Type | Description |
|---|---|---|
domain_id |
string | Domain ID |
domain |
string | Domain |
cert_id |
uint64 | Cert ID (= path id) |
ctime |
int64 | Bind time |
Visualization recommendation: table.
POST /api/guard/certs/{id}/bind — Bind cert to domain
Purpose: bind a specific cert to a domain.
Authentication: guard.cert.edit
Input parameters (request body CertBindReq):
| Field | Type | Required | Description |
|---|---|---|---|
domain_id |
string | yes | Target domain ID |
Output fields: data is null.
Visualization recommendation: not a chart.
DELETE /api/guard/certs/{id}/bind/{domainId} — Unbind cert from domain
Purpose: break the binding between a cert and a domain.
Authentication: guard.cert.edit
Input parameters: path id = cert ID; path domainId = domain ID.
Output fields: data is null.
Path: it is
DELETE /bind/{domainId}, notPOST /unbind.
5.3 Policies /api/guard/policies
A policy is a rule container: CC / ACL / bwlist all attach to a policy. One domain binds one policy.
GET /api/guard/policies — Policy list
Authentication: guard.policy.list
Input parameters: page / size / keyword.
Output fields (data.list[] = PolicyVO):
| Field | Type | Description |
|---|---|---|
policy_id |
string | Policy ID |
name |
string | Policy name |
comment |
string | Note (not remark) |
user_id |
string | Owner UUID (kept for legacy platform compatibility) |
user_name |
string | Owner display name (P1.3 added, sourced from cloud sys.users) |
default_main_rule_version |
string | Default main rule version |
is_default |
bool | Whether this is the system default |
schema_id |
int64 | Schema version |
cc_rule_count |
int32 | CC rule count under this policy |
bwl_rule_count |
int32 | bwlist rule count |
acl_rule_count |
int32 | ACL rule count |
ctime / utime |
int64 | Time |
Visualization recommendation: table + three chips (cc/bwl/acl counts).
POST /api/guard/policies — Create policy
Authentication: guard.policy.create
Input parameters (PolicyCreateReq): name (required) / comment.
Output fields: returns the new PolicyVO.
GET /api/guard/policies/{id} — Policy detail
Authentication: guard.policy.view
Input parameters: path id.
Output fields: PolicyVO.
PUT /api/guard/policies/{id} — Update policy
Authentication: guard.policy.edit
Input parameters (PolicyUpdateReq): name / comment.
Output fields: returns the updated PolicyVO.
DELETE /api/guard/policies/{id} — Delete policy
Authentication: guard.policy.delete
Input parameters: path id.
Output fields: data is null. Ensure no domain references this policy before deleting.
5.4 CC Rules /api/guard/policies/{id}/cc/rules
CC = HTTP rate limiting (Connection / Concurrency Control). Lives under a policy and is isolated by policy_id.
GET /api/guard/policies/{id}/cc/rules — CC rule list
Authentication: guard.cc.list
Input parameters: path id = policy_id; query page / size.
Output fields (data.list[] = CcRuleVO):
| Field | Type | Description |
|---|---|---|
rule_id |
int64 | Rule ID |
name |
string | Rule name |
describe |
string | Description |
matches[] |
array | Match conditions (path / method / headers etc.) |
stats |
object | Aggregation dimensions |
limit |
object | Rate limit threshold |
action |
object | Hit action (block / captcha / throttle etc.) |
stime / etime |
int64 | Effective start/end |
status |
int32 | 1 = enabled, 2 = disabled |
ctime / utime |
int64 | Creation / update time |
Visualization recommendation: table + status badge. matches is complex; collapse it under a "Detail" button modal.
POST /api/guard/policies/{id}/cc/rules — Create CC rule
Authentication: guard.cc.create
Input parameters (CcRuleCreateReq):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Rule name |
describe |
string | no | Description |
matches[] |
array | yes | At least 1 match condition |
stats |
object | no | Aggregation dimensions (IP / URI / UA etc.) |
limit |
object | yes | Rate limit |
action |
object | yes | Hit action |
stime / etime |
int64 | no | Effective time window |
Complex shapes: list one rule first as a template.
Output fields: returns the new CcRuleVO.
GET /api/guard/policies/{id}/cc/rules/{rid} — CC rule detail
Authentication: guard.cc.view
Input parameters: path id = policy_id, rid = rule_id. The backend verifies that rule_id belongs to the current policy_id; cross-policy reads return NotFound.
Output fields: CcRuleVO.
PUT /api/guard/policies/{id}/cc/rules/{rid} — Update CC rule
Authentication: guard.cc.edit
Input parameters: path id + rid, body same as Create.
Output fields: returns the updated CcRuleVO.
DELETE /api/guard/policies/{id}/cc/rules/{rid} — Delete CC rule
Authentication: guard.cc.delete
Input parameters: path id + rid.
Output fields: data is null.
PUT /api/guard/policies/{id}/cc/rules/{rid}/status — Toggle CC rule status
Purpose: enable/disable a rule from the list page using a switch component.
Authentication: guard.cc.edit
Input parameters (CcRuleStatusReq):
| Field | Type | Required | Values |
|---|---|---|---|
status |
int32 | yes | 1 = enabled, 2 = disabled |
Important:
statusis an int32 number, not the string"enabled"/"disabled".
Output fields: data is null.
Example request:
curl -X PUT https://waf.example.com/api/guard/policies/p_default/cc/rules/12345/status \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status":1}'
5.5 ACL Rules /api/guard/policies/{id}/acl/rules
ACL = Access Control List (allow/block by IP / Header / URI). Similar shape to CC rules but no
priorityfield.
GET /api/guard/policies/{id}/acl/rules — ACL rule list
Authentication: guard.acl.list
Input parameters: path id, query page / size.
Output fields (data.list[] = AclRuleVO):
| Field | Type | Description |
|---|---|---|
rule_id |
int64 | Rule ID |
name |
string | Rule name |
describe |
string | Description |
matches[] |
array | Match conditions |
action |
object | Hit action (block / page / pass) |
stime / etime |
int64 | Effective start/end |
status |
int32 | 1 = enabled, 2 = disabled |
ctime / utime |
int64 | Time |
Visualization recommendation: table + status badge + action.type chip color (block red / pass green / page blue).
POST /api/guard/policies/{id}/acl/rules — Create ACL rule
Authentication: guard.acl.create
Input parameters (AclRuleCreateReq): name / describe / matches[] (≥1) / action / stime / etime.
When
action.typeisblockorpage, thecontentfield is base64-encoded server-side; the caller passes plaintext.
Output fields: returns the new AclRuleVO.
GET /api/guard/policies/{id}/acl/rules/{rid} — ACL rule detail
Authentication: guard.acl.view
Input parameters: path id + rid.
Output fields: AclRuleVO.
PUT /api/guard/policies/{id}/acl/rules/{rid} — Update ACL rule
Authentication: guard.acl.edit
Input parameters: path id + rid, body same as Create.
Output fields: returns the updated AclRuleVO.
DELETE /api/guard/policies/{id}/acl/rules/{rid} — Delete ACL rule
Authentication: guard.acl.delete
Input parameters: path id + rid.
Output fields: data is null.
PUT /api/guard/policies/{id}/acl/rules/{rid}/status — Toggle ACL rule status
Authentication: guard.acl.edit
Input parameters (AclRuleStatusReq): status int32 (1=enabled 2=disabled, number).
Output fields: data is null.
5.6 BW List /api/guard/bwlist
The bwlist has two layers: a set (logical container, black/white/grey-black/grey-white) and IPs (individual entries within a set). Grey-black and grey-white sets are delivered as blacklist and whitelist rules respectively during config generation.
GET /api/guard/bwlist/sets — Set list
Authentication: guard.bwlist.list
Input parameters: page / size / keyword.
Output fields (data.list[] = IPSetVO):
| Field | Type | Description |
|---|---|---|
id |
uint64 | Set ID |
user_name |
string | Owner display name (P1.3 replaces user_id, sourced from cloud sys.users) |
name |
string | Set name |
policy_id |
string | Bound policy |
ip_set_type |
int32 | 1 = blacklist, 2 = whitelist, 3 = grey-black, 4 = grey-white (numeric enum, not string) |
status |
int32 | 1 = disabled, 2 = enabled |
count |
int64 | Total entries |
enable_count |
int64 | Enabled entries |
unable_count |
int64 | Disabled entries |
describe |
string | Description (not remark) |
is_default |
bool | Whether default set |
is_private |
bool | Whether private |
private_domain_id |
string | Domain associated with private set |
ctime / utime |
int64 | Time |
Visualization recommendation:
- Recommended chart: table (primary) + KPI cards (blacklist count / whitelist count / enabled %)
- Derived metrics: pie chart (
ip_set_type1/2/3/4 split)
POST /api/guard/bwlist/sets — Create set
Authentication: guard.bwlist.create
Input parameters (BWListSetCreateReq):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Set name |
ip_set_type |
int32 | yes | 1 = blacklist, 2 = whitelist, 3 = grey-black, 4 = grey-white |
policy_id |
string | no | Bind policy |
status |
int32 | no | 1 = disabled, 2 = enabled, default 2 |
describe |
string | no | Description |
Output fields: returns the new IPSetVO.
When
policy_idis set, the backend mirrors the set into the policy and domain-level bwlist config; subsequent add, bulk-add, or delete IP operations trigger delivery.
PUT /api/guard/bwlist/sets/{id} — Update set
Authentication: guard.bwlist.edit
Input parameters (BWListSetUpdateReq): name / status / describe (all optional).
Output fields: returns the updated IPSetVO.
Updating
statussyncs policy/domain config and triggers delivery: enabled sets enter black/white lists, disabled sets enter the disabled list.
DELETE /api/guard/bwlist/sets/{id} — Delete set
Authentication: guard.bwlist.delete
Input parameters: path id.
Output fields: data is null. All IPs in the set are deleted as a side effect.
Deleting a set also unbinds it from policy/domain bwlist config and triggers redelivery for affected domains.
GET /api/guard/bwlist/sets/{id}/ips — List IPs in a set
Authentication: guard.bwlist.ip_list
Input parameters: path id = set ID; query page / size.
Output fields (data.list[] = IPVO):
| Field | Type | Description |
|---|---|---|
id |
uint64 | Entry ID |
ipset_id |
uint64 | Owning set |
ip_addr |
string | IP or CIDR (not ip) |
status |
bool | true=enabled false=disabled, default true |
ctime / utime |
int64 | Time |
Fields
expires_at/remarkdo not exist.
Visualization recommendation: table.
POST /api/guard/bwlist/sets/{id}/ips — Add a single IP
Authentication: guard.bwlist.ip_add
Input parameters (BWListIPAddReq):
| Field | Type | Required | Description |
|---|---|---|---|
ip_addr |
string | yes | IP or CIDR |
status |
bool | no | default true |
Output fields: returns the new IPVO.
POST /api/guard/bwlist/sets/{id}/ips/batch — Bulk add IPs
Purpose: import hundreds of IPs in one call (e.g. threat-intel feeds), reducing round trips.
Authentication: guard.bwlist.ip_add
Input parameters (BWListIPBatchAddReq):
| Field | Type | Required | Description |
|---|---|---|---|
ips[] |
array | yes | At least 1 item; each {ip_addr, status?} |
Output fields: data is null or returns the new ID list (implementation-dependent).
POST /api/guard/bwlist/sets/{id}/ips/batch/delete — Bulk delete IPs
Purpose: delete multiple IPs from a set in one call (e.g. clearing expired bans), atomically in a single transaction — avoids the partial-failure and audit-noise of deleting one by one.
Authentication: guard.bwlist.ip_delete
Input parameters (BWListIPBatchDeleteReq):
| Field | Type | Required | Description |
|---|---|---|---|
ip_ids[] |
array<int64> | yes | At least 1 item; IP entry IDs to delete. Scoped to set {id}; IDs not belonging to this set are ignored |
Output fields: data.deleted is the number of entries actually deleted. Returns 404 if no IP in the set matches.
DELETE /api/guard/bwlist/ips/{id} — Delete a single IP
Important: the path is the top-level
DELETE /api/guard/bwlist/ips/{id}, not nested under sets likeDELETE /sets/{sid}/ips/{id}.
Authentication: guard.bwlist.ip_delete
Input parameters: path id = entry ID.
Output fields: data is null.
5.7 IP Forward /api/guard/forwards
This is TCP/UDP port forwarding (L4), not path forwarding / reverse proxy (L7).
GET /api/guard/forwards — Forward list
Authentication: guard.forward.list
Input parameters:
| Field | Type | Description |
|---|---|---|
page / size |
int | Standard pagination |
user_id |
string | Filter by owner |
domain_id |
string | Filter by domain |
status |
int32 | 1 = disabled, 2 = enabled |
keyword |
string | Search domain / describe |
Output fields (data.list[] = ForwardVO):
| Field | Type | Description |
|---|---|---|
id |
uint64 | Forward ID |
user_id |
string | Owner UUID (kept for legacy platform compatibility) |
user_name |
string | Owner display name (P1.3 added, sourced from cloud sys.users) |
domain |
string | Forwarded domain/IP |
domain_id |
string | Associated domain ID |
schema |
int32 | 3 = TCP, 4 = UDP, default 3 |
port |
int32 | Port 1-65535 |
node_ipaddrs |
string | Comma-separated source IPs |
describe |
string | Description |
status |
int32 | 1 = disabled, 2 = enabled |
src_setting |
json | Source setting raw JSON |
adv_settings |
json | Advanced setting raw JSON |
node_setting |
json | Node setting raw JSON |
dev_setting |
string | Device setting |
ctime / utime |
int64 | Time |
Visualization recommendation:
- Recommended chart: table + status / schema dual chips
- Derived metrics: pie chart (TCP/UDP split)
POST /api/guard/forwards — Create forward
Authentication: guard.forward.create
Input parameters (ForwardCreateReq):
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
domain |
string | yes | *.example.com |
Forward domain |
domain_id |
string | yes | d_8a3b1c |
Associated domain |
port |
int32 | yes | 443 |
1-65535 |
schema |
int32 | no | 3 |
3=TCP (default) / 4=UDP |
node_ipaddrs |
string | no | 10.0.0.1,10.0.0.2 |
Source IP list |
describe |
string | no | Description | |
status |
int32 | no | 2 |
1=disabled 2=enabled |
src_setting / adv_settings / node_setting |
json | no | Raw JSON config | |
dev_setting |
string | no | Device config |
Fields
source_path/targetdo not exist.
Output fields: returns the new ForwardVO.
GET /api/guard/forwards/{id} — Forward detail
Authentication: guard.forward.view
Input parameters: path id.
Output fields: ForwardVO.
PUT /api/guard/forwards/{id} — Update forward
Authentication: guard.forward.edit
Input parameters: path id, body same as Create (all fields optional).
Output fields: returns the updated ForwardVO.
DELETE /api/guard/forwards/{id} — Delete forward
Authentication: guard.forward.delete
Input parameters: path id.
Output fields: data is null.
5.8 DNS Scheduling /api/guard/schedules
This module only covers DNS parsing scheduling (mode switch between SRC/NODE and batch record enable/disable). The legacy cron-driven schedule endpoints have been retired.
⚠️ Asynchronous semantics: none of these endpoints calls any third-party DNS API directly. Every write persists to the legacy
guard_db.dns_records+guard_db.dns_affairstables and publishes an NSQ message on topicdns; the legacy zdns service consumes it and applies the change. Callers must poll the affairs endpoint to obtain the final status (initialAffairsStatus_Start→AffairsStatus_Succeed/AffairsStatus_Faild).⚠️ Source of truth:
dns_records.group_typeis the truth for the current mode, whileguard_configs.parsing_stateis synced asynchronously; both live in the legacyguard_dband may lag by up to 30s. The VO returns both; render a "syncing" badge whenever they disagree.
5.8.1 Domain Scheduling
GET /api/guard/schedules/domains — List domains
Authentication: guard.schedule.list
Input parameters:
| Field | Type | Description |
|---|---|---|
page / size |
int | Pagination (size cap 100) |
keyword |
string | Fuzzy match against domain name |
user_id |
string | Filter by owner (super-admin / agency only) |
mode |
int32 | 0=all / 1=SRC / 2=NODE |
Output fields (data = ScheduleDomainListResp):
| Field | Type | Description |
|---|---|---|
list[].domain_id |
string | Domain ID (guard_configs.domain_id) |
list[].domain_name |
string | Domain name |
list[].user_id / user_name |
string | Owner |
list[].parsing_state |
int32 | guard_configs.parsing_state (1=SRC / 2=NODE) |
list[].dns_group_type |
int32 | Majority vote of dns_records.group_type (source of truth, 1=SRC / 2=NODE) |
list[].src_count |
int | Records with group_type=1 |
list[].node_count |
int | Records with group_type=2 |
list[].src_records[] |
array | Source record summaries: subdomain / record_type / record_line / value / status |
list[].node_records[] |
array | Node record summaries: subdomain / record_type / record_line / value / status |
list[].last_affair_status |
string | Latest affair status (AffairsStatus_Start / AffairsStatus_Succeed / AffairsStatus_Faild) |
list[].last_affair_ctime |
int64 | Latest affair creation time (ms) |
list[].last_affair_message |
string | Latest affair message (HTML fragment) |
total |
int64 | Total |
POST /api/guard/schedules/domains/{id}/switch-mode — Switch SRC/NODE
Authentication: guard.schedule.switch
Input parameters:
| Field | Type | Required | Description |
|---|---|---|---|
path id |
string | yes | domain_id |
body target_mode |
int32 | yes | 1=SRC / 2=NODE |
body comment |
string | no | Affair note (written to dns_affairs.message) |
Implementation notes: a single guard_db transaction SELECT FOR UPDATEs the domain's dns_records → updates group_type and switch_state=2 (switching) → inserts a dns_affairs row with status=AffairsStatus_Start. After commit, publishes NSQ Cmd=0.
Output fields: returns the newly created ScheduleAffairVO; the frontend should pin affairs_id for polling.
POST /api/guard/schedules/domains/{id}/init — Initialize parsing
Authentication: guard.schedule.init
Input parameters: path id (domain_id); optional body comment.
Implementation notes: reads source/dispatch settings from guard_domain_settings and node bindings from domain_node_ships, locks existing dns_records, deletes and recreates source/node records, then inserts a dns_affairs row and publishes NSQ Cmd=0 so zdns syncs once.
Output fields: returns the newly created ScheduleAffairVO.
POST /api/guard/schedules/domains/{id}/reset — Reset parsing
Authentication: guard.schedule.reset
Input parameters: path id (domain_id); optional body comment.
Implementation notes: every dns_records.switch_state is restored to 1 and status is rolled back to last_status; an affair is created and NSQ Cmd=0 is published.
Output fields: returns the newly created ScheduleAffairVO.
GET /api/guard/schedules/domains/{id}/records — List DNS records of a domain
Authentication: guard.schedule.records
Input parameters:
| Field | Type | Description |
|---|---|---|
path id |
string | domain_id |
group_type |
int32 | 1=SRC / 2=NODE |
status |
int32 | 1=disabled / 2=enabled |
page / size |
int | Pagination |
Output fields (data = ScheduleRecordsResp):
| Field | Type | Description |
|---|---|---|
list[] |
DnsRecordVO |
DNS record view |
list[].record_id |
string | Primary key |
list[].associated_id |
string | Upstream DNS provider record ID used to verify that the record is rebuilt or linked on the ZDNS/provider side |
list[].domain / subdomain / value |
string | Domain / subdomain / parsing value |
list[].record_type |
int32 | Internal DNS record type code |
list[].record_line |
int32 | Parsing line |
list[].ttl |
int64 | TTL (seconds) |
list[].status / last_status |
int32 | 1=disabled / 2=enabled |
list[].group_type |
int32 | 1=SRC / 2=NODE |
list[].switch_state |
int32 | 1=ready / 2=switching |
list[].ctime / utime |
int64 | Timestamps (ms) |
Read-only query against
zdns_db.dns_records; no affair is created.
5.8.2 Record Scheduling
POST /api/guard/schedules/records/batch-status — Batch enable/disable records
Authentication: guard.schedule.batch
Input parameters (ScheduleBatchStatusReq):
| Field | Type | Required | Description |
|---|---|---|---|
record_ids |
string[] | yes | Target record_id set |
status |
int32 | yes | 1=disabled / 2=enabled |
comment |
string | no | Affair note |
Implementation notes: UPDATE dns_records SET last_status=status, status=? WHERE record_id IN (?); an affair is created and NSQ Cmd=0 is published.
Output fields: returns the newly created ScheduleAffairVO.
5.8.3 Affair Records
GET /api/guard/schedules/affairs — Affair list
Authentication: guard.schedule.affairs
Input parameters:
| Field | Type | Description |
|---|---|---|
page / size |
int | Pagination |
user_id |
string | Filter by owner |
status |
string | AffairsStatus_Start / AffairsStatus_Succeed / AffairsStatus_Faild |
ctime_from / ctime_to |
int64 | Time range (ms) |
domain_id |
string | Filter by affected domain (implemented as content LIKE) |
Output fields (data = ScheduleAffairListResp): see ScheduleAffairVO below.
GET /api/guard/schedules/affairs/{id} — Affair detail
Authentication: guard.schedule.affairs
Input parameters: path id (affairs_id).
Output fields (data = ScheduleAffairVO):
| Field | Type | Description |
|---|---|---|
affairs_id |
string | Affair ID, format {ts}_{rand10} |
user_id / user_name |
string | Operator |
status |
string | AffairsStatus_Start / AffairsStatus_Succeed / AffairsStatus_Faild (string enum, matches the legacy MarshalJSON behaviour) |
message |
string | Affair message, may contain HTML snippets (e.g. <br>) |
content |
string | Affected domain_ids, comma separated |
json_content |
object | Extension JSON; carries outbox retry counters, etc. |
affairs_oper |
string | AffairsOperType_Page / AffairsOperType_Cron / AffairsOperType_Cli |
ctime / utime |
int64 | Created / updated (ms) |
Polling tip: after triggering a write, poll this endpoint every 2-5s until
statusbecomesSucceedorFaild; if it stays atStartfor an extended period, the outbox is still retrying — render a "syncing" indicator.
5.9 WAF Rules /api/guard/waf/rules
A WAF rule group is mounted under a policy (
policy_id). Each group contains a set of sub-rules (rules[]). The gateway matches a request against thezone/patternfields, and on hit appliesaction(block / log / captcha).⚠️ Status convention:
statusis unified as1= disabled,2= enabled (the S-6 fix aligned this withforwards/bwlist/schedules). ThePOST /statusendpoint validatesoneof=1 2.⚠️ Path ID is
tag: inPUT/DELETE /api/guard/waf/rules/{id},{id}is the rule group'stag(uint64 primary key) — NOTrule_id(the business code). The frontend readstagfrom the list response.
GET /api/guard/waf/rules — WAF rule group list
Authentication: guard.waf.list
Input parameters:
| Field | Type | Description |
|---|---|---|
page / size |
int | Standard pagination |
policy_id |
string | Filter by policy (omit to return all rule groups visible to the current user) |
Output fields (data.list[] = WafGroupVO):
| Field | Type | Description |
|---|---|---|
tag |
uint64 | Rule group primary key (used as path {id}) |
rule_id |
int64 | Business rule code |
name |
string | Rule group name |
describe |
string | Description |
waf_type |
int32 | WAF type internal code |
sub_rule_condition |
int32 | Sub-rule combination logic (0 = AND / 1 = OR, per model implementation) |
scope |
string | Scope (domain / path / empty = all) |
action |
int32 | 1 = block, 2 = log, 3 = captcha |
status |
int32 | 1 = disabled, 2 = enabled |
policy_id |
string | Owning policy ID |
rules[] |
WafRuleVO |
Sub-rule list (each has zone / pattern / pattern_type / is_not) |
ctime / utime |
int64 | Timestamps (ms) |
WafRuleVO (sub-rule) fields:
| Field | Type | Description |
|---|---|---|
tag |
uint64 | Sub-rule primary key |
rule_id |
int64 | Business rule code |
group_id |
int64 | Owning rule group (joins WafGroupVO.tag / rule_id) |
zone |
string | Match zone (e.g. URL / ARGS / HEADER / BODY) |
sub_field |
string | Sub-field inside the zone (e.g. header name) |
pattern_type |
int32 | Match type internal code (exact / regex / contains, etc.) |
pattern |
string | Match expression |
describe |
string | Description |
is_not |
bool | true = negated match |
ctime / utime |
int64 | Timestamps (ms) |
Visualization recommendation:
- Recommended chart: table +
action/statusdual chips +rules.lengthcount badge - Derived metric: pie chart of groups grouped by
action(block / log / captcha split)
POST /api/guard/waf/rules — Create WAF rule group
Authentication: guard.waf.create
Input parameters (WafGroupCreateReq):
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
name |
string | yes | "SQL injection guard" |
Rule group name |
policy_id |
string | yes | "1" |
Owning policy |
rule_id |
int64 | no | Business code; generated server-side if omitted | |
describe |
string | no | Description | |
waf_type |
int32 | no | WAF type internal code | |
sub_rule_condition |
int32 | no | Sub-rule combination logic | |
scope |
string | no | "*.example.com" |
Scope |
action |
int32 | no | 1 |
1 = block / 2 = log / 3 = captcha |
status |
int32 | no | 2 |
1 = disabled, 2 = enabled |
rules[] |
object[] | no | Sub-rules (each = WafRuleReq, see below) |
WafRuleReq (sub-rule body):
| Field | Type | Description |
|---|---|---|
rule_id |
int64 | Business code; generated server-side if omitted |
zone |
string | Match zone |
sub_field |
string | Sub-field inside the zone |
pattern_type |
int32 | Match type |
pattern |
string | Match expression |
describe |
string | Description |
is_not |
bool | true = negated |
Example:
curl -X POST https://waf.example.com/api/guard/waf/rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "SQL injection guard",
"policy_id": "1",
"describe": "OWASP Top 10 SQLi blacklist",
"action": 1,
"status": 2,
"rules": [
{ "zone": "ARGS", "pattern_type": 2, "pattern": "union\\s+select", "describe": "SQLi: union select" }
]
}'
Output fields: the new WafGroupVO (including the assigned tag and the persisted rules[]).
Fields that do not exist: a boolean
enabled(use integerstatus) /description(usedescribe) /domains[](scope is thescopestring).
PUT /api/guard/waf/rules/{id} — Update WAF rule group
Authentication: guard.waf.edit
Input parameters (path id = rule group tag; body WafGroupUpdateReq, all fields optional, applied incrementally; rules[] is a full replace):
| Field | Type | Description |
|---|---|---|
name |
string | Rule group name |
describe |
string | Description |
waf_type |
*int32 | Pointer type; omit to keep unchanged |
sub_rule_condition |
*int32 | Pointer type |
scope |
string | Scope |
action |
*int32 | Pointer type |
rules[] |
object[] | Full replace of sub-rules |
This endpoint does not update
status— use the dedicatedPUT /api/guard/waf/rules/{id}/statusfor enable / disable.
Output fields: the updated WafGroupVO.
DELETE /api/guard/waf/rules/{id} — Delete WAF rule group
Authentication: guard.waf.delete
Input parameters: path id (rule group tag).
Implementation note: cascades to sub-rules (waf_rules.group_id = tag).
Output fields: data is null.
PUT /api/guard/waf/rules/{id}/status — Toggle WAF rule group enable / disable
Authentication: guard.waf.status
Input parameters (path id = rule group tag; body WafStatusReq):
| Field | Type | Required | Value | Description |
|---|---|---|---|---|
status |
int32 | yes | 1 / 2 |
1 = disabled, 2 = enabled; validated by oneof=1 2 |
Example:
curl -X PUT https://waf.example.com/api/guard/waf/rules/42/status \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": 1 }'
Output fields: data is null.
§6 Analytics
📊 Analytics · 18 paths (covering 80+ chart-key single-chart endpoints) · used to build WAF monitoring dashboards, operational reports, closed-loop alert handling
All/api/analytics/*paths are public; fields can only be added, paths cannot be modified or removed.
Each chart-key has a clearly recommended chart in the per-page sections below.
6.0 Common Calling Contract
Most Analytics endpoints are homogeneous chart queries. This chapter uses "common contract + index tables + special endpoint expansions".
Authentication & Permissions
Header: Authorization: Bearer <token> or Authorization: ApiKey zck_..., optionally with Accept-Language: zh-CN / en-US.
Permissions fall into three classes:
| Class | How decided | Example |
|---|---|---|
| Page-level read-only | analytics.<page>.view |
GET /api/analytics/overview/kpi requires analytics.overview.view |
| Special action | Called out per row / sub-section | POST /api/analytics/overview/export requires analytics.overview.export |
| Authenticated only | No specific business permission | GET /api/analytics/glossary |
When using an API Key, the key's scopes must cover the required permission. For example, calling GET /api/analytics/access/status requires analytics.access.view in scope.
Single-chart GET template
curl -sS 'https://waf.example.com/api/analytics/access/status?window=last_24h&site_id=site-001' \
-H "Authorization: ApiKey $ZCLOUD_API_KEY" \
-H 'Accept-Language: en-US'
Returns the unified envelope. Chart data always uses the Chart Unified Contract (docs/specs/chart-contract.md). This is the only public contract for the new system; historical PostgreSQL tables, aggregate tables, and ES indexes are internal data sources only and do not affect request parameters or response shape.
Chart data always contains exactly these 5 fields; series / totals / kpis / points / list are not public top-level fields:
| Field | Type | Description |
|---|---|---|
chart_key |
string | Identical to the requested <chart> |
render_hint |
enum | One of 8 vocab words: kpi / categorical_distribution / categorical_distribution_over_time / time_series_single / time_series_multi / topn / geo / table; the frontend selects its renderer accordingly |
schema |
object | Column metadata {dimensions:[{name,type,unit?,values?}], measures:[{name,type,unit?,format?}]} |
rows |
array | Tidy long table — one observation per row (no wide-pivoted columns); empty data returns [] |
meta |
object | Debug fields {source, cache, latency_ms, partial?, available?, ...} |
window.granularityis not a request parameter: clients passwindow=last_24h; the backend automatically picks the 5m/1h/1d aggregation table based on the window size and echoes the chosengranularityin the response.
Batch template
For POST /api/analytics/batch and POST /api/analytics/<page>/batch.
curl -sS -X POST https://waf.example.com/api/analytics/overview/batch \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"time_window": "last_24h",
"site_id": "",
"domain_id": "",
"compare": false,
"charts": [
{ "key": "kpi" },
{ "key": "bandwidth" }
]
}'
The batch response's data.data is keyed by chart-key; data.meta gives total elapsed, cache hit ratio, failed chart count, and effective window. If a single chart fails, inspect the partial / error fields on its node.
6.1 Glossary
GET /api/analytics/glossary — Statistics glossary
Purpose: returns the analytics glossary; the frontend renders tooltips ("what is QPS / block rate") from this map.
Authentication: authenticated session
Input parameters: none.
Output fields:
| Field | Type | Description |
|---|---|---|
data.terms |
map<string,string> | key = term short name, value = localized explanation |
Visualization recommendation:
- Recommended chart: not directly charted; the frontend looks up the glossary by chart-key when attaching tooltips
Example response:
{
"code": 0,
"data": {
"terms": {
"qps": "Queries per second",
"block_rate": "Block rate"
}
}
}
6.2 Cross-page Batch
POST /api/analytics/batch — Cross-page batch query
Purpose: load multiple chart-keys in one round-trip when initializing a dashboard, avoiding N single-chart GETs. The backend runs subqueries in parallel and merges responses.
Authentication: maps to analytics.<page>.view based on the page field.
Input parameters (request body):
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
page |
string | yes | overview |
Must be one of: overview / access / protect / ai / bot / alert / health / ops / closure / cache |
time_window |
string | no | last_24h |
Same as window |
stime / etime |
int64 | no | 1746748800000 |
Custom timestamps |
site_id |
string | no | Site filter | |
domain_id |
string | no | Domain filter | |
target_user_id |
string | no | Tenant-level switch viewed user | |
compare |
bool | no | false |
Enable previous-period comparison |
charts[] |
array | yes | [{key:"kpi"}] |
At least 1 chart-key |
logs(Phase 1 raw logs) andreports(Phase 4 report center) are independent groups and do not support batch.
Output fields:
| Field | Type | Description |
|---|---|---|
data.data |
map | key = chart-key; value is always the 5-field Chart Unified Contract: {chart_key, render_hint, schema, rows, meta} |
data.meta.elapsed_ms |
int | Total elapsed |
data.meta.cache_hit_ratio |
float | Cache hit ratio (0-1) |
data.meta.total_charts |
int | Total charts requested |
data.meta.failed_charts |
int | Failed charts |
data.meta.window |
object | Effective time window |
Visualization recommendation:
- Recommended chart: this endpoint itself is a data-loading layer, not directly charted; frontend dispatches each chart-key result to its own chart component
- Derived metrics:
data.meta.cache_hit_ratiocan be displayed as a KPI card in dev mode to monitor dashboard cache efficiency
Example request/response:
// Request
{
"page": "overview",
"time_window": "last_24h",
"compare": false,
"charts": [
{ "key": "kpi" },
{ "key": "bandwidth" }
]
}
// Response
{
"code": 0,
"data": {
"data": {
"kpi": {
"chart_key": "overview/kpi",
"render_hint": "kpi",
"schema": {
"dimensions": [],
"measures": [
{ "name": "domain_count", "type": "integer", "unit": "" },
{ "name": "requests", "type": "integer", "unit": "requests" },
{ "name": "blocked", "type": "integer", "unit": "events" },
{ "name": "block_rate", "type": "percent", "unit": "%" },
{ "name": "qps", "type": "float", "unit": "qps" },
{ "name": "ai_detect", "type": "integer", "unit": "events" }
]
},
"rows": [
{ "domain_count": 8, "requests": 12345, "blocked": 678, "block_rate": 5.49, "qps": 0.143, "ai_detect": 0 }
],
"meta": { "source": "postgres", "cache": "miss", "latency_ms": 10 }
},
"event-type": {
"chart_key": "overview/event-type",
"render_hint": "categorical_distribution",
"schema": {
"dimensions": [{ "name": "event_type", "type": "string" }],
"measures": [{ "name": "count", "type": "integer", "unit": "events" }]
},
"rows": [
{ "event_type": "sql_injection", "count": 1234 },
{ "event_type": "xss", "count": 567 }
],
"meta": { "source": "elasticsearch", "cache": "miss", "latency_ms": 15, "partial": false }
}
},
"meta": {
"elapsed_ms": 22,
"cache_hit_ratio": 0,
"total_charts": 2,
"failed_charts": 0,
"window": { "stime": 1746662400000, "etime": 1746748800000, "granularity": "1h" }
}
}
}
Note: in the example above,
kpi's measure namesrequests/blockedare the public contract truth names, aligned with thepkg/chart/contractsource-of-truth structs. Underlying database field names are not exposed; the frontend reads values strictly byschema.measures[].name.
POST /api/analytics/{page}/batch — Page-level batch
Purpose: equivalent to POST /api/analytics/batch, but page is fixed by URL (more direct for fixed-page frontends).
Authentication: maps to analytics.<page>.view based on {page}.
Supported pages:
| URL | Permission |
|---|---|
POST /api/analytics/overview/batch |
analytics.overview.view |
POST /api/analytics/access/batch |
analytics.access.view |
POST /api/analytics/protect/batch |
analytics.protect.view |
POST /api/analytics/ai/batch |
analytics.ai.view |
POST /api/analytics/bot/batch |
analytics.bot.view |
POST /api/analytics/alert/batch |
analytics.alert.view |
POST /api/analytics/health/batch |
analytics.health.view |
POST /api/analytics/ops/batch |
analytics.ops.view |
POST /api/analytics/closure/batch |
analytics.closure.view |
POST /api/analytics/cache/batch |
analytics.cache.view |
Input/Output: identical to POST /api/analytics/batch. Callers don't need to send page in the body; if they do, the path's page name wins.
Visualization recommendation: same as above.
6.3 Single-chart GET entry
GET /api/analytics/{page}/{chart} — Single-chart common entry
Purpose: fetch a single chart-key's data. {page} follows the batch enum; {chart} follows the chart-key index per page section.
Authentication: maps to analytics.<page>.view (a few special charts use independent permissions; see per-section).
Input parameters: see §A Common Query Parameters.
Output fields: unified envelope + data:
dataalways uses the Chart Unified Contract:{chart_key, render_hint, schema, rows, meta}schemadeclares the dimension and measure columns ofrows; callers must not infer response fields from underlying database columns
Visualization recommendation: the frontend dispatches each chart by render_hint; complex chart columns are defined by schema.
6.4 Overview page
Use case: WAF protection monitoring dashboard home — KPIs + trends + rankings + map.
All GET endpoints below use the §6.0 Single-chart GET template, §A Common Query Parameters, and the Chart Unified Contract response shape.
| API | chart-key | render_hint | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/overview/kpi |
kpi |
kpi |
KpiGroupCard (6 measures) | Site count / requests / blocked / block_rate / qps / ai_detect |
GET /api/analytics/overview/bandwidth |
bandwidth |
time_series_multi |
Line chart (dual Y-axis) | Total bandwidth + origin bandwidth time-series |
GET /api/analytics/overview/request-attack |
request-attack |
time_series_multi |
Line chart (two series) | Requests vs attacks |
GET /api/analytics/overview/event-type |
event-type |
categorical_distribution |
Pie / donut | Event type distribution (dim=event_type, measure=count) |
GET /api/analytics/overview/waf-type |
waf-type |
categorical_distribution |
Pie / donut | WAF hit type distribution |
GET /api/analytics/overview/geo |
geo |
geo |
Map heatmap (CN/world) | Attack geo distribution |
GET /api/analytics/overview/top-domains |
top-domains |
topn |
Horizontal bar / table | Top 5 attacked domains |
GET /api/analytics/overview/recent-events |
recent-events |
table |
Timeline / table (50 rows) | Recent WAF events |
chart-key data shape
kpi (render_hint = kpi)
schema.measures contains 6 entries:
| measure name | type | unit | Description |
|---|---|---|---|
domain_count |
integer | (empty) | Site count |
requests |
integer | requests | Total requests in window |
blocked |
integer | events | Total blocked events in window |
block_rate |
percent | % | block_rate = blocked / requests |
qps |
float | qps | QPS = requests / window seconds |
ai_detect |
integer | events | AI detection count (currently fixed 0; later connected to ES real data) |
rows (single row): [ { domain_count, requests, blocked, block_rate, qps, ai_detect } ].
Public field names:
requests/blockedare API contract field names. If the underlying database still uses historical fields such asrequest_today/attack_today, the backend converts them in the service layer and never exposes them to callers.
Recommended chart: KpiGroupCard (6 KPI cards). Render block_rate as a percentage with a progress bar; qps with a mini sparkline.
bandwidth / request-attack — Time-series two-series
Output: [{ctime: int64ms, bandwidth: float, origin_bandwidth: float}, ...] or [{ctime, requests, attacks}, ...].
Recommended chart: line chart with ctime on X-axis and two-series Y-axis.
event-type (render_hint = categorical_distribution)
| schema part | Content |
|---|---|
dimensions |
[{ name: "event_type", type: "string" }] |
measures |
[{ name: "count", type: "integer", unit: "events" }] |
rows (long table): [ { event_type: "sql_injection", count: 1234 }, { event_type: "xss", count: 567 }, ... ].
Recommended chart: PieCard (≤8 categories — auto pie) / BarCard (>8 categories — auto horizontal bar); the frontend dispatches by the categorical_distribution vocab word.
waf-type — Dimension distribution
Output: [{key: string, count: int}, ...].
Recommended chart: pie (≤ 8 categories) or donut.
geo — Geo heatmap
Output: [{region: string, count: int}, ...]. region is country/province name.
Recommended chart: map heatmap (CN map + world map).
top-domains — Domain ranking
Output: [{host: string, attack_count: int}, ...], ordered by attack_count DESC, ≤ 5.
Recommended chart: horizontal bar.
recent-events — Recent events stream
Output: [{ctime, domain, attack_type, severity, ...}, ...], ≤ 50.
Recommended chart: timeline / table (sorted by ctime desc), clickable for detail.
POST /api/analytics/overview/export — Overview export
Purpose: export KPIs and chart snapshots as CSV or JSON for offline analysis or reporting. Returns raw file stream, not wrapped in envelope.
Authentication: analytics.overview.export
Input parameters (request body):
| Field | Type | Required | Example | Description |
|---|---|---|---|---|
format |
string | yes | csv / json |
Export format |
window |
string | no | last_24h |
Time window |
charts[] |
array | yes | [{key:"kpi"}] |
Chart-keys to export |
Output: returns the file stream directly with Content-Type: text/csv or application/json and Content-Disposition: attachment.
Visualization recommendation: not a chart; triggers a download.
6.5 Access page
Use case: traffic and quality dashboard — requests, traffic, cache hit, status codes, latency distribution, ISP, top IP/URL, geo.
| API | chart-key | render_hint | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/access/request-hm |
request-hm |
time_series_single (no compare) / time_series_multi (compare) |
LineCard | Requests trend; with compare=true uses sub-shape B (period enum dim distinguishes current/previous) |
GET /api/analytics/access/flow-hm |
flow-hm |
time_series_multi |
LineCard multi-series | 5 measures: total_bytes / request_bytes / response_bytes / upstream_send / upstream_receive |
GET /api/analytics/access/cache-hm |
cache-hm |
time_series_multi |
Line chart (dual Y-axis) | Cache hit count + cache bytes trend |
GET /api/analytics/access/bandwidth |
bandwidth |
time_series_multi |
LineCard multi-series | 4 measures: bandwidth / origin_bandwidth / up_bandwidth / down_bandwidth |
GET /api/analytics/access/status |
status |
categorical_distribution_over_time |
StackedBarCard | 4 HTTP status classes (dim=status_class enum["2xx","3xx","4xx","5xx"] + time) stacked over time |
GET /api/analytics/access/flow-duration |
flow-duration |
time_series_multi |
Line chart (3 percentiles) | Request latency P50/P95/P99 (D4: real-time only when window ≤ 24h) |
GET /api/analytics/access/isp |
isp |
categorical_distribution |
Pie | ISP distribution (mobile/unicom/telecom/other) |
GET /api/analytics/access/top-ip |
top-ip |
topn |
Table / horizontal bar (with geo) | Top access IPs (default 10, configurable via top) |
GET /api/analytics/access/top-url |
top-url |
topn |
Table / horizontal bar | Top URLs; sortable via order=bytes_desc/cache_desc |
GET /api/analytics/access/geo |
geo |
geo |
Map heatmap | Access geo distribution |
chart-key data shape
request-hm
Without compare (render_hint = time_series_single):
| schema part | Content |
|---|---|
dimensions |
[{ name: "time", type: "timestamp", unit: "ms" }] |
measures |
[{ name: "requests", type: "integer", unit: "requests" }] |
rows: [ { time: 1746748800000, requests: 1234 }, ... ].
With compare (render_hint = time_series_multi sub-shape B: 1 categorical + 1 ts + 1 measure):
| schema part | Content |
|---|---|
dimensions |
[{ name: "period", type: "enum", values: ["current","previous"] }, { name: "time", type: "timestamp", unit: "ms" }] |
measures |
[{ name: "requests", type: "integer", unit: "requests" }] |
rows: [ { period: "current", time: ..., requests: ... }, { period: "previous", time: ..., requests: ... }, ... ] long table; pivots into 2 series by period.
Recommended chart: LineCard; in compare mode the frontend pivots by period and renders solid + dashed lines automatically.
flow-hm (render_hint = time_series_multi)
| schema part | Content |
|---|---|
dimensions |
[{ name: "time", type: "timestamp", unit: "ms" }] |
measures |
5 entries: total_bytes / request_bytes / response_bytes / upstream_send / upstream_receive (type=integer, unit=bytes, format=iec) |
rows: [ { time: ..., total_bytes: ..., request_bytes: ..., response_bytes: ..., upstream_send: ..., upstream_receive: ... }, ... ].
Recommended chart: LineCard multi-series (5 lines) / stacked area.
cache-hm — Cache trend
Output: [{ctime, cache_count, cache_bytes, cache_response}, ...].
Truth fields (D8): underlying data uses
total_cache_count/total_cache_bytes/total_cache_response_bytes; the response maps these tocache_count/cache_bytes/cache_response.
Recommended chart: dual Y-axis line (left axis count, right axis bytes).
bandwidth (render_hint = time_series_multi)
| schema part | Content |
|---|---|
dimensions |
[{ name: "time", type: "timestamp", unit: "ms" }] |
measures |
4 entries: bandwidth / origin_bandwidth / up_bandwidth / down_bandwidth (type=float, unit=bps) |
rows: [ { time: ..., bandwidth: ..., origin_bandwidth: ..., up_bandwidth: ..., down_bandwidth: ... }, ... ].
Recommended chart: LineCard multi-series (4 lines).
status (render_hint = categorical_distribution_over_time)
| schema part | Content |
|---|---|
dimensions |
[{ name: "status_class", type: "enum", values: ["2xx","3xx","4xx","5xx"] }, { name: "time", type: "timestamp", unit: "ms" }] |
measures |
[{ name: "count", type: "integer", unit: "requests" }] |
rows tidy long table (no wide-pivoted c2xx/c3xx/c4xx/c5xx columns):
[
{ "status_class": "2xx", "time": 1746748800000, "count": 1200 },
{ "status_class": "3xx", "time": 1746748800000, "count": 30 },
{ "status_class": "4xx", "time": 1746748800000, "count": 8 },
{ "status_class": "5xx", "time": 1746748800000, "count": 0 },
{ "status_class": "2xx", "time": 1746752400000, "count": 1340 },
...
]
Each timestamp must cover all 4 status_class values (fill missing with count: 0 — the stacked bar chart depends on a complete categorical grid).
Recommended chart: StackedBarCard (stacked time-series by status_class). The frontend dispatches by the categorical_distribution_over_time vocab word.
flow-duration — Latency percentiles
Output: {p50, p95, p99} or time-series array.
Truth boundary (D4): percentile fields are computed by ES in real time only when the window ≤ 24h; longer windows return
available:false.
Recommended chart: line chart (3 percentile lines) or 3 KPI cards.
isp — ISP distribution
Output: {mobile, unicom, telecom, other}.
Recommended chart: pie (4 slices).
top-ip — Top IPs
Output: [{remote_addr, count, country, region, isp}, ...], ≤ top.
Recommended chart: table (with country flag + region + ISP); or horizontal bar.
top-url — Top URLs
Output: [{url, request_count, request_bytes, cache_bytes}, ...].
Recommended chart: table by default; or horizontal bar (switchable by bytes/cache).
geo — Geo heatmap
Output: [{region, count}, ...].
Recommended chart: map heatmap.
6.6 Protect page
Use case: hit analysis for the three protection engines — WAF / CC / DDoS.
| API | chart-key | render_hint | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/protect/overview |
overview |
kpi |
Multi-KPI cards | WAF/CC/DDoS totals summary (time-series provided by independent statistics charts) |
GET /api/analytics/protect/waf/statistics |
waf/statistics |
time_series_multi |
Line | WAF hit trend (waf hits + total attacks) |
GET /api/analytics/protect/waf/types |
waf/types |
categorical_distribution |
Pie / horizontal bar | WAF hit type distribution (SQL injection / XSS / scanner...) |
GET /api/analytics/protect/waf/top-ip |
waf/top-ip |
topn |
RankingCard / horizontal bar | dim=ip(string), measure=count(events); country/province go to meta.row_extras |
GET /api/analytics/protect/waf/geo |
waf/geo |
geo |
GeoHeatmapCard | dim=country(geo), measure=count(events); provinces temporarily stored in meta.provinces |
GET /api/analytics/protect/cc/statistics |
cc/statistics |
time_series_single |
Line | CC hit trend |
GET /api/analytics/protect/cc/top-ip |
cc/top-ip |
topn |
Table / horizontal bar | Top CC attack IPs |
GET /api/analytics/protect/cc/geo |
cc/geo |
geo |
Map heatmap | CC attack geo |
GET /api/analytics/protect/cc/top-url |
cc/top-url |
topn |
Table | Top CC attack URLs |
GET /api/analytics/protect/ddos/statistics |
ddos/statistics |
time_series_multi |
Line (dual Y-axis) | DDoS event count + peak bandwidth time-series |
GET /api/analytics/protect/ddos/types |
ddos/types |
categorical_distribution |
Pie / horizontal bar | DDoS attack type distribution (syn flood / udp flood / ...) |
GET /api/analytics/protect/ddos/top-ip |
ddos/top-ip |
topn |
Table | Top DDoS source IPs (with peak bandwidth) |
chart-key data shapes (key differences)
overview: render_hint = kpi, rows = [{waf, cc, ddos_bytes}] (single-row summary, dimensions=[], measures=waf/cc/ddos_bytes). Recommend 3 KPI cards; time-series is provided by the independent protect/waf/statistics and protect/ddos/statistics charts.
waf/statistics: render_hint = time_series_multi, rows = [{time, waf, attack_count}, ...] tidy long table (time=ms timestamp, two measures for two lines). Recommend line chart (two series).
waf/types / ddos/types: render_hint = categorical_distribution, rows = [{waf_type, count}, ...] / [{ddos_type, count}, ...] tidy long table, pie or horizontal bar.
waf/top-ip (render_hint = topn)
| schema part | Content |
|---|---|
dimensions |
[{ name: "ip", type: "string" }] |
measures |
[{ name: "count", type: "integer", unit: "events" }] |
rows: [ { ip: "1.2.3.4", count: 1234 }, { ip: "5.6.7.8", count: 567 }, ... ], sorted DESC by count. Extra columns like country / province go into meta.row_extras (the frontend reads on demand; not declared as schema columns).
Recommended chart: RankingCard / horizontal bar.
ddos/top-ip: top IPs with geo, recommend tables with country flag.
waf/geo (render_hint = geo)
| schema part | Content |
|---|---|
dimensions |
[{ name: "country", type: "geo" }] |
measures |
[{ name: "count", type: "integer", unit: "events" }] |
rows: [ { country: "China", count: 1234 }, { country: "US", count: 567 }, ... ].
Province data is temporarily stored in
meta.provinces(shape[{province, count}]). The frontendGeoHeatmapCardusesrowsfor the main map andmeta.provincesfor province-level drill-down; if province-level analytics needs its own chart later, add a dedicatedprotect/waf/geo-provinceschart-key.
Recommended chart: GeoHeatmapCard (CN / world map heatmap).
cc/geo: render_hint = geo, rows = [{country, count}, ...], map heatmap.
ddos/statistics: render_hint = time_series_multi, rows = [{time, events, bandwidth}, ...] tidy long table (events=integer events, bandwidth=float bytes/iec). Recommend dual Y-axis line.
6.7 AI page
AI page paths are public.
/logsuses an independent permissionanalytics.ai.logs; others useanalytics.ai.view.
Some charts return a BatchChartResult placeholder (rows: [],meta.available = false,meta.reasonexplains why). Paths and contracts are stable; the backend may enable real data later without changing paths.
| API | chart-key | Recommended chart | Description |
|---|---|---|---|
GET /api/analytics/ai/attack-trend |
attack-trend |
Line | AI attack trend |
GET /api/analytics/ai/top-ip |
top-ip |
Table / horizontal bar | Top AI hit IPs |
GET /api/analytics/ai/top-url |
top-url |
Table | Top AI hit URLs |
GET /api/analytics/ai/detection |
detection |
KPI cards / radar | AI detection capability panel |
GET /api/analytics/ai/test-results |
test-results |
Table / bar | AI test results |
GET /api/analytics/ai/logs |
logs |
Table (paged) | AI hit logs (independent permission analytics.ai.logs) |
Placeholder responses: not-yet-upgraded charts return a standard BatchChartResult (all 5 fields present,
rows: [],meta.available = false,meta.reasonexplains why); the frontend should render an "empty" placeholder card, not fake data.
6.8 Bot page
Use case: identify and analyze bot/crawler traffic.
| API | chart-key | Recommended chart | Description |
|---|---|---|---|
GET /api/analytics/bot/statistics |
statistics |
KPI (6) | Bot requests / sessions / IPs / known/unknown bot overview (trend line is served by an independent chart; not rendered until split) |
GET /api/analytics/bot/advance-warn |
advance-warn |
Table | Bot warning list |
GET /api/analytics/bot/browser |
browser |
Pie | Browser distribution (chrome/safari/firefox/edge/wechat/other) |
GET /api/analytics/bot/operating |
operating |
Pie | OS distribution (android/ios/windows/mac/other) |
GET /api/analytics/bot/geo |
geo |
Map heatmap | Bot geo |
GET /api/analytics/bot/top-agent |
top-agent |
Table / horizontal bar | Top User-Agents |
GET /api/analytics/bot/top-ip |
top-ip |
Table (with geo) | Top bot IPs |
GET /api/analytics/bot/scatter |
scatter |
Scatter | Bot warning scatter (X=ctime / Y=top_visit_count, size=top_ip_count) |
GET /api/analytics/bot/sessions |
sessions |
Table (paged) | Bot session list (independent permission analytics.bot.session) |
GET /api/analytics/bot/sessions/{sid} |
- | Timeline | Single bot session timeline detail (independent permission analytics.bot.session) |
chart-key data shapes
statistics: render_hint = kpi, rows = [{requests, sessions, ips, known_bot, unknown_bot, req_per_session}] (single-row summary, 6 measures). Recommend 6 KPI cards; requests / sessions trend is served by an independent chart (currently not split; add bot/sessions-trend when needed).
browser: render_hint = categorical_distribution, rows = [{browser, count}, ...] (6 enum rows: chrome/safari/firefox/edge/wechat/other), pie.
operating: render_hint = categorical_distribution, rows = [{os, count}, ...] (5 enum rows: android/ios/windows/mac/other), pie.
scatter: render_hint = table (no native scatter hint; table fallback), rows = [{time, session_id, top_visit_count, top_ip_count, top_ua_count}, ...]; the frontend interprets X / Y / size to render the scatter chart.
sessions/{sid}: render_hint = table, rows = [{time, uri, remote_addr, method, status}, ...] (5 trimmed fields), the access timeline of that session; returns empty when no session_id is passed (via the order query parameter).
6.9 Alert page
Use case: alert overview + list + detail + acknowledgment.
| API | Method | chart-key | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/alert/total |
GET | total |
KPI card | Total alerts |
GET /api/analytics/alert/hm |
GET | hm |
Line / heatmap | Alert trend (count aggregated by ctime) |
GET /api/analytics/alert/types |
GET | types |
Pie | Alert type distribution (by policy_type) |
GET /api/analytics/alert/domains |
GET | domains |
Horizontal bar / table | Alert domain ranking |
GET /api/analytics/alert/list |
GET | list |
Table (paged) | Alert list |
GET /api/analytics/alert/{id} |
GET | - | Detail card | Single alert detail (with closure metadata) |
PATCH /api/analytics/alert/{id}/ack |
PATCH | - | Not a chart | Acknowledge specified alert |
GET /api/analytics/alert/{id} output fields
| Field | Type | Description |
|---|---|---|
id |
int | Alert ID |
uuid |
string | Alert UUID |
policy_type |
string | Alert type |
title / body |
string | Alert title / body |
domain / domain_id |
string | Associated domain |
status |
int | Alert status (0/1/2/3, see D10) |
ctime |
int64 | Creation time |
last_update_timestamp |
int64 | Last update time |
process_uid |
string | Closer (D10 truth field) |
process_time |
int64 | Closure time (D10 truth field) |
user_id |
string | Owner |
Non-super-admin is filtered by
user_id; cross-tenant reads return 404. Permissionanalytics.alert.view.
PATCH /api/analytics/alert/{id}/ack request
Authentication: analytics.alert.ack
Input: path id; body may be empty.
Output: data is null.
6.10 Health (Phase 3)
Use case: business availability + origin quality + slow URI + geo quality.
| API | chart-key | Recommended chart | Description |
|---|---|---|---|
GET /api/analytics/health/summary |
summary |
KPI cards (6) | c2xx/c3xx/c4xx/c5xx/n4xx/n5xx totals |
GET /api/analytics/health/status-breakdown |
status-breakdown |
Stacked time-series bar | Three-layer c/n/a status code breakdown (c=client, n=gateway, a=app) |
GET /api/analytics/health/origin-errors |
origin-errors |
Table / horizontal bar | Origin error ranking (ES upstream_addr terms, only upstream_status >= 500) |
GET /api/analytics/health/origin-latency |
origin-latency |
Line (3 series) | Origin latency (mobile/unicom/telecom average) |
GET /api/analytics/health/slow-uri |
slow-uri |
Table | Slow URI Top (initially ranked by hit count; slow ranking will use real latency data after it is connected) |
GET /api/analytics/health/availability |
availability |
KPI / multi-line | HTTP/Ping/DNS/TCP/Page/IPv6 availability + downtime |
GET /api/analytics/health/geo-isp-quality |
geo-isp-quality |
Table / map | Geo / ISP quality |
Truth boundary: percentile / p50 / p95 / p99 fields exist in neither chart nor statistic packages; only windows ≤ 24h get real-time ES percentile (D4). The availability table
m_ava_domain.*AvailableDomainkeys ondomain_or_ip(notdomain_id).
6.11 Ops (Phase 5)
Use case: platform-level operational view — high-traffic / high-error users and domains, origin errors, node capacity.
Permission boundary:
analytics.ops.viewis read-only across the platform; does not allow passingtarget_user_idto switch view; onlyanalytics.ops.admincan. Regular tenant accounts cannot access ops even with view.
| API | chart-key | Recommended chart | Description |
|---|---|---|---|
GET /api/analytics/ops/summary |
summary |
KPI cards (6) | Platform capacity overview (requests/bytes/peak bandwidth/origin bandwidth/active domains/active users) |
GET /api/analytics/ops/traffic-users |
traffic-users |
Horizontal bar / table | Top traffic users (by total_bytes DESC) |
GET /api/analytics/ops/traffic-domains |
traffic-domains |
Horizontal bar / table | Top traffic domains |
GET /api/analytics/ops/error-users |
error-users |
Table | Top error users (by c5xx DESC, with c4xx/n5xx) |
GET /api/analytics/ops/error-domains |
error-domains |
Table | Top error domains |
GET /api/analytics/ops/origin-errors |
origin-errors |
Table | Origin error ranking |
GET /api/analytics/ops/nodes |
nodes |
Table / topology | Node / data-center view (RPC GetWafIpWithMachineRoom + ES server_addr/bind_addr) |
GET /api/analytics/ops/query-pressure |
query-pressure |
Line (placeholder) | ES query pressure (depends on instrumentation; returns available:false until metrics are connected) |
Truth boundary: node system metrics (CPU/memory/disk) charts do not exist; use external Zabbix. Not implemented this iteration. Fields
node_id/server_nodeare forbidden.
6.12 Closure (Phase 6)
Use case: handle alerts and risk queues on one page; bulk acknowledgment supported.
Truth fields (D10):
process_uid/process_time/status/level. Forbidden:handle_user/handle_time/risk_score/alert_status. AlertRecord.status (0/1/2/3) and RiskRecord.status (1/2) have different semantics — frontend i18n keys must not be shared.
| API | Method | chart-key | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/closure/summary |
GET | summary |
KPI cards (5) | Pending alerts/risks + handled today + average handle time (ms) |
GET /api/analytics/closure/alerts |
GET | alerts |
Table (paged) | Pending alert queue (status=0) |
GET /api/analytics/closure/risks |
GET | risks |
Table (paged) | Pending risk queue (connects to RiskRecord later; returns an empty list when no data is available) |
GET /api/analytics/closure/trend |
GET | trend |
Line / stacked bar | Closure history trend (group by ctime + status) |
POST /api/analytics/closure/alerts/confirm |
POST | - | Not a chart | Bulk acknowledge alerts (proxies /api/alert/records/confirm) |
POST /api/analytics/closure/risks/confirm |
POST | - | Not a chart | Bulk acknowledge risks (proxies /api/chart/risk/events/:event_id/confirm) |
summary output
{
"alerts_pending": 12,
"alerts_handled_today": 8,
"risks_pending": 0,
"risks_handled_today": 0,
"avg_handle_time_ms": 0
}
confirm request body
{
"ids": ["a1", "a2", "a3"],
"remark": "added to blacklist"
}
Authentication: analytics.closure.confirm
Output: data is null.
6.13 Cache (Phase 6)
Use case: analyze the value of CDN/edge cache in saving origin bandwidth.
Truth fields (D8):
total_cache_count/total_cache_bytes/total_cache_response_bytes. Forbidden: single-field namescache_count/cache_bytes/cache_hit(do not exist).
| API | chart-key | Recommended chart | Description |
|---|---|---|---|
GET /api/analytics/cache/summary |
summary |
KPI cards (4) | Hit rate / saved origin bytes / hit count / average cache object size |
GET /api/analytics/cache/trend |
trend |
Line (dual Y-axis) | Hit rate trend (request count vs cache hit count) |
GET /api/analytics/cache/top-uri |
top-uri |
Table / horizontal bar | URI Top (hit count / hit rate / saved bytes) |
GET /api/analytics/cache/content-types |
content-types |
Pie | Content type distribution (response_content_type aggregation) |
summary output
| Field | Type | Description |
|---|---|---|
hit_rate |
float | Cache hit rate = total_cache_count / request_count |
saved_response_bytes |
int | Saved origin bandwidth (bytes the origin would have served if not cached) |
total_cache_count |
int | Hit count |
total_cache_bytes |
int | Cached bytes |
avg_object_bytes |
float | Average cache object size = total_cache_bytes / total_cache_count |
request_count |
int | Total requests |
Business metrics:
- Cache hit rate =
total_cache_count / request_count - Saved origin bandwidth =
total_cache_response_bytes - Average cache object size =
total_cache_bytes / total_cache_count
6.14 Raw Logs (Phase 1)
Raw logs are detail queries, not single-chart endpoints; they share the same authentication, but requests/responses are list / detail / export.
| API | Method | Permission | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/logs |
GET | analytics.logs.view |
Table (paged + 12-field filters) | Paged query of raw access/attack logs |
GET /api/analytics/logs/{uuid} |
GET | analytics.logs.view |
Detail card (7 sections) | Single log detail (basic / request / response / upstream / protection / waf_detail / ai_detail) |
POST /api/analytics/logs/export |
POST | analytics.logs.export |
Not a chart | Field-whitelist export (csv/json, size ≤ 10000; larger goes async) |
GET /api/analytics/logs filter fields
12 filters: uuid / session_id / remote_addr / host / uri / method / status / z_final_action / z_final_type / z_final_mod / z_final_action_type / z_white.
Truth boundary:
z_final_actionis an integer enum0=pass / 1=block / 2=captcha;z_whiteis an independent bool (whitelist hit), not part of the action enum. Forbidden:match_content/match_area/hit_rule/rule_desc(exist in neither chart nor statistic packages).
Fallback response
The stub returns:
{ "available": false, "reason": "Raw log ES query pending..." }
The list / detail / export contracts are stable; ES zcloud-access-* will be connected without changing the public contract.
6.15 Reports (Phase 4)
The report center is for templates / generation / download; it does not use the single-chart response shape. List and detail still use the unified envelope; download returns a file stream.
| API | Method | Permission | Recommended chart | Description |
|---|---|---|---|---|
GET /api/analytics/reports/templates |
GET | analytics.reports.view |
Table / card grid | Template list (with platform_only flag) |
GET /api/analytics/reports |
GET | analytics.reports.view |
Table (paged) | Report history list |
GET /api/analytics/reports/{id} |
GET | analytics.reports.view |
Detail card | Single report detail (status, params, artifact URL) |
POST /api/analytics/reports/generate |
POST | analytics.reports.generate |
Not a chart | Trigger generation (platform-summary requires analytics.reports.platform) |
GET /api/analytics/reports/{id}/download |
GET | analytics.reports.download |
Not a chart | Download artifact (pdf/csv/json/html) |
Template enum (D7): protection-value / asset-risk / attack-source / business-health / platform-summary (platform ops / super-admin only) / raw-log-export.
Async threshold: estimated rows ≤ 100k goes synchronous; larger forces async with a task_id; the frontend polls /reports/:id for status + download URL. Sync generation times out after 30s and then degrades to async.
generate request body
{
"template": "protection-value",
"format": "pdf",
"window": "last_30d",
"stime": 1735660800000,
"etime": 1738339200000,
"filters": {}
}
| Field | Type | Required | Description |
|---|---|---|---|
template |
string | yes | Template name (D7 closed enum) |
format |
string | yes | pdf / csv / json / html |
window |
string | no | Time window alias |
stime / etime |
int64 | no | Custom timestamps |
filters |
object | no | Template-specific filters |
6.16 CLI Mapping
All Analytics APIs are wrapped by zcloud analytics commands:
# Original 6 pages
zcloud analytics overview kpi --format json
zcloud analytics access status --window last_24h --format json
zcloud analytics protect waf/types --format json
zcloud analytics ai logs --page 1 --size 20 --format json
zcloud analytics bot session <session-id> --format json
zcloud analytics alert ack <alert-id>
# 2026-04-30 chart-rebuild 6 phase extension (13 new commands)
zcloud analytics health summary --window last_24h --format json
zcloud analytics ops traffic-users --top 20 --format json
zcloud analytics closure summary --format json
zcloud analytics cache summary --format json
zcloud analytics logs list --window last_24h --status 403 --format json
zcloud analytics logs detail req-abc123 --format json
zcloud analytics logs export --format csv --fields ctime,uuid,host,uri,status > logs.csv
zcloud analytics closure alerts confirm --ids a1,a2,a3
zcloud analytics closure risks confirm --ids ev_001,ev_002
zcloud analytics reports templates --format json
zcloud analytics reports list --format json
zcloud analytics reports describe r-001 --format json
zcloud analytics reports generate --template protection-value --window last_30d --format pdf
zcloud analytics reports download r-001 --output report.pdf
When new Analytics APIs are added, the CLI mapping must be added or confirmed in sync; for new chart-keys, at least the CLI chart-key list and docs must be updated.
§7 Plan Catalog
External exposure scope: Only the two read-only endpoints below are exposed for external integration. Plan create/edit/delete, user assignment, subscription queries, and all order lifecycle operations (renew / change / refund / audit —
/api/plan/orders/*) are platform-console admin operations that write the live shared billing tables — they are NOT part of the external API/CLI surface (platform ops only, via console + RBAC).
GET /api/plan/plans — Plan list (paginated)
Query the plan catalog with optional product-type and keyword filters, paginated.
Required permission: plan.plan.list
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
int | No | 1 | Page number (1-based) |
size |
int | No | 20 | Page size |
prod_type |
int | No | 0 (all) | Product type filter: 2=WAF · 4=Monitor · 32=GFIP |
keyword |
string | No | — | Fuzzy match on plan name |
Response data shape
{
"list": [ { "plan_id": "...", "name": "Basic", "prod_type": 2, "price": 99.00, "valid": 365, "level": 1, "open_status": true, ... } ],
"total": 10,
"page": 1,
"size": 20
}
Examples
# Bearer Session
curl -H "Authorization: Bearer $TOKEN" \
"$API/api/plan/plans?prod_type=2&keyword=basic&page=1&size=20"
# API Key
curl -H "Authorization: ApiKey zck_prefix.secret" \
"$API/api/plan/plans?prod_type=2"
GET /api/plan/plans/{id} — Plan detail
Retrieve a single plan's full details by ID (including the content quota JSON).
Required permission: plan.plan.view
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Plan ID (UUID) |
Response data fields (PlanVO)
| Field | Type | Description |
|---|---|---|
plan_id |
string | Plan unique ID (UUID) |
name |
string | Plan name |
content |
object | Quota JSON (fields differ per prod_type) |
price |
float | Price (2 decimal places) |
scene |
string | Applicable scenario description |
comment |
string | Remark |
open_status |
bool | Whether publicly listed for purchase |
valid |
int64 | Validity period (days) |
effect |
int32 | Effect mode |
level |
int64 | Plan level |
creator_id |
string | Creator user ID |
ctime |
int64 | Creation time (Unix ms) |
utime |
int64 | Update time (Unix ms) |
version |
string | Plan source version (cloud / zmod) |
prod_type |
int32 | Product type (2=WAF 4=Monitor 32=GFIP) |
Example
curl -H "Authorization: Bearer $TOKEN" \
"$API/api/plan/plans/550e8400-e29b-41d4-a716-446655440000"
Equivalent CLI commands
zcloud plan list --prod-type 2 --keyword basic
zcloud plan describe <plan_id>
§8 Node Install / Upgrade
Purpose: one-line install / upgrade of skynet-node on a protection-node host. Two sides: the management plane (platform session + RBAC) registers packages, mints one-time commands, queries jobs, and revokes tokens; the installer side (install-token only) fetches the script, downloads package/env, and reports results.
Backend:src/backend/internal/node/{handler,service,repo}/install.go, routes insrc/backend/internal/node/route.go, reaper insrc/backend/internal/app/install_reaper.go.
8.0 Auth & status codes
| Dimension | Management endpoints | Installer-side endpoints |
|---|---|---|
| Paths | /install/artifacts /commands /upgrades /jobs /tokens/:id/revoke |
/install/script /package /env /report |
| Auth | Platform session (Bearer Session / API Key) + RBAC | Only Authorization: Bearer <install_token> |
RBAC action (perms.NodeNode) |
artifact / install / upgrade / job / revoke |
none (token self-authenticates) |
| Auth failure | 401 / 403 (platform envelope) | 401 with WWW-Authenticate: Bearer realm="node-install" (challenge) |
Key points:
- The install token is carried only in the
Authorizationheader and never in the URL query. - All four installer-side endpoints return
401+ challenge header on auth failure;/envreturns403when the token is valid but its env payload is no longer available;/reportreturns403whenjob_iddoes not match the token's bound job. - Cloud-owned tables are fixed as
node_install_*(node_install_artifacts/node_install_tokens/node_install_jobs/node_install_reports), living only inguard_localand never writing legacy tables.
8.1 POST /api/node/install/artifacts — register a local package + precheck
Registers a package already present on the backend host, scans it, computes sha256, and runs prechecks.
curl -sS -X POST https://<cloud>/api/node/install/artifacts \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"skynet-node-1.0.0.tar.gz","version":"1.0.0","package_path":"/data/artifacts/skynet-node-1.0.0.tar.gz"}'
Constraints:
package_pathmust be an absolute path on the backend host, ending in.tar.gz/.tgz, and located inside an allowed artifact root.- Default allowed roots are
/data/artifacts,/opt/cloud/artifacts(override via envNODE_INSTALL_ARTIFACT_ROOTS, comma-separated)./tmpis deliberately excluded — it is world-writable, so allowing it would let any local process drop a tarball and ride the register flow; tests needing a temp dir must setNODE_INSTALL_ARTIFACT_ROOTSexplicitly. - A failed blocking check (
severity=error) →status=failed;warning/infodo not block →status=ready. Blocking checks include:VERSIONSmanifest,env.confpresent without production markers, required env keys, required topic hints, complete spoasig-*rule references.binary_version_driftandpulsar_config_riskarewarning(non-blocking).
Response (excerpt — package_path returns the file name only, never the backend absolute path):
{"code":0,"data":{
"artifact_id":"8f1c…","name":"skynet-node-1.0.0.tar.gz","version":"1.0.0",
"sha256":"…64hex…","status":"ready",
"precheck":{"passed":true,"checks":[
{"key":"required_env","severity":"error","passed":true,"message":"required env keys found"},
{"key":"binary_version_drift","severity":"warning","passed":true,"message":"…"}
]}
}}
GET /api/node/install/artifacts lists registered packages; package_path there is also the file name only, and frontends/examples must not show the real backend absolute path.
8.2 POST /api/node/install/commands · POST /api/node/install/upgrades · POST /api/node/install/uninstalls — mint a one-time command
curl -sS -X POST https://<cloud>/api/node/install/commands \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"artifact_id":"8f1c…",
"node_id":"optional-target-node-UUID",
"server_url":"https://<cloud>",
"ttl_seconds":3600,
"max_uses":20,
"env":{
"ZCLOUD_NGX_ACCESS_TOPIC":"cloud/ngx",
"ZCLOUD_CC_TOPIC":"cc/sync",
"ZCLOUD_SYNC_TOPIC":"zcloud-sync",
"ZCLOUD_DELTA_TOPIC":"zcloud-delta",
"ZCLOUD_BLOCK_TOPIC":"zcloud-block"
}
}'
The upgrade command uses /upgrades, equivalent to action=upgrade, with the same body.
The uninstall command uses /uninstalls, equivalent to action=uninstall, with the same body (same as install/upgrade: artifact_id required, server_url required, env/ttl_seconds/max_uses optional). Like upgrade, it targets an existing node and node_id is required (omitting it → 400 "卸载必须指定目标节点"). The response is likewise a one-time command (curl … | sudo bash one-liner; the plaintext token appears only once). Permission: node.node.uninstall.
⚠️ Destructive, irreversible: the bootstrap decides which package script to run from the server-authoritative
X-Install-Actionresponse header returned byGET /api/node/install/package(its value is the action bound to the token's job — hereuninstall), running the package'suninstall.shinstead ofinstall.shand auto-answering its interactive[y/N]confirm via a here-string.uninstall.shstops and removes all of the node's on-host protection services (nginx / agent / waf-spoa, etc.) and their data directories; the operation cannot be undone.Scope: uninstall only removes the node's on-host services; it does not delete the node record from the cloud node list. To also drop the node record, the operator separately calls
DELETE /api/node/nodes/:id.
Response:
{"code":0,"data":{
"job_id":"…","token_id":"…","token_prefix":"nit_xxxxxxx",
"expires_at":1735900000000,
"command":"curl -fsSL --connect-timeout 10 --max-time 60 -H 'Authorization: Bearer nit_…' 'https://<cloud>/api/node/install/script' | sudo bash -s -- --token 'nit_…' --server 'https://<cloud>'"
}}
Constraints / behavior:
server_urlmust match the cloud's configured public base URL (scheme + host[:port]), otherwise 400 — this prevents a privileged caller from embedding an attacker-controlled host in the install command (the installer would then trust that host's package/env/report with the bearer token). Local-test exception: when configured to allow local, a loopbackserver_urlis accepted.server_urlmust be an absolute URL; https is enforced outside localhost; query / fragment are stripped.envis the registration-time node variables (env.conf text); empty falls back to the package's bundledenv.conf. Values are shell single-quote escaped before delivery.- env keys use a deny-list (not an allow-list): the package is variable-agnostic and all variables are supplied at registration by the upper layer, so arbitrary business keys are accepted (e.g.
REG_URL/AGENT_PORT/PULSAR_ADDR). A request is rejected (→ 400, no token / job created) only when a key matches sensitive patterns (PASSWORD/PASSWD/SECRET/PRIVATE/CRED/APIKEY/API_KEY/AUTH_TOKEN/ACCESS_TOKEN/ACCESS_KEY/SECRET_KEY/SIGNING_KEY/BOOTSTRAP_TOKEN, case-insensitive) or looks like an install token / Bearer credential. The env text is persisted tonode_install_jobs.env_payloadso/install/envcan be replayed after a backend restart / across replicas. AGENT_PORT(optional registration variable): when set, this install's agent listens on + registers with that port (default33020), to coexist with an existing agent on the same host. The port is injected by the cloud into the extracted installer scripts after the sha256 check, so the package file itself is left unmodified.ttl_seconds: default3600, cap86400(24h).max_uses: minimum 5 (values below 5 are raised to 5), default20, cap100. Keep retry headroom (downloading package + env consumes at least one each, and failed retries consume more) — do not set it at the minimum.artifact.status != ready→ 400, no token / job created.- The plaintext token appears exactly once, in
command; the DB stores only the tokensha256hash + a 12-charprefix.node_install_jobs.commandstores only a redacted template (<redacted>), never the plaintext token or Bearer header. job.speckeeps only whitelisted fields (server_url/env_sha256/env_keys/artifact/token_prefix);env_payloadis stripped from every job response.
8.3 Installer side: script / package / env / report
The script fetched & run by command (GET /install/script) does: set -euo pipefail + temp-dir cleanup on exit → download package & env with bounded --connect-timeout/--max-time/--retry → verify X-Artifact-SHA256 matches local sha256sum (mismatch → report failed and exit) → overwrite the package's env.conf with the cloud env → if the registration env contains AGENT_PORT, inject it into the extracted install*.sh (changing the agent listen/register port from the default 33020; only the extracted copy is patched, never the verified package) → run the package's install.sh → report running / success / failed via /report.
Token quota semantics (important):
| Endpoint | Auth | Consumes max_uses (use_count)? |
Counter / side effect |
|---|---|---|---|
GET /install/script |
validate token validity | No (ValidateBearerNoUse) |
no consumption; the script can be re-fetched |
GET /install/package |
validate + consume | Yes, use_count+1 |
headers X-Artifact-SHA256 / X-Install-Job-ID; job → running |
GET /install/env |
validate + consume | Yes, use_count+1 |
returns env text; 403 if env payload unavailable |
POST /install/report |
ValidateBearerForReport |
No | independent report_count+1; does not consume max_uses |
reportuses an independentreport_count; multiple installer reports (running→success/failed) never drainmax_uses.failed/runningreports do not close the token; onlysuccessmarks the tokenused(terminal, unusable afterward).- A
failedreport is accepted even on arevokedtoken, so an operator-revoked run can still settle its job tofailed. - If
reportincludesjob_id, it must match the token's bound job, otherwise 403. Once a job is terminal (success/failed), a late report does not overwrite the terminal state.
Manual installer-side testing:
TOKEN=nit_xxx; BASE=https://<cloud>
curl -fsSL -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/script"
curl -fsSL -D - -o pkg.tar.gz -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/package"
curl -fsSL -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/env"
curl -fsS -X POST "$BASE/api/node/install/report" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"status":"success","message":"done","hostname":"node-1","node_version":"1.0.0"}'
8.4 GET /api/node/install/jobs · /jobs/:id — query jobs
curl -sS -H "Authorization: Bearer $TOKEN" https://<cloud>/api/node/install/jobs # latest 50
curl -sS -H "Authorization: Bearer $TOKEN" https://<cloud>/api/node/install/jobs/<job_id> # includes reports[]
status: pending → running → success / failed. started_at / finished_at are millisecond timestamps, 0 meaning not yet occurred. The detail endpoint attaches recent reports (up to 20) and recent_report.
8.5 POST /api/node/install/tokens/:id/revoke — revoke a token
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
https://<cloud>/api/node/install/tokens/<token_id>/revoke
- Only an
activetoken can be revoked; alreadyused/revoked→ 400. - After revocation, installer-side script/package/env return 401 immediately; a
failedreport can still be submitted to settle the job. - Tokens have no separate
expiredstatus; expiry is derived fromexpires_at.status=activeonly means not-revoked / not-used — it is not equivalent to "still usable".
8.6 Stale-job reaper
A single-process background reaper (install_reaper.go) runs once at startup, then every 10m. It atomically marks jobs with ctime older than now - 24h (StaleJobMaxAge) still in pending/running as failed (finished_at=now, message="install job timed out without report"), and revokes any still-active tokens pointing at those jobs in the same transaction — preventing a killed installer from later reusing its bearer token to resurrect a closed job. Terminal jobs are never rewritten; across replicas the transactional WHERE clause keeps it idempotent (only the first process matches; others get RowsAffected=0 and exit silently).
8.7 Frontend display conventions
- The plaintext token in the minted command is shown only once; the UI must warn "shown once — copy and store it now".
- Lists/details show only the
token_prefix(e.g.nit_xxxxxxx) and status, never echoing the plaintext token. - Clear the clipboard after the command is copied (timed clear), so the token does not linger in the clipboard.
- Artifact lists must not show the real backend
package_pathabsolute path (the backend already returns the file name only).
8.8 POST /api/node/reg — node agent self-registration
Node agent self-registration endpoint. Public endpoint: no RBAC, no Bearer auth — same category as POST /api/node/install/report (agent infrastructure, no zcloud CLI command). The only access gate is the shared token dummy_token in the request body, which must equal the agent's hardcoded constant value; a mismatch is rejected.
Backend:
src/backend/internal/node/{handler,service}/reg.go, routesrc/backend/internal/node/route.go(rg.POST("/reg", h.RegNode)).
Request body fields (note manger_addr is the agent's existing misspelling, missing an a — it must not be renamed to manager_addr):
| Field | Type | Required | Description |
|---|---|---|---|
node_id |
string | no | Empty on a fresh install; non-empty means the agent already holds a node ID (an existing node is reused, not re-created) |
manger_addr |
string | yes | Management address host:port, auto-detected by the agent via ip route get and reported. Falls back to default port 33020 when the port is missing |
node_type |
string | no | Proto enum name string, e.g. "NODE_1_WAF". Only WAF defense nodes are supported (empty / "NODE_1_WAF" / "waf" / "1" all map to WAF; any other value returns a non-zero code) |
extend_config |
string | no | url-escaped extended config; not consumed by self-registration yet |
dummy_token |
string | yes | Shared token; must equal the agent's hardcoded constant value, otherwise rejected |
plugin |
string | no | Plugin list, e.g. "waf,detect,agent,ebpf" |
only_acl |
bool | no | ACL-only mode flag; self-registered nodes do not take this path |
ip |
string | no | only_acl companion parameter; empty for standard registration |
acl_tags |
string | no | only_acl companion parameter |
ip_groups |
string | no | only_acl companion parameter |
curl -sS -X POST https://<cloud>/api/node/reg \
-H 'Content-Type: application/json' \
-d '{
"node_id":"",
"manger_addr":"192.168.14.171:33020",
"node_type":"NODE_1_WAF",
"dummy_token":"<agent hardcoded shared token>",
"plugin":"waf,detect,agent,ebpf"
}'
Response: standard envelope {code, message, data}, where data is RegNodeResponse:
{"code":0,"message":"success","data":{
"node_id":"3f2c…",
"listen_addr":":33020",
"tls":false,
"cert":"",
"key":"",
"settings":{},
"plugin":{}
}}
| Response field | Type | Description |
|---|---|---|
node_id |
string | Node ID. Returns the existing ID when an existing node is matched idempotently; returns a newly created ID on first registration |
listen_addr |
string | Listen address, e.g. ":33020", taken from the management address port (falls back to default 33020) |
tls |
bool | Fixed false on the new (NSQ) platform (no longer distributing per-node certs via etcd) |
cert |
string | Empty string on the new platform |
key |
string | Empty string on the new platform |
settings |
object | Pushed config. The new platform distributes config over NSQ pub/sub, so this is always an empty map {} |
plugin |
object | Plugin config. Always an empty map {} on the new platform |
Semantics:
- Idempotent (upsert by management address): re-registering the same
manger_addrreturns the samenode_idand never creates a duplicate node; with old and new agents coexisting it preserves the existing node and does not overwrite its fields. - Fresh registration: creates a node row (
machine_roomdefaults todefault; ops can re-home it to a real machine room from the node list) and derives oneip_addrrow from the management IP — so the node auto-appears in the node list with both the management address and the business IP filled in. Theip_addrinsert is best-effort: a non-fatal error such as an already-used IP does not fail registration. - Failure handling: always returns HTTP
200; business errors are conveyed via a non-zerocodein the response body. A mismatcheddummy_token, a missing or invalidmanger_addr, or a non-WAFnode_typeall return a non-zerocodewith an error message.
§A Analytics Common Query Parameters
These apply to all single-chart GET endpoints (GET /api/analytics/<page>/<chart>). Defaults apply if not specified. Passing an unauthorized target_user_id or cross-OEM resource returns 403.
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
window |
string | no | last_1h / last_24h / last_7d |
Time window alias; default last_24h |
stime |
int64 | no | 1746748800000 |
Custom start time (Unix ms; paired with etime; takes precedence over window) |
etime |
int64 | no | 1746835200000 |
Custom end time (Unix ms) |
site_id |
string | no | site-001 |
Site filter |
domain_id |
string | no | d_8a3b1c |
Domain filter |
target_user_id |
string | no | u-tenant-001 |
Tenant-level switch viewed user; backend enforces cross-tenant authorization |
compare |
bool | no | false |
Enable previous-period comparison (only some charts support it) |
top |
int | no | 10 |
TopN, default 10, max 100 |
order |
string | no | bytes_desc |
Sorting; chart-defined (e.g. top-url supports request_count_desc/bytes_desc/cache_desc) |
page |
int | no | 1 |
Pagination for list-style charts |
size |
int | no | 20 |
Per-page count, max 100 |
Single-chart response skeleton:
{
"code": 0,
"message": "ok",
"data": {
"chart_key": "access/status",
"render_hint": "categorical_distribution_over_time",
"schema": {
"dimensions": [
{ "name": "status_class", "type": "enum", "values": ["2xx", "3xx", "4xx", "5xx"] },
{ "name": "time", "type": "timestamp", "unit": "ms" }
],
"measures": [
{ "name": "count", "type": "integer", "unit": "requests" }
]
},
"rows": [],
"meta": {
"cache": "miss",
"source": "postgres",
"latency_ms": 12,
"window": {
"stime": 1777526400000,
"etime": 1777530000000,
"granularity": "5m",
"bucket_table": "tfs_flow_domains"
}
}
}
}
window.granularityis a response field describing the actual aggregation granularity (5m / 1h / 1d), not a request parameter. The client passeswindow=last_24h, and the backend automatically picks the table.
§B Visualization Recommendations
B.1 Chart Unified Contract — render_hint lookup
All chart-keys are dispatched automatically by the frontend to one of 6 chart components based on render_hint. This is the contract truth (docs/specs/chart-contract.md §2) — no custom vocab words allowed.
render_hint |
schema shape | Recommended frontend component | Typical use |
|---|---|---|---|
kpi |
0~1 dim + 1+ measure | KpiGroupCard |
Multi-metric KPI card group (e.g. overview/kpi with 6 measures) |
categorical_distribution |
1 categorical dim + 1 measure | PieCard (≤8 cats) / BarCard (>8 cats) |
One-dimension share (e.g. overview/event-type) |
categorical_distribution_over_time |
1 categorical + 1 timestamp + 1 measure | StackedBarCard / LineCard multi-series |
Multi-class stacked over time (e.g. access/status) |
time_series_single |
1 timestamp + 1 measure | LineCard |
Single-measure time series (e.g. access/request-hm without compare) |
time_series_multi |
1 timestamp + ≥2 measures, or 1 categorical + 1 timestamp + 1 measure | LineCard multi-series |
Multi-measure time series (e.g. access/flow-hm with 5 measures); compare uses sub-shape B |
topn |
1 string dim + 1 measure | BarCard horizontal / RankingCard |
Pre-sorted TOP-N (e.g. protect/waf/top-ip) |
geo |
1 geo dim + 1 measure | GeoHeatmapCard |
Geographic distribution (e.g. protect/waf/geo) |
table |
any | TableCard |
Fallback for non-visualizable data |
Adding a new hint vocab word requires bilateral review (cloud + Aegeon); never extend the vocab unilaterally.
B.2 Data shape lookup
The table maps "output data shape → recommended chart" — handy as a quick lookup when wiring up the frontend. Actual responses still follow the Chart Unified Contract and are defined by schema + rows.
| Data shape | Typical fields | Recommended chart | Avoid |
|---|---|---|---|
| Single value (scalar) | {count: 12345} |
KPI card | Line / pie |
| Multi-KPI (4-6 scalars) | {domain_count, requests, blocked, block_rate, qps} |
KPI card grid / radar | Single pie |
| Time-series single | [{ctime, value}, ...] |
Line / area | Pie |
| Time-series multi | [{ctime, requests, attacks}, ...] |
Multi-line / stacked area | Pie |
| Time-series compare | {current:[...], previous:[...]} |
Two-line solid+dashed | Single line |
| Dimension distribution (≤ 8) | [{key, count}, ...] |
Pie / donut | Table |
| Dimension distribution (> 8) | [{key, count}, ...] |
Horizontal bar / column | Pie (fragmented) |
| Geo distribution | [{region, count}, ...] |
Map heatmap | Table |
| Top ranking | [{key, count, ...}, ...] |
Horizontal bar / table | Line / pie |
| 2D matrix | [[v11, v12], [v21, v22]] |
Heatmap | Line |
| Scatter | [{x, y, size, ...}, ...] |
Scatter / bubble | Pie |
| Paged list | {list, total, page, size} |
Table (paged) | Any chart |
| Timeline detail | {items: [{ctime, ...}]} |
Vertical timeline | Pie |
| Placeholder | {available: false, ...} |
Render n-empty placeholder, not a chart |
Fake data |
Color suggestions:
- Protection (attack/block): red palette (#ff4d4f / #ff7875)
- Traffic (request/bandwidth): blue palette (#2563eb / #60a5fa)
- Cache / AI (gain): green palette (#10b981 / #34d399)
- Neutral metrics: gray / purple palette (#6b7280 / #8b5cf6)
§C Related Documentation
- Authentication — Bearer session, API Key, and 401 retry
- Examples — three-language calling samples (curl / Python / Go)
- Permission Matrix — permission key per endpoint and OEM isolation rules
- CLI Tool — equivalent CLI commands
- Errors — full business error code table
- Quickstart — make your first call in 5 minutes
- /api/openapi.json — full OpenAPI v3 schema (machine-readable)
- /llms.txt / /llms-full.txt — AI agent quick-read entries
Full API Index
The complete REST set is exported as OpenAPI v3:
GET /api/openapi.json
Importable into Postman / Insomnia / Swagger UI. All routes include full schema, parameters, response samples, and required permission keys.
Cloud WAF · REST API Documentation · the source of truth is /api/openapi.json