Skip to content

Commit 38ce35a

Browse files
authored
Implement appsec jwt (#3352)
1 parent dcf39f3 commit 38ce35a

File tree

18 files changed

+531
-71
lines changed

18 files changed

+531
-71
lines changed

appsec/src/helper/action.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct action {
2828

2929
struct event {
3030
bool keep = false;
31-
std::vector<std::string> data;
31+
std::vector<std::string> triggers; // json fragments
3232
std::vector<action> actions;
3333
};
3434
} // namespace dds

appsec/src/helper/client.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ std::shared_ptr<typename T::response> client::publish(
276276
extra_record_action.parameters = {};
277277
response->actions.push_back(extra_record_action);
278278
}
279-
response->triggers = std::move(res->events);
279+
response->triggers = std::move(res->triggers);
280280
response->force_keep = res->force_keep;
281281

282282
DD_STDLOG(DD_STDLOG_ATTACK_DETECTED);

appsec/src/helper/engine.cpp

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ std::optional<engine::result> engine::context::publish(
9797
DD_STDLOG(DD_STDLOG_IG_DATA_PUSHED, entry.key());
9898
}
9999

100-
event event_;
100+
event event;
101101

102102
auto common =
103103
std::atomic_load_explicit(&common_, std::memory_order_acquire);
@@ -113,25 +113,30 @@ std::optional<engine::result> engine::context::publish(
113113
}
114114
try {
115115
const auto &listener = it->second;
116-
listener->call(data, event_, rasp_rule);
116+
listener->call(data, event, rasp_rule);
117117
} catch (std::exception &e) {
118118
SPDLOG_ERROR("subscriber failed: {}", e.what());
119119
}
120120
}
121121

122-
if (event_.actions.empty() && event_.data.empty()) {
122+
// force_keep indicates to the extension that it should change the
123+
// priority of the span. We set it to true if libddwaf tells us to AND
124+
// if the limiter allows it.
125+
// XXX: the limiter should probably only be invoked once per request
126+
const bool force_keep = event.keep && limiter_.allow();
127+
128+
if (!force_keep && event.actions.empty() && event.triggers.empty()) {
123129
return std::nullopt;
124130
}
125131

126-
const bool force_keep = event_.keep && limiter_.allow();
127-
dds::engine::result res{{}, std::move(event_.data), force_keep};
128-
// Currently the only action the extension can perform is block
129-
if (event_.actions.empty()) {
132+
// no actions, but we have json fragments in triggers. Add a record action
133+
if (event.actions.empty()) {
130134
action record = {dds::action_type::record, {}};
131-
res.actions.emplace_back(std::move(record));
135+
event.actions.emplace_back(std::move(record));
132136
}
133137

134-
for (auto const &action : event_.actions) {
138+
dds::engine::result res{{}, std::move(event.triggers), force_keep};
139+
for (auto const &action : event.actions) {
135140
dds::action new_action;
136141
new_action.type = action.type;
137142
new_action.parameters.insert(

appsec/src/helper/engine.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class engine {
3838

3939
struct result {
4040
std::vector<dds::action> actions;
41-
std::vector<std::string> events;
41+
std::vector<std::string> triggers; // json fragments
4242
bool force_keep;
4343
};
4444

appsec/src/helper/json_helper.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,11 @@ void json_to_object(ddwaf_object *object, T &doc)
132132
}
133133
case rapidjson::kNumberType: {
134134
if (doc.IsInt64()) {
135-
ddwaf_object_string_from_signed(object, doc.GetInt64());
135+
ddwaf_object_signed(object, doc.GetInt64());
136136
} else if (doc.IsUint64()) {
137-
ddwaf_object_string_from_unsigned(object, doc.GetUint64());
137+
ddwaf_object_unsigned(object, doc.GetUint64());
138+
} else if (doc.IsDouble()) {
139+
ddwaf_object_float(object, doc.GetDouble());
138140
}
139141
break;
140142
}

appsec/src/helper/parameter_base.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ class parameter_base : public ddwaf_object {
8989
{
9090
return ddwaf_object::type != DDWAF_OBJ_INVALID;
9191
}
92+
[[nodiscard]] bool is_float() const noexcept
93+
{
94+
return ddwaf_object::type == DDWAF_OBJ_FLOAT;
95+
}
9296
// NOLINTNEXTLINE
9397
operator ddwaf_object *() noexcept { return this; }
9498

@@ -120,7 +124,13 @@ class parameter_base : public ddwaf_object {
120124
}
121125
return intValue;
122126
}
123-
127+
explicit operator double() const
128+
{
129+
if (!is_float()) {
130+
throw bad_cast("parameter not a float");
131+
}
132+
return f64;
133+
}
124134
explicit operator bool() const
125135
{
126136
if (!is_boolean()) {

appsec/src/helper/subscriber/waf.cpp

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ void format_waf_result(
206206
if (events != nullptr) {
207207
const parameter_view events_pv{*events};
208208
for (const auto &event_pv : events_pv) {
209-
event.data.emplace_back(parameter_to_json(event_pv));
209+
event.triggers.emplace_back(parameter_to_json(event_pv));
210210
}
211211
}
212212
} catch (const std::exception &e) {
@@ -563,19 +563,20 @@ void instance::listener::call(
563563
// This converts the events to JSON which is already done in the
564564
// switch below so it's slightly inefficient, albeit since it's only
565565
// done on debug, we can live with it...
566+
std::string events_json;
566567
if (events != nullptr) {
567-
DD_STDLOG(DD_STDLOG_AFTER_WAF,
568-
parameter_to_json(parameter_view{*events}), duration / millis);
568+
events_json = parameter_to_json(parameter_view{*events});
569+
DD_STDLOG(DD_STDLOG_AFTER_WAF, events_json, duration / millis);
569570
}
570-
SPDLOG_DEBUG(
571-
"Waf response: code {} - actions {} - attributes {} - keep {}",
572-
fmt::underlying(code),
571+
SPDLOG_DEBUG("Waf response: code {} - keep {} - duration {} - timeout "
572+
"{} - actions {} - attributes {} - events {}",
573+
fmt::underlying(code), event.keep, duration / millis, timeout,
573574
actions != nullptr ? parameter_to_json(parameter_view{*actions})
574575
: "",
575576
attributes != nullptr
576577
? parameter_to_json(parameter_view{*attributes})
577578
: "",
578-
event.keep);
579+
events_json);
579580
} else {
580581
run_waf();
581582
}
@@ -618,12 +619,40 @@ void instance::listener::call(
618619
}
619620
if (attributes != nullptr) {
620621
const parameter_view attributes_pv{*attributes};
621-
for (const auto &derivative : attributes_pv) {
622+
for (const parameter_view &derivative : attributes_pv) {
622623
if (derivative.key().starts_with("_dd.appsec.s.")) {
623-
attributes_.emplace(
624-
derivative.key(), parameter_to_json(derivative));
624+
std::string json_derivative = parameter_to_json(derivative);
625+
if (json_derivative.length() > max_plain_schema_allowed) {
626+
auto encoded = compress(json_derivative);
627+
if (encoded) {
628+
json_derivative = base64_encode(encoded.value(), false);
629+
}
630+
}
631+
if (json_derivative.length() <= max_schema_size) {
632+
meta_attributes_.emplace(
633+
derivative.key(), std::move(json_derivative));
634+
} else {
635+
SPDLOG_WARN("Schema for key {} is too large to submit",
636+
derivative.key());
637+
}
625638
} else {
626-
attributes_.emplace(derivative.key(), derivative);
639+
if (derivative.is_signed()) {
640+
auto value =
641+
static_cast<double>(static_cast<int64_t>(derivative));
642+
metrics_attributes_.emplace(derivative.key(), value);
643+
} else if (derivative.is_unsigned()) {
644+
auto value =
645+
static_cast<double>(static_cast<uint64_t>(derivative));
646+
metrics_attributes_.emplace(derivative.key(), value);
647+
} else if (derivative.is_float()) {
648+
auto value = static_cast<double>(derivative);
649+
metrics_attributes_.emplace(derivative.key(), value);
650+
} else if (derivative.is_string()) {
651+
meta_attributes_.emplace(derivative.key(), derivative);
652+
} else {
653+
SPDLOG_WARN("Unsupported attribute type for key {}",
654+
derivative.key());
655+
}
627656
}
628657
}
629658
}
@@ -696,21 +725,13 @@ void instance::listener::submit_metrics(
696725
}
697726
}
698727

699-
for (const auto &[key, value] : attributes_) {
700-
std::string derivative = value;
701-
if (value.length() > max_plain_schema_allowed &&
702-
key.starts_with("_dd.appsec.s.")) {
703-
auto encoded = compress(derivative);
704-
if (encoded) {
705-
derivative = base64_encode(encoded.value(), false);
706-
}
707-
}
728+
for (const auto &[key, value] : meta_attributes_) {
729+
msubmitter.submit_span_meta_copy_key(
730+
std::string{key}, std::string(value));
731+
}
708732

709-
if (derivative.length() <= max_schema_size) {
710-
msubmitter.submit_span_meta_copy_key(key, std::move(derivative));
711-
} else {
712-
SPDLOG_WARN("Schema for key {} is too large to submit", key);
713-
}
733+
for (const auto &[key, value] : metrics_attributes_) {
734+
msubmitter.submit_span_metric(key, value);
714735
}
715736
}
716737

appsec/src/helper/subscriber/waf.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ class instance : public dds::subscriber {
7171
unsigned rasp_calls_{0};
7272
unsigned rasp_timeouts_{0};
7373
DDWAF_RET_CODE code_{DDWAF_OK};
74-
std::map<std::string, std::string> attributes_;
74+
std::map<std::string, std::string> meta_attributes_;
75+
std::map<std::string, double> metrics_attributes_;
7576
telemetry::telemetry_tags base_tags_;
7677
bool rule_triggered_{};
7778
bool request_blocked_{};

appsec/tests/helper/engine_test.cpp

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
using dds::remote_config::changeset;
1919

2020
const std::string waf_rule =
21-
R"({"version":"2.1","rules":[{"id":"1","name":"rule1","tags":{"type":"flow1","category":"category1"},"conditions":[{"operator":"match_regex","parameters":{"inputs":[{"address":"arg1","key_path":[]}],"regex":"^string.*"}},{"operator":"match_regex","parameters":{"inputs":[{"address":"arg2","key_path":[]}],"regex":".*"}}]},{"id":"2","name":"rule2","tags":{"type":"flow2","category":"category2"},"conditions":[{"operator":"match_regex","parameters":{"inputs":[{"address":"arg3","key_path":[]}],"regex":"^string.*"}}]}]})";
21+
R"({"version": "2.1", "rules": [{"id": "1", "name": "rule1", "tags": {"type": "flow1", "category": "category1" }, "conditions": [{"operator": "match_regex", "parameters": {"inputs": [{"address": "arg1", "key_path": [] } ], "regex": "^string.*" } }, {"operator": "match_regex", "parameters": {"inputs": [{"address": "arg2", "key_path": [] } ], "regex": ".*" } } ] }, {"id": "2", "name": "rule2", "tags": {"type": "flow2", "category": "category2" }, "conditions": [{"operator": "match_regex", "parameters": {"inputs": [{"address": "arg3", "key_path": [] } ], "regex": "^string.*" } } ] } ], "rules_compat": [{"id": "ttr-000-001", "name": "Trace Tagging Rule: Attributes, Keep, No Event", "tags": {"type": "security_scanner", "category": "attack_attempt" }, "conditions": [{"operator": "match_regex", "parameters": {"inputs": [{"address": "arg4", "key_path": [] } ], "regex": "^string.*" } } ], "output": {"event": false, "keep": true, "attributes": {"_dd.appsec.trace.integer": {"value": 12345 }, "_dd.appsec.trace.string": {"value": "678" }, "_dd.appsec.trace.agent": {"address": "server.request.headers.no_cookies", "key_path": ["user-agent" ] } } }, "on_match": [] }, {"id": "ttr-000-002", "name": "Trace Tagging Rule: Attributes, No Keep, No Event", "tags": {"type": "security_scanner", "category": "attack_attempt" }, "conditions": [{"operator": "match_regex", "parameters": {"inputs": [{"address": "arg5", "key_path": [] } ], "regex": "^string.*" } } ], "output": {"event": false, "keep": false, "attributes": {"_dd.appsec.trace.integer": {"value": 12345 }, "_dd.appsec.trace.string": {"value": "678" }, "_dd.appsec.trace.agent": {"address": "server.request.headers.no_cookies", "key_path": ["user-agent" ] } } }, "on_match": [] } ] })";
2222
const std::string waf_rule_with_data =
2323
R"({"version":"2.1","rules":[{"id":"blk-001-001","name":"Block IP Addresses","tags":{"type":"block_ip","category":"security_response"},"conditions":[{"parameters":{"inputs":[{"address":"http.client_ip"}],"data":"blocked_ips"},"operator":"ip_match"}],"transformers":[],"on_match":["block"]}]})";
2424

@@ -104,7 +104,7 @@ TEST(EngineTest, MultipleSubscriptors)
104104
std::string rasp) -> void {
105105
std::unordered_set<std::string_view> subs{"a", "b", "e", "f"};
106106
if (subs.find(data[0].parameterName) != subs.end()) {
107-
event_.data.push_back("some event");
107+
event_.triggers.push_back("some event");
108108
event_.actions.push_back({dds::action_type::block, {}});
109109
}
110110
}));
@@ -115,7 +115,7 @@ TEST(EngineTest, MultipleSubscriptors)
115115
std::string rasp) -> void {
116116
std::unordered_set<std::string_view> subs{"c", "d", "e", "g"};
117117
if (subs.find(data[0].parameterName) != subs.end()) {
118-
event_.data.push_back("some event");
118+
event_.triggers.push_back("some event");
119119
}
120120
}));
121121

@@ -382,8 +382,8 @@ TEST(EngineTest, WafSubscriptorBasic)
382382
Mock::VerifyAndClearExpectations(&msubmitter);
383383
EXPECT_TRUE(res);
384384
EXPECT_EQ(res->actions[0].type, dds::action_type::record);
385-
EXPECT_EQ(res->events.size(), 1);
386-
for (auto &match : res->events) {
385+
EXPECT_EQ(res->triggers.size(), 1);
386+
for (auto &match : res->triggers) {
387387
rapidjson::Document doc;
388388
doc.Parse(match);
389389
EXPECT_FALSE(doc.HasParseError());
@@ -562,7 +562,7 @@ TEST(EngineTest, WafSubscriptorUpdateRuleData)
562562
auto res = ctx.publish(std::move(p));
563563
EXPECT_TRUE(res);
564564
EXPECT_EQ(res->actions[0].type, dds::action_type::block);
565-
EXPECT_EQ(res->events.size(), 1);
565+
EXPECT_EQ(res->triggers.size(), 1);
566566
}
567567

568568
{
@@ -672,7 +672,7 @@ TEST(EngineTest, WafSubscriptorUpdateRules)
672672
auto res = ctx.publish(std::move(p));
673673
EXPECT_TRUE(res);
674674
EXPECT_EQ(res->actions[0].type, dds::action_type::block);
675-
EXPECT_EQ(res->events.size(), 1);
675+
EXPECT_EQ(res->triggers.size(), 1);
676676
}
677677
}
678678

@@ -791,6 +791,56 @@ TEST(EngineTest, WafSubscriptorUpdateRuleOverrideAndActions)
791791
EXPECT_TRUE(res);
792792
EXPECT_EQ(res->actions[0].type, dds::action_type::record);
793793
}
794+
795+
{ // Test keep is true
796+
auto ctx = e->get_context();
797+
798+
auto p = parameter::map();
799+
p.add("arg4", parameter::string("string 4"sv));
800+
801+
auto res = ctx.publish(std::move(p));
802+
EXPECT_TRUE(res);
803+
EXPECT_EQ(res->actions[0].type, dds::action_type::record);
804+
EXPECT_EQ(res->force_keep, true);
805+
}
806+
}
807+
808+
TEST(EngineTest, TestKeep)
809+
{
810+
auto msubmitter = NiceMock<mock::tel_submitter>{};
811+
812+
auto e{engine::create()};
813+
e->subscribe(waf::instance::from_string(waf_rule, msubmitter));
814+
815+
{
816+
auto ctx = e->get_context();
817+
818+
auto p = parameter::map();
819+
p.add("arg12", parameter::string("string 12"sv));
820+
821+
auto res = ctx.publish(std::move(p));
822+
EXPECT_FALSE(res);
823+
}
824+
{
825+
auto ctx = e->get_context();
826+
827+
auto p = parameter::map();
828+
p.add("arg5", parameter::string("string 5"sv));
829+
830+
auto res = ctx.publish(std::move(p));
831+
EXPECT_FALSE(res);
832+
}
833+
{
834+
auto ctx = e->get_context();
835+
836+
auto p = parameter::map();
837+
p.add("arg4", parameter::string("string 4"sv));
838+
839+
auto res = ctx.publish(std::move(p));
840+
EXPECT_TRUE(res);
841+
EXPECT_EQ(res->actions[0].type, dds::action_type::record);
842+
EXPECT_EQ(res->force_keep, true);
843+
}
794844
}
795845

796846
TEST(EngineTest, WafSubscriptorExclusions)

appsec/tests/helper/remote_config/listeners/engine_listener_test.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ TEST(RemoteConfigEngineListener, EngineRuleUpdate)
164164
auto res = ctx.publish(std::move(p));
165165
ASSERT_TRUE(res);
166166
EXPECT_EQ(res->actions[0].type, dds::action_type::block);
167-
EXPECT_EQ(res->events.size(), 1);
167+
EXPECT_EQ(res->triggers.size(), 1);
168168
}
169169
}
170170

@@ -201,7 +201,7 @@ TEST(RemoteConfigEngineListener, EngineRuleUpdateFallback)
201201
auto res = ctx.publish(std::move(p));
202202
EXPECT_TRUE(res);
203203
EXPECT_EQ(res->actions[0].type, dds::action_type::block);
204-
EXPECT_EQ(res->events.size(), 1);
204+
EXPECT_EQ(res->triggers.size(), 1);
205205
}
206206

207207
remote_config::engine_listener listener(
@@ -586,7 +586,7 @@ TEST(RemoteConfigEngineListener, EngineRuleDataUpdate)
586586
auto res = ctx.publish(std::move(p));
587587
EXPECT_TRUE(res);
588588
EXPECT_EQ(res->actions[0].type, dds::action_type::block);
589-
EXPECT_EQ(res->events.size(), 1);
589+
EXPECT_EQ(res->triggers.size(), 1);
590590
}
591591
}
592592

0 commit comments

Comments
 (0)