This commit is contained in:
Benjamin Bannier 2023-09-18 08:15:43 +02:00
parent 255103b50a
commit 080d7418d7
53 changed files with 2526 additions and 0 deletions

View file

@ -0,0 +1,26 @@
{
"parse": {
"additional_commands": {
"spicy_add_analyzer": {
"kwargs": {
"NAME": "*",
"PACKAGE_NAME": "*",
"SOURCES": "*",
"SCRIPTS": "*"
}
}
}
},
"format": {
"line_width": 100,
"tab_size": 4,
"separate_ctrl_name_with_space": true,
"max_subgroups_hwrap": 3
},
"markup": {
"enable_markup": false
},
"lint": {
"disabled_codes": ["C0103"]
}
}

View file

@ -0,0 +1,35 @@
on:
pull_request:
push:
branches: [main]
jobs:
Check:
strategy:
matrix:
version:
- zeek:6.0
- zeek-dev:latest
fail-fast: false
runs-on: ubuntu-latest
container: zeek/${{ matrix.version }}
steps:
- uses: actions/checkout@v2
- name: Prepare
run: |
apt-get update
apt-get install -y -q --no-install-recommends g++ cmake make libpcap-dev
- name: Install
run: |
git config --global --add safe.directory $PWD
git clean -fd
eval $(zkg env)
echo Y | zkg -vvvvv install .
- name: Show logs
if: always()
run: |
tail -n 1000000 $(zkg config state_dir)/logs/*.log

View file

@ -0,0 +1,14 @@
name: pre-commit
on:
pull_request:
push:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.3

7
src/spicy/spicy-ldap/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
build
.idea/
cmake-build-debug/
.DS_Store
*.swp
*.tmp
tests/.btest*

View file

@ -0,0 +1 @@
rules "~MD033", "~MD013", "~MD046", "~MD010"

View file

@ -0,0 +1,23 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/markdownlint/markdownlint
rev: v0.11.0
hooks:
- id: markdownlint
- repo: https://github.com/cheshirekow/cmake-format-precommit
rev: v0.6.13
hooks:
- id: cmake-format
- id: cmake-lint
exclude: '^tests/Baseline'

View file

@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
project(Messages LANGUAGES C)
list(PREPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
find_package(SpicyPlugin REQUIRED)
# Set mininum versions that this plugin needs. Make sure to use "x.y.z" format.
spicy_require_version("1.2.0")
spicy_plugin_require_version("0.99.0")
zeek_require_version("3.0.0")
if (NOT CMAKE_BUILD_TYPE)
# Default to release build.
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "")
endif ()
add_subdirectory(analyzer)

View file

@ -0,0 +1,29 @@
Copyright (c) 2020-2021 by the Zeek Project through the International Computer
Science Institute. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
(1) Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
(2) Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
(3) Neither the name of the Zeek Project, the International Computer
Science Institute, nor the names of contributors may be used to
endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,74 @@
LDAP Analyzer
=============
Here's what it has:
- ASN.1 structure decoding: this is probably generally useful for more than just the LDAP parser, so it may be of interest for this to be included somehow as part of spicy's standard modules or whatever
- everything is working except for the "constructed" forms of `ASN1BitString` and `ASN1OctetString`
- LDAP: the LDAP parsing is basically "done once" through a single call to `ASN1Message` (which parses itself recursively) and then the application-level data is also parsed via `&parse-from` a byte array belonging to the outer ASN.1 sequence. This second level of parsing is also done using the ASN.1 data types.
- events
- `ldap::message` - called for each LDAP message
- `ldap::bindreq` - when a bind request is made
- `ldap::searchreq` - basic search request information
- `ldap::searchres` - called each time a search result is returned
- enums
- `ProtocolOpcode`
- `ResultCode`
- `BindAuthType`
- `SearchScope`
- `SearchDerefAlias`
- `FilterType`
- Zeek log files
- `ldap.log` - contains information about all LDAP messages except those that are search-related. Log lines are grouped by connection ID + message ID
- `ts` (time)
- `uid` (connection UID)
- `id` (connection ID 4-tuple)
- `proto` (transport protocol)
- `message_id` (LDAP message ID)
- `version` (LDAP version for bind requests)
- `opcode` (set of 1..n operations from this uid+message_id)
- `result` (set of 1..n results from this uid+message_id)
- `diagnostic_message` (vector of 0..n diagnostic message strings)
- `object` (vector of 0..n "objects," the meaning of which depends on the operation)
- `argument` (vector of 0..n "argument," the meaning of which depends on the operation)
- `ldap_search.log` - contains information about LDAP searches. Log lines are grouped by connection ID + message ID
- `ts` (time)
- `uid` (connection UID)
- `id` (connection ID 4-tuple)
- `proto` (transport protocol)
- `message_id` (LDAP message ID)
- `scope` (set of 1..n search scopes defined in this uid+message_id)
- `deref` (set of 1..n search deref alias options defined in this uid+message_id)
- `base_object` (vector of 0..n search base objects specified)
- `result_count` (number of result entries returned)
- `result` (set of 1..n results from this uid+message_id)
- `diagnostic_message` (vector of 0..n diagnostic message strings)
- `filter` (search filter string)
- `attributes` (vector of 0..n "attributes", the attributes that were returned)
- test
- basic tests for detecting plugin presence and simple bind and search result/requests
Here's what it doesn't have, which could be added by future parties interested in expanding it:
- LDAP [referrals](https://tools.ietf.org/html/rfc4511#section-4.1.10) are not parsed out of the results
- [SASL credentials](https://datatracker.ietf.org/doc/html/rfc4511#section-4.2) in bind requests are not being parsed beyond the mechanism string
- SASL information in bind responses are not being parsed; for that matter, SASL-based LDAP stuff hasn't been tested much and may have issues
- Search filters and attributes: the search filters, reconstructed from the query tree, is represented in string format. The AND and OR filters have a tree structure and are parsed with the `ParseNestedAndOr` unit, whereas the NOT filter consist of one single nested SearchFilter and is parsed with a `ParseNestedNot` unit. The remaining filter types can all be decoded to a string using the `DecodedAttributeValue` unit, which takes the `FilterType` as a parameter. The `FILTER_PRESENT` consists of a single octet string and can be parsed directly. By recursively constructing leafs and nodes in the tree, the final search filter can be represented, e.g. `(&(objectclass=*)(sAMAccountName=xxxxxxxx))`. The returned attributes are represented in a list and returned to the `ldap_search.log` if `option default_log_search_attributes = T;` is set (the default is False).
- the details of `SearchResultReference` are not being parsed
- the only detail of `ModifyRequest` being parsed is the object name
- the details of `AddRequest` are not being parsed
- the details of `ModDNRequest` are not being parsed
- the details of `CompareRequest` are not being parsed
- the details of `AbandonRequest` are not being parsed
- the details of `ExtendedRequest` are not being parsed
- the details of `ExtendedResponse` are not being parsed
- the details of `IntermediateResponse` are not being parsed
- [Logging policy](https://docs.zeek.org/en/master/frameworks/logging.html#filtering-log-records) is available.
Useful Links:
- <https://luca.ntop.org/Teaching/Appunti/asn1.html>
- <https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf>
- <https://tools.ietf.org/html/rfc4511#>
- <https://ldap.com/ldapv3-wire-protocol-reference-asn1-ber/>
- <https://lapo.it/asn1js>

View file

@ -0,0 +1,5 @@
spicy_add_analyzer(
NAME LDAP
PACKAGE_NAME spicy-ldap
SOURCES ldap.spicy ldap.evt ldap_zeek.spicy
SCRIPTS __load__.zeek main.zeek dpd.sig)

View file

@ -0,0 +1,2 @@
@load-sigs ./dpd.sig
@load ./main.zeek

View file

@ -0,0 +1,278 @@
# 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 {
: bitfield(8) {
type_: 0..4 &convert=ASN1Type($$);
constructed: 5 &convert=cast<bool>($$);
class: 6..7 &convert=ASN1Class($$);
};
};
#- 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 encoding: hilti::Charset;
on %init {
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.encoding = 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.encoding = hilti::Charset::UTF8;
}
}
}
value: ASN1OctetString(len, tag.constructed) &convert=$$.value.decode(self.encoding);
} &convert=self.value;
#- 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<uint64>;
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<bool>($$) &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);
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<int32>(self.head.tag.type_);
}
};
};

View file

@ -0,0 +1,23 @@
signature dpd_ldap_client_udp {
ip-proto == udp
payload /^\x30.\x02\x01.\x60/
}
signature dpd_ldap_server_udp {
ip-proto == udp
payload /^\x30/
requires-reverse-signature dpd_ldap_client_udp
enable "spicy_LDAP_UDP"
}
signature dpd_ldap_client_tcp {
ip-proto == tcp
payload /^\x30.\x02\x01.\x60/
}
signature dpd_ldap_server_tcp {
ip-proto == tcp
payload /^\x30/
requires-reverse-signature dpd_ldap_client_tcp
enable "spicy_LDAP_TCP"
}

View file

@ -0,0 +1,43 @@
# 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};
protocol analyzer spicy::LDAP_UDP over UDP:
parse with LDAP::Messages,
ports { 389/udp };
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,
self.filter,
self.attributes);
on LDAP::SearchResultEntry -> event LDAP::searchres($conn,
message.messageID,
self.objectName);

View file

@ -0,0 +1,871 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
module LDAP;
import ASN1;
import spicy;
# 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,
};
#- 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,
};
#-----------------------------------------------------------------------------
public type Result = unit {
code: ASN1::ASN1Message(True) &convert=cast<ResultCode>(cast<uint8>($$.body.num_value))
&default=ResultCode::Undef;
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 {
: MessageWrapper[];
};
#-----------------------------------------------------------------------------
type SASLLayer = unit {
# For the time being (before we support parsing the SASL layer) this unit
# is used by MessageWrapper below to strip it (SASL) so that the parser
# can attempt to resume parsing afterward. It also sets the success flag
# if '\x30' is found, otherwise backtracks so that we can deal with encrypted
# SASL payloads without raising a parse error.
var success: bool = False;
: bytes &until=b"\x30" {
self.success = True;
}
on %error {
self.backtrack();
}
};
#-----------------------------------------------------------------------------
public type MessageWrapper = unit {
# A wrapper around 'Message'. First, we try to parse a Message unit.
# There are two possible outcomes:
# (1) Success -> We consumed all bytes and successfully parsed a Message unit
# (2) No success -> self.backtrack() is called in the Message unit,
# so effectively we didn't consume any bytes yet.
# The outcome can be determined by checking the `success` variable of the Message unit
# This success variable is different, because this keeps track of the status for the MessageWrapper object
var success: bool = False;
var message: Message;
# Here, we try to parse the message...
: Message &try {
# ... and only if the Message unit successfully parsed, we can set
# the status of this MessageWrapper's success to 'True'
if ( $$.success == True ) {
self.success = True;
self.message = $$;
}
}
# If we failed to parse the message, then we're going to scan the remaining bytes for the '\x30'
# start byte and try to parse a Message starting from that byte. This effectively
# strips the SASL layer if SASL Signing was enabled. Until now, I haven't found A
# better way to scan / determine the exact SASL header length yet, so we'll stick with this
# for the time being. If the entire LDAP packet was encrypted with SASL, then we skip parsing for
# now (in the long run we need to be parsing SASL/GSSAPI instead, in which case encrypted payloads
# are just another message type).
# SASLLayer (see unit above) just consumes bytes &until=b"\x30" or backtracks if it isn't found
# and sets a success flag we can use later to decide if those bytes contain a parsable message.
var sasl_success: bool = False;
: SASLLayer &try if ( self.success == False ) {
if ( $$.success == True ) {
self.sasl_success = True;
}
}
var remainder: bytes;
# SASLLayer consumes the delimiter ('\x30'), and because this is the first byte of a valid LDAP message
# we should re-add it to the remainder if the delimiter was found. If the delimeter was not found, we
# leave the remainer empty, but note that the bytes must be consumed either way to avoid stalling the
# parser and causing an infinite loop error.
: bytes &eod if ( self.success == False ) {
if ( self.sasl_success == True ) {
self.remainder = b"\x30" + $$;
}
}
# Again, try to parse a Message unit. Be aware that in this will sometimes fail if the '\x30' byte is
# also present in the SASL header.
# Also, we could try to do this recursively or try a few iterations, but for now I would suggest
# to try this extra parsing once to get the best cost/benefit tradeoff.
: Message &try &parse-from=self.remainder if ( self.success == False && self.sasl_success == True ) {
if ( $$.success == True ) {
self.success = True;
self.message = $$;
}
}
# If we still didn't manage to parse a message (so the &try resulted in another backtrack()) then
# this is probably an encrypted LDAP message, so skip it
} &convert=self.message;
#-----------------------------------------------------------------------------
public type Message = unit {
var messageID: int64;
var opcode: ProtocolOpcode = ProtocolOpcode::Undef;
var applicationBytes: bytes;
var unsetResultDefault: Result;
var result: Result& = self.unsetResultDefault;
var obj: string = "";
var arg: string = "";
var success: bool = False;
: 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<ProtocolOpcode>(cast<uint8>($$.body.seq.submessages[1].application_id));
self.applicationBytes = $$.body.seq.submessages[1].application_data;
}
}
}
switch ( self.opcode ) {
ProtocolOpcode::BIND_REQUEST -> BIND_REQUEST: BindRequest(self);
ProtocolOpcode::BIND_RESPONSE -> BIND_RESPONSE: BindResponse(self);
ProtocolOpcode::UNBIND_REQUEST -> UNBIND_REQUEST: UnbindRequest(self);
ProtocolOpcode::SEARCH_REQUEST -> SEARCH_REQUEST: SearchRequest(self);
ProtocolOpcode::SEARCH_RESULT_ENTRY -> SEARCH_RESULT_ENTRY: SearchResultEntry(self);
ProtocolOpcode::SEARCH_RESULT_DONE -> SEARCH_RESULT_DONE: SearchResultDone(self);
ProtocolOpcode::MODIFY_REQUEST -> MODIFY_REQUEST: ModifyRequest(self);
ProtocolOpcode::MODIFY_RESPONSE -> MODIFY_RESPONSE: ModifyResponse(self);
ProtocolOpcode::ADD_RESPONSE -> ADD_RESPONSE: AddResponse(self);
ProtocolOpcode::DEL_REQUEST -> DEL_REQUEST: DelRequest(self);
ProtocolOpcode::DEL_RESPONSE -> DEL_RESPONSE: DelResponse(self);
ProtocolOpcode::MOD_DN_RESPONSE -> MOD_DN_RESPONSE: ModDNResponse(self);
ProtocolOpcode::COMPARE_RESPONSE -> COMPARE_RESPONSE: CompareResponse(self);
ProtocolOpcode::ABANDON_REQUEST -> ABANDON_REQUEST: AbandonRequest(self);
# TODO: not yet implemented, redirect to NotImplemented because when we're
# just commenting this out, it will stop processing LDAP Messages in this connection
ProtocolOpcode::ADD_REQUEST -> ADD_REQUEST: NotImplemented(self);
ProtocolOpcode::COMPARE_REQUEST -> COMPARE_REQUEST: NotImplemented(self);
ProtocolOpcode::EXTENDED_REQUEST -> EXTENDED_REQUEST: NotImplemented(self);
ProtocolOpcode::EXTENDED_RESPONSE -> EXTENDED_RESPONSE: NotImplemented(self);
ProtocolOpcode::INTERMEDIATE_RESPONSE -> INTERMEDIATE_RESPONSE: NotImplemented(self);
ProtocolOpcode::MOD_DN_REQUEST -> MOD_DN_REQUEST: NotImplemented(self);
ProtocolOpcode::SEARCH_RESULT_REFERENCE -> SEARCH_RESULT_REFERENCE: NotImplemented(self);
} &parse-from=self.applicationBytes if ( self.opcode );
on %error {
self.backtrack();
}
on %done {
self.success = True;
}
} &requires=((self?.messageID) && (self?.opcode) && (self.opcode != ProtocolOpcode::Undef));
#-----------------------------------------------------------------------------
# Bind Operation
# https://tools.ietf.org/html/rfc4511#section-4.2
public type BindAuthType = enum {
BIND_AUTH_SIMPLE = 0,
BIND_AUTH_SASL = 3,
};
type SaslCredentials = unit() {
mechanism: ASN1::ASN1Message(True) &convert=$$.body.str_value;
# TODO: if we want to parse the (optional) credentials string
};
# TODO(fox-ds): A helper unit for requests for which no handling has been implemented.
# Eventually all uses of this unit should be replaced with actual parsers so this unit can be removed.
type NotImplemented = unit(inout message: Message) {
# Do nothing
};
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::Undef;
var authData: bytes = b"";
var simpleCreds: string = "";
: ASN1::ASN1Message(True) {
if ($$?.application_id) {
self.authType = cast<BindAuthType>(cast<uint8>($$.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::Undef));
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,
};
public type SearchDerefAlias = enum {
DEREF_NEVER = 0,
DEREF_IN_SEARCHING = 1,
DEREF_FINDING_BASE = 2,
DEREF_ALWAYS = 3,
};
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,
};
public type AttributeSelection = unit {
var attributes: vector<string>;
# 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;
}
}
}
};
# An AND or OR search filter can consist of many sub-searchfilters, so we try to parse these
type ParseNestedAndOr = unit {
searchfilters: SearchFilter[] &eod;
};
type ParseNestedNot = unit {
searchfilter: SearchFilter;
};
# Helper functions to properly format some custom data structures
public function utf16_guid_to_hex_repr(bts: bytes) : string {
# Rather ugly workaround to pretty-print the CLDAP DomainGuid UTF16-LE encoded string
# in the same format as Wireshark (aabbccdd-eeff-gghh-iijj-kkllmmnnoopp)
# We need to have exactly 16 bytes...
if ( |bts| != 16 ) {
# ... and otherwise just return an error code
return "GUID_FORMAT_FAILED";
}
local ret = "";
for ( i in [[3, 2, 1, 0], [5, 4], [7, 6], [8, 9], [10, 11, 12, 13, 14, 15]] ) {
for ( j in i ) {
local bt: uint8 = *bts.at(j);
ret = ret + "%02x" % bt;
if ( j in [0, 4, 6, 9] ) {
ret = ret + "-";
}
}
}
return ret;
}
public function bytes_sid_to_hex_repr(bts: bytes) : string {
local ret = "";
local cnt = 0;
while ( cnt < |bts| ) {
local bt: uint8 = *bts.at(cnt);
ret = ret + "%02x" % bt;
if ( cnt < |bts|-1 ) {
ret = ret + ":";
}
cnt += 1;
}
return ret;
}
public function bytes_sid_to_SID_repr(bts: bytes) : string {
# Example: SID -> S-1-5-21-1153942841-488947194-1912431946
# Needs to be exactly 24 bytes
if ( |bts| != 24 ) {
# ... and otherwise just return an error code
return "SID_FORMAT_FAILED";
}
local ret = "S-";
local cnt = 0;
# Mixed little and big endian, so turn everything to big endian first...
# Byte 1 seems to be skipped when parsing the SID
for ( i in [[0], [2, 3, 4, 5, 6, 7], [11, 10, 9, 8], [15, 14, 13, 12], [19, 18, 17, 16], [23, 22, 21, 20]] ) {
local dec_val_rep: bytes = b"";
for ( j in i ) {
local bt: uint8 = *bts.at(j);
dec_val_rep += bt;
cnt += 1;
}
# ... so we can represent this integer value in big endian
ret = ret + "%u" % dec_val_rep.to_uint(spicy::ByteOrder::Big);
# Only print the dash when we're not at the end
if ( cnt < 23 ) {
ret = ret + "-";
}
}
return ret;
}
public function uint32_to_hex_repr(bts: bytes) : string {
# Needs to be exactly 4 bytes
if ( |bts| != 4 ) {
# ... and otherwise just return an error code
return "HEX_FORMAT_FAILED";
}
# Workaround to print the hex value of an uint32, prepended with '0x'
local ret = "0x";
for ( i in [3, 2, 1, 0] ) {
local bt: uint8 = *bts.at(i);
ret = ret + "%02x" % bt;
}
return ret;
}
# Helper to compute a string representation of a `SearchFilter`.
public function string_representation(search_filter: SearchFilter): string {
local repr: string;
switch ( local fType = search_filter.filterType ) {
# The NOT, AND and OR filter types are trees and may hold many leaf nodes. So recursively get
# the stringPresentations for the leaf nodes and add them all in one final statement.
case FilterType::FILTER_NOT: {
repr = "(!%s)" % search_filter.FILTER_NOT.searchfilter.stringRepresentation;
}
case FilterType::FILTER_AND, FilterType::FILTER_OR: {
local nestedObj: ParseNestedAndOr;
local printChar = "";
if ( fType == FilterType::FILTER_AND ) {
printChar = "&";
nestedObj = search_filter.FILTER_AND;
} else {
printChar = "|";
nestedObj = search_filter.FILTER_OR;
}
# Build the nested AND/OR statement in this loop. When we encounter the first element,
# we open the statement. At the second element, we close our first complete statement. For every
# following statement, we extend the AND/OR statement by wrapping it around the already completed
# statement. Although it is also valid to not do this wrapping, which is logically equivalent, e.g:
#
# (1) (2)
# (?(a=b)(c=d)(e=f)) vs (?(?(a=b)(c=d))(e=f))
#
# the latter version is also shown by Wireshark. So although the parsed structure actually represents
# version (1) of the query, we now choose to print version (2). If this is not desirable, swap the code
# for the following:
#
# # Construct the nested structure, like (1)
# for ( SF in nestedObj.searchfilters ) {
# self.stringRepresentation = self.stringRepresentation + SF.stringRepresentation
# }
# # Close it with brackets and put the correct printChar for AND/OR in the statement
# self.stringRepresentation = "(%s" % printChar + self.stringRepresentation + ")";
#
local i = 0;
for ( searchFilter in nestedObj.searchfilters ) {
switch ( i ) {
case 0: {
repr = "(%s%s%s" % (
printChar,
searchFilter.stringRepresentation,
# If we have exactly one element immediately close the statement since we are done.
|nestedObj.searchfilters| == 1 ? ")" : ""
);
}
case 1: {
repr = repr + searchFilter.stringRepresentation + ")";
}
default: {
repr = "(%s" % printChar + repr + searchFilter.stringRepresentation + ")";
}
}
i += 1;
}
}
# The following FilterTypes are leaf nodes and can thus be represented in a statement
case FilterType::FILTER_EXT: {
# For extended search filters the meaning of the individual fields in
# `DecodedAttributeValue` is slightly different.
repr = "(%s:%s:=%s)" % (search_filter.FILTER_EXT.assertionValueDecoded,
search_filter.FILTER_EXT.attributeDesc.decode(),
search_filter.FILTER_EXT.matchValue);
}
case FilterType::FILTER_APPROX: {
repr = "(%s~=%s)" % (search_filter.FILTER_APPROX.attributeDesc.decode(),
search_filter.FILTER_APPROX.assertionValueDecoded);
}
case FilterType::FILTER_EQ: {
repr = "(%s=%s)" % (search_filter.FILTER_EQ.attributeDesc.decode(),
search_filter.FILTER_EQ.assertionValueDecoded);
}
case FilterType::FILTER_GE: {
repr = "(%s>=%s)" % (search_filter.FILTER_GE.attributeDesc.decode(),
search_filter.FILTER_GE.assertionValueDecoded);
}
case FilterType::FILTER_LE: {
repr = "(%s<=%s)" % (search_filter.FILTER_LE.attributeDesc.decode(),
search_filter.FILTER_LE.assertionValueDecoded);
}
case FilterType::FILTER_SUBSTR: {
repr = "(%s=*%s*)" % (search_filter.FILTER_SUBSTR.attributeDesc.decode(),
search_filter.FILTER_SUBSTR.assertionValueDecoded);
}
case FilterType::FILTER_PRESENT: {
repr = "(%s=*)" % search_filter.FILTER_PRESENT;
}
}
return repr;
}
# Represents an (extended) key-value pair present in SearchFilters
type DecodedAttributeValue = unit(fType: FilterType) {
var assertionValueDecoded: string = "";
: uint8;
attributeDesc_len: uint8;
attributeDesc: bytes &size=self.attributeDesc_len;
# For some reason, two intermediate uint8 values are present in the FILTER_SUBSTR type.
: uint8 if ( fType == FilterType::FILTER_SUBSTR );
: uint8 if ( fType == FilterType::FILTER_SUBSTR );
: uint8;
assertionValue_len: uint8;
assertionValue: bytes &size=self.assertionValue_len;
# Only for the FILTER_EXT type, parse extra fields
: uint8 if ( fType == FilterType::FILTER_EXT );
matchValue_len: uint8 if( fType == FilterType::FILTER_EXT );
matchValue: bytes &size=self.matchValue_len if ( fType == FilterType::FILTER_EXT );
on %done {
switch ( self.attributeDesc ) {
# Special parsing required for some CLDAP attributes,
# see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/895a7744-aff3-4f64-bcfa-f8c05915d2e9
case b"DomainGuid": {
self.assertionValueDecoded = utf16_guid_to_hex_repr(self.assertionValue);
}
case b"objectSid", b"AAC": {
self.assertionValueDecoded = bytes_sid_to_hex_repr(self.assertionValue);
}
case b"DomainSid": {
self.assertionValueDecoded = bytes_sid_to_SID_repr(self.assertionValue);
}
case b"NtVer": {
self.assertionValueDecoded = uint32_to_hex_repr(self.assertionValue);
}
# By default, decode with UTF-8
default: {
self.assertionValueDecoded = self.assertionValue.decode();
}
}
}
};
type SearchFilter = unit {
var filterType: FilterType = FilterType::Undef;
var filterBytes: bytes = b"";
var filterLen: uint64 = 0;
var stringRepresentation: string = "";
: ASN1::ASN1Message(True) {
if ($$?.application_id) {
self.filterType = cast<FilterType>(cast<uint8>($$.application_id));
self.filterBytes = $$.application_data;
self.filterLen = $$.head.len.len;
} else {
self.filterType = FilterType::FILTER_INVALID;
}
}
switch ( self.filterType ) {
# FilterTypes that hold one or more SearchFilters inside them
FilterType::FILTER_AND -> FILTER_AND: ParseNestedAndOr()
&parse-from=self.filterBytes;
FilterType::FILTER_OR -> FILTER_OR: ParseNestedAndOr()
&parse-from=self.filterBytes;
FilterType::FILTER_NOT -> FILTER_NOT: ParseNestedNot()
&parse-from=self.filterBytes;
# FilterTypes that we can actually convert to a string
FilterType::FILTER_EQ -> FILTER_EQ: DecodedAttributeValue(FilterType::FILTER_EQ)
&parse-from=self.filterBytes;
FilterType::FILTER_SUBSTR -> FILTER_SUBSTR: DecodedAttributeValue(FilterType::FILTER_SUBSTR)
&parse-from=self.filterBytes;
FilterType::FILTER_GE -> FILTER_GE: DecodedAttributeValue(FilterType::FILTER_GE)
&parse-from=self.filterBytes;
FilterType::FILTER_LE -> FILTER_LE: DecodedAttributeValue(FilterType::FILTER_LE)
&parse-from=self.filterBytes;
FilterType::FILTER_APPROX -> FILTER_APPROX: DecodedAttributeValue(FilterType::FILTER_APPROX)
&parse-from=self.filterBytes;
FilterType::FILTER_EXT -> FILTER_EXT: DecodedAttributeValue(FilterType::FILTER_EXT)
&parse-from=self.filterBytes;
FilterType::FILTER_PRESENT -> FILTER_PRESENT: ASN1::ASN1OctetString(self.filterLen, False)
&convert=$$.value.decode(hilti::Charset::ASCII)
&parse-from=self.filterBytes;
};
# So when you're done with recursively parsing the filters, we can now leverage the tree structure to
# recursively get the stringRepresentations for those leafs, which are SearchFilters
on %done {
self.stringRepresentation = string_representation(self);
}
on %error {
self.stringRepresentation = "FILTER_PARSING_ERROR";
}
};
public type SearchRequest = unit(inout message: Message) {
baseObject: ASN1::ASN1Message(True) &convert=$$.body.str_value {
message.obj = self.baseObject;
}
scope: ASN1::ASN1Message(True) &convert=cast<SearchScope>(cast<uint8>($$.body.num_value))
&default=SearchScope::Undef {
message.arg = "%s" % self.scope;
}
deref: ASN1::ASN1Message(True) &convert=cast<SearchDerefAlias>(cast<uint8>($$.body.num_value))
&default=SearchDerefAlias::Undef;
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 &convert=$$.stringRepresentation;
attributes: AttributeSelection &convert=$$.attributes;
};
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) {
#
# };

View file

@ -0,0 +1,14 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
module LDAP_Zeek;
import zeek;
import LDAP;
on LDAP::MessageWrapper::%done {
zeek::confirm_protocol();
}
on LDAP::MessageWrapper::%error {
zeek::reject_protocol("error while parsing LDAP message");
}

View file

@ -0,0 +1,502 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
module LDAP;
export {
redef enum Log::ID += { LDAP_LOG,
LDAP_SEARCH_LOG };
## Whether clear text passwords are captured or not.
option default_capture_password = F;
## Whether to log LDAP search attributes or not.
option default_log_search_attributes = F;
## Default logging policy hook for LDAP_LOG.
global log_policy: Log::PolicyHook;
## Default logging policy hook for LDAP_SEARCH_LOG.
global log_policy_search: Log::PolicyHook;
#############################################################################
# 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;
# a string representation of the search filter used in the query
filter: string &log &optional;
# a list of attributes that were returned in the search
attributes: 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", $policy=log_policy]);
Log::create_stream(LDAP::LDAP_SEARCH_LOG, [$columns=Search, $ev=log_ldap_search, $path="ldap_search", $policy=log_policy_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];
}
}
#############################################################################
@if (Version::at_least("5.2.0"))
event analyzer_confirmation_info(atype: AllAnalyzers::Tag, info: AnalyzerConfirmationInfo) {
if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) {
info$c$ldap_proto = "tcp";
}
}
@else @if (Version::at_least("4.2.0"))
event analyzer_confirmation(c: connection, atype: AllAnalyzers::Tag, aid: count) {
@else
event protocol_confirmation(c: connection, atype: Analyzer::Tag, aid: count) {
@endif
if ( atype == Analyzer::ANALYZER_SPICY_LDAP_TCP ) {
c$ldap_proto = "tcp";
}
}
@endif
#############################################################################
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_Undef ) {
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_Undef ) {
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();
if ("bind simple" in c$ldap_messages[message_id]$opcode && !default_capture_password)
c$ldap_messages[message_id]$argument += "REDACTED";
else
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 <method>" 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,
filter: string,
attributes: vector of string) {
set_session(c, message_id, LDAP::ProtocolOpcode_SEARCH_REQUEST);
if ( scope != LDAP::SearchScope_Undef ) {
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_Undef ) {
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;
}
c$ldap_searches[message_id]$filter = filter;
if ( default_log_search_attributes ) {
c$ldap_searches[message_id]$attributes = attributes;
}
}
#############################################################################
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 <method>" 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;
}
}

View file

@ -0,0 +1,78 @@
# Find the Spicy plugin to get access to the infrastructure it provides.
#
# While most of the actual CMake logic for building analyzers comes with the Spicy
# plugin for Zeek, this code bootstraps us by asking "spicyz" for the plugin's
# location. Either make sure that "spicyz" is in PATH, set the environment
# variable SPICYZ to point to its location, or set variable ZEEK_SPICY_ROOT
# in either CMake or environment to point to its installation or build
# directory.
#
# This exports:
#
# SPICY_PLUGIN_FOUND True if plugin and all dependencies were found
# SPICYZ Path to spicyz
# SPICY_PLUGIN_VERSION Version string of plugin
# SPICY_PLUGIN_VERSION_NUMBER Numerical version number of plugin
# Runs `spicyz` with the flags given as second argument and stores the output in the variable named
# by the first argument.
function (run_spicycz output)
execute_process(COMMAND "${SPICYZ}" ${ARGN} OUTPUT_VARIABLE output_
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(STRIP "${output_}" output_)
set(${output} "${output_}" PARENT_SCOPE)
endfunction ()
# Checks that the Spicy plugin version it at least the given version.
function (spicy_plugin_require_version version)
string(REGEX MATCH "([0-9]*)\.([0-9]*)\.([0-9]*).*" _ ${version})
math(EXPR version_number "${CMAKE_MATCH_1} * 10000 + ${CMAKE_MATCH_2} * 100 + ${CMAKE_MATCH_3}")
if ("${SPICY_PLUGIN_VERSION_NUMBER}" LESS "${version_number}")
message(FATAL_ERROR "Package requires at least Spicy plugin version ${version}, "
"have ${SPICY_PLUGIN_VERSION}")
endif ()
endfunction ()
###
### Main
###
if (NOT SPICYZ)
set(SPICYZ "$ENV{SPICYZ}")
endif ()
if (NOT SPICYZ)
# Support an in-tree Spicy build.
find_program(
spicyz spicyz
HINTS ${ZEEK_SPICY_ROOT}/bin ${ZEEK_SPICY_ROOT}/build/bin $ENV{ZEEK_SPICY_ROOT}/bin
$ENV{ZEEK_SPICY_ROOT}/build/bin ${PROJECT_SOURCE_DIR}/../../build/bin)
set(SPICYZ "${spicyz}")
endif ()
message(STATUS "spicyz: ${SPICYZ}")
if (SPICYZ)
set(SPICYZ "${SPICYZ}" CACHE PATH "" FORCE) # make sure it's in the cache
run_spicycz(SPICY_PLUGIN_VERSION "--version")
run_spicycz(SPICY_PLUGIN_VERSION_NUMBER "--version-number")
message(STATUS "Zeek plugin version: ${SPICY_PLUGIN_VERSION}")
run_spicycz(spicy_plugin_path "--print-plugin-path")
set(spicy_plugin_cmake_path "${spicy_plugin_path}/cmake")
message(STATUS "Zeek plugin CMake path: ${spicy_plugin_cmake_path}")
list(PREPEND CMAKE_MODULE_PATH "${spicy_plugin_cmake_path}")
find_package(Zeek REQUIRED)
find_package(Spicy REQUIRED)
zeek_print_summary()
spicy_print_summary()
include(ZeekSpicyAnalyzerSupport)
endif ()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(SpicyPlugin DEFAULT_MSG SPICYZ ZEEK_FOUND)

View file

@ -0,0 +1,13 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap-simpleauth.pcap %INPUT
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: btest-diff ldap.log
# @TEST-EXEC: btest-diff ldap_search.log
#
# @TEST-DOC: Test LDAP search attributes with small trace.
@load analyzer
redef LDAP::default_log_search_attributes = T;

View file

@ -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.

View file

@ -0,0 +1,12 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap-simpleauth.pcap %INPUT >output 2>&1
# @TEST-EXEC: btest-diff output
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @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 analyzer

View file

@ -0,0 +1,11 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap-simpleauth-diff-port.pcap %INPUT
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @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 analyzer

View file

@ -0,0 +1,131 @@
# @TEST-EXEC: spicyc -j -d -L ${DIST}/analyzer %INPUT
#
# @TEST-DOC: Validates helper functions in LDAP module.
module test;
import LDAP;
# ----------------------------------------------------------------------------------
# function utf16_guid_to_hex_repr()
# - requires exactly 16 bytes
# Not enough bytes (15)
assert LDAP::utf16_guid_to_hex_repr(b"1234567890ABCDE") == "GUID_FORMAT_FAILED";
# Too much bytes (17)
assert LDAP::utf16_guid_to_hex_repr(b"1234567890ABCDEFG") == "GUID_FORMAT_FAILED";
# Empty
assert LDAP::utf16_guid_to_hex_repr(b"") == "GUID_FORMAT_FAILED";
# 16 times \x00
assert LDAP::utf16_guid_to_hex_repr(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") == "00000000-0000-0000-0000-000000000000";
# 16 times \xff
assert LDAP::utf16_guid_to_hex_repr(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff") == "ffffffff-ffff-ffff-ffff-ffffffffffff";
# Valid DomainGuidFilter
assert LDAP::utf16_guid_to_hex_repr(b"\x3b\x52\xb3\xb0\x6f\x54\xaf\x4f\x93\xb2\x29\x4a\x38\x50\x98\xf2") == "b0b3523b-546f-4faf-93b2-294a385098f2";
# ----------------------------------------------------------------------------------
# function bytes_sid_to_hex_repr()
# - transforms bytes of arbitrary length to a ':' separated string
# Empty
assert LDAP::bytes_sid_to_hex_repr(b"") == "";
# 10 times \x00
assert LDAP::bytes_sid_to_hex_repr(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") == "00:00:00:00:00:00:00:00:00:00";
# 10 times \xff
assert LDAP::bytes_sid_to_hex_repr(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff") == "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff";
# Valid `AAC` value
assert LDAP::bytes_sid_to_hex_repr(b"\x80\x00\x00\x00") == "80:00:00:00";
# Valid objectSid
assert LDAP::bytes_sid_to_hex_repr(b"\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xd5\x64\xbe\x81\x5d\x68\x9c\x0d\x44\x4a\xae\x74\x01\x02\x00\x00") == "01:05:00:00:00:00:00:05:15:00:00:00:d5:64:be:81:5d:68:9c:0d:44:4a:ae:74:01:02:00:00";
# ----------------------------------------------------------------------------------
# function bytes_sid_to_SID_repr()
# - requires exactly 24 bytes
# Not enough bytes (0 and 10)
assert LDAP::bytes_sid_to_SID_repr(b"") == "SID_FORMAT_FAILED";
assert LDAP::bytes_sid_to_SID_repr(b"1234567890") == "SID_FORMAT_FAILED";
# Too much bytes (25)
assert LDAP::bytes_sid_to_SID_repr(b"1234567890123456789012345") == "SID_FORMAT_FAILED";
# Empty
assert LDAP::bytes_sid_to_SID_repr(b"") == "SID_FORMAT_FAILED";
# Valid SID
assert LDAP::bytes_sid_to_SID_repr(b"\x01\x04\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\x39\xc5\xc7\x44\xfa\xbd\x24\x1d\x4a\x65\xfd\x71") == "S-1-5-21-1153942841-488947194-1912431946";
# Some random bytes - probably an invalid SID but no error
assert LDAP::bytes_sid_to_SID_repr(b"\x02\x08\x00\x02\x00\x00\x00\x05\x15\x20\x00\x12\xd5\x64\xaf\x84\x5d\x68\x9c\x0d\x44\x4c\xad\x73") == "S-2-8589934597-301998101-2226087125-228354141-1940737092";
# All 1's
assert LDAP::bytes_sid_to_SID_repr(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff") == "S-255-281474976710655-4294967295-4294967295-4294967295-4294967295";
# ----------------------------------------------------------------------------------
# function uint32_to_hex_repr()
# Not enough bytes (0 and 2)
assert LDAP::uint32_to_hex_repr(b"") == "HEX_FORMAT_FAILED";
assert LDAP::uint32_to_hex_repr(b"12") == "HEX_FORMAT_FAILED";
# Too much bytes (6)
assert LDAP::uint32_to_hex_repr(b"123456") == "HEX_FORMAT_FAILED";
# Empty
assert LDAP::uint32_to_hex_repr(b"") == "HEX_FORMAT_FAILED";
# Valid `NtVer` value
assert LDAP::uint32_to_hex_repr(b"\x16\x00\x00\x00") == "0x00000016";
# 4 times \x00
assert LDAP::uint32_to_hex_repr(b"\x00\x00\x00\x00") == "0x00000000";
# 4 times \xff
assert LDAP::uint32_to_hex_repr(b"\xff\xff\xff\xff") == "0xffffffff";
# ----------------------------------------------------------------------------------
# function string_representation()
function make_nested_repr(filters: vector<string>): string {
local nestedOr: LDAP::ParseNestedAndOr;
nestedOr.searchfilters = vector<LDAP::SearchFilter>();
for (f in filters) {
local or_: LDAP::SearchFilter;
or_.filterType = LDAP::FilterType::FILTER_PRESENT;
or_.FILTER_PRESENT = f;
or_.stringRepresentation = LDAP::string_representation(or_);
nestedOr.searchfilters.push_back(or_);
}
local searchFilter: LDAP::SearchFilter;
searchFilter.filterType = LDAP::FilterType::FILTER_OR;
searchFilter.FILTER_OR = nestedOr;
return LDAP::string_representation(searchFilter);
}
function test_string_representation() {
local repr0 = make_nested_repr(vector());
assert repr0 == "": repr0;
local repr1 = make_nested_repr(vector("foo"));
assert repr1 == "(|(foo=*))": repr1;
local repr2 = make_nested_repr(vector("foo", "bar"));
assert repr2 == "(|(foo=*)(bar=*))": repr2;
local repr3 = make_nested_repr(vector("foo", "bar", "baz"));
assert repr3 == "(|(|(foo=*)(bar=*))(baz=*))": repr3;
}
test_string_representation();

View file

@ -0,0 +1,23 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap-simpleauth.pcap %INPUT >output 2>&1
# @TEST-EXEC: btest-diff output
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: ! test -f ldap.log
# @TEST-EXEC: ! test -f ldap_search.log
#
# @TEST-DOC: Test LDAP analyzer with small trace using logging policies.
@load analyzer
hook LDAP::log_policy(rec: LDAP::Message, id: Log::ID, filter: Log::Filter)
{
break;
}
hook LDAP::log_policy_search(rec: LDAP::Search, id: Log::ID,
filter: Log::Filter)
{
break;
}

View file

@ -0,0 +1,13 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap-krb5-sign-seal-01.pcap %INPUT
# @TEST-EXEC: cat conn.log | zeek-cut -Cn local_orig local_resp > conn.log2 && mv conn.log2 conn.log
# @TEST-EXEC: btest-diff conn.log
# @TEST-EXEC: btest-diff ldap.log
# @TEST-EXEC: btest-diff ldap_search.log
# @TEST-EXEC: ! test -f weird.log
# @TEST-EXEC: ! test -f dpd.log
#
# @TEST-DOC: Test LDAP analyzer with SASL encrypted payloads.
@load analyzer

View file

@ -0,0 +1,10 @@
# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.
# @TEST-DOC: This test case is a regression test for #23.
#
# @TEST-REQUIRES: have-spicy
# @TEST-EXEC: zeek -C -r ${TRACES}/ldap/issue-32.pcapng %INPUT
# @TEST-EXEC: cat ldap_search.log | zeek-cut -C uid filter base_object > ldap_search.log2 && mv ldap_search.log2 ldap_search.log
# @TEST-EXEC: btest-diff ldap_search.log
#
# @TEST-DOC: Test LDAP analyzer with small trace.

View file

@ -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 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 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 -

View file

@ -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 REDACTED
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 REDACTED

View file

@ -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 filter attributes
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] 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 - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) sAMAccountName

View file

@ -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 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 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 -

View file

@ -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 REDACTED
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 REDACTED

View file

@ -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 filter attributes
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] 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 - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) -

View file

@ -0,0 +1,2 @@
### 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.

View file

@ -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 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 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 32681 tcp spicy_ldap_tcp 181.520479 258 188 RSTO 0 ShADdR 8 590 4 360 -

View file

@ -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 32681 tcp 1 3 bind simple success - xxxxxxxxxxx@xx.xxx.xxxxx.net REDACTED
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp 3 3 bind simple success - CN=xxxxxxxx\x2cOU=Users\x2cOU=Accounts\x2cDC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net REDACTED

View file

@ -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 filter attributes
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
#close XXXX-XX-XX-XX-XX-XX
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 10.0.0.1 25936 10.0.0.2 32681 tcp 2 tree always DC=xx\x2cDC=xxx\x2cDC=xxxxx\x2cDC=net 1 success - (&(objectclass=*)(sAMAccountName=xxxxxxxx)) -

View file

@ -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 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 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 -

View file

@ -0,0 +1,2 @@
### 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.

View file

@ -0,0 +1,4 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
LDAP::Messages {
payload: test string
}

View file

@ -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 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 count string count count count count set[string]
#close XXXX-XX-XX-XX-XX-XX
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 tcp spicy_ldap_tcp 0.813275 1814 2391 S1 0 ShADd 6 2062 4 2559 -

View file

@ -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
#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 172.31.1.104 3116 172.31.1.101 389 tcp 215 3 bind SASL success - - GSS-SPNEGO

View file

@ -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 filter attributes
#types time string addr port addr port string int set[string] set[string] vector[string] count set[string] vector[string] string vector[string]
#close XXXX-XX-XX-XX-XX-XX
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 172.31.1.104 3116 172.31.1.101 389 tcp 213 base never - 1 success - (objectclass=*) -

View file

@ -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 uid filter base_object
#types string string vector[string]
#close XXXX-XX-XX-XX-XX-XX
CHhAvVGS1DHFjwGM9 (departmentNumber:2.16.840.1.113730.3.3.2.46.1:=>=N4709) DC=matrix\x2cDC=local

View file

@ -0,0 +1,34 @@
[btest]
MinVersion = 0.66
TestDirs = analyzer
TmpDir = %(testbase)s/.tmp
BaselineDir = %(testbase)s/baseline
IgnoreDirs = .svn CVS .tmp Baseline Failing traces Traces
IgnoreFiles = .DS_Store *.pcap data.* *.dat *.wmv *.der *.tmp *.swp .*.swp #* CMakeLists.txt
[environment]
DIST=%(testbase)s/..
PATH=%(testbase)s/../tests/scripts:`spicyz --print-plugin-path`/tests/scripts:%(default_path)s
SCRIPTS=`spicyz --print-plugin-path`/tests/Scripts
ZEEK_SPICY_MODULE_PATH=%(testbase)s/../build/spicy-modules
TEST_DIFF_CANONIFIER=`spicyz --print-plugin-path`/tests/Scripts/canonify-zeek-log-sorted
TRACES=%(testbase)s/traces
ZEEKPATH=%(testbase)s/..:`zeek-config --zeekpath`
ZEEK_SEED_FILE=`spicyz --print-plugin-path`/tests/random.seed
# Set variables to well-defined state.
LANG=C
LC_ALL=C
TZ=UTC
CC=
CXX=
CFLAGS=
CPPFLAGS=
CXXFLAGS=
LDFLAGS=
DYLDFLAGS=
[environment-installation]
ZEEK_SPICY_MODULE_PATH=
ZEEKPATH=`%(testbase)s/scripts/zeek-path-install`

View file

@ -0,0 +1,5 @@
#! /bin/sh
#
# Assembles the Zeek path for testing the installed version (can't do that in btest.cfg directly).
echo $(spicyz --print-scripts-path):$(zkg config script_dir):$(zeek-config --zeekpath)

View file

@ -0,0 +1,15 @@
The test suite comes with a set of traces collected from a variety of
places that we document below. While these traces are all coming from
public sources, please note that they may carry their own licenses.
We collect them here for convenience only.
- [ldap-simpleauth.pcap](https://github.com/arkime/arkime/blob/main/tests/pcap/ldap-simpleauth.pcap)
- ldap-simpleauth-diff-port.pcap: made with
`tcprewrite -r 3268:32681 -i ldap-simpleauth.pcap -o ldap-simpleauth-diff-port.pcap`
- ldap-krb5-sign-seal-01.pcap: trace is derived from
<https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/ldap-krb5-sign-seal-01.cap>
- the LDAP flow selected (filtered out the Kerberos packets)
- truncated to 10 packets (where packet 10 contains the SASL encrypted LDAP message)
- one `\x30` byte in the cyphertext changed to `\x00`
- ldap-issue-32.pcapng: Provided by GH user martinvanhensbergen,
<https://github.com/zeek/spicy-ldap/issues/23>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,21 @@
[package]
summary = LDAP analyzer
description = An LDAP analyzer based on Spicy
script_dir = analyzer
plugin_dir = build/spicy-modules
build_command = mkdir -p build && cd build && SPICYZ=$(command -v spicyz || echo %(package_base)s/spicy-plugin/build/bin/spicyz) cmake .. && cmake --build .
test_command = cd tests && PATH=$(zkg config plugin_dir)/packages/spicy-plugin/bin:$PATH btest -d -j $(nproc)
# We depend on spicy-plugin>=1.2.2, but currently cannot express that
# dependency due to zeek/package-manager#106 which has been fixed in
# zkg-2.12.0, but has not been released with zeek yet.
# depends = spicy-plugin >=1.2.2
[template]
source = package-template-spicy
version = master
zkg_version = 2.12.0
[template_vars]
namespace = LDAP
name = Messages

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ldap_search
#open XXXX-XX-XX-XX-XX-XX
#fields uid filter base_object
#types string string vector[string]
CHhAvVGS1DHFjwGM9 (departmentNumber:2.16.840.1.113730.3.3.2.46.1:=>=N4709) DC=matrix\x2cDC=local
#close XXXX-XX-XX-XX-XX-XX