Skip to content
Open
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
20 changes: 19 additions & 1 deletion appsec/helper-rust/src/client/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ pub struct RaspRuleMetrics {

/// Total number of RASP rule timeouts
pub timeouts: u32,

/// Duration of each individual libddwaf call, for the rasp.rule.duration
/// distribution. Unlike rasp.duration, which is the per-request cumulative
/// sum, this metric records one observation per call.
pub durations: Vec<Duration>,
}

impl WafMetrics {
Expand Down Expand Up @@ -180,6 +185,7 @@ impl WafMetrics {
.entry((rule_type.to_string(), rule_variant.to_string()))
.or_default();
entry.evals += 1;
entry.durations.push(run_output.duration());
if run_output.has_events() {
if run_output.is_blocking() {
entry.matches_blocked += 1;
Expand Down Expand Up @@ -234,8 +240,11 @@ impl telemetry::TelemetryMetricsGenerator for WafMetrics {
// RFC-1012: all boolean tags must be emitted regardless of value.
let mut tags = base_tags.clone();
tags.add("rule_triggered", bool_tag(self.had_triggers));
// block_failure is not tracked: the PHP layer is assumed to always succeed at blocking.
// The PHP layer is assumed to always succeed at blocking.
// Therefore request_blocked == "WAF requested a block" == "block succeeded".
if self.request_blocked {
tags.add("block_failure", "false");
}
// request_excluded is not tracked: libddwaf applies exclusion filters internally and
// does not expose whether a request was excluded in RunOutput.
tags.add("request_blocked", bool_tag(self.request_blocked));
Expand Down Expand Up @@ -306,6 +315,15 @@ impl telemetry::TelemetryMetricsGenerator for WafMetrics {
);
}

// rasp.rule.duration distribution: one observation per libddwaf call, in microseconds
for duration in &metrics.durations {
submitter.submit_metric(
telemetry::RASP_RULE_DURATION_DIST,
duration.as_micros() as f64,
tags.clone(),
);
}

// tests expect this to always be sent, even if 0
submitter.submit_metric(telemetry::RASP_TIMEOUT, metrics.timeouts as f64, tags);
}
Expand Down
5 changes: 5 additions & 0 deletions appsec/helper-rust/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub const WAF_ERROR: MetricName = MetricName("waf.error");
pub const WAF_DURATION_DIST: MetricName = MetricName("waf.duration");
pub const RASP_DURATION_DIST: MetricName = MetricName("rasp.duration");
pub const RASP_RULE_EVAL: MetricName = MetricName("rasp.rule.eval");
pub const RASP_RULE_DURATION_DIST: MetricName = MetricName("rasp.rule.duration");
pub const RASP_RULE_MATCH: MetricName = MetricName("rasp.rule.match");
pub const RASP_TIMEOUT: MetricName = MetricName("rasp.timeout");
pub const RASP_ERROR: MetricName = MetricName("rasp.error");
Expand Down Expand Up @@ -136,6 +137,10 @@ pub const KNOWN_METRICS: &[KnownMetric] = &[
name: RASP_DURATION_DIST,
metric_type: ddog_MetricType_DDOG_METRIC_TYPE_DISTRIBUTION,
},
KnownMetric {
name: RASP_RULE_DURATION_DIST,
metric_type: ddog_MetricType_DDOG_METRIC_TYPE_DISTRIBUTION,
},
KnownMetric {
name: RASP_TIMEOUT,
metric_type: ddog_MetricType_DDOG_METRIC_TYPE_COUNT,
Expand Down
7 changes: 5 additions & 2 deletions appsec/src/extension/commands_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,11 @@ void dd_command_process_meta(mpack_node_t root, zend_object *nonnull span)
key_str, key_len, val_str, val_len);
}

if (has_schemas && !get_DD_APM_TRACING_ENABLED()) {
dd_trace_emit_asm_event();
if (has_schemas) {
dd_telemetry_note_schema_extracted();
if (!get_DD_APM_TRACING_ENABLED()) {
dd_trace_emit_asm_event();
}
}
}

Expand Down
22 changes: 13 additions & 9 deletions appsec/src/extension/ddappsec.c
Original file line number Diff line number Diff line change
Expand Up @@ -573,12 +573,6 @@ PHP_FUNCTION(datadog_appsec_push_addresses)
RETURN_FALSE;
}

if (!dd_req_lifecycle_is_active()) {
mlog_g(dd_log_info,
"Not running inside a tracked request; skipping push_addresses");
RETURN_FALSE;
}

zval *addresses;
zend_string *rasp_rule = NULL;
zend_string *rule_variant = NULL;
Expand All @@ -587,12 +581,22 @@ PHP_FUNCTION(datadog_appsec_push_addresses)
RETURN_FALSE;
}

if (rasp_rule && ZSTR_LEN(rasp_rule) > 0 &&
!get_global_DD_APPSEC_RASP_ENABLED()) {
bool is_rasp = rasp_rule != NULL && ZSTR_LEN(rasp_rule) > 0;

if (is_rasp && !get_global_DD_APPSEC_RASP_ENABLED()) {
mlog(dd_log_debug, "RASP is not enabled; skipping push_addresses");
RETURN_FALSE;
}

if (!dd_req_lifecycle_is_active()) {
mlog_g(dd_log_info,
"Not running inside a tracked request; skipping push_addresses");
if (is_rasp) {
dd_telemetry_add_rasp_rule_skipped(rasp_rule, rule_variant);
}
RETURN_FALSE;
}

dd_conn *conn = dd_helper_mgr_cur_conn();
if (conn == NULL) {
mlog_g(dd_log_debug, "No connection; skipping push_addresses");
Expand All @@ -606,7 +610,7 @@ PHP_FUNCTION(datadog_appsec_push_addresses)
dd_result res =
dd_request_exec(conn, Z_ARRVAL_P(addresses), &opts, &block_params);

if (opts.rasp_rule && ZSTR_LEN(opts.rasp_rule) > 0) {
if (is_rasp) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why would you calculate this when DD_APPSEC_RASP_ENABLED is false?

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.

If DD_APPSEC_RASP_ENABLED is false and is_rasp is true, we will have returned on line 588. So add neither the metric on the next line (613) nor the new one I add on line 595. Does this answer your question?

dd_duration_rasp_ext_account(&start);
} else {
dd_duration_waf_ext_account(&start);
Expand Down
2 changes: 2 additions & 0 deletions appsec/src/extension/php_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ extern zend_string *zend_empty_string;
#if PHP_VERSION_ID < 70300
extern const HashTable zend_empty_array;

# define zend_hash_find_ex(ht, key, known) zend_hash_find(ht, key)

# define GC_ADDREF(x) (++GC_REFCOUNT(x))
# define GC_DELREF(x) (--GC_REFCOUNT(x))
static zend_always_inline void _gc_try_addref(zend_refcounted_h *_rc)
Expand Down
36 changes: 29 additions & 7 deletions appsec/src/extension/request_lifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ static void _set_cur_span(zend_object *nullable span);
static void _reset_globals(void);
const zend_array *nonnull _get_server_equiv(
const zend_array *nonnull superglob_equiv);
static uint64_t _calc_sampling_key(zend_object *root_span, int status_code);
static uint64_t _calc_sampling_key(zend_object *root_span, int status_code,
dd_api_sec_outcome *nonnull outcome);
static void _register_testing_objects(void);

static bool _enabled_user_req;
Expand Down Expand Up @@ -388,14 +389,16 @@ static void _do_request_finish_php(bool ignore_verdict)

if (conn && DDAPPSEC_G(active)) {
const int status_code = SG(sapi_headers).http_response_code;
dd_api_sec_outcome api_sec_outcome;
ctx = (struct req_shutdown_info){
.req_info.root_span = dd_req_lifecycle_get_cur_span(),
.req_info.client_ip = dd_req_lifecycle_get_client_ip(),
.status_code = status_code,
.resp_headers_fmt = RESP_HEADERS_LLIST,
.resp_headers_llist = &SG(sapi_headers).headers,
.entity = dd_response_body_buffered(),
.api_sec_samp_key = _calc_sampling_key(_cur_req_span, status_code),
.api_sec_samp_key = _calc_sampling_key(
_cur_req_span, status_code, &api_sec_outcome),
};

struct timespec shutdown_start = dd_monotime_start();
Expand All @@ -412,6 +415,8 @@ static void _do_request_finish_php(bool ignore_verdict)
mlog_g(dd_log_info, "request shutdown failed: %s",
dd_result_to_string(res));
}

dd_telemetry_add_api_security_request(_cur_req_span, api_sec_outcome);
}

dd_helper_rshutdown();
Expand All @@ -438,14 +443,16 @@ static zend_array *_do_request_finish_user_req(bool ignore_verdict,
struct req_shutdown_info ctx = {0};

if (conn && DDAPPSEC_G(active)) {
dd_api_sec_outcome api_sec_outcome;
ctx = (struct req_shutdown_info){
.req_info.root_span = dd_req_lifecycle_get_cur_span(),
.req_info.client_ip = dd_req_lifecycle_get_client_ip(),
.status_code = status_code,
.resp_headers_fmt = RESP_HEADERS_MAP_STRING_LIST,
.resp_headers_arr = resp_headers ? resp_headers : &zend_empty_array,
.entity = entity,
.api_sec_samp_key = _calc_sampling_key(_cur_req_span, status_code),
.api_sec_samp_key = _calc_sampling_key(
_cur_req_span, status_code, &api_sec_outcome),
};

struct timespec shutdown_start = dd_monotime_start();
Expand All @@ -462,6 +469,8 @@ static zend_array *_do_request_finish_user_req(bool ignore_verdict,
mlog_g(dd_log_info, "request shutdown failed: %s",
dd_result_to_string(res));
}

dd_telemetry_add_api_security_request(_cur_req_span, api_sec_outcome);
}

dd_helper_rshutdown();
Expand Down Expand Up @@ -1003,8 +1012,11 @@ static inline uint64_t _hash_zend_string(
return _hash_string(hash, ZSTR_VAL(str), ZSTR_LEN(str));
}

static uint64_t _calc_sampling_key(zend_object *root_span, int status_code)
static uint64_t _calc_sampling_key(zend_object *root_span, int status_code,
dd_api_sec_outcome *nonnull outcome)
{
*outcome = DD_API_SEC_SKIP;

if (!get_DD_API_SECURITY_ENABLED()) {
return 0;
}
Expand Down Expand Up @@ -1079,14 +1091,16 @@ static uint64_t _calc_sampling_key(zend_object *root_span, int status_code)
}

if (!route_or_endpoint) {
goto error;
goto missing_route;
}

zval *method =
zend_hash_str_find(Z_ARRVAL_P(meta), ZEND_STRL("http.method"));
if (!method || Z_TYPE_P(method) != IS_STRING) {
mlog_g(dd_log_debug, "No http.method tag; not sampling");
goto error;
// we treat the absence of http.method also as a missing route, because
// it also prevents schema extraction and it's sort of part of the route
goto missing_route;
}

// use fnv-1a hash with: <route_or_endpoint> NULL <http.method tag> NULL
Expand All @@ -1113,9 +1127,17 @@ static uint64_t _calc_sampling_key(zend_object *root_span, int status_code)
if (free_route_or_endpoint) {
zend_string_release(route_or_endpoint);
}
*outcome = DD_API_SEC_EVALUATED;
return hash;

error:
missing_route:
// Neither the route nor a stand-in for it could be determined. 404s are
// excluded: an endpoint that does not exist has no route to speak of, so
// counting it would be misleading
if (status_code != HTTP_NOT_FOUND) {
*outcome = DD_API_SEC_MISSING_ROUTE;
}

if (free_route_or_endpoint) {
zend_string_release(route_or_endpoint);
}
Expand Down
Loading
Loading