Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fingerprint support to the WAF (and libddwaf 11.0.0) #7436

Merged
merged 4 commits into from
Aug 28, 2024
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
2 changes: 1 addition & 1 deletion dd-java-agent/appsec/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies {
implementation project(':internal-api')
implementation project(':communication')
implementation project(':telemetry')
implementation group: 'io.sqreen', name: 'libsqreen', version: '10.1.0'
implementation group: 'io.sqreen', name: 'libsqreen', version: '11.0.0'
implementation libs.moshi

testImplementation libs.bytebuddy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSIONS;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRUSTED_IPS;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_USER_BLOCKING;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ENDPOINT_FINGERPRINT;

import com.datadog.appsec.AppSecSystem;
import com.datadog.appsec.api.security.ApiSecurityRequestSampler;
Expand Down Expand Up @@ -104,7 +107,12 @@ private void subscribeConfigurationPoller() {
| CAPABILITY_ASM_CUSTOM_RULES
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
| CAPABILITY_ASM_TRUSTED_IPS
| CAPABILITY_ASM_RASP_SQLI);
| CAPABILITY_ASM_RASP_SQLI
| CAPABILITY_ENDPOINT_FINGERPRINT
// TODO enable when usr.id and usr.session_id addresses are added
// | CAPABILITY_ASM_SESSION_FINGERPRINT
| CAPABILITY_ASM_NETWORK_FINGERPRINT
| CAPABILITY_ASM_HEADER_FINGERPRINT);
}

private void subscribeRulesAndData() {
Expand Down Expand Up @@ -345,7 +353,12 @@ public void close() {
| CAPABILITY_ASM_TRUSTED_IPS
| CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE
| CAPABILITY_ASM_RASP_SQLI
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE);
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
| CAPABILITY_ENDPOINT_FINGERPRINT
// TODO enable when usr.id and usr.session_id addresses are added
// | CAPABILITY_ASM_SESSION_FINGERPRINT
| CAPABILITY_ASM_NETWORK_FINGERPRINT
| CAPABILITY_ASM_HEADER_FINGERPRINT);
this.configurationPoller.removeListeners(Product.ASM_DD);
this.configurationPoller.removeListeners(Product.ASM_DATA);
this.configurationPoller.removeListeners(Product.ASM);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.datadog.appsec.gateway;

import static java.util.Collections.emptySet;

import com.datadog.appsec.event.data.Address;
import com.datadog.appsec.event.data.DataBundle;
import com.datadog.appsec.report.AppSecEvent;
Expand Down Expand Up @@ -104,7 +106,7 @@ public class AppSecRequestContext implements DataBundle, Closeable {
private boolean convertedReqBodyPublished;
private boolean respDataPublished;
private boolean pathParamsPublished;
private Map<String, String> apiSchemas;
private Map<String, String> derivatives;

private final AtomicBoolean rateLimited = new AtomicBoolean(false);
private volatile boolean throttled;
Expand Down Expand Up @@ -499,24 +501,29 @@ StackTraceCollection transferStackTracesCollection() {
}
}

public void reportApiSchemas(Map<String, String> schemas) {
if (schemas == null || schemas.isEmpty()) return;
public void reportDerivatives(Map<String, String> data) {
if (data == null || data.isEmpty()) return;

if (apiSchemas == null) {
apiSchemas = schemas;
if (derivatives == null) {
derivatives = data;
} else {
apiSchemas.putAll(schemas);
derivatives.putAll(data);
}
}

boolean commitApiSchemas(TraceSegment traceSegment) {
if (traceSegment == null || apiSchemas == null) {
boolean commitDerivatives(TraceSegment traceSegment) {
if (traceSegment == null || derivatives == null) {
return false;
}
apiSchemas.forEach(traceSegment::setTagTop);
derivatives.forEach(traceSegment::setTagTop);
return true;
}

// Mainly used for testing and logging
Set<String> getDerivativeKeys() {
return derivatives == null ? emptySet() : new HashSet<>(derivatives.keySet());
}

public boolean isThrottled(RateLimiter rateLimiter) {
if (rateLimiter != null && rateLimited.compareAndSet(false, true)) {
throttled = rateLimiter.isThrottled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,9 +481,9 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
// Report minimum set of collected request headers
writeRequestHeaders(traceSeg, DEFAULT_REQUEST_HEADERS_ALLOW_LIST, ctx.getRequestHeaders());
}
// If extracted any Api Schemas - commit them
if (!ctx.commitApiSchemas(traceSeg)) {
log.debug("Unable to commit, api security schemas and will be skipped");
// If extracted any derivatives - commit them
if (!ctx.commitDerivatives(traceSeg)) {
log.debug("Unable to commit, derivatives will be skipped {}", ctx.getDerivativeKeys());
}

if (ctx.isBlocked()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,8 @@ public void onDataAvailable(
}
}

if (resultWithData != null && resultWithData.schemas != null) {
reqCtx.reportApiSchemas(resultWithData.schemas);
if (resultWithData.derivatives != null) {
reqCtx.reportDerivatives(resultWithData.derivatives);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSIONS
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRUSTED_IPS
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_USER_BLOCKING
import static datadog.remoteconfig.Capabilities.CAPABILITY_ENDPOINT_FINGERPRINT
import static datadog.remoteconfig.PollingHinterNoop.NOOP
import static datadog.trace.api.UserIdCollectionMode.ANONYMIZATION
import static datadog.trace.api.UserIdCollectionMode.DISABLED
Expand Down Expand Up @@ -261,7 +264,11 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
| CAPABILITY_ASM_CUSTOM_RULES
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
| CAPABILITY_ASM_TRUSTED_IPS
| CAPABILITY_ASM_RASP_SQLI)
| CAPABILITY_ASM_RASP_SQLI
| CAPABILITY_ENDPOINT_FINGERPRINT
// | CAPABILITY_ASM_SESSION_FINGERPRINT
Copy link
Member

Choose a reason for hiding this comment

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

Leftovers?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it's another capability that will be added in the next PR

| CAPABILITY_ASM_NETWORK_FINGERPRINT
| CAPABILITY_ASM_HEADER_FINGERPRINT)
0 * _._
initialWafConfig.get() != null

Expand Down Expand Up @@ -406,7 +413,11 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
| CAPABILITY_ASM_CUSTOM_RULES
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
| CAPABILITY_ASM_TRUSTED_IPS
| CAPABILITY_ASM_RASP_SQLI)
| CAPABILITY_ASM_RASP_SQLI
| CAPABILITY_ENDPOINT_FINGERPRINT
// | CAPABILITY_ASM_SESSION_FINGERPRINT
Copy link
Member

Choose a reason for hiding this comment

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

Leftovers?

Copy link
Member Author

Choose a reason for hiding this comment

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

Same as before.

| CAPABILITY_ASM_NETWORK_FINGERPRINT
| CAPABILITY_ASM_HEADER_FINGERPRINT)
0 * _._

when:
Expand Down Expand Up @@ -474,7 +485,11 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
| CAPABILITY_ASM_TRUSTED_IPS
| CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE
| CAPABILITY_ASM_RASP_SQLI
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE,)
| CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
| CAPABILITY_ENDPOINT_FINGERPRINT
// | CAPABILITY_ASM_SESSION_FINGERPRINT
| CAPABILITY_ASM_NETWORK_FINGERPRINT
| CAPABILITY_ASM_HEADER_FINGERPRINT)
4 * poller.removeListeners(_)
1 * poller.removeConfigurationEndListener(_)
1 * poller.stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -924,4 +924,20 @@ class GatewayBridgeSpecification extends DDSpecification {
'appsec.events.users.login.failure.track' | true
'appsec.another.unrelated.tag' | false
}

void 'fingerprints are set in the span after a request'() {
given:
final mockAppSecCtx = new AppSecRequestContext(derivatives: ['_dd.appsec.fp.http.endpoint': 'xyz'])
final mockCtx = Stub(RequestContext) {
getData(RequestContextSlot.APPSEC) >> mockAppSecCtx
getTraceSegment() >> traceSegment
}
final spanInfo = Stub(AgentSpan)

when:
requestEndedCB.apply(mockCtx, spanInfo)

then:
1 * traceSegment.setTagTop('_dd.appsec.fp.http.endpoint', 'xyz')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,30 @@ class PowerWAFModuleSpecification extends DDSpecification {
})
}

void 'fingerprint support'() {
given:
final flow = Mock(ChangeableFlow)
setupWithStubConfigService 'fingerprint_config.json'
dataListener = pwafModule.dataSubscriptions.first()
ctx.closeAdditive()
final bundle = MapDataBundle.ofDelegate([
(KnownAddresses.WAF_CONTEXT_PROCESSOR): [fingerprint: true],
(KnownAddresses.REQUEST_METHOD): 'GET',
(KnownAddresses.REQUEST_URI_RAW): 'http://localhost:8080/test',
(KnownAddresses.REQUEST_BODY_OBJECT): [:],
(KnownAddresses.REQUEST_QUERY): [name: ['test']],
(KnownAddresses.HEADERS_NO_COOKIES): new CaseInsensitiveMap<List<String>>(['user-agent': ['Arachni/v1.5.1']])
])

when:
dataListener.onDataAvailable(flow, ctx, bundle, gwCtx)
ctx.closeAdditive()

then:
1 * flow.setAction({ it.blocking })
ctx.derivativeKeys.contains('_dd.appsec.fp.http.endpoint')
}

private Map<String, Object> getDefaultConfig() {
def service = new StubAppSecConfigService()
service.init()
Expand Down
86 changes: 86 additions & 0 deletions dd-java-agent/appsec/src/test/resources/fingerprint_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"version": "2.2",
"metadata": {
"rules_version": "1.8.0"
},
"rules": [
{
"id": "arachni_rule",
"name": "Arachni",
"tags": {
"type": "security_scanner",
"category": "attack_attempt"
},
"conditions": [
{
"parameters": {
"inputs": [
{
"address": "server.request.headers.no_cookies",
"key_path": [
"user-agent"
]
}
],
"regex": "^Arachni\\/v"
},
"operator": "match_regex"
}
],
"transformers": [],
"on_match": ["block"]
}
],
"processors": [
{
"id": "processor-001",
"generator": "http_endpoint_fingerprint",
"conditions": [
{
"operator": "equals",
"parameters": {
"inputs": [
{
"address": "waf.context.processor",
"key_path": [
"fingerprint"
]
}
],
"value": true,
"type": "boolean"
}
}
],
"parameters": {
"mappings": [
{
"method": [
{
"address": "server.request.method"
}
],
"uri_raw": [
{
"address": "server.request.uri.raw"
}
],
"body": [
{
"address": "server.request.body"
}
],
"query": [
{
"address": "server.request.query"
}
],
"output": "_dd.appsec.fp.http.endpoint"
}
]
},
"evaluate": true,
"output": true
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ public interface Capabilities {
long CAPABILITY_APM_TRACING_SAMPLE_RULES = 1 << 29;
long CAPABILITY_CSM_ACTIVATION = 1 << 30;
long CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE = 1L << 31;
long CAPABILITY_ENDPOINT_FINGERPRINT = 1L << 32;
long CAPABILITY_ASM_SESSION_FINGERPRINT = 1L << 33;
long CAPABILITY_ASM_NETWORK_FINGERPRINT = 1L << 34;
long CAPABILITY_ASM_HEADER_FINGERPRINT = 1L << 35;
}
Loading