diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index 714afa77f..f491bd49c 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -48,14 +48,17 @@ rustls-pki-types = { version = "1.0", default-features = false } hyper-rustls = { version = "0.27.7", default-features = false } 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 } +# otlp-only (also pulled transitively via datadog-protos, but the OTLP gRPC server +# needs them as direct deps). Gated behind the `otlp` feature. +tonic = { version = "0.14", features = ["transport", "codegen", "server", "channel", "router"], default-features = false, optional = true } +tonic-types = { version = "0.14", default-features = false, optional = true } zstd = { version = "0.13.3", default-features = false } 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 } -opentelemetry-proto = { version = "0.31.0", features = ["trace", "with-serde", "gen-tonic"] } -opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"] } +# otlp-only. Gated behind the `otlp` feature. +opentelemetry-proto = { version = "0.31.0", features = ["trace", "with-serde", "gen-tonic"], optional = true } +opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"], optional = true } # Pinned to <0.8.3: version 0.8.3 upgraded to openssl-probe 0.2.x which scans all cert # directories and parses ~200 individual cert files on Lambda instead of loading a single # bundle file, adding ~45ms to each reqwest::Client::build() call. @@ -85,7 +88,8 @@ datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev = dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false } datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false } datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false } -libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } +# appsec-only. Gated behind the `appsec` feature. +libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"], optional = true } [dev-dependencies] figment = { version = "0.10", default-features = false, features = ["yaml", "env", "test"] } @@ -129,6 +133,8 @@ tikv-jemallocator = "0.5" [features] default = [ + "otlp", + "appsec", "reqwest/rustls-tls-native-roots", "datadog-fips/default", "datadog-agent-config/https", @@ -137,6 +143,17 @@ default = [ "libdd-trace-obfuscation/https", "libdd-trace-stats/https", ] +# OpenTelemetry Protocol (OTLP) ingest. Pulls in the heavy `tonic` / +# `opentelemetry-proto` stack. On by default so the shipped binary is unchanged. +otlp = [ + "dep:opentelemetry-proto", + "dep:opentelemetry-semantic-conventions", + "dep:tonic", + "dep:tonic-types", +] +# App & API Protection (AAP / AppSec). Pulls in the `libddwaf` WAF engine. On by +# default so the shipped binary is unchanged. +appsec = ["dep:libddwaf"] fips = [ "datadog-agent-config/fips", "libdd-common/fips", @@ -144,6 +161,11 @@ fips = [ "libdd-trace-obfuscation/fips", "libdd-trace-stats/fips", "datadog-fips/fips", + # OTLP is unconditional in the shipped binary; keep it in the FIPS feature set + # too since fips builds use --no-default-features. + "otlp", + # FIPS-mode WAF only makes sense when AppSec is compiled in. + "appsec", "libddwaf/fips", "reqwest/rustls-tls-native-roots-no-provider", "rustls/fips", diff --git a/bottlecap/src/bin/bottlecap/main.rs b/bottlecap/src/bin/bottlecap/main.rs index 440fa2f7b..8fc03444f 100644 --- a/bottlecap/src/bin/bottlecap/main.rs +++ b/bottlecap/src/bin/bottlecap/main.rs @@ -16,11 +16,16 @@ use tikv_jemallocator::Jemalloc; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; +#[cfg(not(feature = "appsec"))] +use bottlecap::AppSecProcessorStub as AppSecProcessor; +#[cfg(feature = "appsec")] +use bottlecap::appsec::processor::{ + Error::FeatureDisabled as AppSecFeatureDisabled, Processor as AppSecProcessor, +}; +#[cfg(feature = "otlp")] +use bottlecap::otlp::{agent::Agent as OtlpAgent, should_enable_otlp_agent}; use bottlecap::{ DOGSTATSD_PORT, LAMBDA_RUNTIME_SLUG, - appsec::processor::{ - Error::FeatureDisabled as AppSecFeatureDisabled, Processor as AppSecProcessor, - }, config::{ self, Config, aws::{AwsConfig, build_lambda_function_arn}, @@ -53,7 +58,6 @@ use bottlecap::{ flusher::LogsFlusher, lambda::DurableContextUpdate, }, - otlp::{agent::Agent as OtlpAgent, should_enable_otlp_agent}, proxy::{interceptor, should_start_proxy}, secrets::decrypt, tags::{ @@ -388,6 +392,7 @@ async fn extension_loop_active( }); // AppSec processor (if enabled) + #[cfg(feature = "appsec")] let appsec_processor = match AppSecProcessor::new(config) { Ok(p) => Some(Arc::new(TokioMutex::new(p))), Err(AppSecFeatureDisabled) => None, @@ -398,6 +403,10 @@ async fn extension_loop_active( None } }; + // AppSec compiled out: the processor is always absent, but the value is still + // threaded through trace/proxy wiring (as `None`) to keep call sites uniform. + #[cfg(not(feature = "appsec"))] + let appsec_processor: Option>> = None; let ( trace_agent_channel, @@ -1441,6 +1450,7 @@ async fn setup_telemetry_client( Ok(cancel_token) } +#[cfg(feature = "otlp")] fn start_otlp_agent( config: &Arc, tags_provider: Arc, @@ -1473,6 +1483,20 @@ fn start_otlp_agent( Some(cancel_token) } +/// No-op fallback when the `otlp` feature is disabled: the OTLP agent is not +/// compiled in, so there is never a cancellation token to return. +#[cfg(not(feature = "otlp"))] +#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] +fn start_otlp_agent( + _config: &Arc, + _tags_provider: Arc, + _trace_processor: Arc, + _trace_tx: Sender, + _stats_concentrator: StatsConcentratorHandle, +) -> Option { + None +} + fn start_api_runtime_proxy( config: &Arc, aws_config: Arc, diff --git a/bottlecap/src/lib.rs b/bottlecap/src/lib.rs index df94fd246..680c10a75 100644 --- a/bottlecap/src/lib.rs +++ b/bottlecap/src/lib.rs @@ -19,6 +19,7 @@ // Allow use of the `coverage_nightly` attribute #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#[cfg(feature = "appsec")] pub mod appsec; pub mod config; pub mod event_bus; @@ -31,6 +32,7 @@ pub mod logger; pub mod logs; pub mod lwa; pub mod metrics; +#[cfg(feature = "otlp")] pub mod otlp; pub mod proc; pub mod proxy; @@ -38,6 +40,16 @@ pub mod secrets; pub mod tags; pub mod traces; +/// Placeholder standing in for [`appsec::processor::Processor`] when the `appsec` +/// feature is disabled. It keeps `Option>>` fields and the proxy +/// state tuple identically shaped across both builds, so call sites that merely +/// thread the value through (always as `None`) compile unchanged. It is an +/// uninhabited enum and can never be constructed; every code path that would +/// actually invoke the WAF is itself `#[cfg(feature = "appsec")]`-gated. +#[cfg(not(feature = "appsec"))] +#[derive(Debug, Clone, Copy)] +pub enum AppSecProcessorStub {} + pub const LAMBDA_RUNTIME_SLUG: &str = "lambda"; // todo: consider making this configurable diff --git a/bottlecap/src/lifecycle/invocation/triggers/body.rs b/bottlecap/src/lifecycle/invocation/triggers/body.rs index 6d09961fd..f283f5701 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/body.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/body.rs @@ -16,6 +16,10 @@ pub struct Body { impl Body { /// Obtains a reader to the data contained in this [`Body`], decoded from /// Base64 if [`Body::is_base64_encoded`] is `true`. + /// + /// Only consumed by the App and API Protection request/response inspection + /// code, so it is dead code when the `appsec` feature is disabled. + #[cfg_attr(not(feature = "appsec"), allow(dead_code))] pub(crate) fn reader<'a>(&'a self) -> Result>, base64::DecodeError> { let Some(body) = &self.body else { return Ok(None); diff --git a/bottlecap/src/proxy/interceptor.rs b/bottlecap/src/proxy/interceptor.rs index 21a018377..0a077764e 100644 --- a/bottlecap/src/proxy/interceptor.rs +++ b/bottlecap/src/proxy/interceptor.rs @@ -1,9 +1,13 @@ +#[cfg(not(feature = "appsec"))] +use crate::AppSecProcessorStub as AppSecProcessor; +#[cfg(feature = "appsec")] +use crate::appsec::processor::Processor as AppSecProcessor; use crate::lifecycle::invocation::processor_service::InvocationProcessorHandle; +#[cfg(feature = "appsec")] use crate::lifecycle::invocation::triggers::IdentifiedTrigger; use crate::traces::propagation::DatadogCompositePropagator; use crate::{ - appsec::processor::Processor as AppSecProcessor, config::aws::AwsConfig, - extension::EXTENSION_HOST, lwa, proxy::tee_body::TeeBodyWithCompletion, + config::aws::AwsConfig, extension::EXTENSION_HOST, lwa, proxy::tee_body::TeeBodyWithCompletion, }; use axum::{ Router, @@ -179,6 +183,7 @@ async fn invocation_next_proxy( if let Ok(body) = intercepted_completion_receiver.await { debug!("PROXY | invocation_next_proxy | intercepted body completed"); + #[cfg(feature = "appsec")] if let Some(appsec_processor) = appsec_processor && let Some(request_id) = intercepted_parts_clone .headers @@ -195,6 +200,10 @@ async fn invocation_next_proxy( } } } + // AppSec compiled out: the processor is always `None`; drop it to keep + // the State tuple shape uniform without an unused-variable warning. + #[cfg(not(feature = "appsec"))] + let _ = appsec_processor; if aws_config.aws_lwa_proxy_lambda_runtime_api.is_some() { lwa::process_invocation_next( @@ -241,6 +250,7 @@ async fn invocation_response_proxy( join_set.spawn(async move { if let Ok(body) = outgoing_completion_receiver.await { debug!("PROXY | invocation_response_proxy | intercepted outgoing body completed"); + #[cfg(feature = "appsec")] if let Some(appsec_processor) = appsec_processor { appsec_processor .lock() @@ -248,6 +258,8 @@ async fn invocation_response_proxy( .process_invocation_result(&request_id, &body) .await; } + #[cfg(not(feature = "appsec"))] + let _ = appsec_processor; if aws_config_clone.aws_lwa_proxy_lambda_runtime_api.is_some() { lwa::process_invocation_response(&invocation_processor, &body).await; @@ -304,14 +316,17 @@ async fn invocation_error_proxy( request: Request, ) -> Response { debug!("PROXY | invocation_error_proxy | api_version: {api_version}, request_id: {request_id}"); - let State((_, _, _, appsec_processor, _, _)) = &state; - if let Some(appsec_processor) = appsec_processor { - // Marking any outstanding security context as finalized by sending a blank response. - appsec_processor - .lock() - .await - .process_invocation_result(&request_id, &Bytes::from("{}")) - .await; + #[cfg(feature = "appsec")] + { + let State((_, _, _, appsec_processor, _, _)) = &state; + if let Some(appsec_processor) = appsec_processor { + // Marking any outstanding security context as finalized by sending a blank response. + appsec_processor + .lock() + .await + .process_invocation_result(&request_id, &Bytes::from("{}")) + .await; + } } passthrough_proxy(state, request).await @@ -442,13 +457,16 @@ mod tests { use hyper_util::rt::TokioIo; use super::*; + #[cfg(feature = "appsec")] + use crate::appsec::processor::Error::FeatureDisabled as AppSecFeatureDisabled; use crate::lifecycle::invocation::processor_service::InvocationProcessorService; use crate::{ - LAMBDA_RUNTIME_SLUG, appsec::processor::Error::FeatureDisabled as AppSecFeatureDisabled, - config::Config, tags::provider::Provider, traces::propagation::DatadogCompositePropagator, + LAMBDA_RUNTIME_SLUG, config::Config, tags::provider::Provider, + traces::propagation::DatadogCompositePropagator, }; #[tokio::test] + #[allow(clippy::too_many_lines)] async fn test_noop_proxy() { let aws_lwa_lambda_runtime_api = "127.0.0.1:12345"; let aws_lambda_runtime_api = "127.0.0.1:12344"; @@ -513,6 +531,7 @@ mod tests { invocation_processor_service.run().await; }); + #[cfg(feature = "appsec")] let appsec_processor = match AppSecProcessor::new(&config) { Ok(p) => Some(Arc::new(TokioMutex::new(p))), Err(AppSecFeatureDisabled) => None, @@ -523,6 +542,8 @@ mod tests { None } }; + #[cfg(not(feature = "appsec"))] + let appsec_processor: Option>> = None; let proxy_handle = start( aws_config, diff --git a/bottlecap/src/traces/trace_agent.rs b/bottlecap/src/traces/trace_agent.rs index 37b71d29f..1bbfeee26 100644 --- a/bottlecap/src/traces/trace_agent.rs +++ b/bottlecap/src/traces/trace_agent.rs @@ -21,9 +21,12 @@ use tokio_util::sync::CancellationToken; use tower_http::limit::RequestBodyLimitLayer; use tracing::{debug, error, warn}; +#[cfg(not(feature = "appsec"))] +use crate::AppSecProcessorStub as AppSecProcessor; +#[cfg(feature = "appsec")] +use crate::appsec::processor::Processor as AppSecProcessor; use crate::traces::trace_processor::SendingTraceProcessor; use crate::{ - appsec::processor::Processor as AppSecProcessor, config, http::{extract_request_body, handler_not_found}, lifecycle::invocation::{ diff --git a/bottlecap/src/traces/trace_processor.rs b/bottlecap/src/traces/trace_processor.rs index 256d8bf14..ad0fa1591 100644 --- a/bottlecap/src/traces/trace_processor.rs +++ b/bottlecap/src/traces/trace_processor.rs @@ -1,7 +1,11 @@ // Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +#[cfg(not(feature = "appsec"))] +use crate::AppSecProcessorStub as AppSecProcessor; +#[cfg(feature = "appsec")] use crate::appsec::processor::Processor as AppSecProcessor; +#[cfg(feature = "appsec")] use crate::appsec::processor::context::HoldArguments; use crate::config; use crate::lifecycle::invocation::processor::S_TO_MS; @@ -522,41 +526,53 @@ impl SendingTraceProcessor { body_size: usize, span_pointers: Option>, ) -> Result<(), SendError> { - traces = if let Some(appsec) = &self.appsec { - let mut appsec = appsec.lock().await; - traces.into_iter().filter_map(|mut trace| { - let Some(span) = AppSecProcessor::service_entry_span_mut(&mut trace) else { - return Some(trace); - }; - - let (finalized, ctx) = appsec.process_span(span); - if finalized { - Some(trace) - } else if let Some(ctx) = ctx{ - debug!("TRACE_PROCESSOR | Holding trace for App & API Protection additional data"); - ctx.hold_trace(trace, SendingTraceProcessor{ appsec: None, processor: self.processor.clone(), trace_tx: self.trace_tx.clone(), stats_generator: self.stats_generator.clone() }, HoldArguments{ - config:Arc::clone(&config), - tags_provider:Arc::clone(&tags_provider), - body_size, - span_pointers:span_pointers.clone(), - tracer_header_tags_lang: header_tags.lang.to_string(), - tracer_header_tags_lang_version: header_tags.lang_version.to_string(), - tracer_header_tags_lang_interpreter: header_tags.lang_interpreter.to_string(), - tracer_header_tags_lang_vendor: header_tags.lang_vendor.to_string(), - tracer_header_tags_tracer_version: header_tags.tracer_version.to_string(), - tracer_header_tags_container_id: header_tags.container_id.to_string(), - tracer_header_tags_client_computed_top_level: header_tags.client_computed_top_level, - tracer_header_tags_client_computed_stats: header_tags.client_computed_stats, - tracer_header_tags_dropped_p0_traces: header_tags.dropped_p0_traces, - tracer_header_tags_dropped_p0_spans: header_tags.dropped_p0_spans, - }); - None + // App & API Protection span processing. Only compiled in when the `appsec` + // feature is enabled; otherwise the traces pass through untouched. The + // outer block keeps `traces` reassigned (hence `mut` used) in both builds. + traces = { + #[cfg(feature = "appsec")] + { + if let Some(appsec) = &self.appsec { + let mut appsec = appsec.lock().await; + traces.into_iter().filter_map(|mut trace| { + let Some(span) = AppSecProcessor::service_entry_span_mut(&mut trace) else { + return Some(trace); + }; + + let (finalized, ctx) = appsec.process_span(span); + if finalized { + Some(trace) + } else if let Some(ctx) = ctx{ + debug!("TRACE_PROCESSOR | Holding trace for App & API Protection additional data"); + ctx.hold_trace(trace, SendingTraceProcessor{ appsec: None, processor: self.processor.clone(), trace_tx: self.trace_tx.clone(), stats_generator: self.stats_generator.clone() }, HoldArguments{ + config:Arc::clone(&config), + tags_provider:Arc::clone(&tags_provider), + body_size, + span_pointers:span_pointers.clone(), + tracer_header_tags_lang: header_tags.lang.to_string(), + tracer_header_tags_lang_version: header_tags.lang_version.to_string(), + tracer_header_tags_lang_interpreter: header_tags.lang_interpreter.to_string(), + tracer_header_tags_lang_vendor: header_tags.lang_vendor.to_string(), + tracer_header_tags_tracer_version: header_tags.tracer_version.to_string(), + tracer_header_tags_container_id: header_tags.container_id.to_string(), + tracer_header_tags_client_computed_top_level: header_tags.client_computed_top_level, + tracer_header_tags_client_computed_stats: header_tags.client_computed_stats, + tracer_header_tags_dropped_p0_traces: header_tags.dropped_p0_traces, + tracer_header_tags_dropped_p0_spans: header_tags.dropped_p0_spans, + }); + None + } else { + Some(trace) + } + }).collect() } else { - Some(trace) + traces } - }).collect() - } else { - traces + } + #[cfg(not(feature = "appsec"))] + { + traces + } }; if traces.is_empty() {