From: Giuseppe Longo Date: Wed, 10 Apr 2024 12:02:50 +0000 (+0200) Subject: rust/ldap: implement logger X-Git-Tag: suricata-8.0.0-beta1~998 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=910a5b226cc2807c26ea9f825c429b93c7f8442e;p=thirdparty%2Fsuricata.git rust/ldap: implement logger --- diff --git a/etc/schema.json b/etc/schema.json index 944410a6b2..cffb15afd3 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -2376,6 +2376,366 @@ }, "additionalProperties": false }, + "ldap": { + "type": "object", + "optional": true, + "properties": { + "request": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "message_id": { + "type": "integer" + }, + "search_request": { + "type": "object", + "optional": "true", + "properties": { + "base_object": { + "type": "string" + }, + "scope": { + "type": "integer" + }, + "deref_alias": { + "type": "integer" + }, + "size_limit": { + "type": "integer" + }, + "time_limit": { + "type": "integer" + }, + "types_online": { + "type": "boolean" + }, + "attributes": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "bind_request": { + "type": "object", + "optional": "true", + "properties": { + "version": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sasl": { + "type": "object", + "optional": "true", + "properties": { + "mechanism": { + "type": "string" + }, + "credentials": { + "type": "string", + "optional": "true" + } + } + } + } + }, + "modify_request": { + "type": "object", + "optional": "true", + "properties": { + "object": { + "type": "string" + }, + "changes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "modification": { + "type": "object", + "properties": { + "attribute_type": { + "type": "string" + }, + "attribute_values": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "add_request": { + "type": "object", + "optional": "true", + "properties": { + "entry": { + "type": "string" + }, + "attributes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "values": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } + } + } + }, + "del_request": { + "type": "object", + "optional": "true", + "properties": { + "dn": { + "type": "string" + } + } + }, + "mod_dn_request": { + "type": "object", + "optional": "true", + "properties": { + "entry": { + "type": "string" + }, + "new_rdn": { + "type": "string" + }, + "delete_old_rdn": { + "type": "boolean" + }, + "new_superior": { + "type": "string", + "optional": "true" + } + } + }, + "compare_request": { + "type": "object", + "optional": "true", + "properties": { + "entry": { + "type": "string" + }, + "attribute_value_assertion": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "abandon_request": { + "type": "object", + "optional": "true", + "properties": { + "message_id": { + "type": "integer" + } + } + }, + "extended_request": { + "type": "object", + "optional": "true", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "optional": "true" + } + } + } + }, + "additionalProperties": false + }, + "responses": { + "type": "array", + "optional": "true", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "search_result_done": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "bind_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + }, + "server_sasl_creds": { + "type": "string", + "optional": "true" + } + } + }, + "modify_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "add_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "del_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "mod_dn_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "compare_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "extended_response": { + "type": "object", + "optional": "true", + "properties": { + "result_code": { + "type": "string" + }, + "matched_dn": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "intermediate_response": { + "type": "object", + "optional": "true", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + } + } + } + }, "metadata": { "type": "object", "optional": true, @@ -4277,6 +4637,10 @@ "Errors encountered parsing Kerberos v5/UDP protocol", "$ref": "#/$defs/stats_applayer_error" }, + "ldap": { + "description": "Errors encountered parsing LDAP protocol", + "$ref": "#/$defs/stats_applayer_error" + }, "modbus": { "description": "Errors encountered parsing Modbus protocol", "$ref": "#/$defs/stats_applayer_error" @@ -4441,6 +4805,10 @@ "description": "Number of flows for Kerberos v5/UDP protocol", "type": "integer" }, + "ldap": { + "description": "Number of flows for LDAP protocol", + "type": "integer" + }, "modbus": { "description": "Number of flows for Modbus protocol", "type": "integer" @@ -4600,6 +4968,10 @@ "Number of transactions for Kerberos v5/UDP protocol", "type": "integer" }, + "ldap": { + "description": "Number of transactions for LDAP protocol", + "type": "integer" + }, "modbus": { "description": "Number of transactions for Modbus protocol", "type": "integer" diff --git a/rust/src/ldap/ldap.rs b/rust/src/ldap/ldap.rs index 3b5db90dce..0817b4c9fb 100644 --- a/rust/src/ldap/ldap.rs +++ b/rust/src/ldap/ldap.rs @@ -422,6 +422,7 @@ pub unsafe extern "C" fn rs_ldap_register_parser() { SCLogError!("Invalid value for ldap.max-tx"); } } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_LDAP); } else { SCLogDebug!("Protocol detection and parser disabled for LDAP."); } diff --git a/rust/src/ldap/logger.rs b/rust/src/ldap/logger.rs new file mode 100644 index 0000000000..f126f97ef2 --- /dev/null +++ b/rust/src/ldap/logger.rs @@ -0,0 +1,351 @@ +/* Copyright (C) 2024 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// written by Giuseppe Longo + +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use crate::ldap::filters::*; +use crate::ldap::ldap::LdapTransaction; +use crate::ldap::types::*; + +fn log_ldap(tx: &LdapTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("ldap")?; + + if let Some(req) = &tx.request { + let protocol_op_str = req.protocol_op.to_string(); + js.open_object("request")?; + js.set_uint("message_id", req.message_id.0.into())?; + js.set_string("operation", &protocol_op_str)?; + + match &req.protocol_op { + ProtocolOp::SearchRequest(msg) => log_search_request(msg, js)?, + ProtocolOp::BindRequest(msg) => log_bind_request(msg, js)?, + ProtocolOp::UnbindRequest => (), + ProtocolOp::ModifyRequest(msg) => log_modify_request(msg, js)?, + ProtocolOp::AddRequest(msg) => log_add_request(msg, js)?, + ProtocolOp::DelRequest(msg) => log_del_request(msg, js)?, + ProtocolOp::ModDnRequest(msg) => log_mod_dn_request(msg, js)?, + ProtocolOp::CompareRequest(msg) => log_compare_request(msg, js)?, + ProtocolOp::ExtendedRequest(msg) => log_extended_request(msg, js)?, + _ => {} + }; + + log_controls(&req.controls, js)?; + + js.close()?; + } + + if !tx.responses.is_empty() { + js.open_array("responses")?; + + for response in &tx.responses { + js.start_object()?; + + let protocol_op_str = response.protocol_op.to_string(); + js.set_string("operation", &protocol_op_str)?; + + if tx.request.is_none() { + js.set_uint("message_id", response.message_id.0.into())?; + } + + match &response.protocol_op { + ProtocolOp::SearchResultEntry(msg) => log_search_result_entry(msg, js)?, + ProtocolOp::SearchResultDone(msg) => log_search_result_done(msg, js)?, + ProtocolOp::BindResponse(msg) => log_bind_response(msg, js)?, + ProtocolOp::ModifyResponse(msg) => log_modify_response(msg, js)?, + ProtocolOp::AddResponse(msg) => log_add_response(msg, js)?, + ProtocolOp::DelResponse(msg) => log_del_response(msg, js)?, + ProtocolOp::ModDnResponse(msg) => log_mod_dn_response(msg, js)?, + ProtocolOp::CompareResponse(msg) => log_compare_response(msg, js)?, + ProtocolOp::ExtendedResponse(msg) => log_extended_response(msg, js)?, + ProtocolOp::IntermediateResponse(msg) => log_intermediate_response(msg, js)?, + _ => {} + } + log_controls(&response.controls, js)?; + js.close()?; + } + js.close()?; + } + + js.close()?; + Ok(()) +} + +fn log_search_request(msg: &SearchRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("search_request")?; + js.set_string("base_object", &msg.base_object.0)?; + js.set_uint("scope", msg.scope.0.into())?; + js.set_uint("deref_alias", msg.deref_aliases.0.into())?; + js.set_uint("size_limit", msg.size_limit.into())?; + js.set_uint("time_limit", msg.time_limit.into())?; + js.set_bool("types_only", msg.types_only)?; + if let Filter::Present(val) = &msg.filter { + js.open_object("filter")?; + js.set_string("type", "present")?; + js.set_string("value", &val.0.to_string())?; + js.close()?; + } + if !msg.attributes.is_empty() { + js.open_array("attributes")?; + for attr in &msg.attributes { + js.append_string(&attr.0)?; + } + js.close()?; + } + + js.close()?; + Ok(()) +} + +fn log_bind_request(msg: &BindRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("bind_request")?; + js.set_uint("version", msg.version.into())?; + js.set_string("name", &msg.name.0)?; + if let AuthenticationChoice::Sasl(sasl) = &msg.authentication { + js.open_object("sasl")?; + js.set_string("mechanism", &sasl.mechanism.0)?; + if let Some(credentials) = &sasl.credentials { + js.set_hex("credentials", credentials)?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_modify_request(msg: &ModifyRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("modify_request")?; + js.set_string("object", &msg.object.0)?; + if !msg.changes.is_empty() { + js.open_array("changes")?; + for change in &msg.changes { + js.start_object()?; + js.set_string("operation", &change.operation.to_string())?; + js.open_object("modification")?; + js.set_string("attribute_type", &change.modification.attr_type.0)?; + if !change.modification.attr_vals.is_empty() { + js.open_array("attribute_values")?; + for attr in &change.modification.attr_vals { + js.append_string_from_bytes(&attr.0[..])?; + } + js.close()?; + } + js.close()?; + js.close()?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_add_request(msg: &AddRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("add_request")?; + js.set_string("entry", &msg.entry.0)?; + if !msg.attributes.is_empty() { + js.open_array("attributes")?; + for attr in &msg.attributes { + js.start_object()?; + js.set_string("name", &attr.attr_type.0)?; + if !attr.attr_vals.is_empty() { + js.open_array("values")?; + for val in &attr.attr_vals { + js.append_string_from_bytes(&val.0)?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_del_request(msg: &LdapDN, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("del_request")?; + js.set_string("dn", &msg.0)?; + js.close()?; + Ok(()) +} + +fn log_mod_dn_request(msg: &ModDnRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("mod_dn_request")?; + js.set_string("entry", &msg.entry.0)?; + js.set_string("new_rdn", &msg.newrdn.0)?; + js.set_bool("delete_old_rdn", msg.deleteoldrdn)?; + if let Some(newsuperior) = &msg.newsuperior { + js.set_string("new_superior", &newsuperior.0)?; + } + js.close()?; + Ok(()) +} + +fn log_compare_request(msg: &CompareRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("compare_request")?; + js.set_string("entry", &msg.entry.0)?; + js.open_object("attribute_value_assertion")?; + js.set_string("description", &msg.ava.attribute_desc.0)?; + js.set_string_from_bytes("value", &msg.ava.assertion_value[..])?; + js.close()?; + js.close()?; + Ok(()) +} + +fn log_extended_request(msg: &ExtendedRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("extended_request")?; + js.set_string("name", &msg.request_name.0)?; + if let Some(value) = &msg.request_value { + js.set_string_from_bytes("value", &value[..])?; + } + js.close()?; + Ok(()) +} + +fn log_search_result_entry(msg: &SearchResultEntry, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("search_result_entry")?; + js.set_string("base_object", &msg.object_name.0)?; + if !msg.attributes.is_empty() { + js.open_array("attributes")?; + for attr in &msg.attributes { + js.start_object()?; + js.set_string("type", &attr.attr_type.0)?; + if !attr.attr_vals.is_empty() { + js.open_array("values")?; + for val in &attr.attr_vals { + js.append_string_from_bytes(&val.0)?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + Ok(()) +} + +fn log_search_result_done(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("search_result_done")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_bind_response(msg: &BindResponse, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("bind_response")?; + log_ldap_result(&msg.result, js)?; + if let Some(creds) = &msg.server_sasl_creds { + js.set_hex("server_sasl_creds", creds)?; + }; + js.close()?; + Ok(()) +} + +fn log_modify_response(msg: &ModifyResponse, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("modify_response")?; + log_ldap_result(&msg.result, js)?; + js.close()?; + Ok(()) +} + +fn log_add_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("add_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_del_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("del_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_mod_dn_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("mod_dn_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_compare_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("compare_response")?; + log_ldap_result(msg, js)?; + js.close()?; + Ok(()) +} + +fn log_extended_response(msg: &ExtendedResponse, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("extended_response")?; + log_ldap_result(&msg.result, js)?; + if let Some(name) = &msg.response_name { + js.set_string("name", &name.0)?; + } + if let Some(value) = &msg.response_value { + js.set_string_from_bytes("value", &value[..])?; + } + js.close()?; + Ok(()) +} + +fn log_intermediate_response( + msg: &IntermediateResponse, js: &mut JsonBuilder, +) -> Result<(), JsonError> { + js.open_object("intermediate_response")?; + if let Some(name) = &msg.response_name { + js.set_string("name", &name.0)?; + } + if let Some(value) = &msg.response_value { + js.set_string_from_bytes("value", &value[..])?; + } + js.close()?; + Ok(()) +} + +fn log_ldap_result(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.set_string("result_code", &msg.result_code.to_string())?; + js.set_string("matched_dn", &msg.matched_dn.0)?; + js.set_string("message", &msg.diagnostic_message.0)?; + Ok(()) +} + +fn log_controls(controls: &Option>, js: &mut JsonBuilder) -> Result<(), JsonError> { + if let Some(ctls) = controls { + js.open_array("controls")?; + for ctl in ctls { + js.start_object()?; + js.set_string("control_type", &ctl.control_type.0)?; + js.set_bool("criticality", ctl.criticality)?; + if let Some(ctl_val) = &ctl.control_value { + js.set_hex("control_value", ctl_val)?; + }; + js.close()?; + } + js.close()?; + } + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ldap_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, LdapTransaction); + log_ldap(tx, js).is_ok() +} diff --git a/rust/src/ldap/mod.rs b/rust/src/ldap/mod.rs index b72b8d6892..2f18058693 100644 --- a/rust/src/ldap/mod.rs +++ b/rust/src/ldap/mod.rs @@ -19,4 +19,5 @@ pub mod filters; pub mod ldap; +pub mod logger; pub mod types; diff --git a/src/output.c b/src/output.c index 7b13913c83..49b2c84ebf 100644 --- a/src/output.c +++ b/src/output.c @@ -1092,6 +1092,10 @@ void OutputRegisterLoggers(void) OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonEnipLog", "eve-log.enip", OutputJsonLogInitSub, ALPROTO_ENIP, JsonGenericDirFlowLogger, JsonLogThreadInit, JsonLogThreadDeinit, NULL); + /* Ldap JSON logger. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonLdapLog", "eve-log.ldap", + OutputJsonLogInitSub, ALPROTO_LDAP, JsonGenericDirPacketLogger, JsonLogThreadInit, + JsonLogThreadDeinit, NULL); /* Template JSON logger. */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, @@ -1147,6 +1151,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_PGSQL, JsonPgsqlAddMetadata }, { ALPROTO_TELNET, NULL }, // no logging { ALPROTO_WEBSOCKET, rs_websocket_logger_log }, + { ALPROTO_LDAP, rs_ldap_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_HTTP2, rs_http2_log_json }, diff --git a/suricata.yaml.in b/suricata.yaml.in index faf9704fb6..e753f42e6d 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -300,6 +300,7 @@ outputs: - rfb - sip - quic + - ldap - arp: enabled: no # Many events can be logged. Disabled by default - dhcp: