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

[PoC] Suspicious attacker blocking #239

Closed
wants to merge 10 commits into from
13 changes: 8 additions & 5 deletions src/collection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ std::optional<event> match_rule(rule *rule, const object_store &store,
return std::nullopt;
}

bool skip_actions = false;
auto exclusion = policy.find(rule);
if (exclusion.mode == exclusion::filter_mode::bypass) {
DDWAF_DEBUG("Bypassing rule '{}'", id);
Expand All @@ -38,7 +37,8 @@ std::optional<event> match_rule(rule *rule, const object_store &store,

if (exclusion.mode == exclusion::filter_mode::monitor) {
DDWAF_DEBUG("Monitoring rule '{}'", id);
skip_actions = true;
} else if (exclusion.mode == exclusion::filter_mode::custom) {
DDWAF_DEBUG("Applying custom action '{}' to rule '{}'", exclusion.action, id);
}

DDWAF_DEBUG("Evaluating rule '{}'", id);
Expand All @@ -54,10 +54,13 @@ std::optional<event> match_rule(rule *rule, const object_store &store,
std::optional<event> event;
event = rule->match(store, rule_cache, exclusion.objects, dynamic_matchers, deadline);

if (event.has_value() && skip_actions) {
event->skip_actions = true;
if (event.has_value()) {
if (exclusion.mode == exclusion::filter_mode::monitor) {
event->skip_actions = true;
} else if (exclusion.mode == exclusion::filter_mode::custom) {
event->override_action = exclusion.action;
}
}

return event;
} catch (const ddwaf::timeout_exception &) {
DDWAF_INFO("Ran out of time while evaluating rule '{}'", id);
Expand Down
7 changes: 4 additions & 3 deletions src/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,11 @@ exclusion::context_policy &context::eval_filters(ddwaf::timer &deadline)
}

rule_filter::cache_type &cache = it->second;
auto exclusion = filter->match(store_, cache, deadline);
auto exclusion = filter->match(store_, cache, ruleset_->dynamic_matchers, deadline);
if (exclusion.has_value()) {
for (const auto &rule : exclusion->rules) {
exclusion_policy_.add_rule_exclusion(rule, exclusion->mode, exclusion->ephemeral);
exclusion_policy_.add_rule_exclusion(
rule, exclusion->mode, exclusion->action, exclusion->ephemeral);
}
}
}
Expand All @@ -183,7 +184,7 @@ exclusion::context_policy &context::eval_filters(ddwaf::timer &deadline)
}

input_filter::cache_type &cache = it->second;
auto exclusion = filter->match(store_, cache, deadline);
auto exclusion = filter->match(store_, cache, ruleset_->dynamic_matchers, deadline);
if (exclusion.has_value()) {
for (const auto &rule : exclusion->rules) {
exclusion_policy_.add_input_exclusion(rule, exclusion->objects);
Expand Down
9 changes: 8 additions & 1 deletion src/event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,14 @@ void event_serializer::serialize(const std::vector<event> &events, ddwaf_result
ddwaf_object_map_add(&rule_map, "name", to_object(tmp, event.rule->get_name()));

const auto &actions = event.rule->get_actions();
if (!actions.empty()) {
if (!event.override_action.empty()) {
all_actions.emplace(event.override_action);

ddwaf_object actions_array;
ddwaf_object_array(&actions_array);
ddwaf_object_array_add(&actions_array, to_object(tmp, event.override_action));
ddwaf_object_map_add(&rule_map, "on_match", &actions_array);
} else if (!actions.empty()) {
ddwaf_object actions_array;
ddwaf_object_array(&actions_array);
if (!event.skip_actions) {
Expand Down
1 change: 1 addition & 0 deletions src/event.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct event {
std::vector<condition_match> matches;
bool ephemeral{false};
bool skip_actions{false};
std::string override_action{};
};

using optional_event = std::optional<event>;
Expand Down
25 changes: 16 additions & 9 deletions src/exclusion/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class rule;

namespace exclusion {

enum class filter_mode : uint8_t { none = 0, monitor = 1, bypass = 2 };
enum class filter_mode : uint8_t { none = 0, custom = 1, monitor = 2, bypass = 3 };

struct object_set {
std::unordered_set<const ddwaf_object *> persistent;
Expand All @@ -35,6 +35,7 @@ struct object_set {

struct rule_policy {
filter_mode mode{filter_mode::none};
std::string_view action{};
std::unordered_set<const ddwaf_object *> objects{};
};

Expand Down Expand Up @@ -63,6 +64,7 @@ struct object_set_ref {

struct rule_policy_ref {
filter_mode mode{filter_mode::none};
std::string_view action{};
object_set_ref objects;
};

Expand All @@ -86,27 +88,29 @@ struct context_policy {

if (p_it == persistent.end()) {
if (e_it == ephemeral.end()) {
return {filter_mode::none, {std::nullopt, std::nullopt}};
return {filter_mode::none, {}, {std::nullopt, std::nullopt}};
}

const auto &e_policy = e_it->second;
return {e_policy.mode, {std::nullopt, e_policy.objects}};
return {e_policy.mode, e_policy.action, {std::nullopt, e_policy.objects}};
}

if (e_it == ephemeral.end()) {
const auto &p_policy = p_it->second;
p_policy.objects.size();
return {p_policy.mode, {p_policy.objects, std::nullopt}};
return {p_policy.mode, p_policy.action, {p_policy.objects, std::nullopt}};
}

const auto &p_policy = p_it->second;
const auto &e_policy = e_it->second;
auto mode = p_policy.mode > e_policy.mode ? p_policy.mode : e_policy.mode;

return {mode, {p_policy.objects, e_policy.objects}};
if (p_policy.mode > e_policy.mode) {
return {p_policy.mode, p_policy.action, {p_policy.objects, e_policy.objects}};
}
return {e_policy.mode, e_policy.action, {p_policy.objects, e_policy.objects}};
}

void add_rule_exclusion(const ddwaf::rule *rule, filter_mode mode, bool ephemeral_exclusion)
void add_rule_exclusion(const ddwaf::rule *rule, filter_mode mode, std::string_view action,
bool ephemeral_exclusion)
{
auto &rule_policy = ephemeral_exclusion ? ephemeral : persistent;

Expand All @@ -115,9 +119,12 @@ struct context_policy {
if (it != rule_policy.end()) {
if (it->second.mode < mode) {
it->second.mode = mode;
it->second.action = action;
}
} else {
rule_policy[rule].mode = mode;
auto &new_policy = rule_policy[rule];
new_policy.mode = mode;
new_policy.action = action;
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/exclusion/input_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ input_filter::input_filter(std::string id, std::shared_ptr<expression> expr,
}
}

std::optional<excluded_set> input_filter::match(
const object_store &store, cache_type &cache, ddwaf::timer &deadline) const
std::optional<excluded_set> input_filter::match(const object_store &store, cache_type &cache,
const std::unordered_map<std::string, std::shared_ptr<matcher::base>> &dynamic_matchers,
ddwaf::timer &deadline) const
{
DDWAF_DEBUG("Evaluating input filter '{}'", id_);

// An event was already produced, so we skip the rule
// Note that conditions in a filter are optional
auto res = expr_->eval(cache.expr_cache, store, {}, {}, deadline);
auto res = expr_->eval(cache.expr_cache, store, {}, dynamic_matchers, deadline);
if (!res.outcome) {
return std::nullopt;
}
Expand Down
5 changes: 3 additions & 2 deletions src/exclusion/input_filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ class input_filter {
input_filter &operator=(input_filter &&) = delete;
virtual ~input_filter() = default;

virtual std::optional<excluded_set> match(
const object_store &store, cache_type &cache, ddwaf::timer &deadline) const;
virtual std::optional<excluded_set> match(const object_store &store, cache_type &cache,
const std::unordered_map<std::string, std::shared_ptr<matcher::base>> &dynamic_matchers,
ddwaf::timer &deadline) const;

std::string_view get_id() { return id_; }

Expand Down
13 changes: 7 additions & 6 deletions src/exclusion/rule_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ namespace ddwaf::exclusion {
using excluded_set = rule_filter::excluded_set;

rule_filter::rule_filter(std::string id, std::shared_ptr<expression> expr,
std::set<rule *> rule_targets, filter_mode mode)
: id_(std::move(id)), expr_(std::move(expr)), mode_(mode)
std::set<rule *> rule_targets, filter_mode mode, std::string action)
: id_(std::move(id)), expr_(std::move(expr)), mode_(mode), action_(std::move(action))
{
if (!expr_) {
throw std::invalid_argument("rule filter constructed with null expression");
Expand All @@ -25,8 +25,9 @@ rule_filter::rule_filter(std::string id, std::shared_ptr<expression> expr,
}
}

std::optional<excluded_set> rule_filter::match(
const object_store &store, cache_type &cache, ddwaf::timer &deadline) const
std::optional<excluded_set> rule_filter::match(const object_store &store, cache_type &cache,
const std::unordered_map<std::string, std::shared_ptr<matcher::base>> &dynamic_matchers,
ddwaf::timer &deadline) const
{
DDWAF_DEBUG("Evaluating rule filter '{}'", id_);

Expand All @@ -35,12 +36,12 @@ std::optional<excluded_set> rule_filter::match(
return std::nullopt;
}

auto res = expr_->eval(cache, store, {}, {}, deadline);
auto res = expr_->eval(cache, store, {}, dynamic_matchers, deadline);
if (!res.outcome) {
return std::nullopt;
}

return {{rule_targets_, res.ephemeral, mode_}};
return {{rule_targets_, res.ephemeral, mode_, action_}};
}

} // namespace ddwaf::exclusion
9 changes: 6 additions & 3 deletions src/exclusion/rule_filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@ class rule_filter {
const std::unordered_set<rule *> &rules;
bool ephemeral{false};
filter_mode mode{filter_mode::none};
std::string_view action;
};

using cache_type = expression::cache_type;

rule_filter(std::string id, std::shared_ptr<expression> expr, std::set<rule *> rule_targets,
filter_mode mode = filter_mode::bypass);
filter_mode mode = filter_mode::bypass, std::string action = {});
rule_filter(const rule_filter &) = delete;
rule_filter &operator=(const rule_filter &) = delete;
rule_filter(rule_filter &&) = default;
rule_filter &operator=(rule_filter &&) = default;
virtual ~rule_filter() = default;

virtual std::optional<excluded_set> match(
const object_store &store, cache_type &cache, ddwaf::timer &deadline) const;
virtual std::optional<excluded_set> match(const object_store &store, cache_type &cache,
const std::unordered_map<std::string, std::shared_ptr<matcher::base>> &dynamic_matchers,
ddwaf::timer &deadline) const;

std::string_view get_id() const { return id_; }

Expand All @@ -50,6 +52,7 @@ class rule_filter {
std::shared_ptr<expression> expr_;
std::unordered_set<rule *> rule_targets_;
filter_mode mode_;
std::string action_{};
};

} // namespace ddwaf::exclusion
4 changes: 2 additions & 2 deletions src/parser/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ rule_data_container parse_rule_data(parameter::vector &rule_data, base_section_i

override_spec_container parse_overrides(parameter::vector &override_array, base_section_info &info);

filter_spec_container parse_filters(
parameter::vector &filter_array, base_section_info &info, const object_limits &limits);
filter_spec_container parse_filters(parameter::vector &filter_array, base_section_info &info,
std::unordered_map<std::string, std::string> &rule_data_ids, const object_limits &limits);

processor_container parse_processors(
parameter::vector &processor_array, base_section_info &info, const object_limits &limits);
Expand Down
32 changes: 19 additions & 13 deletions src/parser/parser_v2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "condition/lfi_detector.hpp"
#include "condition/scalar_condition.hpp"
#include "exception.hpp"
#include "exclusion/common.hpp"
#include "exclusion/object_filter.hpp"
#include "generator/extract_schema.hpp"
#include "indexer.hpp"
Expand Down Expand Up @@ -355,13 +356,13 @@ std::shared_ptr<expression> parse_simplified_expression(const parameter::vector
return parse_expression(conditions_array, data_ids, data_source::values, {}, addresses, limits);
}

input_filter_spec parse_input_filter(
const parameter::map &filter, address_container &addresses, const object_limits &limits)
input_filter_spec parse_input_filter(const parameter::map &filter, address_container &addresses,
std::unordered_map<std::string, std::string> &rule_data_ids, const object_limits &limits)
{
// Check for conditions first
auto conditions_array = at<parameter::vector>(filter, "conditions", {});
auto expr = parse_simplified_expression(conditions_array, addresses, limits);

auto expr = parse_expression(
conditions_array, rule_data_ids, expression::data_source::values, {}, addresses, limits);
std::vector<reference_spec> rules_target;
auto rules_target_array = at<parameter::vector>(filter, "rules_target", {});
if (!rules_target_array.empty()) {
Expand Down Expand Up @@ -394,12 +395,13 @@ input_filter_spec parse_input_filter(
return {std::move(expr), std::move(obj_filter), std::move(rules_target)};
}

rule_filter_spec parse_rule_filter(
const parameter::map &filter, address_container &addresses, const object_limits &limits)
rule_filter_spec parse_rule_filter(const parameter::map &filter, address_container &addresses,
std::unordered_map<std::string, std::string> &rule_data_ids, const object_limits &limits)
{
// Check for conditions first
auto conditions_array = at<parameter::vector>(filter, "conditions", {});
auto expr = parse_simplified_expression(conditions_array, addresses, limits);
auto expr = parse_expression(
conditions_array, rule_data_ids, expression::data_source::values, {}, addresses, limits);

std::vector<reference_spec> rules_target;
auto rules_target_array = at<parameter::vector>(filter, "rules_target", {});
Expand All @@ -412,20 +414,24 @@ rule_filter_spec parse_rule_filter(
}

exclusion::filter_mode on_match;
std::string action{};
auto on_match_str = at<std::string_view>(filter, "on_match", "bypass");
if (on_match_str == "bypass") {
on_match = exclusion::filter_mode::bypass;
} else if (on_match_str == "monitor") {
on_match = exclusion::filter_mode::monitor;
} else if (!on_match_str.empty()) {
on_match = exclusion::filter_mode::custom;
action = on_match_str;
} else {
throw ddwaf::parsing_error("unsupported on_match value: " + std::string(on_match_str));
throw ddwaf::parsing_error("empty on_match value");
}

if (expr->empty() && rules_target.empty()) {
throw ddwaf::parsing_error("empty exclusion filter");
}

return {std::move(expr), std::move(rules_target), on_match};
return {std::move(expr), std::move(rules_target), on_match, std::move(action)};
}

std::vector<processor::target_mapping> parse_processor_mappings(
Expand Down Expand Up @@ -614,8 +620,8 @@ override_spec_container parse_overrides(parameter::vector &override_array, base_
return overrides;
}

filter_spec_container parse_filters(
parameter::vector &filter_array, base_section_info &info, const object_limits &limits)
filter_spec_container parse_filters(parameter::vector &filter_array, base_section_info &info,
std::unordered_map<std::string, std::string> &rule_data_ids, const object_limits &limits)
{
filter_spec_container filters;
for (unsigned i = 0; i < filter_array.size(); i++) {
Expand All @@ -632,11 +638,11 @@ filter_spec_container parse_filters(
}

if (node.find("inputs") != node.end()) {
auto filter = parse_input_filter(node, addresses, limits);
auto filter = parse_input_filter(node, addresses, rule_data_ids, limits);
filters.ids.emplace(id);
filters.input_filters.emplace(id, std::move(filter));
} else {
auto filter = parse_rule_filter(node, addresses, limits);
auto filter = parse_rule_filter(node, addresses, rule_data_ids, limits);
filters.ids.emplace(id);
filters.rule_filters.emplace(id, std::move(filter));
}
Expand Down
1 change: 1 addition & 0 deletions src/parser/specification.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ struct rule_filter_spec {
std::shared_ptr<expression> expr;
std::vector<reference_spec> targets;
exclusion::filter_mode on_match;
std::string action;
};

struct input_filter_spec {
Expand Down
6 changes: 4 additions & 2 deletions src/ruleset_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ std::shared_ptr<ruleset> ruleset_builder::build(parameter::map &root, base_rules
rule_targets.merge(references_to_rules(filter.targets, final_user_rules_));

auto filter_ptr = std::make_shared<exclusion::rule_filter>(
id, filter.expr, std::move(rule_targets), filter.on_match);
id, filter.expr, std::move(rule_targets), filter.on_match, filter.action);
rule_filters_.emplace(filter_ptr->get_id(), filter_ptr);
}

Expand Down Expand Up @@ -321,8 +321,10 @@ ruleset_builder::change_state ruleset_builder::load(parameter::map &root, base_r
auto &section = info.add_section("exclusions");
try {
auto exclusions = static_cast<parameter::vector>(it->second);
exclusion_data_ids_.clear();
if (!exclusions.empty()) {
exclusions_ = parser::v2::parse_filters(exclusions, section, limits_);
exclusions_ =
parser::v2::parse_filters(exclusions, section, rule_data_ids_, limits_);
} else {
DDWAF_DEBUG("Clearing all exclusions");
exclusions_.clear();
Expand Down
Loading
Loading