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

WAF Builder: independent configuration manager to generate WAF instances #363

Merged
merged 58 commits into from
Jan 30, 2025

Conversation

Anilm3
Copy link
Collaborator

@Anilm3 Anilm3 commented Jan 7, 2025

This PR introduces a large change which is hereby referred to as the WAF builder. This new builder allows for the generation of WAF instances through independent configurations, while keeping track of all independent configurations so that they can be updated and / or removed in order to update an existing instance.

Configurations are uniquely identified by their path and can provide any and all of the supported primitives, which will be subsequently consolidated in order to generate a consistent instance. Generally, the WAF builder holds both the configurations (and specifications), as well as common evaluation primitives, for example, generated rules are kept and reused on each new instance for as long as they don't need to be updated. Similarly, expressions within filters, rules and processors are also preserved across updates, when relevant.

The change has also involved a large refactoring of the ruleset parser into multiple independent parsers all feeding into a single configuration, while keeping track of their individual changes. This refactor has resulted in configuration parsers and a separate set of "builders" which can be used to generate instances of each evaluation primitive based on all of the required elements that feed into their creation.

An example of using the new builder interface can be the following:

ddwaf_builder builder = ddwaf_builder_init(nullptr);

ddwaf_object rules = read_file("custom_rules.yaml");
ddwaf_builder_add_or_update_config(builder, "rules", sizeof("rules") - 1, &rules, nullptr);
ddwaf_object_free(&rules);

ddwaf_object custom_rules = read_file("custom_rules.yaml");
ddwaf_builder_add_or_update_config(builder, "custom_rules", sizeof("custom_rules") - 1, &custom_rules, nullptr);
ddwaf_object_free(&custom_rules);

ddwaf_handle handle = ddwaf_builder_build_instance(builder);

// Use the handle...

ddwaf_destroy(&handle); // the handle can be destroyed at any point

ddwaf_object custom_rules2 = read_file("custom_rules_2.yaml");
ddwaf_builder_add_or_update_config(builder, "custom_rules", sizeof("custom_rules") - 1, &custom_rules2, nullptr);
ddwaf_object_free(&custom_rules2);

handle = ddwaf_builder_build_instance(builder);

// Use the handle...

ddwaf_destroy(handle);

// Remove the custom rules
ddwaf_builder_remove_config(builder, "custom_rules", sizeof("custom_rules") - 1);
handle = ddwaf_builder_build_instance(builder);

// Use the handle...

ddwaf_destroy(handle);

ddwaf_builder_destroy(builder);

Pending tasks (to be addressed in new PRs)

Required:

  • More test variants for configuration updates
  • Diagnostics severity level (error, warning)
  • Smoketest update to include builder symbols

Optional:

  • WAF builder "fuzzer" test
  • Rename ddwaf_config?

Related Jira: APPSEC-55064

@codecov-commenter
Copy link

codecov-commenter commented Jan 7, 2025

Codecov Report

Attention: Patch coverage is 87.05752% with 117 lines in your changes missing coverage. Please review.

Project coverage is 85.32%. Comparing base (73ad8e7) to head (b3101b5).

Files with missing lines Patch % Lines
src/interface.cpp 41.81% 23 Missing and 9 partials ⚠️
src/configuration/configuration_manager.cpp 83.43% 2 Missing and 26 partials ⚠️
src/configuration/legacy_rule_parser.cpp 71.73% 10 Missing and 3 partials ⚠️
src/transformer/common/utf8.cpp 73.68% 2 Missing and 3 partials ⚠️
src/builder/ruleset_builder.cpp 95.91% 1 Missing and 3 partials ⚠️
src/configuration/common/common.hpp 69.23% 2 Missing and 2 partials ⚠️
src/builder/action_mapper_builder.cpp 84.21% 1 Missing and 2 partials ⚠️
src/configuration/actions_parser.cpp 87.50% 0 Missing and 3 partials ⚠️
...c/configuration/common/configuration_collector.hpp 96.00% 1 Missing and 2 partials ⚠️
src/configuration/rule_override_parser.cpp 76.92% 2 Missing and 1 partial ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #363      +/-   ##
==========================================
+ Coverage   85.13%   85.32%   +0.18%     
==========================================
  Files         157      163       +6     
  Lines        7993     8169     +176     
  Branches     3551     3600      +49     
==========================================
+ Hits         6805     6970     +165     
+ Misses        459      439      -20     
- Partials      729      760      +31     
Flag Coverage Δ
waf_test 85.32% <87.05%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pr-commenter
Copy link

pr-commenter bot commented Jan 7, 2025

Benchmarks

Benchmark execution time: 2025-01-30 13:30:14

Comparing candidate commit fd841fc in PR branch anilm3/waf_builder with baseline commit 73ad8e7 in branch master.

Found 1 performance improvements and 0 performance regressions! Performance is the same for 0 metrics, 0 unstable metrics.

scenario:global-benchmark.random

  • 🟩 execution_time [-17.852ms; -17.794ms] or [-5.975%; -5.955%]

@Anilm3 Anilm3 changed the title [WIP] WAF Builder WAF Builder: independent configuration manager to generate WAF instances Jan 22, 2025
@Anilm3 Anilm3 marked this pull request as ready for review January 22, 2025 18:27
@Anilm3 Anilm3 requested a review from a team as a code owner January 22, 2025 18:27
@Anilm3 Anilm3 self-assigned this Jan 22, 2025
@Anilm3 Anilm3 requested a review from cataphract January 28, 2025 08:51
src/builder/action_mapper_builder.hpp Outdated Show resolved Hide resolved
src/builder/action_mapper_builder.cpp Outdated Show resolved Hide resolved
src/builder/rule_builder.hpp Outdated Show resolved Hide resolved
}
}

ancillary_tags_.merge(spec_.tags);
Copy link
Contributor

Choose a reason for hiding this comment

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

do you really want to modify spec_.tags here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As mentioned below, the rule builders are single use, I added a comment to make sure it's clear.


return std::make_shared<core_rule>(std::move(id_), std::move(spec_.name),
std::move(ancillary_tags_), std::move(spec_.expr), std::move(spec_.actions),
spec_.enabled, spec_.source, verdict);
Copy link
Contributor

Choose a reason for hiding this comment

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

your builders get to a bad state after calls to build(). Perhaps this is something you want to tackle in the future to prevent misusage

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In reference to this comment and the one about tags.

This particular builder is currently single-use so after build it's expected that it won't be useful (e.g. same as after move). I guess I could reset the state and throw if a second build is attempted, but it seems unnecessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've added a comment to make it clear that after build it should be considered invalid.

I did consider caching the rule builders and reusing them later but I'll leave that for a future improvement.

src/builder/ruleset_builder.cpp Outdated Show resolved Hide resolved
}

auto rs = std::make_shared<ruleset>();
rs->insert_rules(final_base_rules_.items(), final_user_rules_.items());
Copy link
Contributor

Choose a reason for hiding this comment

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

why are rules individually managed through shared_ptr instead of final_base_rules_ being themselves behind a shared pointer (like the actions). Because AFAICT, whenever we have a base_rule_update, we rebuild the rules from scratch:

    if ((current_changes & base_rule_update) != change_set::none) {
        final_base_rules_.clear();
        // ...
        for (const auto &builder : rule_builders) {
            if (builder->is_enabled()) {
                final_base_rules_.emplace(builder->build(*actions_));
            }
        }

Same applies to the user rules.

As mentioned earlier, I would feel much safer if these rules had their immutability enforced by the compiler, to prevent races.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll address this in the next PR.

Comment on lines +177 to +182
rs->insert_filters(rule_filters_);
rs->insert_filters(input_filters_);
rs->insert_preprocessors(preprocessors_);
rs->insert_postprocessors(postprocessors_);
rs->rule_matchers = rule_matchers_;
rs->exclusion_matchers = exclusion_matchers_;
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment as with the rules. These are always build all together, so they could all be behind one single (for instance) shared_ptr<const map<string, unique_ptr<matcher::base>>>.

It seems the only case where this doesn't apply is the scanners, where a copy of the vector is needed because global_config.scanners can be modified at any point and actions, which already do it that way

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same as above, I'll address this in the next PR.

@Anilm3
Copy link
Collaborator Author

Anilm3 commented Jan 30, 2025

The remaining comments will be addressed in the next PR as this one is just too large at this stage.

@Anilm3 Anilm3 merged commit 1de83d9 into master Jan 30, 2025
50 of 51 checks passed
@Anilm3 Anilm3 deleted the anilm3/waf_builder branch January 30, 2025 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants