Skip to content

Commit

Permalink
[ASM] Suspicious attacker blocking (#6057)
Browse files Browse the repository at this point in the history
## Summary of changes

This PR adds the required code to support the suspicious attack
functionality.

It has added the required code to read the exclusion data from the RC.

It also has modified the existing code regarding RC actions. Previously,
if a configuration was received with an action, the configuration was
stored and later sent to the WAF. If new values with an empty action
array would come later, the previous action configurations would be
deleted, but if a new array would come with a new action different than
the previous one, we would report them both to the WAF. This behavior
was making the suspicious attacker system tests fail because we would
keep RC changes from previous tests. This change seems to be aligned
with the behavior of other libraries.

The file
[AspNetBase.cs](https://github.com/DataDog/dd-trace-dotnet/pull/6057/files#diff-0faff2451113067d7669566ba9199908b720a3764914b00d6f33d4b376098d74)
has been updated. Now, tests have more control over the used headers by
allowing them to remove headers or replace previous values with new
ones.

## Reason for change

## Implementation details

## Test coverage

## Other details
<!-- Fixes #{issue} -->

<!-- ⚠️ Note: where possible, please obtain 2 approvals prior to
merging. Unless CODEOWNERS specifies otherwise, for external teams it is
typically best to have one review from a team member, and one review
from apm-dotnet. Trivial changes do not require 2 reviews. -->
  • Loading branch information
NachoEchevarria authored Oct 2, 2024
1 parent b637501 commit f93ed0a
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 8 deletions.
24 changes: 19 additions & 5 deletions tracer/src/Datadog.Trace/AppSec/Rcm/AsmDataProduct.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="AsmDataProduct.cs" company="Datadog">
// <copyright file="AsmDataProduct.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>
Expand All @@ -18,26 +18,40 @@ public void ProcessUpdates(ConfigurationStatus configurationStatus, List<RemoteC
{
var rawFile = new NamedRawFile(file.Path, file.Contents);
var asmDataConfig = rawFile.Deserialize<Payload>();
var rulesData = asmDataConfig.TypedFile!.RulesData;
var rulesData = asmDataConfig.TypedFile?.RulesData;
if (rulesData != null)
{
configurationStatus.RulesDataByFile[rawFile.Path.Path] = rulesData;
configurationStatus.IncomingUpdateState.WafKeysToApply.Add(ConfigurationStatus.WafRulesDataKey);
}

var exclusionsData = asmDataConfig.TypedFile?.ExclusionsData;
if (exclusionsData != null)
{
configurationStatus.ExclusionsDataByFile[rawFile.Path.Path] = exclusionsData;
configurationStatus.IncomingUpdateState.WafKeysToApply.Add(ConfigurationStatus.WafExclusionsDataKey);
}
}
}

public void ProcessRemovals(ConfigurationStatus configurationStatus, List<RemoteConfigurationPath> removedConfigsForThisProduct)
{
var removedData = false;
var removedRulesData = false;
var removedExclusionsData = false;
foreach (var configurationPath in removedConfigsForThisProduct)
{
removedData |= configurationStatus.RulesDataByFile.Remove(configurationPath.Path);
removedRulesData |= configurationStatus.RulesDataByFile.Remove(configurationPath.Path);
removedExclusionsData |= configurationStatus.ExclusionsDataByFile.Remove(configurationPath.Path);
}

if (removedData)
if (removedRulesData)
{
configurationStatus.IncomingUpdateState.WafKeysToApply.Add(ConfigurationStatus.WafRulesDataKey);
}

if (removedExclusionsData)
{
configurationStatus.IncomingUpdateState.WafKeysToApply.Add(ConfigurationStatus.WafExclusionsDataKey);
}
}
}
9 changes: 9 additions & 0 deletions tracer/src/Datadog.Trace/AppSec/Rcm/ConfigurationStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal record ConfigurationStatus
internal const string WafRulesOverridesKey = "rules_override";
internal const string WafExclusionsKey = "exclusions";
internal const string WafRulesDataKey = "rules_data";
internal const string WafExclusionsDataKey = "exclusion_data";
internal const string WafCustomRulesKey = "custom_rules";
internal const string WafActionsKey = "actions";
private readonly IAsmConfigUpdater _asmFeatureProduct = new AsmFeaturesProduct();
Expand All @@ -57,6 +58,8 @@ internal record ConfigurationStatus

internal Dictionary<string, RuleData[]> RulesDataByFile { get; } = new();

internal Dictionary<string, RuleData[]> ExclusionsDataByFile { get; } = new();

internal Dictionary<string, JArray> ExclusionsByFile { get; } = new();

internal Dictionary<string, RuleSet> RulesByFile { get; } = new();
Expand Down Expand Up @@ -123,6 +126,12 @@ internal Dictionary<string, object> BuildDictionaryForWafAccordingToIncomingUpda
dictionary.Add(WafRulesDataKey, rulesData.Select(r => r.ToKeyValuePair()).ToArray());
}

if (IncomingUpdateState.WafKeysToApply.Contains(WafExclusionsDataKey))
{
var rulesData = MergeRuleData(ExclusionsDataByFile.SelectMany(x => x.Value));
dictionary.Add(WafExclusionsDataKey, rulesData.Select(r => r.ToKeyValuePair()).ToArray());
}

if (IncomingUpdateState.WafKeysToApply.Contains(WafActionsKey))
{
var actions = ActionsByFile.SelectMany(x => x.Value).ToList();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="Payload.cs" company="Datadog">
// <copyright file="Payload.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>
Expand All @@ -11,4 +11,7 @@ internal class Payload
{
[JsonProperty("rules_data")]
public RuleData[]? RulesData { get; set; }

[JsonProperty("exclusion_data")]
public RuleData[]? ExclusionsData { get; set; }
}
3 changes: 2 additions & 1 deletion tracer/src/Datadog.Trace/AppSec/Waf/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@ private Context(IntPtr contextHandle, Waf waf, WafLibraryInvoker wafLibraryInvok
if (Log.IsEnabled(LogEventLevel.Debug))
{
Log.Debug(
"DDAS-0011-00: AppSec In-App WAF returned: {ReturnCode} {Data}",
"DDAS-0011-00: AppSec In-App WAF returned: {ReturnCode} {BlockInfo} {Data}",
result.ReturnCode,
result.BlockInfo,
result.Data);
}

Expand Down
7 changes: 7 additions & 0 deletions tracer/src/Datadog.Trace/AppSec/Waf/Waf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using Datadog.Trace.ExtensionMethods;
using Datadog.Trace.Logging;
using Datadog.Trace.Telemetry;
using Datadog.Trace.Vendors.Newtonsoft.Json;
using Datadog.Trace.Vendors.Serilog.Events;

namespace Datadog.Trace.AppSec.Waf
{
Expand Down Expand Up @@ -218,6 +220,11 @@ private UpdateResult Update(IDictionary<string, object> arguments)
UpdateResult updated;
try
{
if (Log.IsEnabled(LogEventLevel.Debug))
{
Log.Debug("Updating WAF with new configuration: {Arguments}", JsonConvert.SerializeObject(arguments));
}

var encodedArgs = _encoder.Encode(arguments, applySafetyLimits: false);
updated = UpdateWafAndDispose(encodedArgs);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,15 @@ protected async Task TestRateLimiter(bool enableSecurity, string url, MockTracer
{
foreach (var header in headers)
{
_httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
if (_httpClient.DefaultRequestHeaders.Contains(header.Key))
{
_httpClient.DefaultRequestHeaders.Remove(header.Key);
}

if (header.Value is not null)
{
_httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// <copyright file="AspNetCore5AsmAttackerBlocking.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#if NETCOREAPP3_0_OR_GREATER

using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Datadog.Trace.AppSec;
using Datadog.Trace.AppSec.Rcm.Models.AsmFeatures;
using Datadog.Trace.Configuration;
using Datadog.Trace.TestHelpers;
using Datadog.Trace.Vendors.Newtonsoft.Json.Linq;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
using Action = Datadog.Trace.AppSec.Rcm.Models.Asm.Action;

namespace Datadog.Trace.Security.IntegrationTests.Rcm;

public class AspNetCore5AsmAttackerBlocking : RcmBase
{
private const string AsmProduct = "ASM";

public AspNetCore5AsmAttackerBlocking(AspNetCoreTestFixture fixture, ITestOutputHelper outputHelper)
: base(fixture, outputHelper, enableSecurity: true, testName: nameof(AspNetCore5AsmAttackerBlocking))
{
SetEnvironmentVariable(ConfigurationKeys.DebugEnabled, "0");
SetEnvironmentVariable("DD_APPSEC_WAF_DEBUG", "0");
}

[Fact]
[Trait("RunOnWindows", "True")]
public async Task TestSuspiciousAttackerBlocking()
{
List<KeyValuePair<string, string>> headersAttacker = new()
{
new KeyValuePair<string, string>("http.client_ip", "34.65.27.85"),
new KeyValuePair<string, string>("X-Real-Ip", "34.65.27.85"),
new KeyValuePair<string, string>("accept-encoding", "identity"),
new KeyValuePair<string, string>("x-forwarded-for", null),
};

List<KeyValuePair<string, string>> headersRegular = new()
{
new KeyValuePair<string, string>("X-Real-Ip", null),
new KeyValuePair<string, string>("accept-encoding", "identity"),
new KeyValuePair<string, string>("x-forwarded-for", null),
};

var headersAttackerArachni = new List<KeyValuePair<string, string>>(headersAttacker)
{
new KeyValuePair<string, string>("User-Agent", "Arachni/v1"),
};

var headersRegularArachni = new List<KeyValuePair<string, string>>(headersRegular)
{
new KeyValuePair<string, string>("User-Agent", "Arachni/v1"),
};

var headersAttackerScanner = new List<KeyValuePair<string, string>>(headersAttacker)
{
new KeyValuePair<string, string>("User-Agent", "dd-test-scanner-log-block"),
};

var headersRegularScanner = new List<KeyValuePair<string, string>>(headersRegular)
{
new KeyValuePair<string, string>("User-Agent", "dd-test-scanner-log-block"),
};

string url = "/Health";
IncludeAllHttpSpans = true;
await TryStartApp();
var agent = Fixture.Agent;
var result = SubmitRequest(url, null, null, headers: headersAttackerScanner);
result.Result.StatusCode.Should().Be(HttpStatusCode.Forbidden);

var configurationInitial = new[]
{
((object)new AppSec.Rcm.Models.Asm.Payload
{
Actions = new[]
{
new Action { Id = "block", Type = BlockingAction.BlockRequestType, Parameters = new AppSec.Rcm.Models.Asm.Parameter { StatusCode = 403, Type = "json" } }
},
},
AsmProduct,
nameof(TestSuspiciousAttackerBlocking)),
((object)new AsmFeatures
{
Asm = new AsmFeature { Enabled = true },
},
"ASM_FEATURES",
nameof(TestSuspiciousAttackerBlocking))
};

await agent.SetupRcmAndWait(Output, configurationInitial);
result = SubmitRequest(url, null, null, headers: headersAttackerScanner);

var exclusions = "[{\"id\": \"exc-000-001\",\"on_match\": \"block_custom\",\"conditions\": [{\"operator\": \"ip_match\",\"parameters\": {\"data\": \"suspicious_ips_data_id\", \"inputs\": [{\"address\": \"http.client_ip\"}]}}]}]";
var configuration = new[]
{
(new AppSec.Rcm.Models.Asm.Payload
{
Actions = new[]
{
new Action { Id = "block_custom", Type = BlockingAction.BlockRequestType, Parameters = new AppSec.Rcm.Models.Asm.Parameter { StatusCode = 405, Type = "auto" } }
},
Exclusions = (JArray)JToken.Parse(exclusions)
},
AsmProduct,
nameof(TestSuspiciousAttackerBlocking)),
((object)new AppSec.Rcm.Models.AsmData.Payload
{
ExclusionsData = new[]
{
new AppSec.Rcm.Models.AsmData.RuleData { Id = "suspicious_ips_data_id", Type = "ip_with_expiration", Data = new AppSec.Rcm.Models.AsmData.Data[] { new() { Value = "34.65.27.85" } } }
},
},
"ASM_DATA",
nameof(TestSuspiciousAttackerBlocking)),
};

await agent.SetupRcmAndWait(Output, configuration);
result = SubmitRequest(url + "?a=3", null, null, headers: headersAttackerScanner);
result.Result.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed);
result = SubmitRequest(url + "?a=4", null, null, headers: headersRegularScanner);
result.Result.StatusCode.Should().Be(HttpStatusCode.Forbidden);
result = SubmitRequest(url + "?a=5", null, null, headers: headersAttackerArachni);
result.Result.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed);
result = SubmitRequest(url + "?a=6", null, null, headers: headersRegularArachni);
result.Result.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
#endif

0 comments on commit f93ed0a

Please sign in to comment.