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
4 changes: 2 additions & 2 deletions tracer/build/_build/Build.Steps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ AbsolutePath GetProjectBinDirectory(string projectName, string tfm) =>

AbsolutePath NativeBuildDirectory => BuildArtifactsDirectory / "native-obj";

const string LibDdwafVersion = "1.30.0";
const string LibDdwafVersion = "2.0.1";

string[] OlderLibDdwafVersions = { "1.3.0", "1.10.0", "1.14.0", "1.16.0", "1.23.0" };
string[] OlderLibDdwafVersions = { "1.3.0", "1.10.0", "1.14.0", "1.16.0", "1.23.0", "1.30.0" };

AbsolutePath LibDdwafDirectory(string libDdwafVersion = null) => (NugetPackageDirectory ?? RootDirectory / "packages") / $"libddwaf.{libDdwafVersion ?? LibDdwafVersion}";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"type": "web",
"meta": {
"_dd.appsec.event_rules.version": "1.15.0",
"_dd.appsec.waf.version": "1.30.0",
"_dd.appsec.waf.version": "2.0.1",
"_dd.runtime_family": "dotnet",
"_dd.appsec.fp.http.endpoint": "http-get-7460da9d--",
"_dd.appsec.fp.http.header": "hdr-0000000000--3-98425651",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"type": "web",
"meta": {
"_dd.appsec.event_rules.version": "1.15.0",
"_dd.appsec.waf.version": "1.30.0",
"_dd.appsec.waf.version": "2.0.1",
"_dd.runtime_family": "dotnet",
"_dd.appsec.fp.http.endpoint": "http-get-7460da9d--",
"_dd.appsec.fp.http.header": "hdr-0000000000--3-98425651",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"type": "web",
"meta": {
"_dd.appsec.event_rules.version": "1.15.0",
"_dd.appsec.waf.version": "1.30.0",
"_dd.appsec.waf.version": "2.0.1",
"_dd.runtime_family": "dotnet",
"_dd.appsec.fp.http.endpoint": "http-get-7460da9d--",
"_dd.appsec.fp.http.header": "hdr-0000000000--3-98425651",
Expand Down
1 change: 0 additions & 1 deletion tracer/missing-nullability-files.csv
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ src/Datadog.Trace/Telemetry/Transports/TelemetryTransportStrategy.cs
src/Datadog.Trace/Util/Http/HttpRequestExtensions.Core.cs
src/Datadog.Trace/Util/Http/QueryStringManager.cs
src/Datadog.Trace/AppSec/Waf/Initialization/LibraryLocationHelper.cs
src/Datadog.Trace/AppSec/Waf/NativeBindings/DdwafConfigStruct.cs
src/Datadog.Trace/AppSec/Waf/NativeBindings/DDWAF_OBJ_TYPE.cs
src/Datadog.Trace/AppSec/Waf/NativeBindings/NativeLibrary.cs
src/Datadog.Trace/AppSec/Waf/NativeBindings/ReducedRegistryAccess.cs
Expand Down
10 changes: 6 additions & 4 deletions tracer/src/Datadog.Trace/AppSec/Rasp/RaspModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,19 @@ private static void RunWafRasp(Dictionary<string, object> arguments, Span rootSp
// the blockings, so we report first and then block
try
{
var matchSuccesCode = result.ReturnCode == WafReturnCode.Match && result.ShouldBlock ?
// since libddwaf 2.x a run that only produces attributes or actions also returns
// DDWAF_MATCH, so the rule match metrics have to go by the event aware status
var matchSuccesCode = result.ShouldReportSecurityResult && result.ShouldBlock ?
BlockType.Success : BlockType.Irrelevant;

securityCoordinator.Value.ReportAndBlock(result, () => RecordRaspTelemetry(address, result.ReturnCode == Waf.WafReturnCode.Match, result.Timeout, matchSuccesCode));
securityCoordinator.Value.ReportAndBlock(result, () => RecordRaspTelemetry(address, result.ShouldReportSecurityResult, result.Timeout, matchSuccesCode));
}
catch (Exception ex) when (ex is not BlockException)
{
var matchFailureCode = result.ReturnCode == WafReturnCode.Match && result.ShouldBlock ?
var matchFailureCode = result.ShouldReportSecurityResult && result.ShouldBlock ?
BlockType.Failure : BlockType.Irrelevant;

RecordRaspTelemetry(address, result.ReturnCode == Waf.WafReturnCode.Match, result.Timeout, matchFailureCode);
RecordRaspTelemetry(address, result.ShouldReportSecurityResult, result.Timeout, matchFailureCode);
Log.Error(ex, "RASP: Error while reporting and blocking.");
}
}
Expand Down
113 changes: 71 additions & 42 deletions tracer/src/Datadog.Trace/AppSec/Waf/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal sealed class Context : IContext
private readonly IWafLibraryInvoker _wafLibraryInvoker;
private readonly IEncoder _encoder;
private readonly UserEventsState _userEventsState = new();

private bool _disposed;
private ulong _totalRuntimeOverRuns;

Expand Down Expand Up @@ -61,10 +62,10 @@ private Context(IntPtr contextHandle, IWaf waf, IWafLibraryInvoker wafLibraryInv
}

public IResult? Run(IDictionary<string, object> addressData, ulong timeoutMicroSeconds)
=> RunInternal(addressData, null, timeoutMicroSeconds);
=> RunInternal(addressData, false, timeoutMicroSeconds);

public IResult? RunWithEphemeral(IDictionary<string, object> ephemeralAddressData, ulong timeoutMicroSeconds, bool isRasp)
=> RunInternal(null, ephemeralAddressData, timeoutMicroSeconds, isRasp);
=> RunInternal(ephemeralAddressData, true, timeoutMicroSeconds, isRasp);

public Dictionary<string, object> FilterAddresses(IDatadogSecurity security, string? userId = null, string? userLogin = null, string? userSessionId = null, bool fromSdk = false)
{
Expand Down Expand Up @@ -125,7 +126,15 @@ private bool ShouldRunWith(IDatadogSecurity security, UserEventsState.UserRecord
return differentValue && (fromSdk || !previousValueFromSdk);
}

private unsafe Result? RunInternal(IDictionary<string, object>? persistentAddressData, IDictionary<string, object>? ephemeralAddressData, ulong timeoutMicroSeconds, bool isRasp = false)
/// <summary>
/// Runs the WAF over one batch of addresses.
/// </summary>
/// <param name="addressData">the addresses to evaluate</param>
/// <param name="ephemeral">when true the batch is evaluated in its own subcontext, so that its side
/// effects don't outlive the call; this is what RASP relies on</param>
/// <param name="timeoutMicroSeconds">the WAF budget for this run</param>
/// <param name="isRasp">whether this run should be reported as a RASP run</param>
private unsafe Result? RunInternal(IDictionary<string, object>? addressData, bool ephemeral, ulong timeoutMicroSeconds, bool isRasp = false)
{
DdwafObjectStruct retNative = default;

Expand All @@ -137,18 +146,17 @@ private bool ShouldRunWith(IDatadogSecurity security, UserEventsState.UserRecord

if (Log.IsEnabled(LogEventLevel.Debug))
{
var persistentParameters = persistentAddressData == null ? string.Empty : Encoder.FormatArgs(persistentAddressData);
var ephemeralParameters = ephemeralAddressData == null ? string.Empty : Encoder.FormatArgs(ephemeralAddressData);
var parameters = addressData == null ? string.Empty : Encoder.FormatArgs(addressData);
Log.Debug(
"DDAS-0010-00: Executing AppSec In-App WAF with parameters: persistent: {PersistentParameters}, ephemeral: {EphemeralParameters}",
persistentParameters,
ephemeralParameters);
"DDAS-0010-00: Executing AppSec In-App WAF with {Kind} parameters: {Parameters}",
ephemeral ? "ephemeral" : "persistent",
parameters);
}

// not restart because it's the total runtime over runs, and we run several * during request
_stopwatch.Start();
WafReturnCode code;
bool truncated = false;
bool truncated;
lock (_stopwatch)
{
if (_disposed)
Expand All @@ -157,49 +165,68 @@ private bool ShouldRunWith(IDatadogSecurity security, UserEventsState.UserRecord
return null;
}

// NOTE: the WAF must be called with either pwPersistentArgs or pwEphemeralArgs (or both) pointing to
// a valid structure. Failure to do so, results in a WAF error. It doesn't makes sense to propagate this
// error.
// Calling _encoder.Encode(null) results in a null object that will cause the WAF to error
// The WAF can be called with an empty dictionary (though we should avoid doing this).

DdwafObjectStruct pwPersistentArgs = default;
DdwafObjectStruct pwEphemeralArgsValue = default;

if (persistentAddressData is not null)
// NOTE: the WAF must be called with a valid map. Calling _encoder.Encode(null) results in an
// invalid object that will cause the WAF to error, and it doesn't make sense to propagate that
// error. The WAF can be called with an empty dictionary (though we should avoid doing this),
// but an empty ephemeral batch is pointless so it is rejected like it was before subcontexts.
if (ephemeral ? addressData is not { Count: > 0 } : addressData is null)
{
var persistentArgs = _encoder.Encode(persistentAddressData, applySafetyLimits: true);
pwPersistentArgs = persistentArgs.ResultDdwafObject;
_encodeResults.Add(persistentArgs);
truncated |= persistentArgs.Truncated;
Log.Error("The WAF was called without any address data");
return null;
}

// pwEphemeralArgs follow a different lifecycle and should be disposed immediately
using var ephemeralArgs = ephemeralAddressData is { Count: > 0 }
? _encoder.Encode(ephemeralAddressData, applySafetyLimits: true)
: null;
var args = _encoder.Encode(addressData!, applySafetyLimits: true);
truncated = args.Truncated;

if (persistentAddressData is null && ephemeralArgs is null)
// WARNING: Don't use ref here, we need to make a copy because args is on the heap
var argsValue = args.ResultDdwafObject;

if (ephemeral)
{
Log.Error("Both pwPersistentArgs and pwEphemeralArgs are null");
return null;
// One subcontext per ephemeral batch. Its evaluation caches are what makes a rule report
// its match only once, so a subcontext shared by every RASP call of a request would
// silently swallow all matches but the first one.
var subcontextHandle = _waf.SubcontextInit(_contextHandle);
if (subcontextHandle == IntPtr.Zero)
{
Log.Error("WAF ddwaf_subcontext_init failed, the ephemeral run was skipped");
args.Dispose();

// nothing ran, so don't let this call's wall clock leak into the aggregated runtime
_stopwatch.Stop();
return null;
}

try
{
code = _waf.SubcontextEval(subcontextHandle, &argsValue, ref retNative, timeoutMicroSeconds);
}
finally
{
// the subcontext is the only reader of this batch, so once it is gone the input
// buffers can be released instead of piling up for the whole request
_wafLibraryInvoker.SubcontextDestroy(subcontextHandle);
args.Dispose();
}
}

if (ephemeralArgs is not null)
else
{
// WARNING: Don't use ref here, we need to make a copy because ephemeralArgs is on the heap
pwEphemeralArgsValue = ephemeralArgs.ResultDdwafObject;
truncated |= ephemeralArgs.Truncated;
// WARNING: DO NOT DISPOSE the encoded arguments until the end of this class's lifecycle,
// i.e. in Dispose. libddwaf is given a null allocator on evaluation, so it never copies
// nor frees the input: those buffers have to outlive the context that reads them,
// otherwise the waf might crash with a fatal exception. They don't need to be pinned, as
// behind the scenes they are heap allocated pointers (through waf helpers via the legacy
// encoder or manually HC allocs via the new encoder).
_encodeResults.Add(args);
code = _waf.ContextEval(_contextHandle, &argsValue, ref retNative, timeoutMicroSeconds);
}

// WARNING: DO NOT DISPOSE pwPersistentArgs until the end of this class's lifecycle, i.e in the dispose. Otherwise waf might crash with fatal exception.
// pwPersistentArgs doesn't need to be pinpointed because we already have: _encodeResults.Add(persistentArgs); and behind the scenes they are heap allocated pointers (through waf helpers via the legacy encoder or manually HC allocs via the new encoder)
code = _waf.Run(_contextHandle, persistentAddressData != null ? &pwPersistentArgs : null, ephemeralArgs != null ? &pwEphemeralArgsValue : null, ref retNative, timeoutMicroSeconds);
}

_stopwatch.Stop();
var result = new Result(ref retNative, code, ref _totalRuntimeOverRuns, (ulong)(_stopwatch.Elapsed.TotalMilliseconds * 1000), isRasp, truncated);
_wafLibraryInvoker.ObjectFree(ref retNative);

// the result was allocated by the WAF with the output allocator given to ddwaf_context_init, which is the default one
_wafLibraryInvoker.ObjectDestroy(ref retNative);

if (Log.IsEnabled(LogEventLevel.Debug))
{
Expand All @@ -224,13 +251,15 @@ public void Dispose(bool disposing)

_disposed = true;

// destroy the consumer of our input buffers first: the context reads the memory owned by
// _encodeResults, which is why that one is released last
_wafLibraryInvoker.ContextDestroy(_contextHandle);

// WARNING do not move this above, this should only be disposed in the end of the context's life
foreach (var encodeResult in _encodeResults)
{
encodeResult.Dispose();
}

_wafLibraryInvoker.ContextDestroy(_contextHandle);
}
}

Expand Down
16 changes: 15 additions & 1 deletion tracer/src/Datadog.Trace/AppSec/Waf/IWaf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ internal interface IWaf : IDisposable

public IContext? CreateContext();

internal unsafe WafReturnCode Run(IntPtr contextHandle, DdwafObjectStruct* rawPersistentData, DdwafObjectStruct* rawEphemeralData, ref DdwafObjectStruct retNative, ulong timeoutMicroSeconds);
/// <summary>
/// Evaluates persistent data, whose side effects live for the whole context.
/// </summary>
internal unsafe WafReturnCode ContextEval(IntPtr contextHandle, DdwafObjectStruct* rawData, ref DdwafObjectStruct retNative, ulong timeoutMicroSeconds);

/// <summary>
/// Creates a subcontext, whose side effects are discarded when it is destroyed. This replaces
/// the ephemeral data of libddwaf 1.x.
/// </summary>
internal IntPtr SubcontextInit(IntPtr contextHandle);

/// <summary>
/// Evaluates data within a subcontext, so that its side effects don't leak into the context.
/// </summary>
internal unsafe WafReturnCode SubcontextEval(IntPtr subcontextHandle, DdwafObjectStruct* rawData, ref DdwafObjectStruct retNative, ulong timeoutMicroSeconds);

UpdateResult Update(ConfigurationState configurationStatus);

Expand Down
Loading
Loading