diff --git a/README.md b/README.md
index 325ceabf..b9c5a225 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Currently, the following analyzers are included:
- DNS [1]
- HTTP [1]
- IPSec
+- LDAP
- OpenVPN
- PNG
- Portable Executable (PE) [2]
diff --git a/analyzer/__load__.zeek b/analyzer/__load__.zeek
index 061ff90b..99b27b98 100644
--- a/analyzer/__load__.zeek
+++ b/analyzer/__load__.zeek
@@ -5,6 +5,7 @@
@load ./protocol/dns
@load ./protocol/http
@load ./protocol/ipsec
+@load ./protocol/ldap
@load ./protocol/openvpn
@load ./protocol/tftp
@load ./protocol/wireguard
diff --git a/analyzer/protocol/CMakeLists.txt b/analyzer/protocol/CMakeLists.txt
index 8f8b51a9..3d2c670e 100644
--- a/analyzer/protocol/CMakeLists.txt
+++ b/analyzer/protocol/CMakeLists.txt
@@ -4,6 +4,7 @@ add_subdirectory(dhcp)
add_subdirectory(dns)
add_subdirectory(http)
add_subdirectory(ipsec)
+add_subdirectory(ldap)
add_subdirectory(openvpn)
add_subdirectory(tftp)
add_subdirectory(wireguard)
diff --git a/analyzer/protocol/ldap/CMakeLists.txt b/analyzer/protocol/ldap/CMakeLists.txt
new file mode 100644
index 00000000..a0ea1632
--- /dev/null
+++ b/analyzer/protocol/ldap/CMakeLists.txt
@@ -0,0 +1,3 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+spicy_add_analyzer(ldap ldap.spicy ldap_zeek.spicy ldap.evt)
diff --git a/analyzer/protocol/ldap/__load__.zeek b/analyzer/protocol/ldap/__load__.zeek
new file mode 100644
index 00000000..6a29e295
--- /dev/null
+++ b/analyzer/protocol/ldap/__load__.zeek
@@ -0,0 +1,3 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+@load ./main
diff --git a/analyzer/protocol/ldap/asn1.spicy b/analyzer/protocol/ldap/asn1.spicy
new file mode 100644
index 00000000..cb06f86f
--- /dev/null
+++ b/analyzer/protocol/ldap/asn1.spicy
@@ -0,0 +1,286 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+module asn1;
+
+###############################################################################
+# ASN.1 structure decoding
+#
+# A Layman's Guide to a Subset of ASN.1, BER, and DER
+# http://luca.ntop.org/Teaching/Appunti/asn1.html
+#
+# ASN.1 Tutorial from Computer Networks and Open Systems:
+# An Application Development Perspective
+# https://www.obj-sys.com/asn1tutorial/asn1only.html
+#
+# The ASN1JS tool (http://lapo.it/asn1js and https://github.com/lapo-luchini/asn1js)
+# is invaluable in debugging ASN.1
+###############################################################################
+
+import spicy;
+
+#- ASN.1 data types ----------------------------------------------------------
+# https://www.obj-sys.com/asn1tutorial/node124.html
+# https://www.obj-sys.com/asn1tutorial/node10.html
+
+public type ASN1Type = enum {
+ Boolean = 1,
+ Integer = 2,
+ BitString = 3,
+ OctetString = 4,
+ NullVal = 5,
+ ObjectIdentifier = 6,
+ ObjectDescriptor = 7,
+ InstanceOf = 8,
+ Real = 9,
+ Enumerated = 10,
+ EmbeddedPDV = 11,
+ UTF8String = 12,
+ RelativeOID = 13,
+ Sequence = 16,
+ Set = 17,
+ NumericString = 18,
+ PrintableString = 19,
+ TeletextString = 20,
+ VideotextString = 21,
+ IA5String = 22,
+ UTCTime = 23,
+ GeneralizedTime = 24,
+ GraphicString = 25,
+ VisibleString = 26,
+ GeneralString = 27,
+ UniversalString = 28,
+ CharacterString = 29,
+ BMPString = 30
+};
+
+#- ASN.1 data classes --------------------------------------------------------
+
+public type ASN1Class = enum {
+ Universal = 0,
+ Application = 1,
+ ContextSpecific = 2,
+ Private = 3
+};
+
+#- ASN.1 tag definition (including length) ------------------------------------
+
+type LengthType = unit {
+ var len: uint64;
+ var tag_len: uint8;
+
+ data : bitfield(8) {
+ num: 0..6;
+ islong: 7;
+ };
+
+
+ switch ( self.data.islong ) {
+ 0 -> : void {
+ self.len = self.data.num;
+ self.tag_len = 1;
+ }
+ 1 -> : bytes &size=self.data.num
+ &convert=$$.to_uint(spicy::ByteOrder::Network) {
+ self.len = $$;
+ self.tag_len = self.data.num + 1;
+ }
+ };
+};
+
+type ASN1Tag = unit {
+ var type_: ASN1Type;
+ var class: ASN1Class;
+ var constructed: bool;
+
+ : bitfield(8) {
+ num: 0..4;
+ constructed: 5;
+ class: 6..7;
+ } {
+ self.type_ = ASN1Type($$.num);
+ self.class = ASN1Class($$.class);
+ self.constructed = cast($$.constructed);
+ }
+};
+
+#- ASN.1 bit string -----------------------------------------------------------
+# https://www.obj-sys.com/asn1tutorial/node10.html
+
+type ASN1BitString = unit(len: uint64, constructed: bool) {
+ : uint8; # unused bits
+ value_bits: bytes &size=(len - 1);
+
+ # TODO - constructed form
+ # https://github.com/zeek/spicy/issues/921
+ # `bytes` needs << and >> support before we can implement complex bitstrings
+ #
+};
+
+#- ASN.1 octet string ---------------------------------------------------------
+# https://www.obj-sys.com/asn1tutorial/node10.html
+
+type ASN1OctetString = unit(len: uint64, constructed: bool) {
+ value: bytes &size = len;
+
+ # TODO - constructed form
+};
+
+#- ASN.1 various string types -------------------------------------------------
+# https://www.obj-sys.com/asn1tutorial/node124.html
+
+type ASN1String = unit(tag: ASN1Tag, len: uint64) {
+ var value: string = "";
+
+ : ASN1OctetString(len, tag.constructed) {
+ switch ( tag.type_ ) {
+
+ # see "Restricted Character String Types" in
+ # "Generic String Encoding Rules (GSER) for ASN.1 Types"
+ # (https://datatracker.ietf.org/doc/html/rfc3641#section-3.2)
+
+ case ASN1Type::PrintableString,
+ ASN1Type::GeneralizedTime,
+ ASN1Type::UTCTime: {
+ self.value = $$.value.decode(hilti::Charset::ASCII);
+ }
+
+ case ASN1Type::UTF8String,
+ ASN1Type::GeneralString,
+ ASN1Type::CharacterString,
+ ASN1Type::GraphicString,
+ ASN1Type::IA5String,
+ ASN1Type::NumericString,
+ ASN1Type::TeletextString,
+ ASN1Type::VideotextString,
+ ASN1Type::VisibleString,
+ # TODO: RFC3641 mentions special UTF-8 mapping rules for
+ # BMPString and UniversalString. This *may* not be correct.
+ ASN1Type::BMPString,
+ ASN1Type::UniversalString: {
+ self.value = $$.value.decode(hilti::Charset::UTF8);
+ }
+ }
+ }
+};
+
+#- ASN.1 OID ------------------------------------------------------------------
+# https://www.obj-sys.com/asn1tutorial/node124.html
+
+type ASN1ObjectIdentifierNibble = unit {
+ data : bitfield(8) {
+ num: 0..6;
+ more: 7;
+ };
+} &convert=self.data;
+
+type ASN1ObjectIdentifier = unit(len: uint64) {
+ var oid: vector;
+ var temp: uint64;
+ var oidstring: string;
+
+ : uint8 if ( len >= 1 ) {
+ self.temp = $$ / 40;
+ self.oid.push_back( self.temp );
+ self.oidstring = "%d" % (self.temp);
+ self.temp = $$ % 40;
+ self.oid.push_back( self.temp );
+ self.oidstring = self.oidstring + ".%d" % (self.temp);
+ self.temp = 0;
+ }
+
+ sublist: ASN1ObjectIdentifierNibble[len - 1] foreach {
+ self.temp = ( self.temp<<7 ) | $$.num;
+ if ( $$.more != 1 ) {
+ self.oid.push_back(self.temp);
+ self.oidstring = self.oidstring + ".%d" % (self.temp);
+ self.temp = 0;
+ }
+ }
+};
+
+
+#- ASN.1 message header (tag + length information) ----------------------------
+
+public type ASN1Header = unit {
+ tag: ASN1Tag;
+ len: LengthType;
+};
+
+#- ASN.1 message body ---------------------------------------------------------
+
+public type ASN1Body = unit(head: ASN1Header, recursive: bool) {
+ switch ( head.tag.type_ ) {
+
+ ASN1Type::Boolean -> bool_value: uint8 &convert=cast($$) &requires=head.len.len==1;
+
+ ASN1Type::Integer,
+ ASN1Type::Enumerated -> num_value: bytes &size=head.len.len
+ &convert=$$.to_int(spicy::ByteOrder::Big);
+
+ ASN1Type::NullVal -> null_value: bytes &size=0 &requires=head.len.len==0;
+
+ ASN1Type::BitString -> bitstr_value: ASN1BitString(head.len.len, head.tag.constructed);
+
+ ASN1Type::OctetString -> str_value: ASN1OctetString(head.len.len, head.tag.constructed)
+ &convert=$$.value.decode(hilti::Charset::ASCII);
+
+ ASN1Type::ObjectIdentifier -> str_value: ASN1ObjectIdentifier(head.len.len)
+ &convert=$$.oidstring;
+
+ ASN1Type::BMPString,
+ ASN1Type::CharacterString,
+ ASN1Type::GeneralizedTime,
+ ASN1Type::GeneralString,
+ ASN1Type::GraphicString,
+ ASN1Type::IA5String,
+ ASN1Type::NumericString,
+ ASN1Type::PrintableString,
+ ASN1Type::TeletextString,
+ ASN1Type::UTCTime,
+ ASN1Type::UTF8String,
+ ASN1Type::VideotextString,
+ ASN1Type::VisibleString,
+ ASN1Type::UniversalString -> str_value: ASN1String(head.tag, head.len.len)
+ &convert=$$.value;
+
+ ASN1Type::Sequence, ASN1Type::Set -> seq: ASN1SubMessages(head.len.len) if (recursive);
+
+ # TODO: ASN1Type values not handled yet
+ ASN1Type::ObjectDescriptor,
+ ASN1Type::InstanceOf,
+ ASN1Type::Real,
+ ASN1Type::EmbeddedPDV,
+ ASN1Type::RelativeOID -> unimplemented_value: bytes &size=head.len.len;
+
+ # unknown (to me) ASN.1 enumeration, skip over silently
+ * -> unimplemented_value: bytes &size=head.len.len;
+ };
+};
+
+#- ASN.1 array of ASN.1 sequence/set sub-messages (up to msgLen bytes) --------
+
+public type ASN1SubMessages = unit(msgLen: uint64) {
+ submessages: ASN1Message(True)[] &eod;
+} &size=msgLen;
+
+#- ASN.1 message with header and body -----------------------------------------
+# Universal or Application/ContextSpecific/Private
+# - if Universal, body:ASN1Body is parsed
+# - else, application_data:bytes stores data array
+
+public type ASN1Message = unit(recursive: bool) {
+ var application_id: int32;
+
+ head: ASN1Header;
+ switch ( self.head.tag.class ) {
+
+ ASN1Class::Universal -> body: ASN1Body(self.head, recursive);
+
+ ASN1Class::Application,
+ ASN1Class::ContextSpecific,
+ ASN1Class::Private -> application_data: bytes &size=self.head.len.len {
+ self.application_id = cast(self.head.tag.type_);
+ }
+
+ };
+};
diff --git a/analyzer/protocol/ldap/ldap.evt b/analyzer/protocol/ldap/ldap.evt
new file mode 100644
index 00000000..2a97f22d
--- /dev/null
+++ b/analyzer/protocol/ldap/ldap.evt
@@ -0,0 +1,39 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+protocol analyzer spicy::ldap_tcp over TCP:
+ parse with ldap::Messages,
+ ports {389/tcp, 3268/tcp};
+
+# TODO: LDAP can also use UDP transport
+
+import ldap;
+import ldap_zeek;
+
+on ldap::Message -> event ldap::message($conn,
+ self.messageID,
+ self.opcode,
+ self.result.code,
+ self.result.matchedDN,
+ self.result.diagnosticMessage,
+ self.obj,
+ self.arg);
+
+on ldap::BindRequest -> event ldap::bindreq($conn,
+ message.messageID,
+ self.version,
+ self.name,
+ self.authType,
+ message.arg);
+
+on ldap::SearchRequest -> event ldap::searchreq($conn,
+ message.messageID,
+ self.baseObject,
+ self.scope,
+ self.deref,
+ self.sizeLimit,
+ self.timeLimit,
+ self.typesOnly);
+
+on ldap::SearchResultEntry -> event ldap::searchres($conn,
+ message.messageID,
+ self.objectName);
diff --git a/analyzer/protocol/ldap/ldap.spicy b/analyzer/protocol/ldap/ldap.spicy
new file mode 100644
index 00000000..9e0a51fd
--- /dev/null
+++ b/analyzer/protocol/ldap/ldap.spicy
@@ -0,0 +1,536 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+module ldap;
+
+import asn1;
+
+# https://tools.ietf.org/html/rfc4511#
+# https://ldap.com/ldapv3-wire-protocol-reference-asn1-ber/
+# https://lapo.it/asn1js
+
+#- Operation opcode ----------------------------------------------------------
+public type ProtocolOpcode = enum {
+ BIND_REQUEST = 0,
+ BIND_RESPONSE = 1,
+ UNBIND_REQUEST = 2,
+ SEARCH_REQUEST = 3,
+ SEARCH_RESULT_ENTRY = 4,
+ SEARCH_RESULT_DONE = 5,
+ MODIFY_REQUEST = 6,
+ MODIFY_RESPONSE = 7,
+ ADD_REQUEST = 8,
+ ADD_RESPONSE = 9,
+ DEL_REQUEST = 10,
+ DEL_RESPONSE = 11,
+ MOD_DN_REQUEST = 12,
+ MOD_DN_RESPONSE = 13,
+ COMPARE_REQUEST = 14,
+ COMPARE_RESPONSE = 15,
+ ABANDON_REQUEST = 16,
+ SEARCH_RESULT_REFERENCE = 19,
+ EXTENDED_REQUEST = 23,
+ EXTENDED_RESPONSE = 24,
+ INTERMEDIATE_RESPONSE = 25,
+ # TODO: remove once zeek/spicy-plugin#35 is fixed
+ NOT_SET = 255
+};
+
+#- Result code ---------------------------------------------------------------
+public type ResultCode = enum {
+ SUCCESS = 0,
+ OPERATIONS_ERROR = 1,
+ PROTOCOL_ERROR = 2,
+ TIME_LIMIT_EXCEEDED = 3,
+ SIZE_LIMIT_EXCEEDED = 4,
+ COMPARE_FALSE = 5,
+ COMPARE_TRUE = 6,
+ AUTH_METHOD_NOT_SUPPORTED = 7,
+ STRONGER_AUTH_REQUIRED = 8,
+ PARTIAL_RESULTS = 9,
+ REFERRAL = 10,
+ ADMIN_LIMIT_EXCEEDED = 11,
+ UNAVAILABLE_CRITICAL_EXTENSION = 12,
+ CONFIDENTIALITY_REQUIRED = 13,
+ SASL_BIND_IN_PROGRESS = 14,
+ NO_SUCH_ATTRIBUTE = 16,
+ UNDEFINED_ATTRIBUTE_TYPE = 17,
+ INAPPROPRIATE_MATCHING = 18,
+ CONSTRAINT_VIOLATION = 19,
+ ATTRIBUTE_OR_VALUE_EXISTS = 20,
+ INVALID_ATTRIBUTE_SYNTAX = 21,
+ NO_SUCH_OBJECT = 32,
+ ALIAS_PROBLEM = 33,
+ INVALID_DNSYNTAX = 34,
+ ALIAS_DEREFERENCING_PROBLEM = 36,
+ INAPPROPRIATE_AUTHENTICATION = 48,
+ INVALID_CREDENTIALS = 49,
+ INSUFFICIENT_ACCESS_RIGHTS = 50,
+ BUSY = 51,
+ UNAVAILABLE = 52,
+ UNWILLING_TO_PERFORM = 53,
+ LOOP_DETECT = 54,
+ SORT_CONTROL_MISSING = 60,
+ OFFSET_RANGE_ERROR = 61,
+ NAMING_VIOLATION = 64,
+ OBJECT_CLASS_VIOLATION = 65,
+ NOT_ALLOWED_ON_NON_LEAF = 66,
+ NOT_ALLOWED_ON_RDN = 67,
+ ENTRY_ALREADY_EXISTS = 68,
+ OBJECT_CLASS_MODS_PROHIBITED = 69,
+ RESULTS_TOO_LARGE = 70,
+ AFFECTS_MULTIPLE_DSAS = 71,
+ CONTROL_ERROR = 76,
+ OTHER = 80,
+ SERVER_DOWN = 81,
+ LOCAL_ERROR = 82,
+ ENCODING_ERROR = 83,
+ DECODING_ERROR = 84,
+ TIMEOUT = 85,
+ AUTH_UNKNOWN = 86,
+ FILTER_ERROR = 87,
+ USER_CANCELED = 88,
+ PARAM_ERROR = 89,
+ NO_MEMORY = 90,
+ CONNECT_ERROR = 91,
+ NOT_SUPPORTED = 92,
+ CONTROL_NOT_FOUND = 93,
+ NO_RESULTS_RETURNED = 94,
+ MORE_RESULTS_TO_RETURN = 95,
+ CLIENT_LOOP = 96,
+ REFERRAL_LIMIT_EXCEEDED = 97,
+ INVALID_RESPONSE = 100,
+ AMBIGUOUS_RESPONSE = 101,
+ TLS_NOT_SUPPORTED = 112,
+ INTERMEDIATE_RESPONSE = 113,
+ UNKNOWN_TYPE = 114,
+ LCUP_INVALID_DATA = 115,
+ LCUP_UNSUPPORTED_SCHEME = 116,
+ LCUP_RELOAD_REQUIRED = 117,
+ CANCELED = 118,
+ NO_SUCH_OPERATION = 119,
+ TOO_LATE = 120,
+ CANNOT_CANCEL = 121,
+ ASSERTION_FAILED = 122,
+ AUTHORIZATION_DENIED = 123,
+ # TODO: remove once zeek/spicy-plugin#35 is fixed
+ NOT_SET = 255
+};
+
+#-----------------------------------------------------------------------------
+public type Result = unit {
+ code: asn1::ASN1Message(True) &convert=cast(cast($$.body.num_value))
+ &default=ResultCode::NOT_SET;
+ matchedDN: asn1::ASN1Message(True) &convert=$$.body.str_value
+ &default="";
+ diagnosticMessage: asn1::ASN1Message(True) &convert=$$.body.str_value
+ &default="";
+
+ # TODO: if we want to parse referral URIs in result
+ # https://tools.ietf.org/html/rfc4511#section-4.1.10
+};
+
+#-----------------------------------------------------------------------------
+public type Messages = unit {
+ : Message[];
+};
+
+public type Message = unit {
+ var messageID: int64;
+ var opcode: ProtocolOpcode = ProtocolOpcode::NOT_SET;
+ var applicationBytes: bytes;
+ var unsetResultDefault: Result;
+ var result: Result& = self.unsetResultDefault;
+ var obj: string = "";
+ var arg: string = "";
+
+ : asn1::ASN1Message(True) {
+ if (($$.head.tag.type_ == asn1::ASN1Type::Sequence) &&
+ ($$.body?.seq) &&
+ (|$$.body.seq.submessages| >= 2)) {
+ if ($$.body.seq.submessages[0].body?.num_value) {
+ self.messageID = $$.body.seq.submessages[0].body.num_value;
+ }
+ if ($$.body.seq.submessages[1]?.application_id) {
+ self.opcode = cast(cast($$.body.seq.submessages[1].application_id));
+ self.applicationBytes = $$.body.seq.submessages[1].application_data;
+ }
+ }
+ }
+
+ switch ( self.opcode ) {
+ ProtocolOpcode::BIND_REQUEST -> BIND_REQUEST: BindRequest(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::BIND_RESPONSE -> BIND_RESPONSE: BindResponse(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::UNBIND_REQUEST -> UNBIND_REQUEST: UnbindRequest(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::SEARCH_REQUEST -> SEARCH_REQUEST: SearchRequest(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::SEARCH_RESULT_ENTRY -> SEARCH_RESULT_ENTRY: SearchResultEntry(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::SEARCH_RESULT_DONE -> SEARCH_RESULT_DONE: SearchResultDone(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::MODIFY_REQUEST -> MODIFY_REQUEST: ModifyRequest(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::MODIFY_RESPONSE -> MODIFY_RESPONSE: ModifyResponse(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::ADD_RESPONSE -> ADD_RESPONSE: AddResponse(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::DEL_REQUEST -> DEL_REQUEST: DelRequest(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::DEL_RESPONSE -> DEL_RESPONSE: DelResponse(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::COMPARE_RESPONSE -> COMPARE_RESPONSE: CompareResponse(self) &parse-from=self.applicationBytes;
+ ProtocolOpcode::ABANDON_REQUEST -> ABANDON_REQUEST: AbandonRequest(self) &parse-from=self.applicationBytes;
+
+ # TODO: not yet implemented
+ # ProtocolOpcode::ADD_REQUEST -> ADD_REQUEST: AddRequest(self) &parse-from=self.applicationBytes;
+ # ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: CompareRequest(self) &parse-from=self.applicationBytes;
+ # ProtocolOpcode::EXTENDED_REQUEST -> EXTENDED_REQUEST: ExtendedRequest(self) &parse-from=self.applicationBytes;
+ # ProtocolOpcode::EXTENDED_RESPONSE -> EXTENDED_RESPONSE: ExtendedResponse(self) &parse-from=self.applicationBytes;
+ # ProtocolOpcode::INTERMEDIATE_RESPONSE -> INTERMEDIATE_RESPONSE: IntermediateResponse(self) &parse-from=self.applicationBytes;
+ # ProtocolOpcode::MOD_DN_REQUEST -> MOD_DN_REQUEST: ModDNRequest(self) &parse-from=self.applicationBytes;
+ # ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: SearchResultReference(self) &parse-from=self.applicationBytes;
+ };
+ # TODO: add support for switch-level &parse-from/&parse-at
+ # https://github.com/zeek/spicy/issues/913
+} &requires=((self?.messageID) && (self?.opcode) && (self.opcode != ProtocolOpcode::NOT_SET));
+
+#-----------------------------------------------------------------------------
+# Bind Operation
+# https://tools.ietf.org/html/rfc4511#section-4.2
+
+public type BindAuthType = enum {
+ BIND_AUTH_SIMPLE = 0,
+ BIND_AUTH_SASL = 3,
+ # TODO: remove once zeek/spicy-plugin#35 is fixed
+ NOT_SET = 127
+};
+
+type SaslCredentials = unit() {
+ mechanism: asn1::ASN1Message(True) &convert=$$.body.str_value;
+ # TODO: if we want to parse the (optional) credentials string
+};
+
+type BindRequest = unit(inout message: Message) {
+ version: asn1::ASN1Message(True) &convert=$$.body.num_value;
+ name: asn1::ASN1Message(True) &convert=$$.body.str_value {
+ message.obj = self.name;
+ }
+ var authType: BindAuthType = BindAuthType::NOT_SET;
+ var authData: bytes = b"";
+ var simpleCreds: string = "";
+
+ : asn1::ASN1Message(True) {
+ if ($$?.application_id) {
+ self.authType = cast(cast($$.application_id));
+ self.authData = $$.application_data;
+ }
+ if ((self.authType == BindAuthType::BIND_AUTH_SIMPLE) && (|self.authData| > 0)) {
+ self.simpleCreds = self.authData.decode();
+ if (|self.simpleCreds| > 0) {
+ message.arg = self.simpleCreds;
+ }
+ }
+ }
+ saslCreds: SaslCredentials() &parse-from=self.authData if ((self.authType == BindAuthType::BIND_AUTH_SASL) &&
+ (|self.authData| > 0)) {
+ message.arg = self.saslCreds.mechanism;
+ }
+} &requires=((self?.authType) && (self.authType != BindAuthType::NOT_SET));
+
+type BindResponse = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+
+ # TODO: if we want to parse SASL credentials returned
+};
+
+#-----------------------------------------------------------------------------
+# Unbind Operation
+# https://tools.ietf.org/html/rfc4511#section-4.3
+
+type UnbindRequest = unit(inout message: Message) {
+ # this page intentionally left blank
+};
+
+#-----------------------------------------------------------------------------
+# Search Operation
+# https://tools.ietf.org/html/rfc4511#section-4.5
+
+public type SearchScope = enum {
+ SEARCH_BASE = 0,
+ SEARCH_SINGLE = 1,
+ SEARCH_TREE = 2,
+ # TODO: remove once zeek/spicy-plugin#35 is fixed
+ NOT_SET = 255
+};
+
+public type SearchDerefAlias = enum {
+ DEREF_NEVER = 0,
+ DEREF_IN_SEARCHING = 1,
+ DEREF_FINDING_BASE = 2,
+ DEREF_ALWAYS = 3,
+ # TODO: remove once zeek/spicy-plugin#35 is fixed
+ NOT_SET = 255
+};
+
+type FilterType = enum {
+ FILTER_AND = 0,
+ FILTER_OR = 1,
+ FILTER_NOT = 2,
+ FILTER_EQ = 3,
+ FILTER_SUBSTR = 4,
+ FILTER_GE = 5,
+ FILTER_LE = 6,
+ FILTER_PRESENT = 7,
+ FILTER_APPROX = 8,
+ FILTER_EXT = 9,
+ FILTER_INVALID = 254,
+ # TODO: remove once zeek/spicy-plugin#35 is fixed
+ NOT_SET = 255
+};
+
+type AttributeSelection = unit {
+ var attributes: vector;
+
+ # TODO: parse AttributeSelection as per
+ # https://tools.ietf.org/html/rfc4511#section-4.5.1
+ # and decide how deep that should be fleshed out.
+ : asn1::ASN1Message(True) {
+ if (($$.head.tag.type_ == asn1::ASN1Type::Sequence) &&
+ ($$.body?.seq)) {
+ for (i in $$.body.seq.submessages) {
+ if (i.body?.str_value) {
+ self.attributes.push_back(i.body.str_value);
+ }
+ }
+ }
+ }
+};
+
+type AttributeValueAssertion = unit {
+ var desc: string = "";
+ var val: string = "";
+
+ : asn1::ASN1Message(True) {
+ if (($$.head.tag.type_ == asn1::ASN1Type::Sequence) &&
+ ($$.body?.seq) &&
+ (|$$.body.seq.submessages| >= 2)) {
+ if ($$.body.seq.submessages[0].body?.str_value) {
+ self.desc = $$.body.seq.submessages[0].body.str_value;
+ }
+ if ($$.body.seq.submessages[1].body?.str_value) {
+ self.val = $$.body.seq.submessages[1].body.str_value;
+ }
+ }
+ }
+};
+
+type SubstringFilter = unit {
+ var ftype: string = "";
+ var substrings: asn1::ASN1Message;
+
+ : asn1::ASN1Message(True) {
+ if (($$.head.tag.type_ == asn1::ASN1Type::Sequence) &&
+ ($$.body?.seq) &&
+ (|$$.body.seq.submessages| >= 2)) {
+ if ($$.body.seq.submessages[0].body?.str_value) {
+ self.ftype = $$.body.seq.submessages[0].body.str_value;
+ }
+ if ($$.body.seq.submessages[1].head.tag.type_ == asn1::ASN1Type::Sequence) {
+ self.substrings = $$.body.seq.submessages[1];
+ }
+ }
+ # TODO: if we want to descend deeper into the substrings filter
+ # if (self?.substrings) {
+ #
+ #}
+ }
+};
+
+type SearchFilter = unit {
+ var filterType: FilterType = FilterType::NOT_SET;
+ var filterBytes: bytes = b"";
+ var filterLen: uint64 = 0;
+
+ : asn1::ASN1Message(True) {
+ if ($$?.application_id) {
+ self.filterType = cast(cast($$.application_id));
+ self.filterBytes = $$.application_data;
+ self.filterLen = $$.head.len.len;
+ } else {
+ self.filterType = FilterType::FILTER_INVALID;
+ }
+ }
+
+ # TODO: parse search request filter as per
+ # https://tools.ietf.org/html/rfc4511#section-4.5.1.7
+ # This descent gets pretty involved... I wonder what is
+ # the best way to represent this as a string in a log.
+ # I've just left some of them as asn1::ASN1Message for now.
+
+ switch ( self.filterType ) {
+ FilterType::FILTER_AND -> FILTER_AND: asn1::ASN1Message(True)
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_OR -> FILTER_OR: asn1::ASN1Message(True)
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_NOT -> FILTER_NOT: SearchFilter()
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_EQ -> FILTER_EQ: AttributeValueAssertion()
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: SubstringFilter()
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_GE -> FILTER_GE: AttributeValueAssertion()
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_LE -> FILTER_LE: AttributeValueAssertion()
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_PRESENT -> FILTER_PRESENT: asn1::ASN1OctetString(self.filterLen, False)
+ &convert=$$.value.decode(hilti::Charset::ASCII)
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_APPROX -> FILTER_APPROX: AttributeValueAssertion()
+ &parse-from=self.filterBytes;
+ FilterType::FILTER_EXT -> FILTER_EXT: asn1::ASN1Message(True)
+ &parse-from=self.filterBytes;
+ };
+};
+
+type SearchRequest = unit(inout message: Message) {
+ baseObject: asn1::ASN1Message(True) &convert=$$.body.str_value {
+ message.obj = self.baseObject;
+ }
+ scope: asn1::ASN1Message(True) &convert=cast(cast($$.body.num_value))
+ &default=SearchScope::NOT_SET {
+ message.arg = "%s" % self.scope;
+ }
+ deref: asn1::ASN1Message(True) &convert=cast(cast($$.body.num_value))
+ &default=SearchDerefAlias::NOT_SET;
+ sizeLimit: asn1::ASN1Message(True) &convert=$$.body.num_value &default=0;
+ timeLimit: asn1::ASN1Message(True) &convert=$$.body.num_value &default=0;
+ typesOnly: asn1::ASN1Message(True) &convert=$$.body.bool_value &default=False;
+ filter: SearchFilter;
+ attributes: AttributeSelection;
+};
+
+type SearchResultEntry = unit(inout message: Message) {
+ objectName: asn1::ASN1Message(True) &convert=$$.body.str_value {
+ message.obj = self.objectName;
+ }
+ # TODO: if we want to descend down into PartialAttributeList
+ attributes: asn1::ASN1Message(True);
+};
+
+type SearchResultDone = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+};
+
+# TODO: implement SearchResultReference
+# type SearchResultReference = unit(inout message: Message) {
+#
+# };
+
+#-----------------------------------------------------------------------------
+# Modify Operation
+# https://tools.ietf.org/html/rfc4511#section-4.6
+
+type ModifyRequest = unit(inout message: Message) {
+ objectName: asn1::ASN1Message(True) &convert=$$.body.str_value {
+ message.obj = self.objectName;
+ }
+
+ # TODO: parse changes
+};
+
+type ModifyResponse = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+};
+
+#-----------------------------------------------------------------------------
+# Add Operation
+# https://tools.ietf.org/html/rfc4511#section-4.7
+
+# TODO: implement AddRequest
+# type AddRequest = unit(inout message: Message) {
+#
+#
+# };
+
+type AddResponse = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+};
+
+#-----------------------------------------------------------------------------
+# Delete Operation
+# https://tools.ietf.org/html/rfc4511#section-4.8
+
+type DelRequest = unit(inout message: Message) {
+ objectName: asn1::ASN1Message(True) &convert=$$.body.str_value {
+ message.obj = self.objectName;
+ }
+};
+
+type DelResponse = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+};
+
+#-----------------------------------------------------------------------------
+# Modify DN Operation
+# https://tools.ietf.org/html/rfc4511#section-4.8
+
+# TODO: implement ModDNRequest
+# type ModDNRequest = unit(inout message: Message) {
+#
+# };
+
+type ModDNResponse = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+};
+
+#-----------------------------------------------------------------------------
+# Compare Operation
+# https://tools.ietf.org/html/rfc4511#section-4.10
+
+# TODO: implement CompareRequest
+# type CompareRequest = unit(inout message: Message) {
+#
+# };
+
+type CompareResponse = unit(inout message: Message) {
+ : Result {
+ message.result = $$;
+ }
+};
+
+#-----------------------------------------------------------------------------
+# Abandon Operation
+# https://tools.ietf.org/html/rfc4511#section-4.11
+
+type AbandonRequest = unit(inout message: Message) {
+ messageID: asn1::ASN1Message(True) &convert=$$.body.num_value {
+ message.obj = "%d" % (self.messageID);
+ }
+};
+
+#-----------------------------------------------------------------------------
+# Extended Operation
+# https://tools.ietf.org/html/rfc4511#section-4.12
+
+# TODO: implement ExtendedRequest
+# type ExtendedRequest = unit(inout message: Message) {
+#
+# };
+
+# TODO: implement ExtendedResponse
+# type ExtendedResponse = unit(inout message: Message) {
+#
+# };
+
+#-----------------------------------------------------------------------------
+# IntermediateResponse Message
+# https://tools.ietf.org/html/rfc4511#section-4.13
+
+# TODO: implement IntermediateResponse
+# type IntermediateResponse = unit(inout message: Message) {
+#
+# };
diff --git a/analyzer/protocol/ldap/ldap_zeek.spicy b/analyzer/protocol/ldap/ldap_zeek.spicy
new file mode 100644
index 00000000..c3411503
--- /dev/null
+++ b/analyzer/protocol/ldap/ldap_zeek.spicy
@@ -0,0 +1,14 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+module ldap_zeek;
+
+import zeek;
+import ldap;
+
+on ldap::Message::%done {
+ zeek::confirm_protocol();
+}
+
+on ldap::Message::%error {
+ zeek::reject_protocol("error while parsing LDAP message");
+}
diff --git a/analyzer/protocol/ldap/main.zeek b/analyzer/protocol/ldap/main.zeek
new file mode 100644
index 00000000..13320e76
--- /dev/null
+++ b/analyzer/protocol/ldap/main.zeek
@@ -0,0 +1,467 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+module ldap;
+
+export {
+ redef enum Log::ID += { LDAP_LOG,
+ LDAP_SEARCH_LOG };
+
+ #############################################################################
+ # This is the format of ldap.log (ldap operations minus search-related)
+ # Each line represents a unique connection+message_id (requests/responses)
+ type Message: record {
+
+ # Timestamp for when the event happened.
+ ts: time &log;
+
+ # Unique ID for the connection.
+ uid: string &log;
+
+ # The connection's 4-tuple of endpoint addresses/ports.
+ id: conn_id &log;
+
+ # transport protocol
+ proto: string &log &optional;
+
+ # Message ID
+ message_id: int &log &optional;
+
+ # LDAP version
+ version: int &log &optional;
+
+ # normalized operations (e.g., bind_request and bind_response to "bind")
+ opcode: set[string] &log &optional;
+
+ # Result code(s)
+ result: set[string] &log &optional;
+
+ # result diagnostic message(s)
+ diagnostic_message: vector of string &log &optional;
+
+ # object(s)
+ object: vector of string &log &optional;
+
+ # argument(s)
+ argument: vector of string &log &optional;
+ };
+
+ #############################################################################
+ # This is the format of ldap_search.log (search-related messages only)
+ # Each line represents a unique connection+message_id (requests/responses)
+ type Search: record {
+
+ # Timestamp for when the event happened.
+ ts: time &log;
+
+ # Unique ID for the connection.
+ uid: string &log;
+
+ # The connection's 4-tuple of endpoint addresses/ports.
+ id: conn_id &log;
+
+ # transport protocol
+ proto: string &log &optional;
+
+ # Message ID
+ message_id: int &log &optional;
+
+ # sets of search scope and deref alias
+ scope: set[string] &log &optional;
+ deref: set[string] &log &optional;
+
+ # base search objects
+ base_object: vector of string &log &optional;
+
+ # number of results returned
+ result_count: count &log &optional;
+
+ # Result code (s)
+ result: set[string] &log &optional;
+
+ # result diagnostic message(s)
+ diagnostic_message: vector of string &log &optional;
+
+ };
+
+ # Event that can be handled to access the ldap record as it is sent on
+ # to the logging framework.
+ global log_ldap: event(rec: ldap::Message);
+ global log_ldap_search: event(rec: ldap::Search);
+
+ # Event called for each LDAP message (either direction)
+ global ldap::message: event(c: connection,
+ message_id: int,
+ opcode: ldap::ProtocolOpcode,
+ result: ldap::ResultCode,
+ matched_dn: string,
+ diagnostic_message: string,
+ object: string,
+ argument: string);
+
+ const PROTOCOL_OPCODES = {
+ [ldap::ProtocolOpcode_BIND_REQUEST] = "bind",
+ [ldap::ProtocolOpcode_BIND_RESPONSE] = "bind",
+ [ldap::ProtocolOpcode_UNBIND_REQUEST] = "unbind",
+ [ldap::ProtocolOpcode_SEARCH_REQUEST] = "search",
+ [ldap::ProtocolOpcode_SEARCH_RESULT_ENTRY] = "search",
+ [ldap::ProtocolOpcode_SEARCH_RESULT_DONE] = "search",
+ [ldap::ProtocolOpcode_MODIFY_REQUEST] = "modify",
+ [ldap::ProtocolOpcode_MODIFY_RESPONSE] = "modify",
+ [ldap::ProtocolOpcode_ADD_REQUEST] = "add",
+ [ldap::ProtocolOpcode_ADD_RESPONSE] = "add",
+ [ldap::ProtocolOpcode_DEL_REQUEST] = "delete",
+ [ldap::ProtocolOpcode_DEL_RESPONSE] = "delete",
+ [ldap::ProtocolOpcode_MOD_DN_REQUEST] = "modify",
+ [ldap::ProtocolOpcode_MOD_DN_RESPONSE] = "modify",
+ [ldap::ProtocolOpcode_COMPARE_REQUEST] = "compare",
+ [ldap::ProtocolOpcode_COMPARE_RESPONSE] = "compare",
+ [ldap::ProtocolOpcode_ABANDON_REQUEST] = "abandon",
+ [ldap::ProtocolOpcode_SEARCH_RESULT_REFERENCE] = "search",
+ [ldap::ProtocolOpcode_EXTENDED_REQUEST] = "extended",
+ [ldap::ProtocolOpcode_EXTENDED_RESPONSE] = "extended",
+ [ldap::ProtocolOpcode_INTERMEDIATE_RESPONSE] = "intermediate"
+ } &default = "unknown";
+
+ const BIND_SIMPLE = "bind simple";
+ const BIND_SASL = "bind SASL";
+
+ const RESULT_CODES = {
+ [ldap::ResultCode_SUCCESS] = "success",
+ [ldap::ResultCode_OPERATIONS_ERROR] = "operations error",
+ [ldap::ResultCode_PROTOCOL_ERROR] = "protocol error",
+ [ldap::ResultCode_TIME_LIMIT_EXCEEDED] = "time limit exceeded",
+ [ldap::ResultCode_SIZE_LIMIT_EXCEEDED] = "size limit exceeded",
+ [ldap::ResultCode_COMPARE_FALSE] = "compare false",
+ [ldap::ResultCode_COMPARE_TRUE] = "compare true",
+ [ldap::ResultCode_AUTH_METHOD_NOT_SUPPORTED] = "auth method not supported",
+ [ldap::ResultCode_STRONGER_AUTH_REQUIRED] = "stronger auth required",
+ [ldap::ResultCode_PARTIAL_RESULTS] = "partial results",
+ [ldap::ResultCode_REFERRAL] = "referral",
+ [ldap::ResultCode_ADMIN_LIMIT_EXCEEDED] = "admin limit exceeded",
+ [ldap::ResultCode_UNAVAILABLE_CRITICAL_EXTENSION] = "unavailable critical extension",
+ [ldap::ResultCode_CONFIDENTIALITY_REQUIRED] = "confidentiality required",
+ [ldap::ResultCode_SASL_BIND_IN_PROGRESS] = "SASL bind in progress",
+ [ldap::ResultCode_NO_SUCH_ATTRIBUTE] = "no such attribute",
+ [ldap::ResultCode_UNDEFINED_ATTRIBUTE_TYPE] = "undefined attribute type",
+ [ldap::ResultCode_INAPPROPRIATE_MATCHING] = "inappropriate matching",
+ [ldap::ResultCode_CONSTRAINT_VIOLATION] = "constraint violation",
+ [ldap::ResultCode_ATTRIBUTE_OR_VALUE_EXISTS] = "attribute or value exists",
+ [ldap::ResultCode_INVALID_ATTRIBUTE_SYNTAX] = "invalid attribute syntax",
+ [ldap::ResultCode_NO_SUCH_OBJECT] = "no such object",
+ [ldap::ResultCode_ALIAS_PROBLEM] = "alias problem",
+ [ldap::ResultCode_INVALID_DNSYNTAX] = "invalid DN syntax",
+ [ldap::ResultCode_ALIAS_DEREFERENCING_PROBLEM] = "alias dereferencing problem",
+ [ldap::ResultCode_INAPPROPRIATE_AUTHENTICATION] = "inappropriate authentication",
+ [ldap::ResultCode_INVALID_CREDENTIALS] = "invalid credentials",
+ [ldap::ResultCode_INSUFFICIENT_ACCESS_RIGHTS] = "insufficient access rights",
+ [ldap::ResultCode_BUSY] = "busy",
+ [ldap::ResultCode_UNAVAILABLE] = "unavailable",
+ [ldap::ResultCode_UNWILLING_TO_PERFORM] = "unwilling to perform",
+ [ldap::ResultCode_LOOP_DETECT] = "loop detect",
+ [ldap::ResultCode_SORT_CONTROL_MISSING] = "sort control missing",
+ [ldap::ResultCode_OFFSET_RANGE_ERROR] = "offset range error",
+ [ldap::ResultCode_NAMING_VIOLATION] = "naming violation",
+ [ldap::ResultCode_OBJECT_CLASS_VIOLATION] = "object class violation",
+ [ldap::ResultCode_NOT_ALLOWED_ON_NON_LEAF] = "not allowed on non-leaf",
+ [ldap::ResultCode_NOT_ALLOWED_ON_RDN] = "not allowed on RDN",
+ [ldap::ResultCode_ENTRY_ALREADY_EXISTS] = "entry already exists",
+ [ldap::ResultCode_OBJECT_CLASS_MODS_PROHIBITED] = "object class mods prohibited",
+ [ldap::ResultCode_RESULTS_TOO_LARGE] = "results too large",
+ [ldap::ResultCode_AFFECTS_MULTIPLE_DSAS] = "affects multiple DSAs",
+ [ldap::ResultCode_CONTROL_ERROR] = "control error",
+ [ldap::ResultCode_OTHER] = "other",
+ [ldap::ResultCode_SERVER_DOWN] = "server down",
+ [ldap::ResultCode_LOCAL_ERROR] = "local error",
+ [ldap::ResultCode_ENCODING_ERROR] = "encoding error",
+ [ldap::ResultCode_DECODING_ERROR] = "decoding error",
+ [ldap::ResultCode_TIMEOUT] = "timeout",
+ [ldap::ResultCode_AUTH_UNKNOWN] = "auth unknown",
+ [ldap::ResultCode_FILTER_ERROR] = "filter error",
+ [ldap::ResultCode_USER_CANCELED] = "user canceled",
+ [ldap::ResultCode_PARAM_ERROR] = "param error",
+ [ldap::ResultCode_NO_MEMORY] = "no memory",
+ [ldap::ResultCode_CONNECT_ERROR] = "connect error",
+ [ldap::ResultCode_NOT_SUPPORTED] = "not supported",
+ [ldap::ResultCode_CONTROL_NOT_FOUND] = "control not found",
+ [ldap::ResultCode_NO_RESULTS_RETURNED] = "no results returned",
+ [ldap::ResultCode_MORE_RESULTS_TO_RETURN] = "more results to return",
+ [ldap::ResultCode_CLIENT_LOOP] = "client loop",
+ [ldap::ResultCode_REFERRAL_LIMIT_EXCEEDED] = "referral limit exceeded",
+ [ldap::ResultCode_INVALID_RESPONSE] = "invalid response",
+ [ldap::ResultCode_AMBIGUOUS_RESPONSE] = "ambiguous response",
+ [ldap::ResultCode_TLS_NOT_SUPPORTED] = "TLS not supported",
+ [ldap::ResultCode_INTERMEDIATE_RESPONSE] = "intermediate response",
+ [ldap::ResultCode_UNKNOWN_TYPE] = "unknown type",
+ [ldap::ResultCode_LCUP_INVALID_DATA] = "LCUP invalid data",
+ [ldap::ResultCode_LCUP_UNSUPPORTED_SCHEME] = "LCUP unsupported scheme",
+ [ldap::ResultCode_LCUP_RELOAD_REQUIRED] = "LCUP reload required",
+ [ldap::ResultCode_CANCELED] = "canceled",
+ [ldap::ResultCode_NO_SUCH_OPERATION] = "no such operation",
+ [ldap::ResultCode_TOO_LATE] = "too late",
+ [ldap::ResultCode_CANNOT_CANCEL] = "cannot cancel",
+ [ldap::ResultCode_ASSERTION_FAILED] = "assertion failed",
+ [ldap::ResultCode_AUTHORIZATION_DENIED] = "authorization denied"
+ } &default = "unknown";
+
+ const SEARCH_SCOPES = {
+ [ldap::SearchScope_SEARCH_BASE] = "base",
+ [ldap::SearchScope_SEARCH_SINGLE] = "single",
+ [ldap::SearchScope_SEARCH_TREE] = "tree",
+ } &default = "unknown";
+
+ const SEARCH_DEREF_ALIASES = {
+ [ldap::SearchDerefAlias_DEREF_NEVER] = "never",
+ [ldap::SearchDerefAlias_DEREF_IN_SEARCHING] = "searching",
+ [ldap::SearchDerefAlias_DEREF_FINDING_BASE] = "finding",
+ [ldap::SearchDerefAlias_DEREF_ALWAYS] = "always",
+ } &default = "unknown";
+}
+
+#############################################################################
+global OPCODES_FINISHED: set[ldap::ProtocolOpcode] = { ldap::ProtocolOpcode_BIND_RESPONSE,
+ ldap::ProtocolOpcode_UNBIND_REQUEST,
+ ldap::ProtocolOpcode_SEARCH_RESULT_DONE,
+ ldap::ProtocolOpcode_MODIFY_RESPONSE,
+ ldap::ProtocolOpcode_ADD_RESPONSE,
+ ldap::ProtocolOpcode_DEL_RESPONSE,
+ ldap::ProtocolOpcode_MOD_DN_RESPONSE,
+ ldap::ProtocolOpcode_COMPARE_RESPONSE,
+ ldap::ProtocolOpcode_ABANDON_REQUEST,
+ ldap::ProtocolOpcode_EXTENDED_RESPONSE };
+
+global OPCODES_SEARCH: set[ldap::ProtocolOpcode] = { ldap::ProtocolOpcode_SEARCH_REQUEST,
+ ldap::ProtocolOpcode_SEARCH_RESULT_ENTRY,
+ ldap::ProtocolOpcode_SEARCH_RESULT_DONE,
+ ldap::ProtocolOpcode_SEARCH_RESULT_REFERENCE };
+
+#############################################################################
+redef record connection += {
+ ldap_proto: string &optional;
+ ldap_messages: table[int] of Message &optional;
+ ldap_searches: table[int] of Search &optional;
+};
+
+#############################################################################
+event zeek_init() &priority=5 {
+ Log::create_stream(ldap::LDAP_LOG, [$columns=Message, $ev=log_ldap, $path="ldap"]);
+ Log::create_stream(ldap::LDAP_SEARCH_LOG, [$columns=Search, $ev=log_ldap_search, $path="ldap_search"]);
+}
+
+#############################################################################
+function set_session(c: connection, message_id: int, opcode: ldap::ProtocolOpcode) {
+
+ if (! c?$ldap_messages )
+ c$ldap_messages = table();
+
+ if (! c?$ldap_searches )
+ c$ldap_searches = table();
+
+ if ((opcode in OPCODES_SEARCH) && (message_id !in c$ldap_searches)) {
+ c$ldap_searches[message_id] = [$ts=network_time(),
+ $uid=c$uid,
+ $id=c$id,
+ $message_id=message_id,
+ $result_count=0];
+
+ } else if ((opcode !in OPCODES_SEARCH) && (message_id !in c$ldap_messages)) {
+ c$ldap_messages[message_id] = [$ts=network_time(),
+ $uid=c$uid,
+ $id=c$id,
+ $message_id=message_id];
+ }
+
+}
+
+#############################################################################
+event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) &priority=5 {
+
+ if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) {
+ c$ldap_proto = "tcp";
+ }
+
+}
+
+#############################################################################
+event ldap::message(c: connection,
+ message_id: int,
+ opcode: ldap::ProtocolOpcode,
+ result: ldap::ResultCode,
+ matched_dn: string,
+ diagnostic_message: string,
+ object: string,
+ argument: string) {
+
+ if (opcode == ldap::ProtocolOpcode_SEARCH_RESULT_DONE) {
+ set_session(c, message_id, opcode);
+
+ if ( result != ldap::ResultCode_NOT_SET ) {
+ if ( ! c$ldap_searches[message_id]?$result )
+ c$ldap_searches[message_id]$result = set();
+ add c$ldap_searches[message_id]$result[RESULT_CODES[result]];
+ }
+
+ if ( diagnostic_message != "" ) {
+ if ( ! c$ldap_searches[message_id]?$diagnostic_message )
+ c$ldap_searches[message_id]$diagnostic_message = vector();
+ c$ldap_searches[message_id]$diagnostic_message += diagnostic_message;
+ }
+
+ if (( ! c$ldap_searches[message_id]?$proto ) && c?$ldap_proto)
+ c$ldap_searches[message_id]$proto = c$ldap_proto;
+
+ Log::write(ldap::LDAP_SEARCH_LOG, c$ldap_searches[message_id]);
+ delete c$ldap_searches[message_id];
+
+ } else if (opcode !in OPCODES_SEARCH) {
+ set_session(c, message_id, opcode);
+
+ if ( ! c$ldap_messages[message_id]?$opcode )
+ c$ldap_messages[message_id]$opcode = set();
+ add c$ldap_messages[message_id]$opcode[PROTOCOL_OPCODES[opcode]];
+
+ if ( result != ldap::ResultCode_NOT_SET ) {
+ if ( ! c$ldap_messages[message_id]?$result )
+ c$ldap_messages[message_id]$result = set();
+ add c$ldap_messages[message_id]$result[RESULT_CODES[result]];
+ }
+
+ if ( diagnostic_message != "" ) {
+ if ( ! c$ldap_messages[message_id]?$diagnostic_message )
+ c$ldap_messages[message_id]$diagnostic_message = vector();
+ c$ldap_messages[message_id]$diagnostic_message += diagnostic_message;
+ }
+
+ if ( object != "" ) {
+ if ( ! c$ldap_messages[message_id]?$object )
+ c$ldap_messages[message_id]$object = vector();
+ c$ldap_messages[message_id]$object += object;
+ }
+
+ if ( argument != "" ) {
+ if ( ! c$ldap_messages[message_id]?$argument )
+ c$ldap_messages[message_id]$argument = vector();
+ c$ldap_messages[message_id]$argument += argument;
+ }
+
+ if (opcode in OPCODES_FINISHED) {
+
+ if ((BIND_SIMPLE in c$ldap_messages[message_id]$opcode) ||
+ (BIND_SASL in c$ldap_messages[message_id]$opcode)) {
+ # don't have both "bind" and "bind " in the operations list
+ delete c$ldap_messages[message_id]$opcode[PROTOCOL_OPCODES[ldap::ProtocolOpcode_BIND_REQUEST]];
+ }
+
+ if (( ! c$ldap_messages[message_id]?$proto ) && c?$ldap_proto)
+ c$ldap_messages[message_id]$proto = c$ldap_proto;
+
+ Log::write(ldap::LDAP_LOG, c$ldap_messages[message_id]);
+ delete c$ldap_messages[message_id];
+ }
+ }
+
+}
+
+#############################################################################
+event ldap::searchreq(c: connection,
+ message_id: int,
+ base_object: string,
+ scope: ldap::SearchScope,
+ deref: ldap::SearchDerefAlias,
+ size_limit: int,
+ time_limit: int,
+ types_only: bool) {
+
+ set_session(c, message_id, ldap::ProtocolOpcode_SEARCH_REQUEST);
+
+ if ( scope != ldap::SearchScope_NOT_SET ) {
+ if ( ! c$ldap_searches[message_id]?$scope )
+ c$ldap_searches[message_id]$scope = set();
+ add c$ldap_searches[message_id]$scope[SEARCH_SCOPES[scope]];
+ }
+
+ if ( deref != ldap::SearchDerefAlias_NOT_SET ) {
+ if ( ! c$ldap_searches[message_id]?$deref )
+ c$ldap_searches[message_id]$deref = set();
+ add c$ldap_searches[message_id]$deref[SEARCH_DEREF_ALIASES[deref]];
+ }
+
+ if ( base_object != "" ) {
+ if ( ! c$ldap_searches[message_id]?$base_object )
+ c$ldap_searches[message_id]$base_object = vector();
+ c$ldap_searches[message_id]$base_object += base_object;
+ }
+
+}
+
+#############################################################################
+event ldap::searchres(c: connection,
+ message_id: int,
+ object_name: string) {
+
+ set_session(c, message_id, ldap::ProtocolOpcode_SEARCH_RESULT_ENTRY);
+
+ c$ldap_searches[message_id]$result_count += 1;
+}
+
+#############################################################################
+event ldap::bindreq(c: connection,
+ message_id: int,
+ version: int,
+ name: string,
+ authType: ldap::BindAuthType,
+ authInfo: string) {
+
+ set_session(c, message_id, ldap::ProtocolOpcode_BIND_REQUEST);
+
+ if ( ! c$ldap_messages[message_id]?$version )
+ c$ldap_messages[message_id]$version = version;
+
+ if ( ! c$ldap_messages[message_id]?$opcode )
+ c$ldap_messages[message_id]$opcode = set();
+
+ if (authType == ldap::BindAuthType_BIND_AUTH_SIMPLE) {
+ add c$ldap_messages[message_id]$opcode[BIND_SIMPLE];
+ } else if (authType == ldap::BindAuthType_BIND_AUTH_SASL) {
+ add c$ldap_messages[message_id]$opcode[BIND_SASL];
+ }
+
+}
+
+#############################################################################
+event connection_state_remove(c: connection) {
+
+ # log any "pending" unlogged LDAP messages/searches
+
+ if ( c?$ldap_messages && (|c$ldap_messages| > 0) ) {
+ for ( [mid], m in c$ldap_messages ) {
+ if (mid > 0) {
+
+ if ((BIND_SIMPLE in m$opcode) || (BIND_SASL in m$opcode)) {
+ # don't have both "bind" and "bind " in the operations list
+ delete m$opcode[PROTOCOL_OPCODES[ldap::ProtocolOpcode_BIND_REQUEST]];
+ }
+
+ if (( ! m?$proto ) && c?$ldap_proto)
+ m$proto = c$ldap_proto;
+
+ Log::write(ldap::LDAP_LOG, m);
+ }
+ }
+ delete c$ldap_messages;
+ }
+
+ if ( c?$ldap_searches && (|c$ldap_searches| > 0) ) {
+ for ( [mid], s in c$ldap_searches ) {
+ if (mid > 0) {
+
+ if (( ! s?$proto ) && c?$ldap_proto)
+ s$proto = c$ldap_proto;
+
+ Log::write(ldap::LDAP_SEARCH_LOG, s);
+ }
+ }
+ delete c$ldap_searches;
+ }
+
+}
+
diff --git a/tests/Baseline/protocol.ldap.basic/conn.log b/tests/Baseline/protocol.ldap.basic/conn.log
new file mode 100644
index 00000000..5ab37878
--- /dev/null
+++ b/tests/Baseline/protocol.ldap.basic/conn.log
@@ -0,0 +1,12 @@
+### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
+### NOTE: This file has been sorted with diff-sort.
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path conn
+#open XXXX-XX-XX-XX-XX-XX
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state local_orig local_resp missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
+#types time string addr port addr port enum string interval count count string bool bool count string count count count count set[string]
+#close XXXX-XX-XX-XX-XX-XX
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp spicy_ldap_tcp 181.520479 258 188 RSTO - - 0 ShADdR 8 590 4 360 -
diff --git a/tests/Baseline/protocol.ldap.basic/ldap.log b/tests/Baseline/protocol.ldap.basic/ldap.log
new file mode 100644
index 00000000..021bf93c
--- /dev/null
+++ b/tests/Baseline/protocol.ldap.basic/ldap.log
@@ -0,0 +1,13 @@
+### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
+### NOTE: This file has been sorted with diff-sort.
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path ldap
+#open XXXX-XX-XX-XX-XX-XX
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto message_id version opcode result diagnostic_message object argument
+#types time string addr port addr port string int int set[string] set[string] vector[string] vector[string] vector[string]
+#close XXXX-XX-XX-XX-XX-XX
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net passwor8d1
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net /dev/rdsk/c0t0d0s0
diff --git a/tests/Baseline/protocol.ldap.basic/ldap_search.log b/tests/Baseline/protocol.ldap.basic/ldap_search.log
new file mode 100644
index 00000000..8e44022e
--- /dev/null
+++ b/tests/Baseline/protocol.ldap.basic/ldap_search.log
@@ -0,0 +1,12 @@
+### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
+### NOTE: This file has been sorted with diff-sort.
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path ldap_search
+#open XXXX-XX-XX-XX-XX-XX
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto message_id scope deref base_object result_count result diagnostic_message
+#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string]
+#close XXXX-XX-XX-XX-XX-XX
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 3268 tcp 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success -
diff --git a/tests/Traces/README b/tests/Traces/README
index d0e41308..e2aaf5cc 100644
--- a/tests/Traces/README
+++ b/tests/Traces/README
@@ -19,6 +19,9 @@ IPSec
- [ipsec-ikev1-isakmp-main-mode.pcap](https://www.cloudshark.org/captures/ff740838f1c2)
- [ipsec-ikev1-isakmp-aggressive-mode.pcap](https://www.cloudshark.org/captures/e51f5c8a6b24)
+LDAP
+- [ldap-simpleauth.pcap](https://github.com/arkime/arkime/blob/main/tests/pcap/ldap-simpleauth.pcap)
+
OpenVPN
- [openvpn-tcp-tls-auth.pcap](https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=get&target=OpenVPN_TCP_tls-auth.pcapng)
- openvpn.pcap (self-made)
diff --git a/tests/Traces/ldap-simpleauth.pcap b/tests/Traces/ldap-simpleauth.pcap
new file mode 100644
index 00000000..1cf904a0
Binary files /dev/null and b/tests/Traces/ldap-simpleauth.pcap differ
diff --git a/tests/protocol/ldap/availability.zeek b/tests/protocol/ldap/availability.zeek
new file mode 100644
index 00000000..68cb285b
--- /dev/null
+++ b/tests/protocol/ldap/availability.zeek
@@ -0,0 +1,5 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+# @TEST-EXEC: ${ZEEK} -NN | grep -q ANALYZER_SPICY_LDAP_TCP
+#
+# @TEST-DOC: Check that LDAP (TCP) is analyzer is available.
diff --git a/tests/protocol/ldap/basic.zeek b/tests/protocol/ldap/basic.zeek
new file mode 100644
index 00000000..8e03fb0c
--- /dev/null
+++ b/tests/protocol/ldap/basic.zeek
@@ -0,0 +1,10 @@
+# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
+
+# @TEST-EXEC: ${ZEEK} -C -r ${TRACES}/ldap-simpleauth.pcap %INPUT
+# @TEST-EXEC: btest-diff conn.log
+# @TEST-EXEC: btest-diff ldap.log
+# @TEST-EXEC: btest-diff ldap_search.log
+#
+# @TEST-DOC: Test LDAP analyzer with small trace.
+
+@load spicy-analyzers/protocol/ldap