{"id":44868108,"url":"https://github.com/doctena-org/octorules","last_synced_at":"2026-07-01T16:01:25.079Z","repository":{"id":338966440,"uuid":"1159897817","full_name":"doctena-org/octorules","owner":"doctena-org","description":"Manage WAF rules across providers as code","archived":false,"fork":false,"pushed_at":"2026-06-08T09:49:18.000Z","size":1362,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T11:25:50.770Z","etag":null,"topics":["firewall","iac","octorules","security","waf","yaml"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doctena-org.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-17T09:50:58.000Z","updated_at":"2026-06-08T09:49:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/doctena-org/octorules","commit_stats":null,"previous_names":["doctena-org/octorules"],"tags_count":53,"template":false,"template_full_name":null,"purl":"pkg:github/doctena-org/octorules","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctena-org%2Foctorules","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctena-org%2Foctorules/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctena-org%2Foctorules/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctena-org%2Foctorules/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doctena-org","download_url":"https://codeload.github.com/doctena-org/octorules/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctena-org%2Foctorules/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35013184,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["firewall","iac","octorules","security","waf","yaml"],"created_at":"2026-02-17T12:00:29.905Z","updated_at":"2026-07-01T16:01:25.072Z","avatar_url":"https://github.com/doctena-org.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# octorules\n\n## WAF rules as code — manage rules across providers declaratively\n\nIn the vein of [infrastructure as code](https://en.wikipedia.org/wiki/Infrastructure_as_Code), octorules provides tools \u0026 patterns to manage WAF and security rules as YAML files. The resulting config can live in a repository and be deployed just like the rest of your code, maintaining a clear history and using your existing review \u0026 workflow.\n\n[octodns](https://github.com/octodns/octodns) manages DNS records, but can't touch WAF rules. **octorules** fills that gap — one YAML file per domain/policy, plan-before-apply, fail-fast on errors.\n\n### Provider ecosystem\n\noctorules is provider-agnostic. Each provider is a separate package:\n\n| Package | Provider | Status |\n|---|---|---|\n| [octorules-cloudflare](https://github.com/doctena-org/octorules-cloudflare) | Cloudflare Rules (23 phases) | Stable |\n| [octorules-aws](https://github.com/doctena-org/octorules-aws) | AWS WAF v2 (4 phases) | Beta |\n| [octorules-google](https://github.com/doctena-org/octorules-google) | Google Cloud Armor (4 phases) | Beta |\n| [octorules-azure](https://github.com/doctena-org/octorules-azure) | Azure WAF — Front Door \u0026 App Gateway (3 phases) | Alpha |\n| [octorules-bunny](https://github.com/doctena-org/octorules-bunny) | Bunny.net Shield WAF (4 phases) | Alpha |\n\n## Getting started\n\n### Installation\n\nInstall the provider package for your WAF. This pulls in octorules core automatically:\n\n```bash\npip install octorules-cloudflare    # Cloudflare (includes wirefilter expression engine)\npip install octorules-aws           # AWS WAF v2\npip install octorules-google        # Google Cloud Armor (includes cel-python)\npip install octorules-azure         # Azure WAF (Front Door / Application Gateway)\npip install octorules-bunny         # Bunny.net Shield WAF\n```\n\nCore only (offline lint, no provider):\n\n```bash\npip install octorules\n```\n\n### Configuration\n\nCreate a config file pointing at your zones:\n\n```yaml\n# config.yaml\nproviders:\n  cloudflare:\n    token: env/CLOUDFLARE_API_TOKEN\n  rules:\n    directory: ./rules\n\nzones:\n  example.com:\n    sources:\n      - rules\n```\n\nThe `env/` prefix resolves values from environment variables at runtime — keep secrets out of YAML. This is the built-in secret handler; see [Secret handlers](#secret-handlers) for pluggable backends (Vault, AWS Secrets Manager, etc.).\n\n\u003e **Examples** — the [`examples/`](examples/) directory here contains the multi-provider orchestration config (`config.yaml`) plus representative rules under `rules/multi/`. **Complete, per-phase single-provider examples live in each provider package's own `examples/` directory** — see [octorules-cloudflare](https://github.com/doctena-org/octorules-cloudflare/tree/main/examples), [octorules-aws](https://github.com/doctena-org/octorules-aws/tree/main/examples), [octorules-azure](https://github.com/doctena-org/octorules-azure/tree/main/examples), [octorules-google](https://github.com/doctena-org/octorules-google/tree/main/examples), and [octorules-bunny](https://github.com/doctena-org/octorules-bunny/tree/main/examples). Start there rather than writing config from scratch.\n\nAll keys under a provider section (except `class` and `safety`) are forwarded as keyword arguments to the provider constructor — octodns-style passthrough. See each provider's documentation for available settings.\n\n#### Multi-provider setup\n\nTo manage rules across multiple providers, add each provider as a named section under `providers:` and assign zones to providers via `targets:`. When only one provider is configured, `targets` is auto-assigned and can be omitted. See [`examples/config.yaml`](examples/config.yaml) for a full multi-provider config with all five providers.\n\n#### Multi-target zones\n\nA zone can target multiple providers of the **same class** (e.g. two Cloudflare accounts, or prod + staging). List multiple names under `targets:` — octorules plans and applies independently for each. Requires explicit `class:` since auto-discovery can't distinguish two instances of the same provider. See [`examples/config.yaml`](examples/config.yaml) for a commented example.\n\n#### Provider auto-discovery\n\nProvider classes are auto-discovered via the `octorules.providers` entry-point group (installed provider packages register themselves). To override auto-discovery, set `class:` explicitly:\n\n```yaml\nproviders:\n  custom:\n    class: my_package.MyProvider\n    api_key: env/MY_API_KEY\n```\n\n#### YAML includes\n\nYAML files support `!include` directives to split large configs:\n\n```yaml\nzones:\n  example.com: !include zones/example.yaml\n```\n\n```yaml\n# rules/example.com.yaml\nredirect_rules: !include shared/redirects.yaml\n```\n\nIncludes resolve relative to the file containing the directive. Nested includes and circular include detection are supported. Includes are confined to the directory tree of the parent file.\n\n### Defining rules\n\nCreate a rules file for each zone. The filename must match the zone name used as the key under `zones:` in `config.yaml`, which maps to the provider's own concept of a \"zone\":\n\n| Provider | Zone concept | Example zone name | Rules file |\n|---|---|---|---|\n| Cloudflare | DNS domain | `example.com` | `\u003crules-dir\u003e/example.com.yaml` |\n| AWS WAF | Web ACL name | `my-web-acl` | `\u003crules-dir\u003e/my-web-acl.yaml` |\n| Google Cloud Armor | Security policy name | `my-security-policy` | `\u003crules-dir\u003e/my-security-policy.yaml` |\n| Azure WAF | WAF policy name | `my-waf-policy` | `\u003crules-dir\u003e/my-waf-policy.yaml` |\n| Bunny Shield | Pull zone name | `my-pull-zone` | `\u003crules-dir\u003e/my-pull-zone.yaml` |\n\nThe mapping is: `zones.\u003cname\u003e` in `config.yaml` → `\u003crules-dir\u003e/\u003cname\u003e.yaml` on disk → `resolve_zone_id(\"\u003cname\u003e\")` at runtime, which resolves the name to the provider's internal ID. `\u003crules-dir\u003e` defaults to `./rules/` and is set under `providers.rules.directory` — point it wherever each customer's rules live (the [`examples/`](examples/) directory here ships `rules/multi/` for the multi-provider `config.yaml`; each provider package ships its own single-provider example under its `examples/` directory).\n\nEach rule requires a **`ref`** (stable identifier, unique within a phase) and an **`expression`** (provider-specific filter expression). Optional fields include `description`, `enabled` (defaults to `true`), `action`, and `action_parameters`. Phase names, available actions, and expression syntax are provider-specific — see your provider's documentation and that provider package's own `examples/` directory for complete per-provider examples.\n\noctorules lints each rule file only against the plugin matching its zone's target provider. With per-provider lint plugins installed in the same venv (e.g. both `octorules-cloudflare` and `octorules-aws`), a Cloudflare zone file never gets validated against the AWS schema and vice versa — eliminating cross-provider false positives on same-named blocks (`custom_rulesets`, `lists`).\n\n#### Rule-level metadata\n\nRules support an `octorules:` key for per-rule metadata that controls octorules behavior without affecting the provider API.\n\n**Ignoring rules** — keep a rule in YAML (for documentation, version control, review) while skipping it during plan/sync:\n\n```yaml\nwaf_custom_rules:\n  - ref: experimental-geo-block\n    description: \"Testing geo-block — not ready for production\"\n    expression: 'ip.geoip.country in {\"RU\" \"CN\"}'\n    action: block\n    octorules:\n      ignored: true\n```\n\nIgnored rules are still validated and linted (catch errors before un-ignoring), but are invisible to the planner on both sides — they produce no ADD/MODIFY/REMOVE changes, and if the rule exists upstream it will not be deleted or overwritten. This matches the octodns convention: the rule can be edited manually on the provider without octorules interfering.\n\n**Targeting providers** — in multi-provider or multi-target setups, restrict a rule to specific targets:\n\n```yaml\nwaf_custom_rules:\n  # Only deploy to Cloudflare\n  - ref: cf-specific-rule\n    expression: 'http.request.uri.path matches \"^/api/.*\"'\n    action: block\n    octorules:\n      included:\n        - cloudflare\n\n  # Deploy everywhere EXCEPT staging\n  - ref: prod-only-rule\n    expression: 'ip.src in $blocklist'\n    action: block\n    octorules:\n      excluded:\n        - cf-staging\n```\n\n`included` and `excluded` are mutually exclusive (matching octodns convention). Names match the provider config key (e.g. `cloudflare`, `aws`, `cf-prod`). Rules without `included`/`excluded` apply to all targets.\n\nThe `octorules:` key is always stripped before sending rules to the provider API.\n\n#### Multi-line expressions\n\nComplex expressions can use YAML block scalars (`|-`) for readability. octorules normalizes whitespace (collapsing newlines and indentation to single spaces outside quoted strings) before sending to the provider and before linting, so formatting is purely cosmetic:\n\n```yaml\nwaf_custom_rules:\n  - ref: geo-block\n    description: Block by country outside active regions\n    action: block\n    expression: |-\n      (ip.geoip.asnum in {\n        9009\n        64080\n      } and not ip.geoip.country in {\n        \"AT\"\n        \"BE\"\n        \"DE\"\n        \"FR\"\n      })\n```\n\nUse `|-` (strip trailing newline) rather than `|` (preserves trailing newline).\n\n### Usage\n\noctorules uses separate commands for planning and applying — like Terraform's\n`plan`/`apply` split.  WAF rules have a high blast radius (a bad rule can\nblock all traffic), so the two-step workflow forces an explicit review before\nchanges reach the provider.  This also enables CI patterns where `plan` runs\non PR open (posting results as a PR comment) and `sync` runs on merge with\nchecksum verification to catch drift.\n\n```bash\n# Preview changes (dry-run)\noctorules plan --config config.yaml\n\n# Apply changes\noctorules sync --doit --config config.yaml\n\n# Validate config only (no API calls, useful in CI)\noctorules lint --config-only --config config.yaml\n\n# Export existing rules to YAML\noctorules dump --config config.yaml\n\n# Lint rules files offline\noctorules lint --config config.yaml\n\n# Audit for IP overlaps, CDN conflicts, and zone drift\noctorules audit --config config.yaml\n```\n\n## Secret handlers\n\nConfig string values use `handler/reference` syntax to resolve secrets at load time. The built-in `env` handler resolves environment variables (`env/MY_TOKEN` → `$MY_TOKEN`). You can add custom handlers for Vault, AWS Secrets Manager, GCP Secret Manager, etc.\n\n### Config-declared handlers\n\n```yaml\nsecret_handlers:\n  vault:\n    class: octorules_vault.VaultSecrets\n    url: https://vault.internal\n    token: env/VAULT_TOKEN           # bootstrap: resolved via env handler\n\nproviders:\n  cloudflare:\n    token: vault/secret/data/cf#token  # resolved via vault handler\n```\n\nHandler kwargs are resolved through already-registered handlers (env + entry-points), so you can bootstrap credentials with `env/`.\n\n### Entry-point discovery\n\nSecret handlers can also be auto-discovered via the `octorules.secret_handlers` entry-point group:\n\n```toml\n# In your handler package's pyproject.toml\n[project.entry-points.\"octorules.secret_handlers\"]\nvault = \"octorules_vault:VaultSecrets\"\n```\n\n### Writing a secret handler\n\nSubclass `BaseSecrets` from `octorules.secret`:\n\n```python\nfrom octorules.secret import BaseSecrets, SecretsException\n\nclass VaultSecrets(BaseSecrets):\n    def __init__(self, name, url=\"\", token=\"\"):\n        super().__init__(name)\n        self.client = VaultClient(url=url, token=token)\n\n    def fetch(self, ref, source):\n        try:\n            return self.client.read(ref)\n        except VaultError as e:\n            raise SecretsException(f\"Vault lookup failed for {ref!r}: {e}\")\n```\n\n### Resolution rules\n\n1. Split string on first `/` → `(prefix, reference)`\n2. Look up `prefix` in the handler registry\n3. Found → call `handler.fetch(reference, source_context)`\n4. Not found → return string unchanged (paths like `./rules` or `https://...` pass through safely)\n\n## Processors\n\nProcessors hook into the plan/sync pipeline to transform rules before planning and filter changes after planning. They're useful for injecting shared rules, enforcing policy, or suppressing changes across zones.\n\n```yaml\nprocessors:\n  add_standard_headers:\n    class: my_package.StandardHeaderProcessor\n    header_name: X-Frame-Options\n\nzones:\n  example.com:\n    sources:\n      - rules\n    processors:\n      - add_standard_headers\n```\n\nA processor is a Python class with two optional hooks:\n\n- **`process_desired(zone_name, desired, provider)`** — transform the desired rules dict before planning. Return the modified dict.\n- **`process_changes(zone_name, plan, provider)`** — transform the ZonePlan after planning. Return the modified plan.\n\nBoth default to no-op (pass-through). Processors run in the order listed. The `class` key is required; all other keys are forwarded as kwargs.\n\n### Built-in filters\n\noctorules ships four ready-to-use processors in `octorules.processor.filters`:\n\n| Filter | Description |\n|--------|-------------|\n| **PhaseFilter** | Include or exclude phases by name (`include`/`exclude` lists) |\n| **RefFilter** | Include or exclude rules by regex on the `ref` field |\n| **ChangeTypeFilter** | Block specific change types: `ADD`, `REMOVE`, `MODIFY`, `REORDER` |\n| **PreserveFilter** | Protect rules whose `ref` matches a regex from `REMOVE`/`REORDER` changes |\n\n`allow_unmanaged` is all-or-nothing: off, every live rule absent from your YAML\nis planned for removal; on, nothing unmanaged is ever removed. **PreserveFilter**\nis the scoped middle ground — keep `allow_unmanaged` off so genuine drift is\nstill cleaned up, but spare rules whose `ref` matches a pattern (e.g. rules a\nsecurity vendor or another team injects out-of-band) from deletion and\nreordering. It reads only `ref` and `change_type`, so it works across every\nprovider.\n\nSee [`examples/config.yaml`](examples/config.yaml) for working examples of all four.\n\n## Zone discovery\n\nZones can be discovered automatically from providers that support it. Use `'*'` as a zone template in your config — octorules calls `list_zones()` on target providers at init time and expands the template for each discovered zone that has a matching YAML rules file. Explicit zone configs always take precedence. See the wildcard entry in [`examples/config.yaml`](examples/config.yaml).\n\n## Optional features\n\nProviders declare optional feature support via a `SUPPORTS` class variable. The framework checks support before calling optional methods. Features include:\n\n| Feature | Description | Providers |\n|---------|-------------|-----------|\n| `custom_rulesets` | Account-level WAF rulesets (rule groups) | Cloudflare, AWS |\n| `lists` | IP/ASN/hostname/redirect/regex lists (IP sets, regex pattern sets) | Cloudflare, AWS |\n| `page_shield` | Content Security Policy management | Cloudflare |\n| `zone_discovery` | Automatic zone enumeration via `list_zones()` | Cloudflare, AWS, Google, Azure, Bunny |\n\nSee each provider's documentation for feature details and YAML syntax.\n\n## Linting\n\n`octorules lint` runs offline static analysis on your rules files — no API calls, no credentials needed. Lint rules are provider-registered; install a provider package to get its rules. See [`octorules lint`](#octorules-lint) in the CLI reference for flags and options.\n\nSuppression comments work like shellcheck — add `# octorules:disable=CF015` (comma-separated for multiple rules) before a rule to suppress specific findings. Audit findings use `# octorules:accept=ip-overlap`.\n\n### Core lint rules\n\n| Rule | Severity | Description |\n|------|----------|-------------|\n| CORE002 | WARNING | Orphaned rules file (no matching zone in config) |\n| CORE003 | WARNING | All rules in a phase are disabled (2+ rules, all `enabled: false`) |\n| CORE004 | WARNING | Same `ref` string used in multiple phases within a zone |\n| CORE006 | INFO | Rules file contains no actual rules (all phases empty) |\n\nProvider-specific rules (CF, WA, GA, AZ, BN prefixes) are documented in each provider's `docs/lint.md`.\n\n### Config validation (not lint rules)\n\nThese checks run at config load time and emit log warnings or raise errors. They are **not** lint diagnostics — they cannot be suppressed with `# octorules:disable=...` and do not appear in lint output.\n\n| Check | Level | Description |\n|-------|-------|-------------|\n| Duplicate YAML key | ERROR | Raises `ConfigError` on duplicate keys (silent data loss — last value wins) |\n| Inverted safety thresholds | WARNING | Logs a warning when `delete_threshold` \u003c `update_threshold` (deletes less restricted than updates) |\n\n## CLI reference\n\n### `octorules plan`\n\nDry-run: shows what would change without touching the provider. Exit code 2 when changes are detected (with `--exit-code`). Output format and destination are controlled via `manager.plan_outputs` in the config file (defaults to text on stdout).\n\n```bash\noctorules plan [--zone example.com] [--phase redirect_rules] [--checksum] [--exit-code]\n```\n\n### `octorules sync --doit`\n\nApplies changes to the provider. Requires `--doit` as a safety flag. Atomic PUT per phase, fail-fast on errors.\n\n```bash\noctorules sync --doit [--zone example.com] [--phase redirect_rules] [--checksum HASH] [--force]\n```\n\n| Flag | Description |\n|------|-------------|\n| `--doit` | Required safety flag to confirm changes should be applied |\n| `--checksum HASH` | Verify plan hasn't drifted since `plan --checksum` |\n| `--force` | Bypass safety threshold checks |\n| `--audit-log PATH` | Write JSON lines audit log of sync results |\n| `--format json` | Print structured JSON results to stdout (zone, status, synced phases, errors) |\n\n### `octorules rule`\n\nBrowse and search the lint rule catalog.\n\n```bash\noctorules rule --all                  # List all rules\noctorules rule CF                     # Filter by prefix\noctorules rule CF201                  # Show one rule\noctorules rule --all --format json    # JSON output\n```\n\n### `octorules dump`\n\nExports existing provider rules to YAML files. Useful for bootstrapping or importing an existing setup.\n\n```bash\noctorules dump [--zone example.com] [--output-dir ./rules]\n```\n\n### `octorules lint`\n\nLint rules files offline for errors, warnings, and style issues. Supports text, JSON, SARIF, and summary output.\n\n```bash\noctorules lint [FILE] [--config-only] [--format text|json|sarif] [--severity error|warning|info] [--plan free|pro|business|enterprise] [--rule RULE_ID] [--output PATH] [--exit-code]\n```\n\n| Flag | Description |\n|------|-------------|\n| `FILE` | Lint a single rules file (no config needed). When omitted, uses the config file to discover all zones |\n| `--config-only` | Only validate config file structure (skip rules files) |\n| `--format` | Output format: `text` (default), `json`, `sarif`, `summary` |\n| `--severity` | Minimum severity to report (default: `info`) |\n| `--plan` | Plan tier for entitlement checks (default: `enterprise`) |\n\nWhen `--plan` is not specified, `lint` reads `.zone_plans_cache.json` (written\nautomatically by `plan`, `sync`, and `dump`) for automatic per-zone tier\ndetection. If neither `--plan` nor the cache provides a tier, `enterprise` is\nassumed (most permissive, fewest false positives). Add `.zone_plans_cache.json`\nto your `.gitignore` — it contains no secrets, just zone-to-tier mappings.\n\n| `--rule` | Only check specific rule ID(s); can be repeated |\n| `--output` | Write results to a file instead of stdout |\n| `--exit-code` | Exit with 1 on errors, 2 on warnings (for CI) |\n\n### `octorules audit`\n\nAudit rules for cross-rule IP overlaps, shadowed rules, CDN range conflicts, and cross-zone inconsistencies. Processes every `*.yaml` file in the rules directory (not just configured zones). No API credentials needed.\n\n```bash\noctorules audit [--check ...] [--severity error|warning|info] [--format text|json] [--output FILE] [--exit-code] [--cdn-timeout N] [--cdn-stale-days N]\n```\n\n| Flag | Description |\n|------|-------------|\n| `--check` | Only run specific check(s); can be repeated (default: all) |\n| `--severity` | Minimum severity to report (default: `info`) |\n| `--format` | Output format: `text` (default), `json`, `summary` |\n| `--output` | Write results to a file instead of stdout |\n| `--exit-code` | Exit with 1 on errors, 2 on warnings (for CI) |\n| `--cdn-timeout` | Timeout in seconds for CDN range API fetches (default: 15) |\n| `--cdn-stale-days` | Warn if baked-in CDN ranges are older than N days (default: 60) |\n\n**Checks:**\n\n- **ip-overlap** -- Cross-rule and cross-list IP range overlaps within a zone.\n- **ip-shadow** -- Rules shadowed by broader rules in earlier phases (e.g. a rate-limit rule whose IPs are already blocked by a WAF rule).\n- **cdn-ranges** -- Rules that match known CDN provider IP ranges (Cloudflare, AWS CloudFront, Google Cloud, Bunny, Azure Front Door). Fetches fresh ranges from public APIs; falls back to baked-in data when offline. (Azure's list is scraped from the Microsoft Download Center page — the JSON URL rotates weekly.)\n- **zone-drift** -- Same CIDR treated differently across zones (e.g. blocked in zone A, allowed in zone B).\n\nAcceptance comments suppress known findings (check names must be lowercase).\nLike the `disable=` lint directives, they are positional: a comment attaches to\nthe rule anchor (a `ref:` or `description:` line) that follows it, scoping the\nacceptance to that rule. A comment placed before any rule (e.g. at the top of\nthe file) is file-wide:\n\n```yaml\n# octorules:accept=zone-drift          # file-wide (before any rule)\n\nwaf_custom_rules:\n  # octorules:accept=ip-overlap,cdn-ranges   # scoped to the rule below\n  - ref: r1\n    expression: ...\n```\n\nTo scope an acceptance to a finding on a **list** (whose finding ref is\n`list:\u003cname\u003e`), place the comment above the list's `name:` in the list's own\nfile — `!include`d files are followed:\n\n```yaml\n# custom_lists/block_known_attackers.yaml\n# octorules:accept=cdn-ranges\nname: block_known_attackers\nkind: ip\nitems: [...]\n```\n\n### `octorules versions`\n\nPrint versions of octorules and key dependencies.\n\n```bash\noctorules versions\n```\n\n### Common flags\n\n| Flag | Description |\n|------|-------------|\n| `--config PATH` | Path to config file (default: `config.yaml`) |\n| `--zone NAME` | Process only specified zone(s); can be repeated (default: all) |\n| `--phase NAME` | Limit to specific phase(s); can be repeated |\n| `--scope SCOPE` | Scope: `all` (default), `zones`, or `account` |\n| `--debug` | Enable debug logging |\n| `--quiet` | Suppress all informational stdout output (plan tables, lint results, audit findings). Only errors and the exit code are reported. File output (`--output`) is unaffected |\n| `--syslog ADDRESS` | Send logs to syslog (`host:port` for UDP, or `/path/to/socket`) |\n\n### Environment variables\n\n| Variable | Effect |\n|----------|--------|\n| `NO_COLOR` | Disable colored terminal output (any value, including empty) |\n| `FORCE_COLOR` | Force colored output even when stdout is not a TTY |\n\n`NO_COLOR` takes precedence over `FORCE_COLOR`. See https://no-color.org/.\n\n### Exit codes\n\n| Code | Meaning |\n|------|---------|\n| 0 | Success / no changes |\n| 1 | Error (or lint errors found with `--exit-code`) |\n| 2 | Changes detected (`plan --exit-code`) / lint warnings found (`lint --exit-code`) |\n\nAfter every command, a summary line is printed to stderr:\n```\noctorules plan: exit 0 (no changes) 0.3s\n```\n\n### Tab completion\n\nTab completion is built in (bash, zsh, tcsh). Generate the completion\nscript once and place it in the standard location:\n\n```bash\n# Bash\noctorules completion bash \u003e ~/.local/share/bash-completion/completions/octorules\n\n# Zsh (add fpath+=~/.zfunc before compinit in .zshrc)\noctorules completion zsh \u003e ~/.zfunc/_octorules\n```\n\nRegenerate after upgrading octorules (new subcommands/flags).\n\n## Config reference\n\n```yaml\nsecret_handlers:                     # Optional — custom secret backends\n  vault:\n    class: octorules_vault.VaultSecrets  # Required: dotted class path\n    url: https://vault.internal          # All other keys forwarded as kwargs\n    token: env/VAULT_TOKEN               # Handler kwargs resolved via env + entry-points\n\nproviders:\n  my_provider:                       # Provider name (any name works)\n    token: env/API_TOKEN             # All keys forwarded to provider constructor\n    class: my_package.MyProvider     # Optional: override auto-discovered provider\n    safety:                          # Framework-owned (NOT forwarded to provider)\n      delete_threshold: 30.0         # Max % of rules that can be deleted (default: 30)\n      update_threshold: 30.0         # Max % of rules that can be updated (default: 30)\n      min_existing: 3                # Min rules before thresholds apply (default: 3)\n  rules:\n    directory: ./rules               # Path to rules directory\n  lists:\n    directory: ./rules/custom_lists  # Path for externalized list items (default: {rules_dir}/custom_lists)\n\nprocessors:\n  my_proc:\n    class: my_package.MyProcessor    # Required: dotted class path\n    setting: value                   # All other keys forwarded as kwargs\n\nmanager:\n  max_workers: 4                     # Parallel processing (default: 1)\n  plan_outputs:                      # Config-driven plan output\n    text:\n      class: octorules.plan_output.PlanText\n    html:\n      class: octorules.plan_output.PlanHtml\n      path: /tmp/plan.html           # Optional: write to file instead of stdout\n\nzones:\n  example.com:\n    sources:\n      - rules\n    targets:\n      - my_provider\n    processors:\n      - my_proc\n    allow_unmanaged: false           # Keep rules not in YAML (default: false)\n    always_dry_run: true             # Never apply changes (default: false)\n    safety:                          # Per-zone overrides\n      delete_threshold: 50.0\n\n  '*':                               # Zone discovery template\n    sources:\n      - rules\n    targets:\n      - my_provider\n```\n\n## Programmatic usage\n\nThe `Manager` class provides a Python API for all octorules operations:\n\n```python\nfrom octorules import Manager\n\nwith Manager(\"config.yaml\") as mgr:\n    # Preview changes (returns exit code)\n    rc = mgr.plan(exit_code=True)\n\n    # Apply changes\n    mgr.sync(force=True)\n\n    # Lint specific zones\n    mgr.lint(zones=[\"example.com\"], severity=\"warning\")\n\n    # Export rules\n    mgr.dump(output_dir=\"/tmp/rules\")\n```\n\nAll methods accept the same options as the CLI (`zones`, `phases`, `scope`, etc.) and return the same exit codes. The Manager handles provider/processor initialization and executor lifecycle.\n\n## How it works\n\n1. **Plan** — Reads your YAML rules, fetches current rules from the provider, computes a diff by matching rules on `ref` (phases), `name` (lists), or `description` (policies). Processors transform desired rules before diffing and filter changes after.\n2. **Sync** — Executes the plan in order: lists, policies, custom rulesets, then phases. Each phase uses an atomic PUT (full replacement of the phase ruleset). Fail-fast on errors.\n3. **Dump** — Fetches all rules from the provider and writes them to YAML files, stripping API-only fields (`id`, `version`, `last_updated`, etc.).\n\nPerformance (all parallelism controlled via `manager.max_workers`, default: 1):\n- **Parallel phase fetching** — phases within each scope are fetched concurrently.\n- **Parallel phase apply** — phase PUTs within a zone are applied concurrently during sync.\n- **Parallel apply stages** — list item updates, custom ruleset PUTs, and policy operations within each stage run concurrently.\n- **Parallel zone processing** — multiple zones are planned/synced concurrently.\n- **Parallel zone ID resolution** — zone name lookups run concurrently.\n- **Concurrent account planning** — account-level rules are planned in parallel with zone rules.\n- **Scope-aware phase filtering** — only zone-level phases are fetched for zone scopes, and only account-level phases for account scopes, eliminating wasted API calls.\n- **Rules caching** — YAML rule files are parsed once and cached for the duration of each run.\n\nSafety features:\n- **`--doit` flag** — sync requires explicit confirmation.\n- **Delete thresholds** — blocks mass deletions above a configurable percentage.\n- **Checksum verification** — `plan --checksum` produces a hash; `sync --checksum HASH` verifies the plan hasn't changed.\n- **Auth error propagation** — authentication and permission errors fail immediately instead of being silently swallowed.\n- **Failed phase filtering** — phases that can't be fetched are excluded from planning to prevent accidental mass deletions.\n- **Path traversal protection** — `!include` directives and file operations are confined to their expected directories.\n\n#### How safety thresholds work\n\nSafety thresholds prevent accidental mass changes. When a plan would delete or update more than a configurable percentage of existing rules in any phase, the sync is blocked. Defaults: `delete_threshold: 30.0`, `update_threshold: 30.0`, `min_existing: 3` (thresholds only apply once a phase has at least this many rules). Can be set per-provider or per-zone — see [Config reference](#config-reference). Override with `--force`.\n\n### Troubleshooting\n\n| Error | Cause | Recovery |\n|-------|-------|----------|\n| `ProviderAuthError` | Invalid or expired API token | Check token permissions and expiry |\n| `delete_threshold exceeded` | Plan would delete too many rules | Review the plan; raise `delete_threshold` or use `--force` |\n| `HTTP 429 Too Many Requests` | Provider API rate limit hit | Wait and retry; reduce `max_workers` |\n| Partial zone failure | One zone failed, others succeeded | Re-run for the failed zone only (`--zone \u003cname\u003e`) |\n| `Checksum mismatch` | State changed between plan and sync | Re-run `plan` to get a fresh checksum |\n| `No rules file for zone` | Zone configured but YAML file missing | Create `rules/\u003czone\u003e.yaml` or remove zone from config |\n\n## Writing a provider\n\nA provider is a Python package that:\n\n1. **Implements `BaseProvider`** — the `@runtime_checkable` Protocol in `octorules.provider.base` defining 19 methods + 4 properties.\n2. **Declares `SUPPORTS`** — a `frozenset[str]` of optional features (`custom_rulesets`, `lists`, `page_shield`, `zone_discovery`).\n3. **Registers phases** — calls `register_phases()` at import time with the provider's phase definitions. Each `Phase` can include a `prepare_rule` callable for provider-specific rule preparation (expression normalization, default fields, action injection). The core planner calls this hook — it contains no provider-specific logic itself.\n4. **Registers a linter plugin** — optional; provides provider-specific lint rules. Linters should only check their own phases (not phases owned by other providers).\n5. **Declares an entry point** — in `pyproject.toml`:\n\n```toml\n[project.entry-points.\"octorules.providers\"]\nmy_provider = \"my_package:MyProvider\"\n```\n\nUnsupported optional methods must still exist to satisfy the Protocol. The convention: read methods (`list_*`, `get_*`, `get_all_*`) return empty collections; mutation methods (`create_*`, `update_*`, `put_*`, `delete_*`) raise `ProviderError`.\n\n### Provider utilities\n\n`octorules.provider.utils` and related modules provide shared helpers so providers don't reinvent common patterns:\n\n- **`retry_with_backoff()`** (`octorules.retry`) — exponential backoff with jitter for retrying transient API errors.\n- **`fetch_parallel()`** (`octorules.provider.utils`) — concurrent fetching with error propagation and worker capping.\n- **`to_plain_dict()`** — convert provider SDK objects to plain dicts.\n- **`normalize_fields()` / `denormalize_fields()`** — bidirectional field name mapping between YAML and provider API formats.\n- **`validate_path_within()`** (`octorules.pathutil`) — path traversal protection for file operations.\n- **`make_error_wrapper()`** — decorator factory for mapping provider SDK exceptions to `ProviderError`/`ProviderAuthError`.\n- **Linter helpers** (`octorules.linter.helpers`) — `CATCH_ALL_CIDRS` plus `find_duplicate_priorities()` and `find_first_priority_gap()` for the duplicate-priority / non-contiguous-priority lint checks providers mirror by convention.\n\nExtension hooks (plan, apply, format, validate, dump, audit) registered via `octorules.extensions` are validated at registration time — the framework checks the callable's signature against the expected parameters and raises `TypeError` immediately if they don't match, so provider authors get clear errors during development rather than at runtime.\n\n## CI/CD integration\n\nFor GitHub Actions, see [octorules-sync](https://github.com/doctena-org/octorules-sync) — a ready-made action that runs plan on PRs and sync on merge to main.\n\n## Development\n\n### Local setup\n\n```bash\ngit clone git@github.com:doctena-org/octorules.git\ncd octorules\npython -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev,wirefilter]\"\n```\n\n### Pre-commit hook\n\n```bash\npre-commit install\n```\n\n### Running tests and linting\n\n```bash\npytest\nruff check octorules/ tests/\nruff format --check octorules/ tests/\n```\n\n### Releasing a new version\n\n1. Update the version in `pyproject.toml` (single source of truth).\n2. Commit and push to `main`.\n3. Tag the release and push the tag:\n\n```bash\ngit tag -a v0.17.0 -m \"v0.17.0\"\ngit push origin v0.17.0\n```\n\nPushing a `v*` tag triggers the release workflow, which runs the full lint and test suites before building, publishing to [PyPI](https://pypi.org/project/octorules/), and creating a GitHub Release.\n\n## License\n\noctorules is licensed under the [Apache License 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoctena-org%2Foctorules","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoctena-org%2Foctorules","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoctena-org%2Foctorules/lists"}