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
775 changes: 264 additions & 511 deletions bottlecap/Cargo.lock

Large diffs are not rendered by default.

35 changes: 18 additions & 17 deletions bottlecap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ rand = { version = "0.8", default-features = false }
prost = { version = "0.14", default-features = false }
tonic = { version = "0.14", features = ["transport", "codegen", "server", "channel", "router"], default-features = false }
tonic-types = { version = "0.14", default-features = false }
zstd = { version = "0.13.3", default-features = false }
zstd = { version = "0.14.3", default-features = false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the obsolete zstd advisory suppressions

Remove the zstd-related entries for RUSTSEC-2026-0391, RUSTSEC-2026-0398, and RUSTSEC-2026-0403 from bottlecap/deny.toml as part of this upgrade. The comments at deny.toml:49-52 explicitly say the new zstd 0.14/zstd-safe 8.2 dependency line fixes these advisories and that the ignores should be dropped with the bump; leaving them active means a future dependency resolution that reintroduces an affected zstd version will still be silently accepted by the cargo-deny audit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acked

futures = { version = "0.3.31", default-features = false }
serde-aux = { version = "4.7", default-features = false }
serde_html_form = { version = "0.2", default-features = false }
Expand All @@ -84,22 +84,23 @@ indexmap = {version = "2.11.0", default-features = false}
# be found in the clippy.toml file adjacent to this Cargo.toml.
datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "f863626dbfe3c59bb390985fa6530b0621c2a0a2"}
ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "f863626dbfe3c59bb390985fa6530b0621c2a0a2"}
# These are pinned to the same crates.io versions that dd-trace-rs's datadog-opentelemetry
# depends on, so cargo unifies them instead of compiling duplicate copies. Re-check with
# `cargo tree --duplicates | grep ^libdd-` when bumping either side.
libdd-capabilities = "3.0.0"
libdd-common = { version = "5.2.0", default-features = false }
libdd-trace-protobuf = "4.0.1"
libdd-trace-utils = { version = "10.1.0", default-features = false, features = ["mini_agent"] }
libdd-trace-normalization = "3.0.1"
libdd-trace-obfuscation = { version = "6.0.0", default-features = false }
libdd-trace-stats = { version = "7.0.0", default-features = false }
datadog-opentelemetry = { version = "0.5.2", default-features = false, features = ["_unstable_propagation"] }
dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "5859602d609405a9de4d32638673a643caf37534", default-features = false }
datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "5859602d609405a9de4d32638673a643caf37534", default-features = false }
datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "5859602d609405a9de4d32638673a643caf37534", default-features = false }
datadog-agent-trace-sampler = { git = "https://github.com/DataDog/serverless-components", rev = "5859602d609405a9de4d32638673a643caf37534", default-features = false }
libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] }
# Pin the libdd crates used by Bottlecap's trace pipeline to the release line
# required by serverless-components PR #196.
libdd-capabilities = "4.0.1"
libdd-common = { version = "10.0.0", default-features = false }
libdd-trace-protobuf = "5.1.1"
libdd-trace-utils = { version = "16.0.0", default-features = false, features = ["mini_agent"] }
Comment on lines +91 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Upgrade normalization alongside the protobuf stack

Align libdd-trace-normalization with the newly upgraded trace packages. The resulting lockfile now contains normalization 3.0.1, which pulls libdd-trace-protobuf 4.0.1, alongside normalization 4.3.1 and protobuf 5.1.1 pulled by libdd-trace-utils 16.0.0. Every build therefore compiles two normalization/protobuf generations, undermining the dependency alignment and increasing build/artifact overhead; pinning the direct normalization dependency to the 4.3.1 release line removes the obsolete copies.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acked

libdd-trace-normalization = "4.3.1"
libdd-trace-obfuscation = { version = "12.0.0", default-features = false }
libdd-trace-stats = { version = "13.0.0", default-features = false }
datadog-opentelemetry = { version = "0.6.0", default-features = false, features = ["_unstable_propagation"] }
dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "e462105e918904e6b1ad2ebc153ae561cebe5a34", default-features = false }
Comment on lines +96 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the summary to match the final dependency revisions

Update the PR overview before merging: it says this change pins serverless-components to fd24c365ade64078b697e00f57f823aff32a65f7 and datadog-opentelemetry to 0.5.4, while the final manifest actually pins e462105e918904e6b1ad2ebc153ae561cebe5a34 and 0.6.0. As written, reviewers and release audits are being asked to validate different dependency revisions from the ones that will ship.

AGENTS.md reference: AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acked

datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "e462105e918904e6b1ad2ebc153ae561cebe5a34", default-features = false }
datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "e462105e918904e6b1ad2ebc153ae561cebe5a34", default-features = false }
datadog-agent-trace-sampler = { git = "https://github.com/DataDog/serverless-components", rev = "e462105e918904e6b1ad2ebc153ae561cebe5a34", default-features = false }
libddwaf = { version = "2.1.0", git = "https://github.com/DataDog/libddwaf-rust", rev = "bbada5255bf8e9105c06b03ce6d5d63a7c337e2a", default-features = false, features = ["serde"] }
# Enables the crypto provider libddwaf-sys needs to download prebuilt WAF binaries.
libddwaf-sys = { version = "2.1.0", git = "https://github.com/DataDog/libddwaf-rust", rev = "bbada5255bf8e9105c06b03ce6d5d63a7c337e2a", default-features = false, features = ["aws-lc-rs"] }

[dev-dependencies]
figment = { version = "0.10", default-features = false, features = ["yaml", "env", "test"] }
Expand Down
58 changes: 13 additions & 45 deletions bottlecap/LICENSE-3rdparty.csv

Large diffs are not rendered by default.

14 changes: 0 additions & 14 deletions bottlecap/deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,12 @@ unsound = "workspace"
# once a FIPS-certified aws-lc-rs is available. Trivy reports
# RUSTSEC-2026-0285 as GHSA-2mjx-qc3c-rqvc, ignored in .trivyignore.yaml.
#
# The zstd advisories cover APIs this graph does not call: bottlecap, the
# serverless-components crates and libdd-trace-utils compress through
# `stream::write::Encoder` / the bulk API; the only decoding is
# `stream::read::Decoder` on the embedded AppSec ruleset and `decode_all` in
# a test helper, both excluded by RUSTSEC-2026-0403. Only libddwaf-sys's build
# script enables zstd's default (legacy) features; build-dependency features
# resolve separately, so the shipped binary has no legacy-frame support.
# zstd-safe 7.2.4 also has unsound notices RUSTSEC-2026-0395 to 0406 that the
# `workspace` scope does not report; the same zstd 0.14 / zstd-safe 8.2 bump
# fixes them (needs the same bump in serverless-components and libdatadog);
# drop these ignores with it.
ignore = [
{ id = "RUSTSEC-2026-0285", reason = "rustls fix needs aws-lc-rs >= 1.18 (AWS-LC-FIPS 4.x, uncertified)" },
{ id = "RUSTSEC-2026-0346", reason = "rustls fix needs aws-lc-rs >= 1.18 (AWS-LC-FIPS 4.x, uncertified)" },
{ id = "RUSTSEC-2026-0348", reason = "rustls fix needs aws-lc-rs >= 1.18 (AWS-LC-FIPS 4.x, uncertified)" },
{ id = "RUSTSEC-2026-0349", reason = "rustls fix needs aws-lc-rs >= 1.18 (AWS-LC-FIPS 4.x, uncertified)" },
{ id = "RUSTSEC-2026-0382", reason = "rustls-webpki fix needs aws-lc-rs ^1.18 (AWS-LC-FIPS 4.x, uncertified)" },
{ id = "RUSTSEC-2026-0391", reason = "zstd `with_context` is not used anywhere in the graph; fix is zstd 0.14" },
{ id = "RUSTSEC-2026-0398", reason = "zstd-safe `seekable` feature is not enabled; fix is zstd 0.14" },
{ id = "RUSTSEC-2026-0403", reason = "only `stream::read::Decoder` and `decode_all` are used, which the advisory excludes; runtime zstd-sys is built without the `legacy` feature" },
{ id = "RUSTSEC-2026-0410", reason = "wasm32 only; the extension ships on Linux" },
{ id = "RUSTSEC-2023-0050", reason = "multipart 0.18 is unmaintained; it parses AppSec multipart request bodies; no maintained replacement is chosen yet" },
{ id = "RUSTSEC-2025-0134", reason = "rustls-pemfile is unmaintained; replace with rustls-pki-types PEM parsing (code change, not a version bump)" },
Expand Down
27 changes: 16 additions & 11 deletions bottlecap/src/appsec/processor/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use std::time::Duration;
use bytes::{Buf, Bytes};
use libdd_trace_protobuf::pb::Span;
use libddwaf::object::{Keyed, WafMap, WafObject};
use libddwaf::{Context as WafContext, Handle, RunError, RunOutput, RunResult, waf_map};
use libddwaf::{
Context as WafContext, Handle, RunError, RunOutput, RunResult, RunnableContext, waf_map,
};
use mime::Mime;
use multipart::server::Multipart;
use tracing::{debug, info, warn};
Expand Down Expand Up @@ -56,7 +58,7 @@ impl Context {
(TagName::AppsecEnabled, TagValue::Metric(1.0)),
(
TagName::AppsecWafVersion,
TagValue::Always(libddwaf::get_version().to_string_lossy().to_string()),
TagValue::Always(libddwaf::version().to_string_lossy().to_string()),
),
]);
if !ruleset_version.is_empty() {
Expand Down Expand Up @@ -185,7 +187,7 @@ impl Context {
};

// Evaluate the WAF ruleset and handle the result.
match self.waf.run(Some(address_data), None, self.waf_timeout) {
match self.waf.run(address_data, self.waf_timeout) {
Ok(RunResult::Match(result) | RunResult::NoMatch(result)) => {
self.process_result(result);
}
Expand Down Expand Up @@ -218,7 +220,7 @@ impl Context {
("extract-schema", true)
})
};
match self.waf.run(Some(address_data), None, self.waf_timeout) {
match self.waf.run(address_data, self.waf_timeout) {
Ok(RunResult::Match(result) | RunResult::NoMatch(result)) => {
self.process_result(result);
}
Expand Down Expand Up @@ -454,7 +456,7 @@ impl Context {
let value = if let Some(value) = item.to_str() {
value.to_string()
} else {
match serde_json::to_string(item.inner()) {
match serde_json::to_string(item.value()) {
Ok(value) => value,
Err(e) => {
warn!(
Expand Down Expand Up @@ -649,7 +651,8 @@ fn to_address_data(payload: &dyn InvocationPayload) -> Option<WafMap> {
($field:ident[] => $name:literal) => {
let value = payload.$field();
if !value.is_empty() {
let mut waf_value = libddwaf::object::WafMap::new(value.len() as u64);
let mut waf_value = libddwaf::object::WafMap::new(value.len())
.expect("WAF map length is representable");
for (idx, (key, item)) in value.iter().enumerate() {
waf_value[idx] = (key.as_str(), item.as_str()).into();
}
Expand All @@ -661,9 +664,11 @@ fn to_address_data(payload: &dyn InvocationPayload) -> Option<WafMap> {
($field:ident[][] => $name:literal) => {
let value = payload.$field();
if !value.is_empty() {
let mut waf_value = libddwaf::object::WafMap::new(value.len() as u64);
let mut waf_value = libddwaf::object::WafMap::new(value.len())
.expect("WAF map length is representable");
for (idx, (key, elements)) in value.iter().enumerate() {
let mut waf_elements = libddwaf::object::WafArray::new(elements.len() as u64);
let mut waf_elements = libddwaf::object::WafArray::new(elements.len())
.expect("WAF array length is representable");
for (idx, elt) in elements.iter().enumerate() {
waf_elements[idx] = elt.as_str().into();
}
Expand Down Expand Up @@ -692,7 +697,7 @@ fn to_address_data(payload: &dyn InvocationPayload) -> Option<WafMap> {
return None;
}

let mut result = WafMap::new(addresses.len() as u64);
let mut result = WafMap::new(addresses.len()).expect("WAF map length is representable");
for (idx, item) in addresses.into_iter().enumerate() {
result[idx] = item;
}
Expand All @@ -717,7 +722,7 @@ fn try_parse_body_with_mime(body: impl Read, mime_type: Mime) -> Result<WafObjec
}
(mime::APPLICATION, mime::WWW_FORM_URLENCODED, None) => {
let pairs: Vec<(String, String)> = serde_html_form::from_reader(body)?;
let mut res = WafMap::new(pairs.len() as u64);
let mut res = WafMap::new(pairs.len()).expect("WAF map length is representable");
for (i, (key, value)) in pairs.into_iter().enumerate() {
res[i] = (key.as_str(), value.as_str()).into();
}
Expand Down Expand Up @@ -758,7 +763,7 @@ fn try_parse_body_with_mime(body: impl Read, mime_type: Mime) -> Result<WafObjec
}
}
}
let mut res = WafMap::new(items.len() as u64);
let mut res = WafMap::new(items.len()).expect("WAF map length is representable");
for (idx, (name, value)) in items.into_iter().enumerate() {
res[idx] = (name.as_str(), value).into();
}
Expand Down
24 changes: 13 additions & 11 deletions bottlecap/src/appsec/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use bytes::Bytes;
use cookie::Cookie;
use itertools::Itertools;
use libdd_trace_protobuf::pb::Span;
use libddwaf::object::{WafMap, WafOwned};
use libddwaf::object::{WafMap, WafOwnedDefaultAllocator};
use libddwaf::{Builder, Config as WafConfig, Handle};
use tokio::sync::Mutex;
use tracing::{debug, info};
Expand Down Expand Up @@ -66,22 +66,24 @@ impl Processor {
);
}

let Some(mut builder) = Builder::new(&WafConfig::default()) else {
let Some(mut builder) = Builder::new(Some(&WafConfig::default())) else {
return Err(Error::WafBuilderCreationFailed);
};

let rules = Self::get_rules(cfg)?;
let mut diagnostics = WafOwned::<WafMap>::default();
let mut diagnostics = WafOwnedDefaultAllocator::<WafMap>::default();
if !builder.add_or_update_config("rules", &rules, Some(&mut diagnostics)) {
return Err(Error::WafRulesetLoadingError(diagnostics));
}
let ruleset_version =
if let Some(version) = diagnostics.get(b"ruleset_version").and_then(|o| o.to_str()) {
debug!("aap: loaded ruleset version {version}");
version.to_string()
} else {
String::new()
};
let ruleset_version = if let Some(version) = diagnostics
.get_bstr(b"ruleset_version")
.and_then(|o| o.to_str())
{
debug!("aap: loaded ruleset version {version}");
version.to_string()
} else {
String::new()
};

let Some(handle) = builder.build() else {
return Err(Error::WafInitializationFailed);
Expand Down Expand Up @@ -299,7 +301,7 @@ pub enum Error {
WafRulesetParseError(serde_json::Error),
/// The App & API Protection ruleset could not be loaded into the WAF
#[error("aap: failed to load configured WAF ruleset: {0:?}")]
WafRulesetLoadingError(WafOwned<WafMap>),
WafRulesetLoadingError(WafOwnedDefaultAllocator<WafMap>),
/// The WAF initialization produced a [`None`] handle (this happens when the
/// configured ruleset contains no active rule nor processor)
#[error(
Expand Down
55 changes: 44 additions & 11 deletions bottlecap/src/config/propagation_wrapper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::sync::Arc;

use datadog_opentelemetry::configuration::TracePropagationBehaviorExtract;
use datadog_opentelemetry::configuration::{
TracePropagationBehaviorExtract, TracePropagationStyle as OtelTracePropagationStyle,
};
use datadog_opentelemetry::propagation::PropagationConfig;

use crate::config::{Config, TracePropagationStyle};
Expand All @@ -11,39 +13,70 @@ use crate::config::{Config, TracePropagationStyle};
/// wrapper is the local type the impl can attach to. Callers that need a
/// propagator hand it an `Arc<PropConfig>` instead of an `Arc<Config>`.
#[derive(Debug, Clone)]
pub struct PropConfig(Arc<Config>);
pub struct PropConfig {
config: Arc<Config>,
trace_propagation_style: Vec<OtelTracePropagationStyle>,
trace_propagation_style_extract: Vec<OtelTracePropagationStyle>,
}

impl PropConfig {
#[must_use]
pub fn new(config: Arc<Config>) -> Arc<Self> {
Arc::new(Self(config))
Arc::new(Self {
trace_propagation_style: config
.trace_propagation_style
.iter()
.copied()
.map(convert_trace_propagation_style)
.collect(),
trace_propagation_style_extract: config
.trace_propagation_style_extract
.iter()
.copied()
.map(convert_trace_propagation_style)
.collect(),
config,
})
}
}

const fn convert_trace_propagation_style(
style: TracePropagationStyle,
) -> OtelTracePropagationStyle {
match style {
TracePropagationStyle::Datadog => OtelTracePropagationStyle::Datadog,
TracePropagationStyle::TraceContext => OtelTracePropagationStyle::TraceContext,
TracePropagationStyle::Baggage => OtelTracePropagationStyle::Baggage,
TracePropagationStyle::B3Multi => OtelTracePropagationStyle::B3Multi,
TracePropagationStyle::B3SingleHeader => OtelTracePropagationStyle::B3SingleHeader,
TracePropagationStyle::None => OtelTracePropagationStyle::None,
}
}

impl PropagationConfig for PropConfig {
fn trace_propagation_style(&self) -> Option<&[TracePropagationStyle]> {
if self.0.trace_propagation_style.is_empty() {
fn trace_propagation_style(&self) -> Option<&[OtelTracePropagationStyle]> {
if self.trace_propagation_style.is_empty() {
None
} else {
Some(&self.0.trace_propagation_style)
Some(&self.trace_propagation_style)
}
}

fn trace_propagation_style_extract(&self) -> Option<&[TracePropagationStyle]> {
if self.0.trace_propagation_style_extract.is_empty() {
fn trace_propagation_style_extract(&self) -> Option<&[OtelTracePropagationStyle]> {
if self.trace_propagation_style_extract.is_empty() {
None
} else {
Some(&self.0.trace_propagation_style_extract)
Some(&self.trace_propagation_style_extract)
}
}

fn trace_propagation_style_inject(&self) -> Option<&[TracePropagationStyle]> {
fn trace_propagation_style_inject(&self) -> Option<&[OtelTracePropagationStyle]> {
// Bottlecap does not configure injection styles separately.
None
}

fn trace_propagation_extract_first(&self) -> bool {
self.0.trace_propagation_extract_first
self.config.trace_propagation_extract_first
}

fn trace_propagation_behavior_extract(&self) -> TracePropagationBehaviorExtract {
Expand Down
Loading
Loading