Skip to content

Commit 988ec06

Browse files
Add configurable DataDome protection exclusions
1 parent d975718 commit 988ec06

9 files changed

Lines changed: 1356 additions & 106 deletions

File tree

crates/trusted-server-adapter-fastly/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ async fn route_request(
330330
settings,
331331
services: runtime_services,
332332
req: &mut req,
333+
geo_info: geo_info.as_ref(),
333334
})
334335
.await
335336
{

crates/trusted-server-core/src/integrations/datadome.rs

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,15 @@ use crate::platform::RuntimeServices;
7878
use crate::settings::{IntegrationConfig, Settings};
7979

8080
mod protection;
81+
mod protection_scope;
8182

82-
const DATADOME_INTEGRATION_ID: &str = "datadome";
83+
pub use protection_scope::{
84+
ProtectionExclusionRuleConfig, ProtectionIpCidrSourceConfig, ProtectionMatcherConfig,
85+
};
86+
87+
use protection_scope::ProtectionScope;
88+
89+
pub(super) const DATADOME_INTEGRATION_ID: &str = "datadome";
8390

8491
/// Regex pattern for matching and rewriting `DataDome` URLs in script content.
8592
///
@@ -154,13 +161,36 @@ pub struct DataDomeConfig {
154161
#[validate(range(min = 1, max = 10000))]
155162
pub timeout_ms: u32,
156163

157-
/// Regex for URLs to exclude from Protection API validation.
158-
#[serde(default = "default_url_pattern_exclusion")]
159-
pub url_pattern_exclusion: String,
160-
161-
/// Regex for URLs to include in Protection API validation.
162-
#[serde(default)]
163-
pub url_pattern_inclusion: String,
164+
/// HTTP methods excluded from Protection API validation.
165+
#[serde(
166+
default = "default_protection_excluded_methods",
167+
deserialize_with = "crate::settings::vec_from_seq_or_map"
168+
)]
169+
pub protection_excluded_methods: Vec<String>,
170+
171+
/// Client autonomous system numbers excluded from Protection API validation.
172+
#[serde(default, deserialize_with = "crate::settings::vec_from_seq_or_map")]
173+
pub protection_excluded_asns: Vec<u32>,
174+
175+
/// Client IP CIDR ranges excluded from Protection API validation.
176+
#[serde(default, deserialize_with = "crate::settings::vec_from_seq_or_map")]
177+
pub protection_excluded_ip_cidrs: Vec<String>,
178+
179+
/// Config Store-backed client IP CIDR ranges excluded from Protection API validation.
180+
#[serde(default, deserialize_with = "crate::settings::vec_from_seq_or_map")]
181+
pub protection_excluded_ip_cidr_sources: Vec<ProtectionIpCidrSourceConfig>,
182+
183+
/// Cache TTL for Config Store-backed IP CIDR lists, in seconds.
184+
#[serde(default = "default_protection_ip_list_cache_ttl_seconds")]
185+
#[validate(range(min = 1, max = 86400))]
186+
pub protection_ip_list_cache_ttl_seconds: u64,
187+
188+
/// Structured exclusion rules for Protection API validation.
189+
#[serde(
190+
default = "default_protection_exclusion_rules",
191+
deserialize_with = "crate::settings::vec_from_seq_or_map"
192+
)]
193+
pub protection_exclusion_rules: Vec<ProtectionExclusionRuleConfig>,
164194

165195
/// Reserved flag for future GraphQL payload extraction.
166196
#[serde(default)]
@@ -219,8 +249,27 @@ fn default_timeout_ms() -> u32 {
219249
1500
220250
}
221251

222-
fn default_url_pattern_exclusion() -> String {
223-
r"\.(avi|flv|mka|mkv|mov|mp4|mpeg|mpg|mp3|flac|ogg|ogm|opus|wav|webm|webp|bmp|gif|ico|jpeg|jpg|png|svg|svgz|swf|eot|otf|ttf|woff|woff2|css|less|js|map)$".to_string()
252+
fn default_static_asset_exclusion_pattern() -> String {
253+
r"(?i)\.(avi|flv|mka|mkv|mov|mp4|mpeg|mpg|mp3|flac|ogg|ogm|opus|wav|webm|webp|bmp|gif|ico|jpeg|jpg|png|svg|svgz|swf|eot|otf|ttf|woff|woff2|css|less|js|map)$".to_string()
254+
}
255+
256+
fn default_protection_excluded_methods() -> Vec<String> {
257+
vec!["OPTIONS".to_string()]
258+
}
259+
260+
fn default_protection_ip_list_cache_ttl_seconds() -> u64 {
261+
300
262+
}
263+
264+
fn default_protection_exclusion_rules() -> Vec<ProtectionExclusionRuleConfig> {
265+
vec![ProtectionExclusionRuleConfig {
266+
id: "default-static-assets".to_string(),
267+
enabled: true,
268+
methods: Vec::new(),
269+
matcher: ProtectionMatcherConfig::PathRegex {
270+
patterns: vec![default_static_asset_exclusion_pattern()],
271+
},
272+
}]
224273
}
225274

226275
fn default_inject_client_side_tag() -> bool {
@@ -248,8 +297,12 @@ impl Default for DataDomeConfig {
248297
server_side_key_secret_name: default_server_side_key_secret_name(),
249298
protection_api_origin: default_protection_api_origin(),
250299
timeout_ms: default_timeout_ms(),
251-
url_pattern_exclusion: default_url_pattern_exclusion(),
252-
url_pattern_inclusion: String::new(),
300+
protection_excluded_methods: default_protection_excluded_methods(),
301+
protection_excluded_asns: Vec::new(),
302+
protection_excluded_ip_cidrs: Vec::new(),
303+
protection_excluded_ip_cidr_sources: Vec::new(),
304+
protection_ip_list_cache_ttl_seconds: default_protection_ip_list_cache_ttl_seconds(),
305+
protection_exclusion_rules: default_protection_exclusion_rules(),
253306
enable_graphql_support: false,
254307
client_side_key: String::new(),
255308
inject_client_side_tag: default_inject_client_side_tag(),
@@ -268,8 +321,7 @@ impl IntegrationConfig for DataDomeConfig {
268321
/// `DataDome` integration implementation.
269322
pub struct DataDomeIntegration {
270323
config: DataDomeConfig,
271-
protection_exclusion: Option<Regex>,
272-
protection_inclusion: Option<Regex>,
324+
protection_scope: ProtectionScope,
273325
}
274326

275327
impl DataDomeIntegration {
@@ -299,15 +351,11 @@ impl DataDomeIntegration {
299351
log::warn!("[datadome] enable_graphql_support is reserved and ignored in v1");
300352
}
301353

302-
let protection_exclusion =
303-
Self::compile_optional_regex(&config.url_pattern_exclusion, "url_pattern_exclusion")?;
304-
let protection_inclusion =
305-
Self::compile_optional_regex(&config.url_pattern_inclusion, "url_pattern_inclusion")?;
354+
let protection_scope = ProtectionScope::compile(&config)?;
306355

307356
Ok(Arc::new(Self {
308357
config,
309-
protection_exclusion,
310-
protection_inclusion,
358+
protection_scope,
311359
}))
312360
}
313361

@@ -343,19 +391,6 @@ impl DataDomeIntegration {
343391
Ok(())
344392
}
345393

346-
fn compile_optional_regex(
347-
pattern: &str,
348-
name: &str,
349-
) -> Result<Option<Regex>, Report<TrustedServerError>> {
350-
if pattern.trim().is_empty() {
351-
return Ok(None);
352-
}
353-
354-
Regex::new(&format!("(?i:{pattern})"))
355-
.map(Some)
356-
.map_err(|err| Report::new(Self::error(format!("Invalid {name}: {err}"))))
357-
}
358-
359394
fn error(message: impl Into<String>) -> TrustedServerError {
360395
TrustedServerError::Integration {
361396
integration: DATADOME_INTEGRATION_ID.to_string(),

crates/trusted-server-core/src/integrations/datadome/protection.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::integrations::{
1616
use crate::platform::{PlatformBackendSpec, PlatformHttpRequest, RuntimeServices, StoreName};
1717
use crate::redacted::Redacted;
1818

19+
use super::protection_scope::{ProtectionRequestFacts, ProtectionScopeDecision};
1920
use super::DataDomeIntegration;
2021

2122
const VALIDATE_REQUEST_PATH: &str = "/validate-request";
@@ -43,7 +44,7 @@ impl DataDomeIntegration {
4344
&self,
4445
input: RequestFilterInput<'_>,
4546
) -> RequestFilterDecision {
46-
if !self.config.enable_protection || !self.is_request_protected(input.request) {
47+
if !self.config.enable_protection || !self.is_request_protected(&input) {
4748
return RequestFilterDecision::Continue(RequestFilterEffects::default());
4849
}
4950

@@ -98,26 +99,24 @@ impl DataDomeIntegration {
9899
Ok(self.classify_protection_response(platform_response.response))
99100
}
100101

101-
fn is_request_protected(&self, req: &fastly::Request) -> bool {
102-
if req.get_method() == Method::OPTIONS {
103-
return false;
104-
}
105-
102+
fn is_request_protected(&self, input: &RequestFilterInput<'_>) -> bool {
103+
let req = input.request;
106104
let path = req.get_path();
107105
if is_internal_path(path) {
108106
return false;
109107
}
110108

111-
let target = format!("{}{}", request_host(req), path);
112-
113-
if let Some(inclusion) = &self.protection_inclusion {
114-
if !inclusion.is_match(&target) {
115-
return false;
116-
}
117-
}
118-
119-
if let Some(exclusion) = &self.protection_exclusion {
120-
if exclusion.is_match(&target) {
109+
let facts = ProtectionRequestFacts {
110+
method: req.get_method().as_str(),
111+
path,
112+
query: req.get_query_str(),
113+
client_ip: input.services.client_info().client_ip,
114+
asn: input.geo_info.and_then(|geo| geo.asn),
115+
};
116+
match self.protection_scope.evaluate(&facts, input.services) {
117+
ProtectionScopeDecision::Protect => {}
118+
ProtectionScopeDecision::Skip { rule_id, reason } => {
119+
log::debug!("[datadome] Skipping Protection API for rule {rule_id} ({reason})");
121120
return false;
122121
}
123122
}

0 commit comments

Comments
 (0)