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
663 changes: 342 additions & 321 deletions src/native/Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions src/native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ opt-level = 's'
codegen-units = 1

[features]
crashtracker = ["dep:anyhow", "dep:datadog-crashtracker"]
profiling = ["dep:datadog-profiling-ffi"]
crashtracker = ["dep:anyhow", "dep:libdd-crashtracker"]
profiling = ["dep:libdd-profiling-ffi"]

[dependencies]
anyhow = { version = "1.0", optional = true }
datadog-crashtracker = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0", optional = true }
datadog-ddsketch = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0" }
datadog-library-config = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0" }
datadog-log = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0" }
data-pipeline = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0" }
datadog-profiling-ffi = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0", optional = true, features = [
libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0", optional = true }
libdd-ddsketch = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0" }
libdd-library-config = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0" }
libdd-log = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0" }
libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0" }
libdd-profiling-ffi = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0", optional = true, features = [
"cbindgen",
] }
ddcommon = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0" }
libdd-common = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0" }
pyo3 = { version = "0.25", features = ["extension-module", "anyhow"] }
tracing = { version = "0.1", default-features = false }

[build-dependencies]
pyo3-build-config = "0.25"
build_common = { git = "https://github.com/DataDog/libdatadog", rev = "v23.0.0", features = [
build_common = { git = "https://github.com/DataDog/libdatadog", rev = "v24.0.0", features = [
"cbindgen",
] }

Expand Down
14 changes: 7 additions & 7 deletions src/native/crashtracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Once;
use std::time::Duration;

use datadog_crashtracker::{
use libdd_common::Endpoint;
use libdd_crashtracker::{
CrashtrackerConfiguration, CrashtrackerReceiverConfig, Metadata, StacktraceCollection,
};
use ddcommon::Endpoint;
use pyo3::prelude::*;

pub trait RustWrapper {
Expand All @@ -20,7 +20,7 @@ pub trait RustWrapper {
}
}

// We redefine the Enum here to expose it to Python as datadog_crashtracker::StacktraceCollection
// We redefine the Enum here to expose it to Python as libdd_crashtracker::StacktraceCollection
// is defined in an external crate.
#[pyclass(
eq,
Expand Down Expand Up @@ -92,7 +92,7 @@ impl CrashtrackerConfigurationPy {
use_alt_stack,
endpoint,
resolve_frames,
datadog_crashtracker::default_signals(),
libdd_crashtracker::default_signals(),
Some(Duration::from_millis(timeout_ms)),
unix_socket_path,
true, /* demangle_names */
Expand Down Expand Up @@ -235,7 +235,7 @@ pub fn crashtracker_init<'py>(
if let (Some(config), Some(receiver_config), Some(metadata)) =
(config_opt, receiver_config_opt, metadata_opt)
{
match datadog_crashtracker::init(config, receiver_config, metadata) {
match libdd_crashtracker::init(config, receiver_config, metadata) {
Ok(_) => CRASHTRACKER_STATUS
.store(CrashtrackerStatus::Initialized as u8, Ordering::SeqCst),
Err(e) => {
Expand Down Expand Up @@ -269,7 +269,7 @@ pub fn crashtracker_on_fork<'py>(

// Note to self: is it possible to call crashtracker_on_fork before crashtracker_init?
// dd-trace-py seems to start crashtracker early on.
datadog_crashtracker::on_fork(inner_config, inner_receiver_config, inner_metadata)
libdd_crashtracker::on_fork(inner_config, inner_receiver_config, inner_metadata)
}

#[pyfunction(name = "crashtracker_status")]
Expand All @@ -286,5 +286,5 @@ pub fn crashtracker_status() -> anyhow::Result<CrashtrackerStatus> {
// binary names for the receiver, since Python installs the script as a command.
#[pyfunction(name = "crashtracker_receiver")]
pub fn crashtracker_receiver() -> anyhow::Result<()> {
datadog_crashtracker::receiver_entry_point_stdin()
libdd_crashtracker::receiver_entry_point_stdin()
}
2 changes: 1 addition & 1 deletion src/native/data_pipeline/exceptions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use data_pipeline::trace_exporter::error::TraceExporterError;
use libdd_data_pipeline::trace_exporter::error::TraceExporterError;
use pyo3::{create_exception, exceptions::PyException, prelude::*, PyErr};

create_exception!(
Expand Down
2 changes: 1 addition & 1 deletion src/native/data_pipeline/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use data_pipeline::trace_exporter::{
use libdd_data_pipeline::trace_exporter::{
agent_response::AgentResponse, TelemetryConfig, TraceExporter, TraceExporterBuilder,
TraceExporterInputFormat, TraceExporterOutputFormat,
};
Expand Down
2 changes: 1 addition & 1 deletion src/native/ddsketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;

use datadog_ddsketch::DDSketch;
use libdd_ddsketch::DDSketch;

#[pyclass(name = "DDSketch", module = "ddtrace.internal._native")]
pub struct DDSketchPy {
Expand Down
6 changes: 3 additions & 3 deletions src/native/library_config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use datadog_library_config::{
use libdd_library_config::{
tracer_metadata::{store_tracer_metadata, AnonymousFileHandle, TracerMetadata},
Configurator, ProcessInfo,
};
Expand Down Expand Up @@ -42,7 +42,7 @@ impl PyConfigurator {
&ProcessInfo::detect_global("python".to_string()),
);
match res_config {
datadog_library_config::LoggedResult::Ok(config, logs) => {
libdd_library_config::LoggedResult::Ok(config, logs) => {
// Previously, `libdatadog` printed debug logs to stderr. However,
// in v21.0.0, we changed the behavior to buffer them and return
// them in the logs returned by this `LoggedResult`.
Expand All @@ -60,7 +60,7 @@ impl PyConfigurator {
}
Ok(list.into())
}
datadog_library_config::LoggedResult::Err(e) => {
libdd_library_config::LoggedResult::Err(e) => {
let err_msg = format!("Failed to get configuration: {e:?}");
Err(PyException::new_err(err_msg))
}
Expand Down
2 changes: 1 addition & 1 deletion src/native/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub mod logger {
use pyo3::types::PyDict;
use pyo3::{exceptions::PyValueError, PyResult};

use datadog_log::logger::{
use libdd_log::logger::{
logger_configure_file, logger_configure_std, logger_disable_file, logger_disable_std,
logger_set_log_level, FileConfig, LogEventLevel, StdConfig, StdTarget,
};
Expand Down
10 changes: 8 additions & 2 deletions tests/appsec/iast/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,18 @@ def iast_context_defaults():

@pytest.fixture
def iast_context_deduplication_enabled(tracer):
yield from iast_context(dict(DD_IAST_ENABLED="true"), deduplication=True, vulnerabilities_per_requests=2)
yield from iast_context(
dict(DD_IAST_ENABLED="true", DD_IAST_REQUEST_SAMPLING="100.0"),
deduplication=True,
vulnerabilities_per_requests=2,
)


@pytest.fixture
def iast_context_2_vulnerabilities_per_requests(tracer):
yield from iast_context(dict(DD_IAST_ENABLED="true"), vulnerabilities_per_requests=2)
yield from iast_context(
dict(DD_IAST_ENABLED="true", DD_IAST_REQUEST_SAMPLING="100.0"), vulnerabilities_per_requests=2
)


@pytest.fixture
Expand Down
12 changes: 11 additions & 1 deletion tests/appsec/iast/taint_tracking/test_native_taint_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,13 +602,23 @@ def test_context_race_conditions_threads(caplog, telemetry_writer):
destroying contexts
"""
_end_iast_context_and_oce()
# Clear telemetry logs from previous tests
telemetry_writer._logs.clear()

pool = ThreadPool(processes=3)
results_async = [pool.apply_async(reset_contexts_loop) for _ in range(20)]
results = [res.get() for res in results_async]
pool.close()
pool.join()

assert results.count(True) <= 2
log_messages = [record.message for record in caplog.get_records("call")]
assert len([message for message in log_messages if IAST_VALID_LOG.search(message)]) == 0
list_metrics_logs = list(telemetry_writer._logs)

# Filter out telemetry connection errors which are expected in test environment
list_metrics_logs = [
log for log in telemetry_writer._logs if not log["message"].startswith("failed to send, dropping")
]
assert len(list_metrics_logs) == 0


Expand Down
74 changes: 65 additions & 9 deletions tests/appsec/iast/test_overhead_control_engine.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from time import sleep

from ddtrace.appsec._iast._iast_env import _get_iast_env
from ddtrace.appsec._iast._iast_request_context import get_iast_reporter
from ddtrace.appsec._iast._iast_request_context_base import is_iast_request_enabled
from ddtrace.appsec._iast._taint_tracking._context import clear_all_request_context_slots
from ddtrace.appsec._iast._taint_tracking._context import debug_context_array_free_slots_number
from ddtrace.appsec._iast._taint_tracking._context import debug_context_array_size
from ddtrace.appsec._iast._taint_tracking._context import finish_request_context
from ddtrace.appsec._iast._taint_tracking._context import start_request_context
from ddtrace.appsec._iast.sampling.vulnerability_detection import reset_request_vulnerabilities
from ddtrace.appsec._iast.taint_sinks.weak_hash import WeakHash
from ddtrace.internal.settings.asm import config as asm_config


Expand Down Expand Up @@ -46,30 +49,83 @@ def function_with_vulnerabilities_1(tracer):
def test_oce_max_vulnerabilities_per_request(iast_context_deduplication_enabled):
import hashlib

# Reset deduplication cache to ensure clean state
WeakHash._prepare_report._reset_cache()

# Verify IAST context is enabled
assert is_iast_request_enabled(), "IAST request context should be enabled"

m = hashlib.md5()
m.update(b"Nobody inspects")
m.digest()
m.digest()
m.digest()
m.digest()
# Each digest() call must be on a different line to avoid deduplication
result1 = m.digest() # vulnerability 1
result2 = m.digest() # vulnerability 2
result3 = m.digest() # This should not be reported (exceeds max)
result4 = m.digest() # This should not be reported (exceeds max)

# Ensure all digest calls completed
assert result1 is not None and result2 is not None and result3 is not None and result4 is not None

span_report = get_iast_reporter()
if span_report is None:
# Debug: check if any vulnerabilities were attempted
env = _get_iast_env()
if env:
print(
f"DEBUG: vulnerability_budget={env.vulnerability_budget}, "
f"vulnerabilities_request_limit={env.vulnerabilities_request_limit}"
)
assert False, (
f"IAST reporter should be initialized after vulnerability detection. "
f"IAST enabled: {is_iast_request_enabled()}, env: {env is not None}"
)

assert len(span_report.vulnerabilities) == asm_config._iast_max_vulnerabilities_per_requests


def test_oce_reset_vulnerabilities_report(iast_context_deduplication_enabled):
import hashlib

# Reset deduplication cache to ensure clean state
WeakHash._prepare_report._reset_cache()

# Verify IAST context is enabled
assert is_iast_request_enabled(), "IAST request context should be enabled"

m = hashlib.md5()
m.update(b"Nobody inspects")
m.digest()
m.digest()
m.digest()
reset_request_vulnerabilities()
m.digest()
# Each digest() call must be on a different line to avoid deduplication
result1 = m.digest() # vulnerability 1
result2 = m.digest() # vulnerability 2
result3 = m.digest() # This should not be reported (exceeds max)

# Ensure all digest calls completed
assert result1 is not None and result2 is not None and result3 is not None

# Ensure reporter exists before reset
span_report = get_iast_reporter()
if span_report is None:
# Debug: check if any vulnerabilities were attempted
env = _get_iast_env()
if env:
print(
f"DEBUG: vulnerability_budget={env.vulnerability_budget}, "
f"vulnerabilities_request_limit={env.vulnerabilities_request_limit}"
)
assert (
False
), f"IAST reporter should exist before reset. IAST enabled: {is_iast_request_enabled()}, env: {env is not None}"

initial_count = len(span_report.vulnerabilities)
assert initial_count == asm_config._iast_max_vulnerabilities_per_requests

reset_request_vulnerabilities()
result4 = m.digest() # vulnerability 3 (after reset)
assert result4 is not None

span_report = get_iast_reporter()
assert span_report is not None, "IAST reporter should still exist after reset"
# After reset, we should have the original 2 vulnerabilities + 1 new one = 3 total
assert len(span_report.vulnerabilities) == asm_config._iast_max_vulnerabilities_per_requests + 1


Expand Down
38 changes: 30 additions & 8 deletions tests/appsec/iast/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,34 +174,49 @@ def test_metric_request_tainted(no_request_sampling, telemetry_writer):
assert filtered_metrics == ["executed.source", "request.tainted"]
assert len(filtered_metrics) == 2, "Expected 2 generate_metrics"
assert span.get_metric(IAST_SPAN_TAGS.TELEMETRY_REQUEST_TAINTED) > 0
assert span.get_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SOURCE + ".http_request_parameter") > 0


def test_log_metric(telemetry_writer):
with override_global_config(dict(_iast_debug=True)):
# Clear any existing logs first
telemetry_writer._logs.clear()
# Reset the deduplication cache to ensure clean state
_set_iast_error_metric._reset_cache()

with override_global_config(
dict(_iast_enabled=True, _iast_debug=True, _iast_deduplication_enabled=False, _iast_request_sampling=100.0)
):
_set_iast_error_metric("test_format_key_error_and_no_log_metric raises")

list_metrics_logs = list(telemetry_writer._logs)
assert len(list_metrics_logs) == 1
assert len(list_metrics_logs) == 1, f"Expected 1 log entry, got {len(list_metrics_logs)}"
assert list_metrics_logs[0]["message"] == "test_format_key_error_and_no_log_metric raises"
assert "stack_trace" not in list_metrics_logs[0].keys()


def test_log_metric_debug_disabled(telemetry_writer):
with override_global_config(dict(_iast_debug=False)):
with override_global_config(
dict(_iast_enabled=True, _iast_debug=False, _iast_deduplication_enabled=False, _iast_request_sampling=100.0)
):
_set_iast_error_metric("test_log_metric_debug_disabled raises")

list_metrics_logs = list(telemetry_writer._logs)
assert len(list_metrics_logs) == 0


def test_log_metric_debug_deduplication(telemetry_writer):
with override_global_config(dict(_iast_debug=True)):
# Clear any existing logs first
telemetry_writer._logs.clear()
# Reset the deduplication cache to ensure clean state
_set_iast_error_metric._reset_cache()

with override_global_config(
dict(_iast_enabled=True, _iast_debug=True, _iast_deduplication_enabled=False, _iast_request_sampling=100.0)
):
for i in range(10):
_set_iast_error_metric("test_log_metric_debug_deduplication raises 2")

list_metrics_logs = list(telemetry_writer._logs)
assert len(list_metrics_logs) == 1
assert len(list_metrics_logs) == 1, f"Expected 1 log entry, got {len(list_metrics_logs)}"
assert list_metrics_logs[0]["message"] == "test_log_metric_debug_deduplication raises 2"
assert "stack_trace" not in list_metrics_logs[0].keys()

Expand All @@ -216,12 +231,19 @@ def test_log_metric_debug_disabled_deduplication(telemetry_writer):


def test_log_metric_debug_deduplication_different_messages(telemetry_writer):
with override_global_config(dict(_iast_debug=True)):
# Clear any existing logs first
telemetry_writer._logs.clear()
# Reset the deduplication cache to ensure clean state
_set_iast_error_metric._reset_cache()

with override_global_config(
dict(_iast_enabled=True, _iast_debug=True, _iast_deduplication_enabled=False, _iast_request_sampling=100.0)
):
for i in range(10):
_set_iast_error_metric(f"test_log_metric_debug_deduplication_different_messages raises {i}")

list_metrics_logs = list(telemetry_writer._logs)
assert len(list_metrics_logs) == 10
assert len(list_metrics_logs) == 10, f"Expected 10 log entries, got {len(list_metrics_logs)}"
assert list_metrics_logs[0]["message"].startswith(
"test_log_metric_debug_deduplication_different_messages raises"
)
Expand Down
Loading
Loading