Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- Live `/gateway` transactions consult a Coraza sidecar when `CORAZA_WAF_URL` is set. The sidecar response is parsed with the existing Coraza audit adapter (OWASP CRS authority, not a hand-rolled engine). Sidecar outage is fail-closed when `PROVEN_ENGINE_FAIL_CLOSED` is true. `GET /api/waf/engine-status` and `/healthz.proven_engine` report the mode.
- Live `/gateway` transactions consult in-process libcoraza when `CORAZA_LIB_PATH` is set (with `CORAZA_RULES_PATH` and/or `CORAZA_DIRECTIVES`). Missing library, missing rules, or an empty ruleset fail startup before bind. Otherwise a Coraza sidecar is consulted when `CORAZA_WAF_URL` is set. The sidecar response is parsed with the existing Coraza audit adapter (OWASP CRS authority, not a hand-rolled engine). Engine outage is fail-closed when `PROVEN_ENGINE_FAIL_CLOSED` is true. `GET /api/waf/engine-status` and `/healthz.proven_engine` report `coraza_in_process`, `coraza_sidecar`, or `ingest_hints_only`.
- Fail-closed destination policy on every outbound `http`/`https` call, including the Coraza sidecar URL (gateway upstream, threat-intel fetch, Clearfolio, SOC LLM). Private/loopback/metadata classes are denied unless `DESTINATION_ALLOWLIST` (or loopback development) permits them; `DESTINATION_DENYLIST` wins. Clients ignore ambient HTTP proxies and do not follow redirects. CIDR allowlist matches apply per resolved address; CIDR entries authorize non-default ports; IPv6 site-local (`fec0::/10`) is denied; invalid CIDR prefixes fail startup; `/healthz.destination_mode` reports the policy class. Blocking DNS runs on `spawn_blocking` with a 2s timeout. Persistence and destination-list validation complete before the readiness line is printed. After a host is allowed, the HTTP client connects only to those evaluated addresses (Host/SNI unchanged) so a rebinding answer cannot bypass the policy.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The core stays an in-repo workspace crate on purpose (no git submodule) until it

## Runtime Configuration

Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`), `ADMIN_TOKEN` (write token for `X-Admin-Token`), `ADMIN_TOKENS` (comma-separated `token:actor` pairs for multi-token RBAC with per-token audit actors), `WAF_IDS_STATE_PATH` (optional JSON state file; omitted = seeded in-memory state), `DNSBL_ORIGIN` (default `dnsbl.local`), `EVENT_LIMIT` (default 1000, must be > 0), `RATE_LIMIT` / `RATE_LIMIT_WINDOW`, `CORAZA_WAF_URL` (optional in-path Coraza sidecar), `PROVEN_ENGINE_FAIL_CLOSED` (boolean; default false — set true in production when a sidecar URL is set).
Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`), `ADMIN_TOKEN` (write token for `X-Admin-Token`), `ADMIN_TOKENS` (comma-separated `token:actor` pairs for multi-token RBAC with per-token audit actors), `WAF_IDS_STATE_PATH` (optional JSON state file; omitted = seeded in-memory state), `DNSBL_ORIGIN` (default `dnsbl.local`), `EVENT_LIMIT` (default 1000, must be > 0), `RATE_LIMIT` / `RATE_LIMIT_WINDOW`, `CORAZA_LIB_PATH` / `CORAZA_RULES_PATH` / `CORAZA_DIRECTIVES` (optional in-process libcoraza), `CORAZA_WAF_URL` (optional in-path Coraza sidecar), `PROVEN_ENGINE_FAIL_CLOSED` (boolean; default false — set true in production when an engine is set).

## Key Conventions

Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ resolver = "3"
axum = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "multipart", "json", "stream"] }
futures-util = { version = "0.3", default-features = false, features = ["std"] }
libloading = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["fs", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Useful environment variables:
- `WAF_IDS_STATE_PATH`: optional JSON state path. When omitted, the service runs with seeded in-memory state.
- `DNSBL_ORIGIN`: DNSBL zone origin, default `dnsbl.local`
- `EVENT_LIMIT`: retained event count, default `1000`; must be greater than zero
- `CORAZA_LIB_PATH` / `CORAZA_RULES_PATH` / `CORAZA_DIRECTIVES`: optional in-process libcoraza. A missing library or empty ruleset fails startup. `/healthz.proven_engine` reports `coraza_in_process`.
- `CORAZA_WAF_URL`: optional Coraza sidecar URL used when libcoraza is not loaded
- `PROVEN_ENGINE_FAIL_CLOSED`: when true, a configured engine outage returns 503 instead of degrading to builtin scoring

Example with persistent local state:

Expand Down
60 changes: 60 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Compile the libcoraza C-ABI fixture used by in-process engine tests.
//!
//! Production loads operator-supplied libcoraza (`CORAZA_LIB_PATH`). CI stays
//! hermetic: this stub implements the same exported symbols without Go.

use std::path::PathBuf;
use std::process::Command;

fn main() {
println!("cargo:rerun-if-changed=src/coraza_abi_stub.rs");

let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by cargo");
let output = stub_output_path(&out_dir);
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let source = PathBuf::from(&manifest_dir).join("src/coraza_abi_stub.rs");

let mut cmd = Command::new(&rustc);
cmd.arg("--crate-type")
.arg("cdylib")
.arg("--crate-name")
.arg("coraza_abi_stub")
.arg("--edition")
.arg("2024")
.arg("-D")
.arg("warnings")
.arg("-C")
.arg("opt-level=0")
.arg("-o")
.arg(&output)
.arg(&source);

if let (Ok(host), Ok(target)) = (std::env::var("HOST"), std::env::var("TARGET"))
&& host != target
{
cmd.arg("--target").arg(target);
}

let status = cmd.status().unwrap_or_else(|error| {
panic!("failed to spawn rustc for libcoraza ABI stub: {error}");
});
if !status.success() {
panic!("rustc failed to build libcoraza ABI stub: {status}");
}

println!(
"cargo:rustc-env=WARDNET_CORAZA_ABI_STUB={}",
output.display()
);
}

fn stub_output_path(out_dir: &str) -> PathBuf {
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let filename = match os.as_str() {
"windows" => "coraza_abi_stub.dll",
"macos" => "libcoraza_abi_stub.dylib",
_ => "libcoraza_abi_stub.so",
};
PathBuf::from(out_dir).join(filename)
}
2 changes: 1 addition & 1 deletion crates/waf-ids-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ fn buyer_evidence_endpoints() -> Vec<BuyerEvidenceEndpoint> {
"GET",
"/api/waf/engine-status",
"application/json",
"in-path Coraza sidecar vs ingest-hint enforcement status (no sidecar URL)",
"in-path Coraza libcoraza/sidecar vs ingest-hint enforcement status (no library path or sidecar URL)",
false,
),
buyer_evidence_endpoint(
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ flowchart LR

## Near-Term Integrations

- **WAF**: Coraza/OWASP CRS audit JSON/NDJSON ingest is available at `POST /api/waf/coraza/audit` (admin token). Interrupted transactions and CRS rule messages become `SecurityEvent` rows and feed gateway enforcement (DNSBL + `client_ip`/`path` threat indicators) so subsequent gateway decisions block matching clients. When `CORAZA_WAF_URL` is set, each live `/gateway` transaction is POSTed to that sidecar and the response is parsed with the same Coraza audit adapter — CRS authority stays in the sidecar; Wardnet does not invent WAF rules. `GET /api/waf/engine-status` reports `coraza_sidecar` vs `ingest_hints_only`. In-process libcoraza embedding remains a follow-up.
- **WAF**: Coraza/OWASP CRS audit JSON/NDJSON ingest is available at `POST /api/waf/coraza/audit` (admin token). Interrupted transactions and CRS rule messages become `SecurityEvent` rows and feed gateway enforcement (DNSBL + `client_ip`/`path` threat indicators) so subsequent gateway decisions block matching clients. When `CORAZA_LIB_PATH` is set, each live `/gateway` transaction is evaluated in-process through the libcoraza C ABI (operator-supplied library + CRS file/directives). Otherwise, when `CORAZA_WAF_URL` is set, the transaction is POSTed to that sidecar and the response is parsed with the same Coraza audit adapter — CRS authority stays in Coraza; Wardnet does not invent WAF rules. `GET /api/waf/engine-status` reports `coraza_in_process`, `coraza_sidecar`, or `ingest_hints_only`. Suricata tail/shipper and detection-quality corpora remain follow-ups.
- **IDS**: Suricata EVE JSON/NDJSON ingest is available at `POST /api/ids/suricata/eve` (admin token). Alert records become `SecurityEvent` rows for SOC export/KPI; full route correlation and live EVE tailing remain follow-ups.
- **Threat Intelligence**: STIX 2.x indicator/bundle ingest is available at `POST /api/threat-intel/stix` (admin token), MISP Event/attribute JSON ingest at `POST /api/threat-intel/misp` (admin token), TAXII 2.1 collection poll at `POST /api/threat-intel/taxii/poll` (admin token; Basic/Bearer optional), and OpenCTI observable/indicator export ingest at `POST /api/threat-intel/opencti` (admin token). All update `ThreatIndicator` / `DnsblEntry` plus feed freshness. Live MISP REST pull and live OpenCTI GraphQL pull remain follow-ups.
- **DNSBL Serving**: Hickory DNS should serve authoritative DNSBL responses directly after zone export semantics stabilize.
Expand Down
10 changes: 6 additions & 4 deletions docs/doctoring/in-path-coraza-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ https://doi.org/10.6028/NIST.SP.800-218

## Operator next action

Point `CORAZA_WAF_URL` at a Coraza (or CRS-compatible) evaluate endpoint that
returns Coraza audit JSON. Set `PROVEN_ENGINE_FAIL_CLOSED=true` in production.
Confirm `GET /api/waf/engine-status` reports `mode=coraza_sidecar` and
`in_path=true` before exposing `/gateway`.
Prefer in-process libcoraza: set `CORAZA_LIB_PATH` to the shared library and
`CORAZA_RULES_PATH` (or `CORAZA_DIRECTIVES`) to a pinned OWASP CRS bundle. Point
`CORAZA_WAF_URL` at a Coraza evaluate endpoint only when the process cannot
load the library. Set `PROVEN_ENGINE_FAIL_CLOSED=true` in production. Confirm
`GET /api/waf/engine-status` reports `in_path=true` (`coraza_in_process` or
`coraza_sidecar`) before exposing `/gateway`.
49 changes: 49 additions & 0 deletions docs/doctoring/in-process-libcoraza.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Doctoring — in-process libcoraza adapter

This note grounds the issue #86 remainder shipped this loop: live `/gateway`
transactions are evaluated by libcoraza inside the Wardnet process. IEEE PDFs
are not redistributed.

## Adopted standards and literature

Coraza. (n.d.). *Coraza Web Application Firewall*.
https://coraza.io/docs/

- **Design impact:** CRS remains the detection authority. Wardnet `dlopen`s
operator-supplied libcoraza (`CORAZA_LIB_PATH`) and drives the documented C
ABI (`coraza_new_waf_config`, `coraza_rules_add_file` / `coraza_rules_add`,
`coraza_process_uri` / headers / body, `coraza_intervention`). Builtin
signatures stay a residual scorer.

OWASP Foundation. (n.d.). *OWASP Core Rule Set documentation*.
https://coreruleset.org/docs/

- **Design impact:** Rules come from `CORAZA_RULES_PATH` and optional
`CORAZA_DIRECTIVES`. An empty or missing ruleset fails startup before bind so
production cannot silently skip CRS.

Saltzer, J. H., & Schroeder, M. D. (1975). The protection of information in
computer systems. *Proceedings of the IEEE*, *63*(9), 1278–1308.
https://doi.org/10.1109/PROC.1975.9939

- **Design impact:** Fail-safe defaults. `PROVEN_ENGINE_FAIL_CLOSED` remains
opt-in per transaction; a configured library that cannot load is always
fail-closed at process start. Unset `CORAZA_LIB_PATH` keeps the sidecar path
(`CORAZA_WAF_URL`) from the previous slice.

National Institute of Standards and Technology. (2022). *Secure Software
Development Framework (SSDF) version 1.1* (NIST SP 800-218).
https://doi.org/10.6028/NIST.SP.800-218

- **Design impact:** PW.1 / PW.4 — reuse a well-secured component. Building
libcoraza still needs Go+C; CI stays hermetic by compiling a fixture cdylib
that exports the same symbols. Production points `CORAZA_LIB_PATH` at a real
libcoraza build.

## Operator next action

Install libcoraza and a pinned OWASP CRS bundle. Set `CORAZA_LIB_PATH` and
`CORAZA_RULES_PATH`. Set `PROVEN_ENGINE_FAIL_CLOSED=true`. Confirm
`GET /api/waf/engine-status` reports `mode=coraza_in_process`,
`in_path=true`, and a non-zero `in_process_rules` before exposing `/gateway`.
The library path is not published on health or engine-status surfaces.
Loading
Loading