Guides by plugin kind

Rule pack (rule_pack) guide

A rule pack is data, not code: write rules in the WAF / CC / ACL / challenge models the edge already runs; the control plane checks conflicts before publishing and the core engine interprets them.

← All guides

Quick start

  1. Finish onboarding on the seller site and register a publisher key (see the plugin signing page).
  2. On the seller site’s “My plugins”, create a plugin of kind rule_pack whose ID is “your namespace.name” (e.g. acme.dev.sqli-extra).
  3. Write the pack source rulepack.src.json as below: pack_id must be dev/<namespace>/<name>, layer developer, and version equal to the plugin version; then produce the canonical rulepack.json with the compiler in “Local debugging”.
  4. Put the SHA-256 of rulepack.json in the manifest’s spec.entrypoint.sha256, sign the manifest and upload the version.
{
  "schema": "cdnca.rule-pack/v1",
  "pack_id": "dev/acme.dev/sqli-extra",
  "version": "1.0.0",
  "layer": "developer",
  "engine": "waf_v2",
  "engine_min_version": 1,
  "name": "SQL 注入加强",
  "description": "参数 SQL 注入检测与后台路径拦截",
  "params_schema": {
    "action": {"type": "string", "enum": ["log", "block"], "bind": {"target": "waf.action", "rules": ["r010"]}}
  },
  "exports": ["r010", "r020"],
  "rules": [
    {"id": "r010", "name": "参数注入", "waf": {"match": {"inspect": [{"target": "args", "operator": "detect", "values": ["sqli"]}]}, "action": "block", "flow": "stop"}},
    {"id": "r020", "name": "后台路径", "waf": {"match": {"path": {"kind": "prefix", "value": "/wp-admin"}}, "action": "block", "flow": "stop"}}
  ],
  "defaults": {"action": "block"}
}

Signing and key registration are on the plugin signing page. Signing tool and steps →

Manifest example and fields

{
  "api_version": "marketplace.cdnca.io/v1",
  "kind": "Plugin",
  "metadata": {
    "id": "acme.dev.sqli-extra",
    "version": "1.0.0",
    "name": {"zh-CN": "SQL 注入加强", "en-US": "SQLi extra"},
    "description": {"zh-CN": "参数注入检测与后台路径拦截"},
    "publisher": {"id": "acme.dev", "name": "Acme", "support_url": "https://acme.example/support"}
  },
  "spec": {
    "categories": ["security_rules"],
    "compatibility": {"control_api": ">=1.0.35 <2.0.0"},
    "entrypoint": {"type": "rule_pack", "path": "rulepack.json", "sha256": "<rulepack.json 的 SHA-256>"},
    "permissions": ["security.rule_packs.provide"],
    "configuration_schema": {"path": "schemas/config.schema.json", "sha256": "<配置 Schema 的 SHA-256>"},
    "supply_chain": {"sbom_sha256": "<SBOM 的 SHA-256>", "provenance_sha256": "<构建来源证明的 SHA-256>"}
  }
}
FieldRequirement
spec.entrypoint.typeAlways rule_pack; a rule_pack plugin only uploads rule_pack versions
spec.entrypoint.pathRelative path of the pack file: no leading /, no .., not the configuration schema path and not manifest.json
spec.entrypoint.sha256Lower-case hex SHA-256 of the pack file (the uploaded content)
spec.permissionsExactly one: security.rule_packs.provide
spec.providesMust be absent
metadata.versionPlain major.minor.patch; data packages allow no pre-release or build suffix
spec.categoriesOne or more of dns_provider, notification_provider, payment_provider, analytics_exporter, automation_connector, security_rules, theme, ui_page
spec.supply_chainTwo SHA-256 values, sbom_sha256 and provenance_sha256; the licence service requires them when it syncs the catalog
integrityWritten by cdnca-plugin sign-manifest; do not edit by hand

Protocol and interfaces

Format cdnca.rule-pack/v1 with strict fields (unknown fields, duplicate keys and trailing data are rejected), at most 1 MiB.

FieldMeaning
engineOne of waf_v2, acl, cc_v2, challenge, modsecurity (one engine per pack; modsecurity rules select CRS rule sets)
engine_min_versionAt least what the rules need and at most what the compiler knows (1 for waf_v2 / acl / modsecurity, 6 for cc_v2 / challenge)
rules[]1–256 rules (128 for cc_v2 / challenge); ids match ^[a-z0-9][a-z0-9._-]{0,63}$, unique, run in id order; each rule has exactly the body of the pack’s engine (waf, cc or crs); the waf / cc body is the node rule model itself, and rule_id, priority, host and port are assigned per site by the compiler and stay empty in the pack
exportsRule ids a site may switch individually (at most 256)
params_schemaAt most 32 parameters named ^[a-z][a-z0-9_]{0,31}$; type integer / string / boolean with optional minimum, maximum, enum; bind.target names the rule field, empty bind.rules means every rule
defaultsDefault parameter values

Budget: one pack at most 256 rules, 64 regexes and 4096 cost units (with defaults and at every parameter boundary); all mounts of one site together at most 1024 rules, 256 regexes and 16384 units. A regex is at most 512 bytes with repeats up to 256.

A rule pack can only match and then block, challenge, rate-limit or ban; it cannot rewrite requests, origins or responses or compute anything. A site administrator mounts the pack in the control plane; user-layer exceptions run before developer-layer rules.

Packaging and upload fields

Form fieldRequiredContent and checks
manifestYesThe signed manifest, 1 byte to 256 KiB; it must name this plugin and namespace, and integrity.key_id must be an active key of the namespace whose signature verifies
configuration_schemaYesThe configuration JSON Schema file, 1 byte to 64 KiB; its SHA-256 must equal spec.configuration_schema.sha256
changelog_mdNoRelease notes in Markdown, at most 65536 bytes
channelNostable (default) or beta
auto_publishNotrue / false: publish automatically once approved
contentYesThe pack file, 1 byte to 8 MiB, SHA-256 equal to spec.entrypoint.sha256; it must be the compiler’s canonical bytes (see local debugging)
package:<platform>Must be absentA rule_pack version carries no executable package

Endpoint: POST /seller/v1/plugins/{id}/versions, Content-Type: multipart/form-data, with exactly one Idempotency-Key header of 1–128 characters; a retry with the same key returns the same result. The body limit defaults to 200 MiB. Any field name other than those in the table and the kind-specific ones returns 400 invalid_upload.

A plugin version number cannot repeat and must be higher than every reviewed version (approved, published, hidden or delisted). After upload the version is “scanning”; the seller site’s version detail shows the scan report.

Review rules

After upload the review worker runs the validating phase (manifest, entrypoint, permissions, the kind’s own checks); any reject stops it before scanning, then the scanning phase runs. Outcome: a reject → changes requested (fix it and press “Resubmit”, at most 10 scans per version); a quarantine → human review; neither → approved directly or sent to human review under the conditions below.

  • The first version (the plugin has no reviewed version yet)
  • Permissions differ from the last reviewed version (added or removed)
  • Privileged kinds: module, engine_module, provider
  • Sellers who joined less than 90 days ago (market default, adjustable by the operator)
  • Three scan attempts in a row errored

Any one of them sends the version to human review. The full rule table (rule ID, severity, reject or quarantine, evaluation mode) is on the review rules page.

  • rulepack.compile_failed / rulepack.budget_exceeded: does not compile or is over budget (reject).
  • rulepack.layer_invalid: pack_id is not dev/<your namespace>/<name> or layer is not developer (reject).
  • rulepack.version_mismatch: the pack version differs from the plugin version (reject).
  • rulepack.not_canonical: the upload is not the canonical bytes (reject).
  • rulepack.gate_failed: the pack fails the publication gate on its own (reject).
  • entrypoint.data_pack_permissions / entrypoint.data_pack_version / entrypoint.packages_present / entrypoint.content_mismatch: wrong permission, version, extra packages or file digest (reject).

Rules for this kind are in the review rule list: metadata.* entrypoint.* permissions.* rulepack.*

Versions and compatibility

metadata.version is canonical SemVer (no leading v); every upload must be higher than the reviewed versions, and a version number cannot be uploaded twice.

spec.compatibility.control_api is 1–8 space-separated comparators (>=, <=, =, >, < followed by canonical SemVer), at most 128 characters; all must hold. A control plane that does not satisfy it cannot install or upgrade to the version; an installed plugin keeps its last compatible version.

"compatibility": {"control_api": ">=1.0.35 <2.0.0"}

When a new version adds permissions, the customer’s administrator must confirm them again in the control plane; any version whose permissions change goes to human review.

The pack version must equal the plugin version. Nodes decide by engine_min_version whether they can run it; for rules that need JA4, IP intelligence, the CRS module or multipart inspection, the control plane warns before publishing when nodes lack the capability.

Local debugging

Review, the control plane and the edge use the same compiler, libs/rulepack. The canonical bytes are Compile’s Canonical output. From the repository root, compile your rulepack.src.json into the canonical rulepack.json:

mkdir -p libs/rulepack/cmd/rpc && cat > libs/rulepack/cmd/rpc/main.go <<'EOF'
package main

import (
	"fmt"
	"os"

	"github.com/cdnca/cdnca/libs/rulepack"
)

func main() {
	raw, _ := os.ReadFile(os.Args[1])
	c, err := rulepack.Compile(raw)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	os.WriteFile(os.Args[2], c.Canonical, 0o644)
	fmt.Println(c.SHA256, c.Cost.Rules, c.Cost.Regexes, c.Cost.Units)
}
EOF
GOWORK=off go -C libs/rulepack run ./cmd/rpc "$PWD/rulepack.src.json" "$PWD/rulepack.json"

It prints the canonical SHA-256 (put it in spec.entrypoint.sha256) followed by rule, regex and cost-unit counts. A compile error reads the same as the rulepack.compile_failed message in the scan report.