From 3c6eb4c1d8e15fac6faf914d894e7c1f9719b4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20dos=20Santos=20Lopes?= Date: Wed, 8 Jul 2026 15:02:24 +0100 Subject: [PATCH 1/2] Embed default ruleset in helper-rust --- appsec/cmake/update_helper_rust_stamp.cmake | 7 +- appsec/helper-rust/src/service.rs | 3 +- appsec/helper-rust/src/service/waf_ruleset.rs | 83 +++++-------------- appsec/tests/integration/build.gradle | 1 + datadog-setup.php | 5 +- 5 files changed, 32 insertions(+), 67 deletions(-) diff --git a/appsec/cmake/update_helper_rust_stamp.cmake b/appsec/cmake/update_helper_rust_stamp.cmake index 297aba7b359..191ceaac610 100644 --- a/appsec/cmake/update_helper_rust_stamp.cmake +++ b/appsec/cmake/update_helper_rust_stamp.cmake @@ -1,13 +1,14 @@ cmake_minimum_required(VERSION 3.14) # Called at build time via cmake -P with HELPER_RUST_DIR and STAMP_FILE -# defined on the command line. Touches STAMP_FILE only when at least one Rust -# source file (*.rs, Cargo.toml, Cargo.lock) is newer than the stamp, so that -# cargo is not re-run on every build when nothing has changed. +# defined on the command line. Touches STAMP_FILE only when at least one helper +# input is newer than the stamp, so that cargo is not re-run on every build when +# nothing has changed. set(_LIBDDWAF_RUST_DIR "${HELPER_RUST_DIR}/../third_party/libddwaf-rust") file(GLOB_RECURSE _sources "${HELPER_RUST_DIR}/*.rs" "${HELPER_RUST_DIR}/Cargo.toml" "${HELPER_RUST_DIR}/Cargo.lock" + "${HELPER_RUST_DIR}/../recommended.json" "${_LIBDDWAF_RUST_DIR}/*.rs" "${_LIBDDWAF_RUST_DIR}/Cargo.toml" "${_LIBDDWAF_RUST_DIR}/Cargo.lock" diff --git a/appsec/helper-rust/src/service.rs b/appsec/helper-rust/src/service.rs index aa3bc5edc8a..086e054277c 100644 --- a/appsec/helper-rust/src/service.rs +++ b/appsec/helper-rust/src/service.rs @@ -258,8 +258,7 @@ impl Service { waf_ruleset::WafRuleset::from_file(PathBuf::from(path)) .with_context(|| format!("Error loading WAF ruleset from file {:?}", path)) } else { - waf_ruleset::WafRuleset::from_default_file() - .with_context(|| "Error loading WAF ruleset from default file") + Ok(waf_ruleset::WafRuleset::from_default()) }; let ruleset = match maybe_ruleset { diff --git a/appsec/helper-rust/src/service/waf_ruleset.rs b/appsec/helper-rust/src/service/waf_ruleset.rs index 3bc11848ca3..cd3d18e1e01 100644 --- a/appsec/helper-rust/src/service/waf_ruleset.rs +++ b/appsec/helper-rust/src/service/waf_ruleset.rs @@ -1,11 +1,14 @@ use crate::client::log; use std::{ fs::File, - io::{BufRead, BufReader, Read, Seek}, - path::{Path, PathBuf}, + io::{BufReader, Read, Seek}, + path::Path, }; -use anyhow::{anyhow, Context}; +use anyhow::Context; + +const DEFAULT_RULESET: &[u8] = + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../recommended.json")); pub struct WafRuleset { doc: libddwaf::object::WafObject, @@ -36,9 +39,13 @@ impl WafRuleset { Ok(WafRuleset::new(doc, rules_version)) } - pub fn from_default_file() -> anyhow::Result { - let file = get_default_rules_file()?; - WafRuleset::from_file(&file) + pub fn from_default() -> WafRuleset { + let ruleset = WafRuleset::from_slice(DEFAULT_RULESET) + .expect("embedded default ruleset is valid JSON"); + + log::info!("Loaded embedded default WAF ruleset"); + + ruleset } pub fn from_slice(slice: &[u8]) -> anyhow::Result { @@ -72,61 +79,17 @@ fn extract_rules_version(reader: R) -> Option { parsed.metadata?.rules_version } -fn get_default_rules_file() -> anyhow::Result { - let helper_path = get_helper_path(); - - let base_path: PathBuf = if let Ok(helper_path) = helper_path { - helper_path - .parent() - .ok_or(anyhow!("No parent for {:?}", helper_path))? - .to_path_buf() - } else { - get_self_path().with_context(|| "Could find neither lib path nor self exe path")? - }; - - let file = base_path.join("../etc/recommended.json"); - if file.exists() { - return Ok(file); - } +#[cfg(test)] +mod tests { + use super::*; - let file_legacy = base_path.join("../etc/dd-appsec/recommended.json"); - if file_legacy.exists() { - return Ok(file_legacy); - } + #[test] + fn default_ruleset_is_embedded() { + let ruleset = WafRuleset::from_default(); + let rules_version = ruleset + .rules_version() + .expect("default ruleset should expose its version"); - Err(anyhow!( - "Could not find recommended.json in either ../etc/ or ../etc/dd-appsec/" - )) -} - -fn get_helper_path() -> anyhow::Result { - // Match both libddappsec-helper.so (C++ helper) and - // libddappsec-helper-rust.so (Rust helper) - const LIBNAME_PREFIX: &str = "/libddappsec-helper"; - const MAPS_PATH: &str = "/proc/self/maps"; - - let file = File::open(MAPS_PATH)?; - let reader = BufReader::new(file); - - for line in reader.lines() { - let line = line?; - if line.contains(LIBNAME_PREFIX) { - if let Some(pos) = line.find('/') { - return Ok(PathBuf::from(&line[pos..])); - } else { - return Err(anyhow!("Should not happen")); - } - } + assert!(!rules_version.is_empty()); } - - Err(anyhow!( - "Could not find libddappsec-helper*.so in /proc/self/maps" - )) -} - -pub fn get_self_path() -> anyhow::Result { - const SELF_EXE: &str = "/proc/self/exe"; - - let path = std::fs::read_link(SELF_EXE)?; - Ok(path) } diff --git a/appsec/tests/integration/build.gradle b/appsec/tests/integration/build.gradle index 65fe130395f..6ab174cb46a 100644 --- a/appsec/tests/integration/build.gradle +++ b/appsec/tests/integration/build.gradle @@ -924,6 +924,7 @@ def helperRustInputs = [ '../../helper-rust/coverage_init.c', '../../helper-rust/outline_atomics.c', '../../helper-rust/.cargo/config.toml', + '../../recommended.json', ], ] diff --git a/datadog-setup.php b/datadog-setup.php index 6df22751f83..3e294277dc7 100644 --- a/datadog-setup.php +++ b/datadog-setup.php @@ -2365,8 +2365,9 @@ function get_ini_settings($sourcesDir, $appsecHelperPath, $appsecRulesPath) 'default' => $appsecRulesPath, 'commented' => true, 'description' => [ - 'The path to the rules json file. The sidecar process must be able to read the', - 'file. This ini setting is configured by the installer', + 'Optional path to a custom rules json file. When this setting is not configured,', + 'the Rust helper uses its embedded default rules and the C++ helper uses the', + 'default rules file installed next to the helper.', ], ], [ From 1e4e71405e284f893abb2f24c118f961aeeacfef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20dos=20Santos=20Lopes?= Date: Thu, 9 Jul 2026 15:05:43 +0100 Subject: [PATCH 2/2] Fix tests --- .../appsec/php/integration/TelemetryTests.groovy | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/TelemetryTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/TelemetryTests.groovy index 2adb26a7e81..1f0e571b5fc 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/TelemetryTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/TelemetryTests.groovy @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.condition.DisabledIf +import org.testcontainers.containers.BindMode import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers @@ -49,6 +50,12 @@ class TelemetryTests { void configure() { super.configure() withEnv('RUST_LIB_BACKTRACE', '1') + // This class strips appsec.rules from php.ini (see beforeAll) to exercise + // the helpers' default-ruleset path, so both helpers must see the real + // production ruleset here instead of the test fixture at + // src/test/waf/recommended.json. + setBinds(binds.findAll { it.volume.path != '/etc/recommended.json' }) + withFileSystemBind('../../recommended.json', '/etc/recommended.json', BindMode.READ_ONLY) } } @@ -935,10 +942,11 @@ class TelemetryTests { } assert requestSup.get() != null - // Blocking request: 80.80.80.80 hits the recommended.json IP blocklist rule - // (on_match: ["block"]). The WAF returns a block_request action. + // Blocking request: this User-Agent hits the recommended.json Datadog test + // scanner rule (ua0-600-56x, on_match: ["block"]). The WAF returns a + // block_request action. HttpRequest req = CONTAINER.buildReq('/hello.php') - .header('X-Forwarded-For', '80.80.80.80').GET().build() + .header('User-Agent', 'dd-test-scanner-log-block').GET().build() CONTAINER.traceFromRequest(req, ofString()) { HttpResponse resp -> assert resp.statusCode() == 403 } @@ -1163,7 +1171,7 @@ class TelemetryTests { ], 'datadog/2/ASM/rasp_lfi_block_override/config': [ rules_override: [[ - rules_target: [[rule_id: 'rasp-001-001']], + rules_target: [[rule_id: 'rasp-930-100']], on_match: ['block'] ]] ]