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

Custom rule filter actions #303

Merged
merged 9 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
11 changes: 7 additions & 4 deletions src/collection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ std::optional<event> match_rule(rule *rule, const object_store &store,
return std::nullopt;
}

action_type action_override = action_type::none;
std::string_view action_override;
auto exclusion = policy.find(rule);
if (exclusion.mode == exclusion::filter_mode::bypass) {
DDWAF_DEBUG("Bypassing rule '{}'", id);
Expand All @@ -38,11 +38,14 @@ std::optional<event> match_rule(rule *rule, const object_store &store,

if (exclusion.mode == exclusion::filter_mode::monitor) {
DDWAF_DEBUG("Monitoring rule '{}'", id);
action_override = action_type::monitor;
action_override = "monitor";
} else if (exclusion.mode == exclusion::filter_mode::custom) {
action_override = exclusion.action;
DDWAF_DEBUG("Evaluating rule '{}' with custom action '{}'", id, action_override);
} else {
DDWAF_DEBUG("Evaluating rule '{}'", id);
}

DDWAF_DEBUG("Evaluating rule '{}'", id);

try {
auto it = cache.find(rule);
if (it == cache.end()) {
Expand Down
3 changes: 2 additions & 1 deletion src/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ exclusion::context_policy &context::eval_filters(ddwaf::timer &deadline)
auto exclusion = filter->match(store_, cache, 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 Down
15 changes: 10 additions & 5 deletions src/event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,26 +178,31 @@ void serialize_empty_rule(ddwaf_object &rule_map)
}

void serialize_and_consolidate_rule_actions(const ddwaf::rule &rule, ddwaf_object &rule_map,
action_type action_override, action_tracker &actions, ddwaf_object &stack_id)
std::string_view action_override, action_tracker &actions, ddwaf_object &stack_id)
{
const auto &rule_actions = rule.get_actions();
if (rule_actions.empty()) {
if (rule_actions.empty() && action_override.empty()) {
return;
}

ddwaf_object tmp;
ddwaf_object actions_array;
ddwaf_object_array(&actions_array);

if (action_override == action_type::monitor) {
ddwaf_object_array_add(&actions_array, to_object(tmp, "monitor"));
if (!action_override.empty()) {
ddwaf_object_array_add(&actions_array, to_object(tmp, action_override));
auto action_it = actions.mapper.find(action_override);
if (action_it != actions.mapper.end()) {
const auto &[type, type_str, parameters] = action_it->second;
add_action_to_tracker(actions, action_override, type);
}
Anilm3 marked this conversation as resolved.
Show resolved Hide resolved
}

for (const auto &action_id : rule_actions) {
auto action_it = actions.mapper.find(action_id);
if (action_it != actions.mapper.end()) {
const auto &[type, type_str, parameters] = action_it->second;
if (action_override == action_type::monitor &&
if (!action_override.empty() &&
(type == action_type::monitor || is_blocking_action(type))) {
// If the rule was in monitor mode, ignore blocking and monitor actions
continue;
Expand Down
2 changes: 1 addition & 1 deletion src/event.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct event {
const ddwaf::rule *rule{nullptr};
std::vector<condition_match> matches;
bool ephemeral{false};
action_type action_override{action_type::none};
std::string_view action_override;
};

using optional_event = std::optional<event>;
Expand Down
32 changes: 17 additions & 15 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,7 +35,8 @@ struct object_set {

struct rule_policy {
filter_mode mode{filter_mode::none};
std::unordered_set<const ddwaf_object *> objects{};
std::string_view action;
std::unordered_set<const ddwaf_object *> objects;
Anilm3 marked this conversation as resolved.
Show resolved Hide resolved
};

struct object_set_ref {
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,38 +88,38 @@ 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}};
Anilm3 marked this conversation as resolved.
Show resolved Hide resolved
}

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;

auto it = rule_policy.find(rule);
auto &policy = rule_policy[rule];
// Bypass has precedence over monitor
if (it != rule_policy.end()) {
if (it->second.mode < mode) {
it->second.mode = mode;
}
} else {
rule_policy[rule].mode = mode;
if (policy.mode < mode) {
policy.mode = mode;
policy.action = action;
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/exclusion/rule_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

#include <exclusion/rule_filter.hpp>
#include <log.hpp>
#include <utility>

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 @@ -40,7 +41,7 @@ std::optional<excluded_set> rule_filter::match(
return std::nullopt;
}

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

} // namespace ddwaf::exclusion
4 changes: 3 additions & 1 deletion src/exclusion/rule_filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ 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;
Expand All @@ -50,6 +51,7 @@ class rule_filter {
std::shared_ptr<expression> expr_;
std::unordered_set<rule *> rule_targets_;
filter_mode mode_;
std::string action_{};
};

} // namespace ddwaf::exclusion
8 changes: 6 additions & 2 deletions src/parser/exclusion_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,23 @@ rule_filter_spec parse_rule_filter(

exclusion::filter_mode on_match;
auto on_match_str = at<std::string_view>(filter, "on_match", "bypass");
std::string on_match_id;
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;
on_match_id = 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(on_match_id)};
}

} // namespace
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 custom_action;
};

struct input_filter_spec {
Expand Down
2 changes: 1 addition & 1 deletion src/ruleset_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,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.custom_action);
rule_filters_.emplace(filter_ptr->get_id(), filter_ptr);
}

Expand Down
46 changes: 43 additions & 3 deletions tests/parser_v2_rule_filters_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ TEST(TestParserV2RuleFilters, ParseOnMatchBypass)
EXPECT_EQ(filter.on_match, exclusion::filter_mode::bypass);
}

TEST(TestParserV2RuleFilters, ParseInvalidOnMatch)
TEST(TestParserV2RuleFilters, ParseCustomOnMatch)
{
ddwaf::object_limits limits;

Expand All @@ -647,6 +647,47 @@ TEST(TestParserV2RuleFilters, ParseInvalidOnMatch)
auto filters = parser::v2::parse_filters(filters_array, section, limits);
ddwaf_object_free(&object);

{
ddwaf::parameter root;
section.to_object(root);

auto root_map = static_cast<parameter::map>(root);

auto loaded = ddwaf::parser::at<parameter::string_set>(root_map, "loaded");
EXPECT_EQ(loaded.size(), 1);
EXPECT_NE(loaded.find("1"), loaded.end());

auto failed = ddwaf::parser::at<parameter::string_set>(root_map, "failed");
EXPECT_EQ(failed.size(), 0);

auto errors = ddwaf::parser::at<parameter::map>(root_map, "errors");
EXPECT_EQ(errors.size(), 0);

ddwaf_object_free(&root);
}

EXPECT_EQ(filters.rule_filters.size(), 1);
EXPECT_EQ(filters.input_filters.size(), 0);

const auto &filter_it = filters.rule_filters.begin();
EXPECT_STR(filter_it->first, "1");

const auto &filter = filter_it->second;
EXPECT_EQ(filter.on_match, exclusion::filter_mode::custom);
EXPECT_STR(filter.custom_action, "obliterate");
}

TEST(TestParserV2RuleFilters, ParseInvalidOnMatch)
{
ddwaf::object_limits limits;

auto object = yaml_to_object(R"([{id: 1, rules_target: [{rule_id: 2939}], on_match: ""}])");

ddwaf::ruleset_info::section_info section;
auto filters_array = static_cast<parameter::vector>(parameter(object));
auto filters = parser::v2::parse_filters(filters_array, section, limits);
ddwaf_object_free(&object);

{
ddwaf::parameter root;
section.to_object(root);
Expand All @@ -662,7 +703,7 @@ TEST(TestParserV2RuleFilters, ParseInvalidOnMatch)

auto errors = ddwaf::parser::at<parameter::map>(root_map, "errors");
EXPECT_EQ(errors.size(), 1);
auto it = errors.find("unsupported on_match value: obliterate");
auto it = errors.find("empty on_match value");
EXPECT_NE(it, errors.end());

auto error_rules = static_cast<ddwaf::parameter::string_set>(it->second);
Expand All @@ -675,5 +716,4 @@ TEST(TestParserV2RuleFilters, ParseInvalidOnMatch)
EXPECT_EQ(filters.rule_filters.size(), 0);
EXPECT_EQ(filters.input_filters.size(), 0);
}

} // namespace
Loading