Skip to content

Commit 663d0f6

Browse files
Merge branch 'master' into rachel.yang/otel-logs
2 parents 5a235e6 + 09ec639 commit 663d0f6

6 files changed

Lines changed: 162 additions & 9 deletions

File tree

.gitlab/generate-appsec.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
variables:
3030
FF_ENABLE_BASH_EXIT_CODE_CHECK: "true"
3131
FF_USE_NEW_BASH_EVAL_STRATEGY: "true"
32+
MAVEN_REPOSITORY_PROXY: "https://depot-read-api-java.us1.ddbuild.io/magicmirror/magicmirror/@current/"
3233
CI_REGISTRY_USER:
3334
value: ""
3435
description: "Your docker hub username"

appsec/helper-rust/src/client/metrics.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,18 @@ pub struct WafMetrics {
7676

7777
#[derive(Default, Debug, Clone)]
7878
pub struct RaspRuleMetrics {
79+
/// Total number of RASP rule evaluations, whether they matched or not
7980
pub evals: u32,
80-
pub matches: u32,
81+
82+
/// Matches whose RASP rule did not request a block (on_match had no block action,
83+
/// or the rule was monitor-only). Emitted as rasp.rule.match with `block:irrelevant`.
84+
pub matches_irrelevant: u32,
85+
86+
/// Matches whose RASP rule requested a block. PHP cannot fail to block once it
87+
/// decides to, so every such match counts as `block:success` (no `block:failure`).
88+
pub matches_blocked: u32,
89+
90+
/// Total number of RASP rule timeouts
8191
pub timeouts: u32,
8292
}
8393

@@ -144,7 +154,11 @@ impl WafMetrics {
144154
.or_default();
145155
entry.evals += 1;
146156
if run_output.has_events() {
147-
entry.matches += 1;
157+
if run_output.is_blocking() {
158+
entry.matches_blocked += 1;
159+
} else {
160+
entry.matches_irrelevant += 1;
161+
}
148162
}
149163
if run_output.timeout() {
150164
entry.timeouts += 1;
@@ -250,11 +264,23 @@ impl telemetry::TelemetryMetricsGenerator for WafMetrics {
250264
);
251265
}
252266

253-
if metrics.matches > 0 {
267+
if metrics.matches_irrelevant > 0 {
268+
let mut match_tags = tags.clone();
269+
match_tags.add("block", "irrelevant");
254270
submitter.submit_metric(
255271
telemetry::RASP_RULE_MATCH,
256-
metrics.matches as f64,
257-
tags.clone(),
272+
metrics.matches_irrelevant as f64,
273+
match_tags,
274+
);
275+
}
276+
277+
if metrics.matches_blocked > 0 {
278+
let mut match_tags = tags.clone();
279+
match_tags.add("block", "success");
280+
submitter.submit_metric(
281+
telemetry::RASP_RULE_MATCH,
282+
metrics.matches_blocked as f64,
283+
match_tags,
258284
);
259285
}
260286

appsec/tests/integration/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ plugins {
1414
}
1515

1616
repositories {
17-
mavenCentral()
17+
def proxy = System.getenv('MAVEN_REPOSITORY_PROXY')
18+
if (proxy) {
19+
maven { url proxy }
20+
} else {
21+
mavenCentral()
22+
}
1823
}
1924

2025
java {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1+
pluginManagement {
2+
repositories {
3+
def proxy = System.getenv('MAVEN_REPOSITORY_PROXY')
4+
if (proxy) {
5+
maven { url proxy }
6+
} else {
7+
gradlePluginPortal()
8+
}
9+
}
10+
}
11+
112
rootProject.name = 'dd-appsec-php-integration'

appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/AppSecContainer.groovy

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class AppSecContainer<SELF extends AppSecContainer<SELF>> extends GenericContain
5656
.connectTimeout(Duration.ofSeconds(5))
5757
.build()
5858

59-
private MockDatadogAgent mockDatadogAgent = new MockDatadogAgent()
59+
private MockDatadogAgent mockDatadogAgent = new MockDatadogAgent()
6060

6161
AppSecContainer(Map options) {
6262
super(imageNameFuture(options))
@@ -190,7 +190,7 @@ class AppSecContainer<SELF extends AppSecContainer<SELF>> extends GenericContain
190190
JsonOutput.toJson(it.value).getBytes(StandardCharsets.UTF_8)
191191
]
192192
}
193-
long newVersion = Instant.now().epochSecond
193+
long newVersion = nextRCVersion
194194
def rcr = new RemoteConfigResponse()
195195
rcr.clientConfigs = files.keySet() as List
196196
rcr.targetFiles = encodedFiles.collect {
@@ -243,7 +243,7 @@ class AppSecContainer<SELF extends AppSecContainer<SELF>> extends GenericContain
243243
*/
244244
Supplier<RemoteConfigRequest> applyRemoteConfigRaw(Target target, Map<String, byte[]> files) {
245245
Map<String, byte[]> encodedFiles = files.findAll { it.value != null }
246-
long newVersion = Instant.now().epochSecond
246+
long newVersion = nextRCVersion
247247
def rcr = new RemoteConfigResponse()
248248
rcr.clientConfigs = files.keySet() as List
249249
rcr.targetFiles = encodedFiles.collect {
@@ -286,6 +286,11 @@ class AppSecContainer<SELF extends AppSecContainer<SELF>> extends GenericContain
286286
5_000 - (Math.max(0, System.currentTimeMillis() - start))) }
287287
}
288288

289+
// The next version number for the new RC. Using the timestamp helpers troubleshooting
290+
private static long getNextRCVersion() {
291+
Instant.now().toEpochMilli()
292+
}
293+
289294
void flushProfilingData() {
290295
if (!System.getProperty('USE_HELPER_RUST_COVERAGE')) {
291296
return

appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/TelemetryTests.groovy

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,111 @@ class TelemetryTests {
11441144
assert 'rate_limited:true' in wafReqRateLimited.tags
11451145
}
11461146

1147+
/**
1148+
* Verifies that appsec.rasp.rule.match carries the `block` tag with the correct value:
1149+
* - block:irrelevant → RASP rule matched but `on_match` did not request a block
1150+
* - block:success → RASP rule matched, block was requested and (per PHP semantics)
1151+
* always succeeds; PHP cannot fail to block once it decides to.
1152+
*
1153+
* Cross-tracer spec (see dd-trace-go, dd-trace-py): the tag is always emitted on
1154+
* rasp.rule.match. PHP never emits block:failure because the layer is assumed to
1155+
* always succeed at terminating the script.
1156+
*
1157+
* Only applies to the Rust helper.
1158+
*/
1159+
@Test
1160+
@Order(12)
1161+
void 'rasp rule match has block tag'() {
1162+
Assumptions.assumeTrue(System.getProperty('USE_HELPER_RUST') != null,
1163+
'block tag on rasp.rule.match is only implemented on the Rust helper')
1164+
1165+
try {
1166+
// Phase 1: non-blocking RASP rule match (recommended.json lfi/ssrf rules have
1167+
// on_match: ["stack_trace"], so a match does not block). Expect block:irrelevant.
1168+
Supplier<RemoteConfigRequest> requestSup = CONTAINER.applyRemoteConfig(RC_TARGET, [
1169+
'datadog/2/ASM_FEATURES/asm_features_activation/config': [
1170+
asm: [enabled: true]
1171+
]
1172+
])
1173+
1174+
CONTAINER.traceFromRequest('/hello.php') { HttpResponse<InputStream> resp ->
1175+
assert resp.statusCode() == 200
1176+
}
1177+
assert requestSup.get() != null
1178+
1179+
CONTAINER.traceFromRequest(
1180+
'/multiple_rasp.php?path=../somefile&other=../otherfile&domain=169.254.169.254') {
1181+
HttpResponse<InputStream> resp -> assert resp.statusCode() == 200
1182+
}
1183+
1184+
// Phase 2: override the LFI rule on_match to ["block"]. The first @fopen in
1185+
// multiple_rasp.php hits an LFI match, the WAF returns block_request, and the
1186+
// PHP layer terminates the request. Expect block:success on the rasp.rule.match
1187+
// emitted for rule_type:lfi from that request.
1188+
Supplier<RemoteConfigRequest> overrideSup = CONTAINER.applyRemoteConfig(RC_TARGET, [
1189+
'datadog/2/ASM_FEATURES/asm_features_activation/config': [
1190+
asm: [enabled: true]
1191+
],
1192+
'datadog/2/ASM/rasp_lfi_block_override/config': [
1193+
rules_override: [[
1194+
rules_target: [[rule_id: 'rasp-001-001']],
1195+
on_match: ['block']
1196+
]]
1197+
]
1198+
])
1199+
assert overrideSup.get() != null
1200+
1201+
// Trigger the blocking LFI; status code is 403 because the rule now blocks.
1202+
HttpRequest blockingReq = CONTAINER.buildReq(
1203+
'/multiple_rasp.php?path=../somefile&other=../otherfile&domain=169.254.169.254')
1204+
.GET().build()
1205+
CONTAINER.traceFromRequest(blockingReq, ofString()) { HttpResponse<String> resp ->
1206+
assert resp.statusCode() == 403
1207+
}
1208+
1209+
TelemetryHelpers.Metric lfiMatchIrrelevant
1210+
TelemetryHelpers.Metric lfiMatchSuccess
1211+
1212+
TelemetryHelpers.waitForMetrics(CONTAINER, 30) { List<TelemetryHelpers.GenerateMetrics> messages ->
1213+
def allSeries = messages.collectMany { it.series }
1214+
lfiMatchIrrelevant = lfiMatchIrrelevant ?: allSeries.find {
1215+
it.name == 'rasp.rule.match' &&
1216+
'rule_type:lfi' in it.tags &&
1217+
'block:irrelevant' in it.tags
1218+
}
1219+
lfiMatchSuccess = lfiMatchSuccess ?: allSeries.find {
1220+
it.name == 'rasp.rule.match' &&
1221+
'rule_type:lfi' in it.tags &&
1222+
'block:success' in it.tags
1223+
}
1224+
lfiMatchIrrelevant != null && lfiMatchSuccess != null
1225+
}
1226+
1227+
assert lfiMatchIrrelevant != null :
1228+
'rasp.rule.match metric with block:irrelevant not found — ' +
1229+
'helper must emit block:irrelevant when the matched RASP rule has no block action'
1230+
assert lfiMatchIrrelevant.namespace == 'appsec'
1231+
assert lfiMatchIrrelevant.type == 'count'
1232+
assert lfiMatchIrrelevant.points[0][1] >= 1.0d
1233+
1234+
assert lfiMatchSuccess != null :
1235+
'rasp.rule.match metric with block:success not found — ' +
1236+
'helper must emit block:success when the matched RASP rule triggered a block'
1237+
assert lfiMatchSuccess.namespace == 'appsec'
1238+
assert lfiMatchSuccess.type == 'count'
1239+
assert lfiMatchSuccess.points[0][1] >= 1.0d
1240+
} finally {
1241+
// Drop the override so subsequent ordered tests in this class run with the
1242+
// default (non-blocking) RASP ruleset, even if assertions above failed.
1243+
CONTAINER.applyRemoteConfig(RC_TARGET, [
1244+
'datadog/2/ASM_FEATURES/asm_features_activation/config': [
1245+
asm: [enabled: true]
1246+
],
1247+
'datadog/2/ASM/rasp_lfi_block_override/config': null
1248+
])
1249+
}
1250+
}
1251+
11471252
@Test
11481253
@Order(21)
11491254
void 'waf config errors emitted with action init for bad init rules'() {

0 commit comments

Comments
 (0)