From 53f313fee935536bc3824681c4e8b99ccfafc452 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Fri, 11 Apr 2025 08:09:53 +0200 Subject: [PATCH 01/23] New libddwaf builder API --- package.json | 2 +- src/log.h | 2 +- src/main.cpp | 132 +++++++++++++++++++++++++++++++++++++++++++-------- src/main.h | 4 +- 4 files changed, 116 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 218ac838..1431e73b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "8.5.2", "description": "Node.js bindings for libddwaf", "main": "index.js", - "libddwaf_version": "1.22.0", + "libddwaf_version": "1.24.1", "scripts": { "install": "exit 0", "rebuild": "node-gyp rebuild", diff --git a/src/log.h b/src/log.h index b94e393c..e77e19e9 100644 --- a/src/log.h +++ b/src/log.h @@ -4,7 +4,7 @@ **/ #ifndef SRC_LOG_H_ #define SRC_LOG_H_ -#define DEBUG 0 +#define DEBUG 1 #include #if DEBUG == 1 diff --git a/src/main.cpp b/src/main.cpp index 2a9696e8..a914252f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,8 @@ Napi::Object DDWAF::Init(Napi::Env env, Napi::Object exports) { mlog("Setting up class DDWAF"); Napi::Function func = DefineClass(env, "DDWAF", { StaticMethod<&DDWAF::version>("version"), - InstanceMethod<&DDWAF::update>("update"), + InstanceMethod<&DDWAF::update_config>("createOrUpdateConfig"), + InstanceMethod<&DDWAF::remove_config>("removeConfig"), InstanceMethod<&DDWAF::createContext>("createContext"), InstanceMethod<&DDWAF::dispose>("dispose"), InstanceAccessor("disposed", &DDWAF::GetDisposed, nullptr, napi_enumerable), @@ -42,29 +43,35 @@ Napi::Value DDWAF::GetDisposed(const Napi::CallbackInfo& info) { DDWAF::DDWAF(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { Napi::Env env = info.Env(); size_t arg_len = info.Length(); - if (arg_len < 1) { - Napi::Error::New(env, "Wrong number of arguments, expected at least 1").ThrowAsJavaScriptException(); + if (arg_len < 2) { + Napi::Error::New(env, "Wrong number of arguments, expected at least 2").ThrowAsJavaScriptException(); return; } + if (!info[0].IsObject()) { Napi::TypeError::New(env, "First argument must be an object").ThrowAsJavaScriptException(); return; } + if (!info[1].IsString()) { + Napi::TypeError::New(env, "Second argument must be a string").ThrowAsJavaScriptException(); + return; + } + ddwaf_config waf_config{{0, 0, 0}, {nullptr, nullptr}, ddwaf_object_free}; // do not touch these strings after the c_str() assigment std::string key_regex_str; std::string value_regex_str; - if (arg_len >= 2) { // TODO(@simon-id): there is a bug here ? + if (arg_len >= 3) { // TODO(@simon-id): there is a bug here ? // TODO(@simon-id) make a macro here someday - if (!info[1].IsObject()) { + if (!info[2].IsObject()) { Napi::TypeError::New(env, "Second argument must be an object").ThrowAsJavaScriptException(); return; } - Napi::Object config = info[1].ToObject(); + Napi::Object config = info[2].ToObject(); if (config.Has("obfuscatorKeyRegex")) { Napi::Value key_regex = config.Get("obfuscatorKeyRegex"); @@ -94,11 +101,16 @@ DDWAF::DDWAF(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { ddwaf_object rules; mlog("building rules"); to_ddwaf_object(&rules, env, info[0], 0, false, false, JsSet::Create(env), nullptr); + std::string config_path_str = info[1].As().Utf8Value(); + const char* config_path = config_path_str.c_str(); + uint32_t config_path_len = (uint32_t)strlen(config_path); ddwaf_object diagnostics; - mlog("Init WAF"); - ddwaf_handle handle = ddwaf_init(&rules, &waf_config, &diagnostics); + mlog("Init Builder"); + ddwaf_builder builder = ddwaf_builder_init(&waf_config); + bool result = ddwaf_builder_add_or_update_config(builder, config_path, config_path_len, &rules, &diagnostics); + ddwaf_object_free(&rules); Napi::Value diagnostics_js = from_ddwaf_object(&diagnostics, env); @@ -106,11 +118,20 @@ DDWAF::DDWAF(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { ddwaf_object_free(&diagnostics); + if (!result) { + Napi::Error::New(env, "Invalid rules").ThrowAsJavaScriptException(); + return; + } + + mlog("Init WAF"); + ddwaf_handle handle = ddwaf_builder_build_instance(builder); + if (handle == nullptr) { Napi::Error::New(env, "Invalid rules").ThrowAsJavaScriptException(); return; } + this->_builder = builder; this->_handle = handle; this->_disposed = false; @@ -132,44 +153,112 @@ void DDWAF::dispose(const Napi::CallbackInfo& info) { return this->Finalize(info.Env()); } -void DDWAF::update(const Napi::CallbackInfo& info) { - mlog("calling update on DDWAF"); +Napi::Value DDWAF::update_config(const Napi::CallbackInfo& info) { + mlog("Calling update config on DDWAF"); Napi::Env env = info.Env(); if (this->_disposed) { Napi::Error::New(env, "Could not update a disposed WAF instance").ThrowAsJavaScriptException(); - return; + return env.Undefined(); } - if (info.Length() < 1) { - Napi::Error::New(env, "Wrong number of arguments, expected at least 1").ThrowAsJavaScriptException(); - return; + if (info.Length() < 2) { + Napi::Error::New(env, "Wrong number of arguments, expected at least 2").ThrowAsJavaScriptException(); + return env.Undefined(); } + if (!info[0].IsObject()) { Napi::TypeError::New(env, "First argument must be an object").ThrowAsJavaScriptException(); - return; + return env.Undefined(); + } + + if (!info[1].IsString()) { + Napi::TypeError::New(env, "Second argument must be a string").ThrowAsJavaScriptException(); + return env.Undefined(); } ddwaf_object update; - mlog("building rules update"); + mlog("Building config update"); to_ddwaf_object(&update, env, info[0], 0, false, false, JsSet::Create(env), nullptr); + mlog("Obtaining config update path"); + std::string config_path_str = info[1].As().Utf8Value(); + const char* config_path = config_path_str.c_str(); + uint32_t config_path_len = (uint32_t)strlen(config_path); + ddwaf_object diagnostics; - mlog("Update DDWAF instance"); - ddwaf_handle updated_handle = ddwaf_update(this->_handle, &update, &diagnostics); - ddwaf_object_free(&update); + mlog("Applying new config to builder"); + bool update_result = ddwaf_builder_add_or_update_config(this->_builder, config_path, config_path_len, &update, &diagnostics); Napi::Value diagnostics_js = from_ddwaf_object(&diagnostics, env); info.This().As().Set("diagnostics", diagnostics_js); ddwaf_object_free(&diagnostics); + if (!update_result) { + mlog("DDWAF Builder update config has failed"); + return Napi::Boolean::New(env, false); + } + + mlog("Update DDWAF instance"); + ddwaf_handle updated_handle = ddwaf_builder_build_instance(this->_builder); + ddwaf_object_free(&update); + if (updated_handle == nullptr) { mlog("DDWAF updated handle is null"); - Napi::Error::New(env, "WAF has not been updated").ThrowAsJavaScriptException(); - return; + return Napi::Boolean::New(env, false); + } + + mlog("New DDWAF updated instance") + ddwaf_destroy(this->_handle); + this->_handle = updated_handle; + + this->update_known_addresses(info); + this->update_known_actions(info); + return Napi::Boolean::New(env, true); +} + +Napi::Value DDWAF::remove_config(const Napi::CallbackInfo& info) { + mlog("Calling remove config on DDWAF"); + + Napi::Env env = info.Env(); + + if (this->_disposed) { + Napi::Error::New(env, "Could not update a disposed WAF instance").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + if (info.Length() < 1) { + Napi::Error::New(env, "Wrong number of arguments, expected at least 1").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + if (!info[0].IsString()) { + Napi::TypeError::New(env, "First argument must be a string").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + mlog("Obtaining config remove path"); + std::string config_path_str = info[0].As().Utf8Value(); + const char* config_path = config_path_str.c_str(); + uint32_t config_path_len = (uint32_t)strlen(config_path); + + mlog("Applying removed config to builder"); + bool remove_result = ddwaf_builder_remove_config(this->_builder, config_path, config_path_len); + + if (!remove_result) { + mlog("DDWAF Builder remove config has failed"); + return Napi::Boolean::New(env, false); + } + + mlog("Update DDWAF instance"); + ddwaf_handle updated_handle = ddwaf_builder_build_instance(this->_builder); + + if (updated_handle == nullptr) { + mlog("DDWAF handle after removing config is null"); + return Napi::Boolean::New(env, false); } mlog("New DDWAF updated instance") @@ -178,6 +267,7 @@ void DDWAF::update(const Napi::CallbackInfo& info) { this->update_known_addresses(info); this->update_known_actions(info); + return Napi::Boolean::New(env, true); } void DDWAF::update_known_addresses(const Napi::CallbackInfo& info) { diff --git a/src/main.h b/src/main.h index 709ab881..574e8e59 100644 --- a/src/main.h +++ b/src/main.h @@ -21,7 +21,8 @@ class DDWAF : public Napi::ObjectWrap { explicit DDWAF(const Napi::CallbackInfo& info); // JS instance methods - void update(const Napi::CallbackInfo& info); + Napi::Value update_config(const Napi::CallbackInfo& info); + Napi::Value remove_config(const Napi::CallbackInfo& info); Napi::Value createContext(const Napi::CallbackInfo& info); void Finalize(Napi::Env env); Napi::Value GetDisposed(const Napi::CallbackInfo& info); @@ -32,6 +33,7 @@ class DDWAF : public Napi::ObjectWrap { void update_known_actions(const Napi::CallbackInfo& info); bool _disposed; + ddwaf_builder _builder; ddwaf_handle _handle; }; From df0dd174bd1bcbd69d47f64a98aeb2e7c7623a13 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Fri, 11 Apr 2025 08:22:05 +0200 Subject: [PATCH 02/23] Update typings --- index.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1f3d1363..065a343f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -70,12 +70,13 @@ export class DDWAF { readonly knownAddresses: Set; readonly knownActions: Set; - constructor(rules: rules, config?: { + constructor(rules: rules, rulesPath: string, config?: { obfuscatorKeyRegex?: string, obfuscatorValueRegex?: string }); - update(rules: rules): void; + createOrUpdateConfig(config: rules, path: string): boolean; + removeConfig(path: string): boolean; createContext(): DDWAFContext; dispose(): void; From 0a805918130a596b05689372d6e3f2b5aaff65e1 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Fri, 11 Apr 2025 15:49:30 +0200 Subject: [PATCH 03/23] Update tests --- test/index.js | 187 +++++++++++++++++++++++--------------------------- 1 file changed, 84 insertions(+), 103 deletions(-) diff --git a/test/index.js b/test/index.js index e9091b2e..5d10b932 100644 --- a/test/index.js +++ b/test/index.js @@ -20,12 +20,13 @@ describe('DDWAF', () => { }) it('should have diagnostics', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') assert.deepStrictEqual(waf.diagnostics, { ruleset_version: '1.3.1', actions: { errors: {}, + warnings: {}, failed: [], loaded: [ 'customblock' @@ -33,18 +34,6 @@ describe('DDWAF', () => { skipped: [] }, rules: { - addresses: { - optional: [], - required: [ - 'http.client_ip', - 'server.request.headers.no_cookies', - 'server.response.status', - 'value_attack', - 'key_attack', - 'custom_value_attack', - 'server.request.body' - ] - }, loaded: [ 'block_ip', 'value_attack', @@ -65,13 +54,14 @@ describe('DDWAF', () => { 'invalid_2', 'invalid_3' ] - } + }, + warnings: {} } }) }) it('should have knownAddresses', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') assert.deepStrictEqual(waf.knownAddresses, new Set([ 'http.client_ip', @@ -85,7 +75,7 @@ describe('DDWAF', () => { }) it('should have knownActions', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') assert.deepStrictEqual(waf.knownActions, new Set([ 'block_request' @@ -93,7 +83,7 @@ describe('DDWAF', () => { }) it('should collect an attack and cleanup everything', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const payload = { persistent: { @@ -129,7 +119,7 @@ describe('DDWAF', () => { }) it('should collect different attacks on ephemeral addresses', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() let result = context.run({ ephemeral: { @@ -162,24 +152,37 @@ describe('DDWAF', () => { describe('WAF update', () => { it('should throw an error when updating a disposed WAF instance', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') waf.dispose() - assert.throws(() => waf.update(rules), new Error('Could not update a disposed WAF instance')) + assert.throws( + () => waf.createOrUpdateConfig(rules, 'config/update'), + new Error('Could not update a disposed WAF instance')) }) it('should throw an error when updating a WAF instance with no arguments', () => { - const waf = new DDWAF(rules) - assert.throws(() => waf.update(), new Error('Wrong number of arguments, expected at least 1')) + const waf = new DDWAF(rules, 'recommended') + assert.throws(() => waf.createOrUpdateConfig(), new Error('Wrong number of arguments, expected at least 2')) + }) + + it('should throw an error when updating a WAF instance with just one argument', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws(() => waf.createOrUpdateConfig({}), new Error('Wrong number of arguments, expected at least 2')) }) it('should throw a type error when updating a WAF instance with invalid arguments', () => { - const waf = new DDWAF(rules) - assert.throws(() => waf.update('string'), new TypeError('First argument must be an object')) + const waf = new DDWAF(rules, 'recommended') + assert.throws( + () => waf.createOrUpdateConfig('string', 'config/update'), + new TypeError('First argument must be an object') + ) }) it('should throw an exception when WAF update has not been updated - nothing to update', () => { - const waf = new DDWAF(rules) - assert.throws(() => waf.update({}), new Error('WAF has not been updated')) + const waf = new DDWAF(rules, 'recommended') + assert.throws( + () => waf.createOrUpdateConfig({}, 'config/update'), + new Error('WAF has not been updated') + ) }) it('should update diagnostics, knownAddresses, and knownActions when updating an instance with new ruleSet', () => { @@ -197,7 +200,7 @@ describe('DDWAF', () => { } }], rules: [{ - id: 'block_ip', + id: 'block_ip_original', name: 'block ip', tags: { type: 'ip_addresses', @@ -219,12 +222,13 @@ describe('DDWAF', () => { 'customredirect' ] }] - }) + }, 'new_ruleset') assert.deepStrictEqual(waf.diagnostics, { ruleset_version: '1.3.0', actions: { errors: {}, + warnings: {}, failed: [], loaded: [ 'customredirect' @@ -232,14 +236,11 @@ describe('DDWAF', () => { skipped: [] }, rules: { - addresses: { - optional: [], - required: ['http.client_ip'] - }, - loaded: ['block_ip'], + loaded: ['block_ip_original'], failed: [], skipped: [], - errors: {} + errors: {}, + warnings: {} } }) assert.deepStrictEqual(waf.knownAddresses, new Set([ @@ -249,11 +250,12 @@ describe('DDWAF', () => { 'redirect_request' ])) - waf.update(rules) + waf.createOrUpdateConfig(rules, 'config/update') assert.deepStrictEqual(waf.diagnostics, { ruleset_version: '1.3.1', actions: { errors: {}, + warnings: {}, failed: [], loaded: [ 'customblock' @@ -261,18 +263,6 @@ describe('DDWAF', () => { skipped: [] }, rules: { - addresses: { - optional: [], - required: [ - 'http.client_ip', - 'server.request.headers.no_cookies', - 'server.response.status', - 'value_attack', - 'key_attack', - 'custom_value_attack', - 'server.request.body' - ] - }, loaded: [ 'block_ip', 'value_attack', @@ -293,7 +283,8 @@ describe('DDWAF', () => { 'invalid_2', 'invalid_3' ] - } + }, + warnings: {} } }) assert.deepStrictEqual(waf.knownAddresses, new Set([ @@ -306,7 +297,8 @@ describe('DDWAF', () => { 'custom_value_attack' ])) assert.deepStrictEqual(waf.knownActions, new Set([ - 'block_request' + 'block_request', + 'redirect_request' ])) waf.dispose() @@ -320,7 +312,7 @@ describe('DDWAF', () => { } } - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const resultBeforeUpdatingRuleData = context.run(payload, TIMEOUT) assert(!resultBeforeUpdatingRuleData.status) @@ -335,7 +327,7 @@ describe('DDWAF', () => { ] } - waf.update(updateWithRulesData) + waf.createOrUpdateConfig(updateWithRulesData, 'config/update') const contextWithRuleData = waf.createContext() const resultAfterUpdatingRuleData = contextWithRuleData.run(payload, TIMEOUT) @@ -385,7 +377,7 @@ describe('DDWAF', () => { value_attack: 'matchall' } } - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const contextToggledOn = waf.createContext() const resultToggledOn = contextToggledOn.run(payload, TIMEOUT) @@ -398,7 +390,7 @@ describe('DDWAF', () => { rules_override: testData.rulesOverride } - waf.update(updateWithRulesOverride) + waf.createOrUpdateConfig(updateWithRulesOverride, 'config/update') const contextToggledOff = waf.createContext() const resultToggledOff = contextToggledOff.run(payload, TIMEOUT) @@ -443,7 +435,7 @@ describe('DDWAF', () => { } } - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const monitorContext = waf.createContext() const resultMonitor = monitorContext.run(payload, TIMEOUT) @@ -457,7 +449,7 @@ describe('DDWAF', () => { rules_override: testData.rulesOverride } - waf.update(updateWithRulesOverride) + waf.createOrUpdateConfig(updateWithRulesOverride, 'config/update') const blockContext = waf.createContext() const resultBlock = blockContext.run(payload, TIMEOUT) @@ -477,7 +469,7 @@ describe('DDWAF', () => { }) it('should support case_sensitive', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result = context.run({ @@ -491,12 +483,12 @@ describe('DDWAF', () => { }) it('should refuse invalid rule', () => { - assert.throws(() => new DDWAF({}), new Error('Invalid rules')) - assert.throws(() => new DDWAF(''), new TypeError('First argument must be an object')) + assert.throws(() => new DDWAF({}, 'empty_rules'), new Error('Invalid rules')) + assert.throws(() => new DDWAF('', 'non_object_rules'), new TypeError('First argument must be an object')) }) it('should refuse to run with bad signatures', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const wronArgsError = new Error('Wrong number of arguments, 2 expected') @@ -546,7 +538,7 @@ describe('DDWAF', () => { [function fn () {}, 'function fn () {}'] ]) - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') for (const [key, expected] of possibleKeys) { const context = waf.createContext() @@ -584,7 +576,7 @@ describe('DDWAF', () => { [function fn () {}, undefined] ]) - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') for (const [value, expected] of possibleValues) { const context = waf.createContext() @@ -608,7 +600,7 @@ describe('DDWAF', () => { }) it('should obfuscate keys', () => { - const waf = new DDWAF(rules, { + const waf = new DDWAF(rules, 'recommended', { obfuscatorKeyRegex: 'password' }) const context = waf.createContext() @@ -630,7 +622,7 @@ describe('DDWAF', () => { }) it('should obfuscate values', () => { - const waf = new DDWAF(rules, { + const waf = new DDWAF(rules, 'recommended', { obfuscatorValueRegex: 'value_attack' }) const context = waf.createContext() @@ -650,13 +642,10 @@ describe('DDWAF', () => { }) it('should collect derivatives information when a rule match', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.query')) - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.body')) - assert(waf.diagnostics.processors.addresses.required.includes('waf.context.processor')) assert(waf.diagnostics.processors.loaded.includes('processor-001')) assert.equal(waf.diagnostics.processors.failed.length, 0) @@ -680,12 +669,9 @@ describe('DDWAF', () => { }) it('should collect derivatives information when a rule does not match', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.query')) - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.body')) - assert(waf.diagnostics.processors.addresses.required.includes('waf.context.processor')) assert(waf.diagnostics.processors.loaded.includes('processor-001')) assert.equal(waf.diagnostics.processors.failed.length, 0) @@ -708,12 +694,9 @@ describe('DDWAF', () => { }) it('should collect all derivatives types', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.query')) - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.body')) - assert(waf.diagnostics.processors.addresses.required.includes('waf.context.processor')) assert(waf.diagnostics.processors.loaded.includes('processor-001')) assert.equal(waf.diagnostics.processors.failed.length, 0) @@ -770,12 +753,9 @@ describe('DDWAF', () => { }) it('should collect derivatives in two consecutive calls', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.query')) - assert(waf.diagnostics.processors.addresses.optional.includes('server.request.body')) - assert(waf.diagnostics.processors.addresses.required.includes('waf.context.processor')) assert(waf.diagnostics.processors.loaded.includes('processor-001')) assert.equal(waf.diagnostics.processors.failed.length, 0) @@ -815,7 +795,7 @@ describe('DDWAF', () => { describe('Action semantics', () => { it('should support action definition in initialisation', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result = context.run({ @@ -838,7 +818,7 @@ describe('DDWAF', () => { }) it('should support action definition in update', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const updatedRules = Object.assign({}, rules) updatedRules.actions = [{ @@ -851,7 +831,8 @@ describe('DDWAF', () => { } }] - waf.update(updatedRules) + waf.removeConfig('recommended') + waf.createOrUpdateConfig(updatedRules, 'recommended_action_modified') const context = waf.createContext() const resultWithUpdatedAction = context.run({ @@ -877,7 +858,7 @@ describe('DDWAF', () => { describe('limit tests', () => { it('should ignore elements too far in the objects', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context1 = waf.createContext() const result1 = context1.run({ @@ -908,7 +889,7 @@ describe('limit tests', () => { }) it('should match a moderately deeply nested object', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result = context.run({ @@ -923,7 +904,7 @@ describe('limit tests', () => { }) it('should set as invalid circular property dependency', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const payload = { key: 'value', @@ -954,7 +935,7 @@ describe('limit tests', () => { }) it('should set as invalid circular property dependency in deeper level', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const payload = { key: 'value', @@ -985,7 +966,7 @@ describe('limit tests', () => { }) it('should set as invalid circular array dependency', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const payload = [] payload.push(payload, payload, payload) @@ -1003,7 +984,7 @@ describe('limit tests', () => { }) it('should set as invalid circular array dependency in deeper levels', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const payload = [] payload.push({ payload }) @@ -1023,7 +1004,7 @@ describe('limit tests', () => { }) it('should not set as invalid same instances in array', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const item = { key: 'value', @@ -1048,7 +1029,7 @@ describe('limit tests', () => { }) it('should not set as invalid same instance in different properties', () => { - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const prop = { key: 'value', @@ -1079,7 +1060,7 @@ describe('limit tests', () => { }) it('should not match an extremely deeply nested object', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result = context.run({ @@ -1094,7 +1075,7 @@ describe('limit tests', () => { }) it('should not limit the rules object', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') // test first item in big rule const context1 = waf.createContext() @@ -1118,7 +1099,7 @@ describe('limit tests', () => { }) it('should use custom toJSON function', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const body = { a: 'not_an_attack' } @@ -1148,7 +1129,7 @@ describe('limit tests', () => { }) it('should use custom toJSON function in arrays', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const body = ['not_an_attack'] @@ -1193,7 +1174,7 @@ describe('limit tests', () => { } } - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const result = context.run({ persistent: { @@ -1236,7 +1217,7 @@ describe('limit tests', () => { } } - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const result = context.run({ persistent: { @@ -1281,7 +1262,7 @@ describe('limit tests', () => { } } - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const result = context.run({ persistent: { @@ -1313,7 +1294,7 @@ describe('limit tests', () => { c: 'c' } - const waf = new DDWAF(processor) + const waf = new DDWAF(processor, 'processor_rules') const context = waf.createContext() const result = context.run({ persistent: { @@ -1335,7 +1316,7 @@ describe('limit tests', () => { }) it('should truncate string values exceeding maximum length', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context1 = waf.createContext() const result1 = context1.run({ @@ -1362,7 +1343,7 @@ describe('limit tests', () => { }) it('should handle multiple truncations in complex nested structure', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const longValue1 = 'a'.repeat(5000) const longValue2 = 'b'.repeat(6000) @@ -1406,7 +1387,7 @@ describe('limit tests', () => { describe('Handle errors', () => { it('should handle invalid arguments number', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() try { @@ -1419,7 +1400,7 @@ describe('Handle errors', () => { }) it('should handle invalid timeout arguments', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() try { @@ -1440,7 +1421,7 @@ describe('Handle errors', () => { }) it('should handle invalid arguments', () => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() try { From 2475e8f6bc323aa5c096ae19a76d48218b5c05fb Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Mon, 14 Apr 2025 15:12:28 +0200 Subject: [PATCH 04/23] Throw error on update/remove config fail --- src/main.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a914252f..e9cb5198 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -208,7 +208,8 @@ Napi::Value DDWAF::update_config(const Napi::CallbackInfo& info) { if (updated_handle == nullptr) { mlog("DDWAF updated handle is null"); - return Napi::Boolean::New(env, false); + Napi::Error::New(env, "WAF has not been updated").ThrowAsJavaScriptException(); + return; } mlog("New DDWAF updated instance") @@ -258,7 +259,8 @@ Napi::Value DDWAF::remove_config(const Napi::CallbackInfo& info) { if (updated_handle == nullptr) { mlog("DDWAF handle after removing config is null"); - return Napi::Boolean::New(env, false); + Napi::Error::New(env, "WAF has not been updated").ThrowAsJavaScriptException(); + return; } mlog("New DDWAF updated instance") From c0c5363bce338089ab9e3b520541268d11a4ea75 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Mon, 14 Apr 2025 17:40:01 +0200 Subject: [PATCH 05/23] Refactor instance update handling --- src/main.cpp | 34 +++++++++------------ test/index.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 27 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index e9cb5198..9ea5c61f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -206,18 +206,15 @@ Napi::Value DDWAF::update_config(const Napi::CallbackInfo& info) { ddwaf_handle updated_handle = ddwaf_builder_build_instance(this->_builder); ddwaf_object_free(&update); - if (updated_handle == nullptr) { - mlog("DDWAF updated handle is null"); - Napi::Error::New(env, "WAF has not been updated").ThrowAsJavaScriptException(); - return; - } + if (updated_handle != nullptr) { + mlog("New DDWAF updated instance") + ddwaf_destroy(this->_handle); + this->_handle = updated_handle; - mlog("New DDWAF updated instance") - ddwaf_destroy(this->_handle); - this->_handle = updated_handle; + this->update_known_addresses(info); + this->update_known_actions(info); + } - this->update_known_addresses(info); - this->update_known_actions(info); return Napi::Boolean::New(env, true); } @@ -257,18 +254,15 @@ Napi::Value DDWAF::remove_config(const Napi::CallbackInfo& info) { mlog("Update DDWAF instance"); ddwaf_handle updated_handle = ddwaf_builder_build_instance(this->_builder); - if (updated_handle == nullptr) { - mlog("DDWAF handle after removing config is null"); - Napi::Error::New(env, "WAF has not been updated").ThrowAsJavaScriptException(); - return; - } + if (updated_handle != nullptr) { + mlog("New DDWAF updated instance") + ddwaf_destroy(this->_handle); + this->_handle = updated_handle; - mlog("New DDWAF updated instance") - ddwaf_destroy(this->_handle); - this->_handle = updated_handle; + this->update_known_addresses(info); + this->update_known_actions(info); + } - this->update_known_addresses(info); - this->update_known_actions(info); return Napi::Boolean::New(env, true); } diff --git a/test/index.js b/test/index.js index 5d10b932..efc7137b 100644 --- a/test/index.js +++ b/test/index.js @@ -151,7 +151,7 @@ describe('DDWAF', () => { }) describe('WAF update', () => { - it('should throw an error when updating a disposed WAF instance', () => { + it('should throw an error when updating configuration on a disposed WAF instance', () => { const waf = new DDWAF(rules, 'recommended') waf.dispose() assert.throws( @@ -159,17 +159,17 @@ describe('DDWAF', () => { new Error('Could not update a disposed WAF instance')) }) - it('should throw an error when updating a WAF instance with no arguments', () => { + it('should throw an error when updating configuration with no arguments', () => { const waf = new DDWAF(rules, 'recommended') assert.throws(() => waf.createOrUpdateConfig(), new Error('Wrong number of arguments, expected at least 2')) }) - it('should throw an error when updating a WAF instance with just one argument', () => { + it('should throw an error when updating configuration with just one argument', () => { const waf = new DDWAF(rules, 'recommended') assert.throws(() => waf.createOrUpdateConfig({}), new Error('Wrong number of arguments, expected at least 2')) }) - it('should throw a type error when updating a WAF instance with invalid arguments', () => { + it('should throw a type error when updating configuration with invalid arguments', () => { const waf = new DDWAF(rules, 'recommended') assert.throws( () => waf.createOrUpdateConfig('string', 'config/update'), @@ -177,14 +177,84 @@ describe('DDWAF', () => { ) }) - it('should throw an exception when WAF update has not been updated - nothing to update', () => { + it('should return false when updating configuration with invalid configuration', () => { const waf = new DDWAF(rules, 'recommended') + assert.strictEqual(waf.createOrUpdateConfig({}, 'config/update'), false) + }) + + it('should return true when updating configuration', () => { + const waf = new DDWAF(rules, 'recommended') + const newConfig = { + version: '2.2', + metadata: { + rules_version: '1.3.0' + }, + actions: [{ + id: 'customredirect', + type: 'redirect_request', + parameters: { + status_code: '301', + location: '/' + } + }], + rules: [{ + id: 'block_ip_original', + name: 'block ip', + tags: { + type: 'ip_addresses', + category: 'blocking' + }, + conditions: [ + { + parameters: { + inputs: [ + { address: 'http.client_ip' } + ], + data: 'blocked_ips' + }, + operator: 'ip_match' + } + ], + transformers: [], + on_match: [ + 'customredirect' + ] + }] + } + assert.strictEqual(waf.createOrUpdateConfig(newConfig, 'config/update'), true) + }) + + it('should throw an error when removing a configuration on a disposed WAF instance', () => { + const waf = new DDWAF(rules, 'recommended') + waf.dispose() assert.throws( - () => waf.createOrUpdateConfig({}, 'config/update'), - new Error('WAF has not been updated') + () => waf.removeConfig('config/update'), + new Error('Could not update a disposed WAF instance')) + }) + + it('should throw an error when removing a configuration with no arguments', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws(() => waf.removeConfig(), new Error('Wrong number of arguments, expected at least 1')) + }) + + it('should throw a type error when removing a configuration with invalid arguments', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws( + () => waf.removeConfig(null), + new TypeError('First argument must be a string') ) }) + it('should return true when removing an existing configuration', () => { + const waf = new DDWAF(rules, 'recommended') + assert.strictEqual(waf.removeConfig('recommended'), true) + }) + + it('should return false when removing a non-existing configuration', () => { + const waf = new DDWAF(rules, 'recommended') + assert.strictEqual(waf.removeConfig('config/update'), false) + }) + it('should update diagnostics, knownAddresses, and knownActions when updating an instance with new ruleSet', () => { const waf = new DDWAF({ version: '2.2', From 626d0a8631ce812a03305902ca67d195fbe0bf7a Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 15 Apr 2025 09:56:11 +0200 Subject: [PATCH 06/23] Get loaded config paths --- index.d.ts | 2 ++ src/main.cpp | 18 ++++++++++++++++++ src/main.h | 1 + test/index.js | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/index.d.ts b/index.d.ts index 065a343f..0abd5d63 100644 --- a/index.d.ts +++ b/index.d.ts @@ -56,6 +56,8 @@ export class DDWAF { readonly disposed: boolean; + readonly configPaths: string[]; + readonly diagnostics: { ruleset_version?: string, rules?: diagnosticsResult, diff --git a/src/main.cpp b/src/main.cpp index 9ea5c61f..bd260944 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,7 @@ Napi::Object DDWAF::Init(Napi::Env env, Napi::Object exports) { StaticMethod<&DDWAF::version>("version"), InstanceMethod<&DDWAF::update_config>("createOrUpdateConfig"), InstanceMethod<&DDWAF::remove_config>("removeConfig"), + InstanceAccessor("configPaths", &DDWAF::GetConfigPaths, nullptr, napi_enumerable), InstanceMethod<&DDWAF::createContext>("createContext"), InstanceMethod<&DDWAF::dispose>("dispose"), InstanceAccessor("disposed", &DDWAF::GetDisposed, nullptr, napi_enumerable), @@ -266,6 +267,23 @@ Napi::Value DDWAF::remove_config(const Napi::CallbackInfo& info) { return Napi::Boolean::New(env, true); } +Napi::Value DDWAF::GetConfigPaths(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + if (this->_disposed) { + return Napi::Array::New(info.Env(), 0); + } + + ddwaf_object config_paths; + uint32_t path_count = ddwaf_builder_get_config_paths(this->_builder, &config_paths, nullptr, 0); + + Napi::Value config_paths_js = from_ddwaf_object(&config_paths, info.Env()); + + ddwaf_object_free(&config_paths); + + return config_paths_js; +} + void DDWAF::update_known_addresses(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); diff --git a/src/main.h b/src/main.h index 574e8e59..fa05e04f 100644 --- a/src/main.h +++ b/src/main.h @@ -23,6 +23,7 @@ class DDWAF : public Napi::ObjectWrap { // JS instance methods Napi::Value update_config(const Napi::CallbackInfo& info); Napi::Value remove_config(const Napi::CallbackInfo& info); + Napi::Value GetConfigPaths(const Napi::CallbackInfo& info); Napi::Value createContext(const Napi::CallbackInfo& info); void Finalize(Napi::Env env); Napi::Value GetDisposed(const Napi::CallbackInfo& info); diff --git a/test/index.js b/test/index.js index efc7137b..9bed7f88 100644 --- a/test/index.js +++ b/test/index.js @@ -255,6 +255,43 @@ describe('DDWAF', () => { assert.strictEqual(waf.removeConfig('config/update'), false) }) + it('should have no loaded configuration paths on WAF disposed instance', () => { + const waf = new DDWAF(rules, 'recommended') + waf.dispose() + assert.strictEqual(waf.configPaths.length, 0) + }) + + it('should have loaded configuration paths', () => { + const waf = new DDWAF(rules, 'recommended') + const newConfig = { + rules: [{ + id: 'block_ip_original', + name: 'block ip', + tags: { + type: 'ip_addresses', + category: 'blocking' + }, + conditions: [ + { + parameters: { + inputs: [ + { address: 'http.client_ip' } + ], + data: 'blocked_ips' + }, + operator: 'ip_match' + } + ], + transformers: [], + on_match: [ + 'customredirect' + ] + }] + } + waf.createOrUpdateConfig(newConfig, 'config/update') + assert.deepStrictEqual(['config/update', 'recommended'], waf.configPaths) + }) + it('should update diagnostics, knownAddresses, and knownActions when updating an instance with new ruleSet', () => { const waf = new DDWAF({ version: '2.2', From cfa038b217cc1a101ef6cf22862ecdd1069434fc Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 16 Apr 2025 11:27:14 +0200 Subject: [PATCH 07/23] Macro for path arguments --- src/main.cpp | 9 +++------ src/main.h | 2 ++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index bd260944..1ff3b1b4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -104,13 +104,12 @@ DDWAF::DDWAF(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { to_ddwaf_object(&rules, env, info[0], 0, false, false, JsSet::Create(env), nullptr); std::string config_path_str = info[1].As().Utf8Value(); const char* config_path = config_path_str.c_str(); - uint32_t config_path_len = (uint32_t)strlen(config_path); ddwaf_object diagnostics; mlog("Init Builder"); ddwaf_builder builder = ddwaf_builder_init(&waf_config); - bool result = ddwaf_builder_add_or_update_config(builder, config_path, config_path_len, &rules, &diagnostics); + bool result = ddwaf_builder_add_or_update_config(builder, LSTRARG(config_path), &rules, &diagnostics); ddwaf_object_free(&rules); @@ -186,12 +185,11 @@ Napi::Value DDWAF::update_config(const Napi::CallbackInfo& info) { mlog("Obtaining config update path"); std::string config_path_str = info[1].As().Utf8Value(); const char* config_path = config_path_str.c_str(); - uint32_t config_path_len = (uint32_t)strlen(config_path); ddwaf_object diagnostics; mlog("Applying new config to builder"); - bool update_result = ddwaf_builder_add_or_update_config(this->_builder, config_path, config_path_len, &update, &diagnostics); + bool update_result = ddwaf_builder_add_or_update_config(this->_builder, LSTRARG(config_path), &update, &diagnostics); Napi::Value diagnostics_js = from_ddwaf_object(&diagnostics, env); info.This().As().Set("diagnostics", diagnostics_js); @@ -242,10 +240,9 @@ Napi::Value DDWAF::remove_config(const Napi::CallbackInfo& info) { mlog("Obtaining config remove path"); std::string config_path_str = info[0].As().Utf8Value(); const char* config_path = config_path_str.c_str(); - uint32_t config_path_len = (uint32_t)strlen(config_path); mlog("Applying removed config to builder"); - bool remove_result = ddwaf_builder_remove_config(this->_builder, config_path, config_path_len); + bool remove_result = ddwaf_builder_remove_config(this->_builder, LSTRARG(config_path)); if (!remove_result) { mlog("DDWAF Builder remove config has failed"); diff --git a/src/main.h b/src/main.h index fa05e04f..a1ca7da8 100644 --- a/src/main.h +++ b/src/main.h @@ -8,6 +8,8 @@ #include #include "src/metrics.h" +#define LSTRARG(value) value, (uint32_t)strlen(value) + // TODO(@vdeturckheim): logs with ddwaf_set_log_cb // TODO(@vdeturckheim): fix issue when used with workers From f271ca6100e0c062045afc030adb067cd8bf80ce Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 22 Apr 2025 10:39:50 +0200 Subject: [PATCH 08/23] Minor fixes --- src/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 1ff3b1b4..da32e043 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -268,13 +268,13 @@ Napi::Value DDWAF::GetConfigPaths(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (this->_disposed) { - return Napi::Array::New(info.Env(), 0); + return Napi::Array::New(env, 0); } ddwaf_object config_paths; - uint32_t path_count = ddwaf_builder_get_config_paths(this->_builder, &config_paths, nullptr, 0); + ddwaf_builder_get_config_paths(this->_builder, &config_paths, nullptr, 0); - Napi::Value config_paths_js = from_ddwaf_object(&config_paths, info.Env()); + Napi::Value config_paths_js = from_ddwaf_object(&config_paths, env); ddwaf_object_free(&config_paths); From f9735ead67cc71d469d4da19d023947c87672bc6 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 22 Apr 2025 11:40:21 +0200 Subject: [PATCH 09/23] Organize test --- test/index.js | 253 ++++++++++++++++++++++++++------------------------ 1 file changed, 130 insertions(+), 123 deletions(-) diff --git a/test/index.js b/test/index.js index 9bed7f88..9aed2237 100644 --- a/test/index.js +++ b/test/index.js @@ -151,145 +151,152 @@ describe('DDWAF', () => { }) describe('WAF update', () => { - it('should throw an error when updating configuration on a disposed WAF instance', () => { - const waf = new DDWAF(rules, 'recommended') - waf.dispose() - assert.throws( - () => waf.createOrUpdateConfig(rules, 'config/update'), - new Error('Could not update a disposed WAF instance')) - }) - it('should throw an error when updating configuration with no arguments', () => { - const waf = new DDWAF(rules, 'recommended') - assert.throws(() => waf.createOrUpdateConfig(), new Error('Wrong number of arguments, expected at least 2')) - }) + describe('Update config', () => { + it('should throw an error when updating configuration on a disposed WAF instance', () => { + const waf = new DDWAF(rules, 'recommended') + waf.dispose() + assert.throws( + () => waf.createOrUpdateConfig(rules, 'config/update'), + new Error('Could not update a disposed WAF instance')) + }) - it('should throw an error when updating configuration with just one argument', () => { - const waf = new DDWAF(rules, 'recommended') - assert.throws(() => waf.createOrUpdateConfig({}), new Error('Wrong number of arguments, expected at least 2')) - }) + it('should throw an error when updating configuration with no arguments', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws(() => waf.createOrUpdateConfig(), new Error('Wrong number of arguments, expected at least 2')) + }) - it('should throw a type error when updating configuration with invalid arguments', () => { - const waf = new DDWAF(rules, 'recommended') - assert.throws( - () => waf.createOrUpdateConfig('string', 'config/update'), - new TypeError('First argument must be an object') - ) - }) + it('should throw an error when updating configuration with just one argument', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws(() => waf.createOrUpdateConfig({}), new Error('Wrong number of arguments, expected at least 2')) + }) - it('should return false when updating configuration with invalid configuration', () => { - const waf = new DDWAF(rules, 'recommended') - assert.strictEqual(waf.createOrUpdateConfig({}, 'config/update'), false) - }) + it('should throw a type error when updating configuration with invalid arguments', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws( + () => waf.createOrUpdateConfig('string', 'config/update'), + new TypeError('First argument must be an object') + ) + }) - it('should return true when updating configuration', () => { - const waf = new DDWAF(rules, 'recommended') - const newConfig = { - version: '2.2', - metadata: { - rules_version: '1.3.0' - }, - actions: [{ - id: 'customredirect', - type: 'redirect_request', - parameters: { - status_code: '301', - location: '/' - } - }], - rules: [{ - id: 'block_ip_original', - name: 'block ip', - tags: { - type: 'ip_addresses', - category: 'blocking' + it('should return false when updating configuration with invalid configuration', () => { + const waf = new DDWAF(rules, 'recommended') + assert.strictEqual(waf.createOrUpdateConfig({}, 'config/update'), false) + }) + + it('should return true when updating configuration', () => { + const waf = new DDWAF(rules, 'recommended') + const newConfig = { + version: '2.2', + metadata: { + rules_version: '1.3.0' }, - conditions: [ - { - parameters: { - inputs: [ - { address: 'http.client_ip' } - ], - data: 'blocked_ips' - }, - operator: 'ip_match' + actions: [{ + id: 'customredirect', + type: 'redirect_request', + parameters: { + status_code: '301', + location: '/' } - ], - transformers: [], - on_match: [ - 'customredirect' - ] - }] - } - assert.strictEqual(waf.createOrUpdateConfig(newConfig, 'config/update'), true) + }], + rules: [{ + id: 'block_ip_original', + name: 'block ip', + tags: { + type: 'ip_addresses', + category: 'blocking' + }, + conditions: [ + { + parameters: { + inputs: [ + { address: 'http.client_ip' } + ], + data: 'blocked_ips' + }, + operator: 'ip_match' + } + ], + transformers: [], + on_match: [ + 'customredirect' + ] + }] + } + assert.strictEqual(waf.createOrUpdateConfig(newConfig, 'config/update'), true) + }) }) - it('should throw an error when removing a configuration on a disposed WAF instance', () => { - const waf = new DDWAF(rules, 'recommended') - waf.dispose() - assert.throws( - () => waf.removeConfig('config/update'), - new Error('Could not update a disposed WAF instance')) - }) + describe('Remove config', () => { + it('should throw an error when removing a configuration on a disposed WAF instance', () => { + const waf = new DDWAF(rules, 'recommended') + waf.dispose() + assert.throws( + () => waf.removeConfig('config/update'), + new Error('Could not update a disposed WAF instance')) + }) - it('should throw an error when removing a configuration with no arguments', () => { - const waf = new DDWAF(rules, 'recommended') - assert.throws(() => waf.removeConfig(), new Error('Wrong number of arguments, expected at least 1')) - }) + it('should throw an error when removing a configuration with no arguments', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws(() => waf.removeConfig(), new Error('Wrong number of arguments, expected at least 1')) + }) - it('should throw a type error when removing a configuration with invalid arguments', () => { - const waf = new DDWAF(rules, 'recommended') - assert.throws( - () => waf.removeConfig(null), - new TypeError('First argument must be a string') - ) - }) + it('should throw a type error when removing a configuration with invalid arguments', () => { + const waf = new DDWAF(rules, 'recommended') + assert.throws( + () => waf.removeConfig(null), + new TypeError('First argument must be a string') + ) + }) - it('should return true when removing an existing configuration', () => { - const waf = new DDWAF(rules, 'recommended') - assert.strictEqual(waf.removeConfig('recommended'), true) - }) + it('should return true when removing an existing configuration', () => { + const waf = new DDWAF(rules, 'recommended') + assert.strictEqual(waf.removeConfig('recommended'), true) + }) - it('should return false when removing a non-existing configuration', () => { - const waf = new DDWAF(rules, 'recommended') - assert.strictEqual(waf.removeConfig('config/update'), false) + it('should return false when removing a non-existing configuration', () => { + const waf = new DDWAF(rules, 'recommended') + assert.strictEqual(waf.removeConfig('config/update'), false) + }) }) - it('should have no loaded configuration paths on WAF disposed instance', () => { - const waf = new DDWAF(rules, 'recommended') - waf.dispose() - assert.strictEqual(waf.configPaths.length, 0) - }) + describe('Config paths', () => { + it('should have no loaded configuration paths on WAF disposed instance', () => { + const waf = new DDWAF(rules, 'recommended') + waf.dispose() + assert.strictEqual(waf.configPaths.length, 0) + }) - it('should have loaded configuration paths', () => { - const waf = new DDWAF(rules, 'recommended') - const newConfig = { - rules: [{ - id: 'block_ip_original', - name: 'block ip', - tags: { - type: 'ip_addresses', - category: 'blocking' - }, - conditions: [ - { - parameters: { - inputs: [ - { address: 'http.client_ip' } - ], - data: 'blocked_ips' - }, - operator: 'ip_match' - } - ], - transformers: [], - on_match: [ - 'customredirect' - ] - }] - } - waf.createOrUpdateConfig(newConfig, 'config/update') - assert.deepStrictEqual(['config/update', 'recommended'], waf.configPaths) + it('should have loaded configuration paths', () => { + const waf = new DDWAF(rules, 'recommended') + const newConfig = { + rules: [{ + id: 'block_ip_original', + name: 'block ip', + tags: { + type: 'ip_addresses', + category: 'blocking' + }, + conditions: [ + { + parameters: { + inputs: [ + { address: 'http.client_ip' } + ], + data: 'blocked_ips' + }, + operator: 'ip_match' + } + ], + transformers: [], + on_match: [ + 'customredirect' + ] + }] + } + waf.createOrUpdateConfig(newConfig, 'config/update') + assert.deepStrictEqual(['config/update', 'recommended'], waf.configPaths) + }) }) it('should update diagnostics, knownAddresses, and knownActions when updating an instance with new ruleSet', () => { From 57caf6e6b5552098ed2b69cb8eb14de4b9e58901 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 22 Apr 2025 16:55:09 +0200 Subject: [PATCH 10/23] Destroy builder on finalize --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index da32e043..f09f824e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -145,6 +145,7 @@ void DDWAF::Finalize(Napi::Env env) { return; } ddwaf_destroy(this->_handle); + ddwaf_builder_destroy(this->_builder); this->_disposed = true; } From 0b04232e45b3aecec5bf3b510f8512a3f0716191 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 22 Apr 2025 17:01:24 +0200 Subject: [PATCH 11/23] Disable log --- src/log.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/log.h b/src/log.h index e77e19e9..b94e393c 100644 --- a/src/log.h +++ b/src/log.h @@ -4,7 +4,7 @@ **/ #ifndef SRC_LOG_H_ #define SRC_LOG_H_ -#define DEBUG 1 +#define DEBUG 0 #include #if DEBUG == 1 From 6014dbc1d8ab4bbff749a23e1c5e191e2f2f3816 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 23 Apr 2025 07:29:50 +0200 Subject: [PATCH 12/23] Fix cpp linting --- src/main.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.h b/src/main.h index a1ca7da8..493b7068 100644 --- a/src/main.h +++ b/src/main.h @@ -8,7 +8,7 @@ #include #include "src/metrics.h" -#define LSTRARG(value) value, (uint32_t)strlen(value) +#define LSTRARG(value) value, static_cast(strlen(value)) // TODO(@vdeturckheim): logs with ddwaf_set_log_cb // TODO(@vdeturckheim): fix issue when used with workers From 1b9e47eada071cc939ad25f6f1d83c687c7f692b Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 23 Apr 2025 07:42:53 +0200 Subject: [PATCH 13/23] Fix JS linting --- test/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/index.js b/test/index.js index 9aed2237..df367320 100644 --- a/test/index.js +++ b/test/index.js @@ -151,7 +151,6 @@ describe('DDWAF', () => { }) describe('WAF update', () => { - describe('Update config', () => { it('should throw an error when updating configuration on a disposed WAF instance', () => { const waf = new DDWAF(rules, 'recommended') From b8b298d9a8ab790f13573384b9398b631629c2d0 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 23 Apr 2025 07:49:53 +0200 Subject: [PATCH 14/23] Fix tests --- test/fuzz.js | 2 +- test/worker.js | 2 +- test/worker_threads.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fuzz.js b/test/fuzz.js index 9b590dc2..4e934206 100644 --- a/test/fuzz.js +++ b/test/fuzz.js @@ -10,7 +10,7 @@ const blns = require('./blns.json') const TIMEOUT = 9999e3 -const waf = new DDWAF(rules) +const waf = new DDWAF(rules, 'recommended') const ENCODINGS = [ // from https://github.com/nodejs/node/blob/master/lib/buffer.js 'utf8', diff --git a/test/worker.js b/test/worker.js index 0cb31c3b..9e377911 100644 --- a/test/worker.js +++ b/test/worker.js @@ -6,7 +6,7 @@ if (!isMainThread) { const { DDWAF } = require('..') const rules = require('./rules.json') - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result = context.run({ diff --git a/test/worker_threads.js b/test/worker_threads.js index 00016a61..f36965a4 100644 --- a/test/worker_threads.js +++ b/test/worker_threads.js @@ -13,7 +13,7 @@ const WORKER_PATH = path.join(__dirname, 'worker.js') describe('worker threads', () => { it('should not crash when worker created after DDWAF', (done) => { - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result1 = context.run({ @@ -47,7 +47,7 @@ describe('worker threads', () => { worker.on('message', (result1) => { assert.strictEqual(result1?.status, 'match') - const waf = new DDWAF(rules) + const waf = new DDWAF(rules, 'recommended') const context = waf.createContext() const result2 = context.run({ From f1fb1b1e48a718249e50c8e9e0a51ba3a9795152 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 23 Apr 2025 08:00:54 +0200 Subject: [PATCH 15/23] Change assertion to be order insensitive --- test/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index df367320..e2cccb08 100644 --- a/test/index.js +++ b/test/index.js @@ -294,7 +294,11 @@ describe('DDWAF', () => { }] } waf.createOrUpdateConfig(newConfig, 'config/update') - assert.deepStrictEqual(['config/update', 'recommended'], waf.configPaths) + assert.ok( + waf.configPaths.includes('recommended') && + waf.configPaths.includes('config/update') && + waf.configPaths.length === 2 + ) }) }) From fb8787356bfb166328cb3dddf87d184ef8ecb482 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 29 Apr 2025 12:33:11 +0200 Subject: [PATCH 16/23] Add some assertions --- test/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/index.js b/test/index.js index e2cccb08..1efef813 100644 --- a/test/index.js +++ b/test/index.js @@ -176,11 +176,13 @@ describe('DDWAF', () => { () => waf.createOrUpdateConfig('string', 'config/update'), new TypeError('First argument must be an object') ) + assert.strictEqual(waf.disposed, false) }) it('should return false when updating configuration with invalid configuration', () => { const waf = new DDWAF(rules, 'recommended') assert.strictEqual(waf.createOrUpdateConfig({}, 'config/update'), false) + assert.strictEqual(waf.disposed, false) }) it('should return true when updating configuration', () => { @@ -251,11 +253,16 @@ describe('DDWAF', () => { it('should return true when removing an existing configuration', () => { const waf = new DDWAF(rules, 'recommended') assert.strictEqual(waf.removeConfig('recommended'), true) + assert.strictEqual(waf.configPaths.length, 0) }) it('should return false when removing a non-existing configuration', () => { const waf = new DDWAF(rules, 'recommended') assert.strictEqual(waf.removeConfig('config/update'), false) + assert.ok( + waf.configPaths.includes('recommended') && + waf.configPaths.length === 1 + ) }) }) From 143013fc1daf2b73a0da2c6b096ff5b83456eeaa Mon Sep 17 00:00:00 2001 From: Carles Capell <107924659+CarlesDD@users.noreply.github.com> Date: Tue, 13 May 2025 06:40:23 +0200 Subject: [PATCH 17/23] Fix Napi returning type Co-authored-by: simon-id --- src/main.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.h b/src/main.h index 493b7068..76d6938e 100644 --- a/src/main.h +++ b/src/main.h @@ -23,8 +23,8 @@ class DDWAF : public Napi::ObjectWrap { explicit DDWAF(const Napi::CallbackInfo& info); // JS instance methods - Napi::Value update_config(const Napi::CallbackInfo& info); - Napi::Value remove_config(const Napi::CallbackInfo& info); + Napi::Boolean update_config(const Napi::CallbackInfo& info); + Napi::Boolean remove_config(const Napi::CallbackInfo& info); Napi::Value GetConfigPaths(const Napi::CallbackInfo& info); Napi::Value createContext(const Napi::CallbackInfo& info); void Finalize(Napi::Env env); From 25da340fb69b1f54a00a0d2a26939d02752cab99 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 13 May 2025 06:45:43 +0200 Subject: [PATCH 18/23] Revert "Fix Napi returning type" This reverts commit d55c6ace567ce4d15c8ab5865b11decc835e6c03. --- src/main.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.h b/src/main.h index 76d6938e..493b7068 100644 --- a/src/main.h +++ b/src/main.h @@ -23,8 +23,8 @@ class DDWAF : public Napi::ObjectWrap { explicit DDWAF(const Napi::CallbackInfo& info); // JS instance methods - Napi::Boolean update_config(const Napi::CallbackInfo& info); - Napi::Boolean remove_config(const Napi::CallbackInfo& info); + Napi::Value update_config(const Napi::CallbackInfo& info); + Napi::Value remove_config(const Napi::CallbackInfo& info); Napi::Value GetConfigPaths(const Napi::CallbackInfo& info); Napi::Value createContext(const Napi::CallbackInfo& info); void Finalize(Napi::Env env); From fc09adbab10c1d0d0d0c65788a439d2127a8cfb7 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 14 May 2025 19:32:17 +0200 Subject: [PATCH 19/23] Refactor getting string pointer for config path --- src/main.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index f09f824e..434877bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -102,14 +102,13 @@ DDWAF::DDWAF(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { ddwaf_object rules; mlog("building rules"); to_ddwaf_object(&rules, env, info[0], 0, false, false, JsSet::Create(env), nullptr); - std::string config_path_str = info[1].As().Utf8Value(); - const char* config_path = config_path_str.c_str(); + std::string config_path = info[1].As().Utf8Value(); ddwaf_object diagnostics; mlog("Init Builder"); ddwaf_builder builder = ddwaf_builder_init(&waf_config); - bool result = ddwaf_builder_add_or_update_config(builder, LSTRARG(config_path), &rules, &diagnostics); + bool result = ddwaf_builder_add_or_update_config(builder, LSTRARG(config_path.c_str()), &rules, &diagnostics); ddwaf_object_free(&rules); @@ -184,13 +183,12 @@ Napi::Value DDWAF::update_config(const Napi::CallbackInfo& info) { to_ddwaf_object(&update, env, info[0], 0, false, false, JsSet::Create(env), nullptr); mlog("Obtaining config update path"); - std::string config_path_str = info[1].As().Utf8Value(); - const char* config_path = config_path_str.c_str(); + std::string config_path = info[1].As().Utf8Value(); ddwaf_object diagnostics; mlog("Applying new config to builder"); - bool update_result = ddwaf_builder_add_or_update_config(this->_builder, LSTRARG(config_path), &update, &diagnostics); + bool update_result = ddwaf_builder_add_or_update_config(this->_builder, LSTRARG(config_path.c_str()), &update, &diagnostics); Napi::Value diagnostics_js = from_ddwaf_object(&diagnostics, env); info.This().As().Set("diagnostics", diagnostics_js); @@ -239,11 +237,10 @@ Napi::Value DDWAF::remove_config(const Napi::CallbackInfo& info) { } mlog("Obtaining config remove path"); - std::string config_path_str = info[0].As().Utf8Value(); - const char* config_path = config_path_str.c_str(); + std::string config_path = info[0].As().Utf8Value(); mlog("Applying removed config to builder"); - bool remove_result = ddwaf_builder_remove_config(this->_builder, LSTRARG(config_path)); + bool remove_result = ddwaf_builder_remove_config(this->_builder, LSTRARG(config_path.c_str())); if (!remove_result) { mlog("DDWAF Builder remove config has failed"); From a159d5ccad73931a0eb327ca83804a412a0a77a6 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Thu, 15 May 2025 16:24:46 +0200 Subject: [PATCH 20/23] Test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a54655a8..40c1cc83 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,6 @@ Therefore, unsupported platforms include: * AIX * PPC -Please feel free to [contact support][support] if you would like to request support for a new platform. +Please feel free to [contact support][support] if you would like to request support for a new platform.. [support]: https://docs.datadoghq.com/help From 4a360479bd95fe3b567adc90915a86cec2b5fd78 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Thu, 15 May 2025 16:24:56 +0200 Subject: [PATCH 21/23] Test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 40c1cc83..a54655a8 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,6 @@ Therefore, unsupported platforms include: * AIX * PPC -Please feel free to [contact support][support] if you would like to request support for a new platform.. +Please feel free to [contact support][support] if you would like to request support for a new platform. [support]: https://docs.datadoghq.com/help From 84396e02ea77ffc0e90694c85803e3021669e9e9 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Thu, 15 May 2025 16:41:35 +0200 Subject: [PATCH 22/23] Fix linting --- src/main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 434877bf..e02d54ef 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -188,7 +188,10 @@ Napi::Value DDWAF::update_config(const Napi::CallbackInfo& info) { ddwaf_object diagnostics; mlog("Applying new config to builder"); - bool update_result = ddwaf_builder_add_or_update_config(this->_builder, LSTRARG(config_path.c_str()), &update, &diagnostics); + bool update_result = ddwaf_builder_add_or_update_config( + this->_builder, + LSTRARG(config_path.c_str()), + &update, &diagnostics); Napi::Value diagnostics_js = from_ddwaf_object(&diagnostics, env); info.This().As().Set("diagnostics", diagnostics_js); From 87431da68732e31d9c1ad8ebbc964aeb297fd92c Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Fri, 16 May 2025 15:46:33 +0200 Subject: [PATCH 23/23] Test the old handle still alive when an new empty config is applied --- test/index.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/index.js b/test/index.js index 1efef813..62497f94 100644 --- a/test/index.js +++ b/test/index.js @@ -185,6 +185,28 @@ describe('DDWAF', () => { assert.strictEqual(waf.disposed, false) }) + it('should keep functional handle after updating an invalid configuration', () => { + const waf = new DDWAF(rules, 'recommended') + waf.createOrUpdateConfig({}, 'config/update') + + assert(!waf.disposed) + + const context = waf.createContext() + const payload = { + persistent: { + 'server.request.headers.no_cookies': 'value_ATTack' + } + } + + const result = context.run(payload, TIMEOUT) + + assert.strictEqual(result.timeout, false) + assert.strictEqual(result.status, 'match') + assert(result.events) + assert.deepStrictEqual(result.actions, {}) + assert(!context.disposed) + }) + it('should return true when updating configuration', () => { const waf = new DDWAF(rules, 'recommended') const newConfig = {