From 8b914f47149cf2ee5cad615bde70af257654cb13 Mon Sep 17 00:00:00 2001 From: Evan Typanski Date: Tue, 10 Jun 2025 13:11:04 -0400 Subject: [PATCH 1/5] Add Redis analyzer array stringification This was going to be how "message" server data was handled, but that ended up being bad. Regardless, this is probably nice to have. --- src/analyzer/protocol/redis/redis.spicy | 49 +++++++++++++------ .../redis.log | 2 +- .../redis.log | 2 +- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/analyzer/protocol/redis/redis.spicy b/src/analyzer/protocol/redis/redis.spicy index a2f23d4c6d..945ae8c83d 100644 --- a/src/analyzer/protocol/redis/redis.spicy +++ b/src/analyzer/protocol/redis/redis.spicy @@ -383,21 +383,40 @@ function bulk_string_content(bulk: RESP::BulkString): bytes { return b""; } +# Returns the bytes string value of this, or Null if it cannot. +function stringify(data: RESP::Data): optional { + if (data?.simple_error) + return data.simple_error.content; + else if (data?.bulk_error) + return bulk_string_content(data.bulk_error); + else if (data?.simple_string) + return data.simple_string.content; + else if (data?.bulk_string) + return bulk_string_content(data.bulk_string); + else if (data?.verbatim_string) + return bulk_string_content(data.verbatim_string); + else if (data?.boolean) + return data.boolean.val ? b"T" : b"F"; + else if (data?.array) { + local res = b"["; + local first = True; + for (ele in data.array.elements) { + if (!first) + res += b", "; + local ele_stringified = stringify(ele); + if (!ele_stringified) + return Null; + res += *ele_stringified; + first = False; + } + res += b"]"; + return res; + } + + return Null; +} + # Gets the server reply in a simpler form public function make_server_reply(data: RESP::ServerData): ReplyData { - local res: ReplyData = [$value = Null]; - if (data.data?.simple_error) - res.value = data.data.simple_error.content; - else if (data.data?.bulk_error) - res.value = bulk_string_content(data.data.bulk_error); - else if (data.data?.simple_string) - res.value = data.data.simple_string.content; - else if (data.data?.bulk_string) - res.value = bulk_string_content(data.data.bulk_string); - else if (data.data?.verbatim_string) - res.value = bulk_string_content(data.data.verbatim_string); - else if (data.data?.boolean) - res.value = data.data.boolean.val ? b"T" : b"F"; - - return res; + return [$value = stringify(data.data)]; } diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log index 2f23f980ef..c9d2b81974 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log @@ -9,5 +9,5 @@ #types time string addr port addr port string string string bool string XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 56162 127.0.0.1 6379 SUBSCRIBE - - T - XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 56163 127.0.0.1 6379 PUBLISH - - T - -XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 56162 127.0.0.1 6379 - - - T - +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 56162 127.0.0.1 6379 - - - T [message, my_channel, hello :)] #close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.stream/redis.log b/testing/btest/Baseline/scripts.base.protocols.redis.stream/redis.log index d7ae07655a..47195e73af 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.stream/redis.log +++ b/testing/btest/Baseline/scripts.base.protocols.redis.stream/redis.log @@ -10,5 +10,5 @@ XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49992 127.0.0.1 6379 XADD - - T 1729622832637-0 XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49992 127.0.0.1 6379 XADD - - T 1729622836953-0 XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49992 127.0.0.1 6379 XADD - - T 1729622840530-0 -XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49992 127.0.0.1 6379 XRANGE - - T - +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49992 127.0.0.1 6379 XRANGE - - T [[1729622770972-0, [rider, Castilla, speed, 30.2, position, 1, location_id, 1]], [1729622778221-0, [rider, Norem, speed, 28.8, position, 3, location_id, 1]]] #close XXXX-XX-XX-XX-XX-XX From a4ce682bc991499450bd230bbff982aad97edc91 Mon Sep 17 00:00:00 2001 From: Evan Typanski Date: Tue, 10 Jun 2025 16:00:22 -0400 Subject: [PATCH 2/5] Handle Redis protocol `message` separately Closes #4504 Messages are not typical responses, so they need special handling. This is different between RESP2 and 3, so this is the first instance where the script layer needs to tell the difference. --- scripts/base/protocols/redis/main.zeek | 77 +++++++++++++++++- .../base/protocols/redis/spicy-events.zeek | 27 +++++- src/analyzer/protocol/redis/redis.spicy | 56 +++++++++++-- src/analyzer/protocol/redis/resp.evt | 9 +- src/analyzer/protocol/redis/resp.spicy | 5 ++ .../coverage.record-fields/out.default | 3 + .../output | 6 ++ .../output | 5 ++ .../redis.log | 10 ++- testing/btest/Traces/redis/pubsub-resp3.pcap | Bin 0 -> 417973 bytes testing/btest/Traces/redis/pubsub.pcap | Bin 1520 -> 4850 bytes .../base/protocols/redis/pubsub-resp3.zeek | 16 ++++ .../scripts/base/protocols/redis/pubsub.zeek | 10 ++- 13 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output create mode 100644 testing/btest/Traces/redis/pubsub-resp3.pcap create mode 100644 testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek diff --git a/scripts/base/protocols/redis/main.zeek b/scripts/base/protocols/redis/main.zeek index ab917df8c9..f214390e91 100644 --- a/scripts/base/protocols/redis/main.zeek +++ b/scripts/base/protocols/redis/main.zeek @@ -41,6 +41,11 @@ export { end: count &optional; }; + type RESPVersion: enum { + RESP2, + RESP3 + }; + type State: record { ## Pending commands. pending: table[count] of Info; @@ -52,14 +57,34 @@ export { ## Each range is one or two elements, one meaning it's unbounded, two meaning ## it begins at one and ends at the second. no_reply_ranges: vector of NoReplyRange; + ## The command indexes (from current_command and current_reply) that will + ## not get responses no matter what. + skip_commands: set[count]; ## We store if this analyzer had a violation to avoid logging if so. ## This should not be super necessary, but worth a shot. violation: bool &default=F; + ## If we are in "subscribed" mode + subscribed_mode: bool &default=F; + ## The RESP version + resp_version: RESPVersion &default=RESP2; }; # Redis specifically mentions 10k commands as a good pipelining threshold, so # we'll piggyback on that. option max_pending_commands = 10000; + + # These commands enter subscribed mode + global enter_subscribed_mode = [KnownCommand_PSUBSCRIBE, + KnownCommand_SSUBSCRIBE, KnownCommand_SUBSCRIBE]; + + # These commands exit subscribed mode + global exit_subscribed_mode = [KnownCommand_RESET, KnownCommand_QUIT]; + + # These commands don't expect a response (ever) - their replies are out of band. + global no_response_commands = [KnownCommand_PSUBSCRIBE, + KnownCommand_PUNSUBSCRIBE, KnownCommand_SSUBSCRIBE, + KnownCommand_SUBSCRIBE, KnownCommand_SUNSUBSCRIBE, + KnownCommand_UNSUBSCRIBE]; } redef record connection += { @@ -122,6 +147,15 @@ function is_last_interval_closed(c: connection): bool c$redis_state$no_reply_ranges[-1]?$end; } +event Redis::hello_command(c: connection, hello: HelloCommand) + { + if ( ! c?$redis_state ) + make_new_state(c); + + if ( hello?$requested_resp_version && hello$requested_resp_version == "3" ) + c$redis_state$resp_version = RESP3; + } + event Redis::command(c: connection, cmd: Command) { if ( ! c?$redis_state ) @@ -139,6 +173,26 @@ event Redis::command(c: connection, cmd: Command) } ++c$redis_state$current_command; + + if ( cmd?$known ) + { + if ( c$redis_state$resp_version == RESP2 ) + { + local should_enter = cmd$known in enter_subscribed_mode; + local should_exit = cmd$known in exit_subscribed_mode; + c$redis_state$subscribed_mode = should_enter && ! should_exit; + + # It's weird if it's in both - in the future users may be able to add that + if ( should_enter && should_exit ) + Reporter::conn_weird("Redis_command_enter_exit_subscribed_mode", c, cat( + cmd$known)); + } + if ( cmd$known in no_response_commands || c$redis_state$subscribed_mode ) + { + add c$redis_state$skip_commands[c$redis_state$current_command]; + } + } + # CLIENT commands can skip a number of replies and may be used with # pipelining. We need special logic in order to track the command/reply # pairs. @@ -177,6 +231,7 @@ event Redis::command(c: connection, cmd: Command) } } } + set_state(c, T); c$redis$cmd = cmd; @@ -187,17 +242,24 @@ event Redis::command(c: connection, cmd: Command) function reply_num(c: connection): count { local resp_num = c$redis_state$current_reply + 1; + local result = resp_num; for ( i in c$redis_state$no_reply_ranges ) { local range = c$redis_state$no_reply_ranges[i]; if ( ! range?$end && resp_num > range$begin ) { } # TODO: This is necessary if not using pipelining if ( range?$end && resp_num >= range$begin && resp_num < range$end ) - return range$end; + result = range$end; } - # Default: no disable/enable shenanigans - return resp_num; + # Account for commands that don't expect a response + while ( result in c$redis_state$skip_commands ) + { + delete c$redis_state$skip_commands[result]; + result += 1; + } + + return result; } # Logs up to and including the last seen command from the last reply @@ -234,6 +296,11 @@ event Redis::reply(c: connection, data: ReplyData) if ( ! c?$redis_state ) make_new_state(c); + if ( c$redis_state$subscribed_mode ) + { + event server_push(c, data); + return; + } local previous_reply_num = c$redis_state$current_reply; c$redis_state$current_reply = reply_num(c); set_state(c, F); @@ -241,6 +308,10 @@ event Redis::reply(c: connection, data: ReplyData) c$redis$reply = data; c$redis$success = T; log_from(c, previous_reply_num); + + # Tidy up the skip_commands when it's up to date + if ( c$redis_state$current_command == c$redis_state$current_reply ) + clear_table(c$redis_state$skip_commands); } event Redis::error(c: connection, data: ReplyData) diff --git a/scripts/base/protocols/redis/spicy-events.zeek b/scripts/base/protocols/redis/spicy-events.zeek index ad9a500f18..5eb9f6d6d6 100644 --- a/scripts/base/protocols/redis/spicy-events.zeek +++ b/scripts/base/protocols/redis/spicy-events.zeek @@ -37,6 +37,12 @@ export { password: string; }; + ## The Redis HELLO command (handshake). + type HelloCommand: record { + ## The sent requested RESP version, such as "2" or "3" + requested_resp_version: string &optional; + }; + ## A generic Redis command from the client. type Command: record { ## The raw command, exactly as parsed @@ -79,6 +85,13 @@ global get_command: event(c: connection, key: string); ## command: The AUTH command sent to the server and its data. global auth_command: event(c: connection, command: AuthCommand); +## Generated for Redis HELLO commands sent to the Redis server. +## +## c: The connection. +## +## command: The HELLO command sent to the server and its data. +global hello_command: event(c: connection, command: HelloCommand); + ## Generated for every command sent by the client to the Redis server. ## ## c: The connection. @@ -87,11 +100,15 @@ global auth_command: event(c: connection, command: AuthCommand); global command: event(c: connection, cmd: Command); ## Generated for every successful response sent by the Redis server to the -## client. +## client. For RESP2, this includes "push" messages, which are out of band. +## These will also raise a server_push event. RESP3 push messages will only +## raise a server_push event. ## ## c: The connection. ## ## data: The server data sent to the client. +## +## .. zeek:see:: Redis::server_push global reply: event(c: connection, data: ReplyData); ## Generated for every error response sent by the Redis server to the @@ -101,3 +118,11 @@ global reply: event(c: connection, data: ReplyData); ## ## data: The server data sent to the client. global error: event(c: connection, data: ReplyData); + +## Generated for out-of-band data, outside of the request-response +## model. +## +## c: The connection. +## +## data: The server data sent to the client. +global server_push: event(c: connection, data: ReplyData); diff --git a/src/analyzer/protocol/redis/redis.spicy b/src/analyzer/protocol/redis/redis.spicy index 945ae8c83d..777857f81d 100644 --- a/src/analyzer/protocol/redis/redis.spicy +++ b/src/analyzer/protocol/redis/redis.spicy @@ -34,6 +34,7 @@ public type KnownCommand = enum { GETRANGE, GETSET, HDEL, + HELLO, HGET, HSET, INCR, @@ -43,11 +44,19 @@ public type KnownCommand = enum { MOVE, MSET, PERSIST, + PSUBSCRIBE, + PUNSUBSCRIBE, + QUIT, RENAME, + RESET, SET, STRLEN, + SUBSCRIBE, + SSUBSCRIBE, + SUNSUBSCRIBE, TTL, TYPE, + UNSUBSCRIBE, }; type Command = struct { @@ -228,7 +237,6 @@ function parse_command(raw: vector): Command { function command_from(cmd_bytes: bytes): optional { local cmd: optional = Null; switch (cmd_bytes.lower()) { - case b"set": cmd = KnownCommand::SET; case b"append": cmd = KnownCommand::APPEND; case b"auth": cmd = KnownCommand::AUTH; case b"bitcount": cmd = KnownCommand::BITCOUNT; @@ -257,6 +265,7 @@ function command_from(cmd_bytes: bytes): optional { case b"getrange": cmd = KnownCommand::GETRANGE; case b"getset": cmd = KnownCommand::GETSET; case b"hdel": cmd = KnownCommand::HDEL; + case b"hello": cmd = KnownCommand::HELLO; case b"hget": cmd = KnownCommand::HGET; case b"hset": cmd = KnownCommand::HSET; case b"incr": cmd = KnownCommand::INCR; @@ -266,10 +275,19 @@ function command_from(cmd_bytes: bytes): optional { case b"move": cmd = KnownCommand::MOVE; case b"mset": cmd = KnownCommand::MSET; case b"persist": cmd = KnownCommand::PERSIST; + case b"psubscribe": cmd = KnownCommand::PSUBSCRIBE; + case b"punsubscribe": cmd = KnownCommand::PUNSUBSCRIBE; + case b"quit": cmd = KnownCommand::QUIT; case b"rename": cmd = KnownCommand::RENAME; + case b"reset": cmd = KnownCommand::RESET; + case b"set": cmd = KnownCommand::SET; case b"strlen": cmd = KnownCommand::STRLEN; + case b"ssubscribe": cmd = KnownCommand::SSUBSCRIBE; + case b"subscribe": cmd = KnownCommand::SUBSCRIBE; + case b"sunsubscribe": cmd = KnownCommand::SUNSUBSCRIBE; case b"ttl": cmd = KnownCommand::TTL; case b"type": cmd = KnownCommand::TYPE; + case b"unsubscribe": cmd = KnownCommand::UNSUBSCRIBE; default: cmd = Null; } @@ -368,12 +386,40 @@ public function is_auth(data: RESP::ClientData): bool { return data.command.known && *(data.command.known) == KnownCommand::AUTH && |data.command.raw| >= 2; } +type Hello = struct { + requested_resp_version: optional; +}; + +public function make_hello(command: Command): Hello { + local hi: Hello = [$requested_resp_version = Null]; + if (|command.raw| > 1) + hi.requested_resp_version = command.raw[1]; + return hi; +} + +public function is_hello(data: RESP::ClientData): bool { + return data.command.known && *(data.command.known) == KnownCommand::HELLO; +} + type ReplyData = struct { value: optional; }; -public function is_err(server_data: RESP::ServerData): bool { - return server_data.data?.simple_error || server_data.data?.bulk_error; +public type ReplyType = enum { + Reply, # A response to a command + Error, # An error response to a command + Push, # A server message that is not responding to a command +}; + +public function classify(server_data: RESP::ServerData): ReplyType { + if (server_data.data?.simple_error || server_data.data?.bulk_error) + return ReplyType::Error; + + # We can tell with RESP3 this is push here, but RESP2 relies on scripts + if (server_data.data?.push) + return ReplyType::Push; + + return ReplyType::Reply; } function bulk_string_content(bulk: RESP::BulkString): bytes { @@ -397,10 +443,10 @@ function stringify(data: RESP::Data): optional { return bulk_string_content(data.verbatim_string); else if (data?.boolean) return data.boolean.val ? b"T" : b"F"; - else if (data?.array) { + else if (data?.array || data?.push) { local res = b"["; local first = True; - for (ele in data.array.elements) { + for (ele in data?.array ? data.array.elements : data.push.elements) { if (!first) res += b", "; local ele_stringified = stringify(ele); diff --git a/src/analyzer/protocol/redis/resp.evt b/src/analyzer/protocol/redis/resp.evt index f67f8fbc1c..c84ab36084 100644 --- a/src/analyzer/protocol/redis/resp.evt +++ b/src/analyzer/protocol/redis/resp.evt @@ -12,9 +12,14 @@ export Redis::KnownCommand; on RESP::ClientData if ( Redis::is_set(self) ) -> event Redis::set_command($conn, Redis::make_set(self.command)); on RESP::ClientData if ( Redis::is_get(self) ) -> event Redis::get_command($conn, Redis::make_get(self.command).key); on RESP::ClientData if ( Redis::is_auth(self) ) -> event Redis::auth_command($conn, Redis::make_auth(self.command)); +on RESP::ClientData if ( Redis::is_hello(self) ) -> event Redis::hello_command($conn, Redis::make_hello(self.command)); # All client data is a command on RESP::ClientData -> event Redis::command($conn, self.command); -on RESP::ServerData if ( ! Redis::is_err(self) ) -> event Redis::reply($conn, Redis::make_server_reply(self)); -on RESP::ServerData if ( Redis::is_err(self) ) -> event Redis::error($conn, Redis::make_server_reply(self)); +on RESP::ServerData if ( Redis::classify(self) == Redis::ReplyType::Reply ) -> + event Redis::reply($conn, Redis::make_server_reply(self)); +on RESP::ServerData if ( Redis::classify(self) == Redis::ReplyType::Error ) -> + event Redis::error($conn, Redis::make_server_reply(self)); +on RESP::ServerData if ( Redis::classify(self) == Redis::ReplyType::Push ) -> + event Redis::server_push($conn, Redis::make_server_reply(self)); diff --git a/src/analyzer/protocol/redis/resp.spicy b/src/analyzer/protocol/redis/resp.spicy index 3216fd6d9b..2fb73277dc 100644 --- a/src/analyzer/protocol/redis/resp.spicy +++ b/src/analyzer/protocol/redis/resp.spicy @@ -79,6 +79,11 @@ public type ServerData = unit { %synchronize-after = b"\x0d\x0a"; var depth: uint8& = new uint8; data: Data(self.depth); + + var type_: Redis::ReplyType; + on %done { + self.type_ = Redis::classify(self); + } }; type Data = unit(depth: uint8&) { diff --git a/testing/btest/Baseline/coverage.record-fields/out.default b/testing/btest/Baseline/coverage.record-fields/out.default index 1be477339c..213a4bc1e0 100644 --- a/testing/btest/Baseline/coverage.record-fields/out.default +++ b/testing/btest/Baseline/coverage.record-fields/out.default @@ -615,6 +615,9 @@ connection { } * pending: table[count] of record Redis::Info, log=F, optional=F Redis::Info { ... } + * resp_version: enum Redis::RESPVersion, log=F, optional=T + * skip_commands: set[count], log=F, optional=F + * subscribed_mode: bool, log=F, optional=T * violation: bool, log=F, optional=T } * removal_hooks: set[func], log=F, optional=T diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output new file mode 100644 index 0000000000..60f704875f --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output @@ -0,0 +1,6 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Got published data!, [value=] +Got published data!, [value=] +Got published data!, [value=[message, Foo, Hi:)]] +Got published data!, [value=[pmessage, F*, Foo, Hi:)]] +Got published data!, [value=[pmessage, F*, Foobar, Hello!]] diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output index 49d861c74c..460df0d529 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output @@ -1 +1,6 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Got published data!, [value=] +Got published data!, [value=] +Got published data!, [value=[message, Foo, Hi there :)]] +Got published data!, [value=[pmessage, F*, Foo, Hi there :)]] +Got published data!, [value=[pmessage, F*, FeeFooFiiFum, Hello! :)]] diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log index c9d2b81974..11ed195257 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log @@ -7,7 +7,11 @@ #open XXXX-XX-XX-XX-XX-XX #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p cmd.name cmd.key cmd.value success reply.value #types time string addr port addr port string string string bool string -XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 56162 127.0.0.1 6379 SUBSCRIBE - - T - -XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 56163 127.0.0.1 6379 PUBLISH - - T - -XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 56162 127.0.0.1 6379 - - - T [message, my_channel, hello :)] +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 60833 127.0.0.1 6379 PUBLISH - - T - +XXXXXXXXXX.XXXXXX C4J4Th3PJpwUYZZ6gc 127.0.0.1 60837 127.0.0.1 6379 PUBLISH - - T - +XXXXXXXXXX.XXXXXX CtPZjS20MLrsMUOJi2 127.0.0.1 60838 127.0.0.1 6379 SET sanity_check you_are_sane T OK +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 60831 127.0.0.1 6379 SUBSCRIBE - - - - +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 60831 127.0.0.1 6379 PSUBSCRIBE - - - - +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 60831 127.0.0.1 6379 RESET - - T RESET +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 60831 127.0.0.1 6379 GET sanity_check - T you_are_sane #close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Traces/redis/pubsub-resp3.pcap b/testing/btest/Traces/redis/pubsub-resp3.pcap new file mode 100644 index 0000000000000000000000000000000000000000..de591be1ce1e033f5dcbf4fe4dd4bee07826a93e GIT binary patch literal 417973 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 F{|Ch7e>MOB literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/redis/pubsub.pcap b/testing/btest/Traces/redis/pubsub.pcap index 458070f948d89e2d4e528d39bd421720125154c1..d02630a7a49c53af4083ed1c72b91087da606b14 100644 GIT binary patch literal 4850 zcmb7|drXyO9LHY)&*0!O(H3XJwWCofP!rUtWb=e^)OkbkR^pJ$Q4#SFM~2R+FqF+^ znwS0%n}p>>HYF4%i=bLD|7Hyt{|*rMM+TF&dRhX zbK%M*eP>DE{hF_VgKQU-YMcwftX<7EeuT}MqE8oryJBosZn7Jjm<)F>!mVPsKQwEh z+{g`UyAVvyjx3Av4n(=6EWDizw^Eapha@-%w{Vz|qlQb_e0{u9HW9ybqjT6Okg!ap z3hAC?y5HC6Q@N451Dw%chA3}CmP-mdrEsk#+*)b(y>KFUH@-zV7UcIw#^QTJCj8nb zIM~7t4$W>{!mot@KxR{QU?xJSOg?^QIk@G=2KxO$!;Q?vmkxE2t1*8AGhn^KvmD%9 z9FQ+7LDWkQd33vHf+O8>>89ssPUQRzPAY2VlDW;4xgC5aH{oNq`xg9S^hueUG;K=i zWCyOeW9Z}@$Es!S0=FY^_)wdbTXgnLwMJEq6kF(3=N}JI#zT`!sv=1zd$Lhe)sVg*nR17norX-9w<3=COGmiJ?P(B*0sBaKGJm z^13Gms_HEKW~Fg%b=CEMo?qkKBJ-a|fthgIf9heyV{l_R3t!w~WHv^wJ4dd@v~&cn zw^ew?g8PJ-Z@@L>*^xsY-3FTANVg|+({nT@a)yBOE^6kIXE#p{g-^QAPVa*OQ@qdU z(>)8_Zj3`$PL8WE4@1$tVufdj7>V2|u6M@MRb#K%NpE}>91|mvr^3pob0m(Hw^8Hp zJ%>&|fwc;ibII!SW%XrR^@i@lNc>=p)_qBx?7sgQbMc&ax=_T6S9dVN;ev@=WJgR# zOPp4fP-ztJpxB4F2p0Lf;kkokyoO!>S?oi=_>9Q7GFFG`$gBg?a zwF!>w&@PLWRHp;HZf zqVI1BEidIMQYn%#XK6ntAi5d?)n()?-;MQ`|j0`n0 zeJ__;$kmvRhbG?Pg=a0e=UMNr2$35(mCB)Oiz9vi8e3i;JpMtFiW46nAvDrN@(Y zW;nDgubtMUbWhqe^+`KfQTW+TeAeWXHcfldo=X%Cu7-6DVVDu*)ZaASSE3f&#}Cn~ zoDWNi*7_MTIdQiFb7a65(N|ZP$(Cp>;LR;$krVP)FFEkp@={NZ*91qN(G{Fgv+5%` zqOQ#?b@1`M*I${0rs0x}Gf_6q@n_D6Z=GrUc|w%z^RRaB8_YTe&!_0oHtXzUbiB<< z?1+N?y*a*W=(TK=FAlDT(ki0K`4k){)VQP`o~BOzkK%-;huHbT#Wtu$U$>kp(b_<8 zVs&0uO?b(ctOhe1kDa?E+XPRPTUvDjUX^YA0{&>jz(H(#GBwWn?W|G#Q2~CuF^Qo? z^XaP}7B(yQ>A*fFuR%8I=T*Fy*QC|I^>JAH`t9Vjwf9q|B10~jBTih+@#r%-3Xa`= hZ_jr|j;d%JVMH&zA6QXFaS@RgG1BG7^igbm{4ai0Ge!Ua literal 1520 zcmb7^O=#0#7{}i>rAlg`+bLR64@Gd!vaP~q87@`_9e5B+Wr%cLYh9dO3&u^BxnEan}pbK)VNsheO$>X4>)I5^k~{F zd}!Ow=OnFK%`g1!=n;7D5rGd!g5l$*hk}D}+1(}Z6W2x(@@P7pObI+@4MSERWZgxEj(5820fEQdCy;CA z54|Ta6S5bv_hlQ+RmW>Hn2ntGhg_r|+;NycU?uFmV)!?4p!e&KnmO1hvb;WXC)ULx zcGNOA_JjYGZ=C7bH0)M1+vd?eeqo$rwtEJrY=Ogih(ZqqJmcDc(Zgyi z2Q2jY^}n1p;2hn`IryC;Cty!-*se%yR~{O>qRL{uaN*q~+(MS@y1o#CdmRIBY#isr z=gZp5y|?f?2LRA508FzzW?dDa^AX&cS5jkFt1;wMEx18f^4q(m=ioQB)UNBLPt9)r pOa2V{8E*U!3vTrHP62lX`meXMb3_jE(9L6k!`xQTt&w9m{R0q)i4*_; diff --git a/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek b/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek new file mode 100644 index 0000000000..59d324edbe --- /dev/null +++ b/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek @@ -0,0 +1,16 @@ +# @TEST-DOC: Test Zeek parsing pubsub commands in RESP3 +# @TEST-REQUIRES: have-spicy +# +# @TEST-EXEC: zeek -b -r $TRACES/redis/pubsub-resp3.pcap %INPUT >output +# @TEST-EXEC: btest-diff output + +# Test pub/sub from Redis. This has two subscribers, one using a pattern. Then, the +# messages that were published get printed to output. + +@load base/protocols/redis + +event Redis::server_push(c: connection, data: Redis::ReplyData) + { + # The first 2 are SUBSCRIBE replies, the other 3 are message and pmessage + print "Got published data!", data; + } diff --git a/testing/btest/scripts/base/protocols/redis/pubsub.zeek b/testing/btest/scripts/base/protocols/redis/pubsub.zeek index 15f6d9bb59..bd79325c86 100644 --- a/testing/btest/scripts/base/protocols/redis/pubsub.zeek +++ b/testing/btest/scripts/base/protocols/redis/pubsub.zeek @@ -5,8 +5,12 @@ # @TEST-EXEC: btest-diff output # @TEST-EXEC: btest-diff redis.log -# Testing the example of pub sub in REDIS docs: -# https://redis.io/docs/latest/develop/interact/pubsub/ -# These are just commands between two different clients, one PUBLISH and one SUBSCRIBE +# Test pub/sub from Redis. This has two subscribers, one using a pattern. Then, the +# messages that were published get printed to output. @load base/protocols/redis + +event Redis::server_push(c: connection, data: Redis::ReplyData) + { + print "Got published data!", data; + } From b34d3ff2f05d4bc320c0e9b89beab981175b19c7 Mon Sep 17 00:00:00 2001 From: Evan Typanski Date: Thu, 26 Jun 2025 09:56:17 -0400 Subject: [PATCH 3/5] Stringify all Redis-RESP serialized data --- src/analyzer/protocol/redis/redis.spicy | 76 ++++++++++++++----- src/analyzer/protocol/redis/resp.spicy | 5 +- .../output | 4 +- .../output | 4 +- .../redis.log | 4 +- 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/analyzer/protocol/redis/redis.spicy b/src/analyzer/protocol/redis/redis.spicy index 777857f81d..0f2c958e98 100644 --- a/src/analyzer/protocol/redis/redis.spicy +++ b/src/analyzer/protocol/redis/redis.spicy @@ -430,36 +430,78 @@ function bulk_string_content(bulk: RESP::BulkString): bytes { } # Returns the bytes string value of this, or Null if it cannot. -function stringify(data: RESP::Data): optional { - if (data?.simple_error) - return data.simple_error.content; - else if (data?.bulk_error) - return bulk_string_content(data.bulk_error); - else if (data?.simple_string) +function stringify(data: RESP::Data): bytes { + if (data?.simple_string) return data.simple_string.content; + else if (data?.simple_error) + return data.simple_error.content; + else if (data?.integer) + return data.integer.val; else if (data?.bulk_string) return bulk_string_content(data.bulk_string); - else if (data?.verbatim_string) - return bulk_string_content(data.verbatim_string); - else if (data?.boolean) - return data.boolean.val ? b"T" : b"F"; - else if (data?.array || data?.push) { + else if (data?.array) { local res = b"["; local first = True; - for (ele in data?.array ? data.array.elements : data.push.elements) { + for (ele in data.array.elements) { if (!first) res += b", "; - local ele_stringified = stringify(ele); - if (!ele_stringified) - return Null; - res += *ele_stringified; + res += stringify(ele); + first = False; + } + res += b"]"; + return res; + } else if (data?.null) + return b"null"; + else if (data?.boolean) + return data.boolean.val ? b"T" : b"F"; + else if (data?.double) + return data.double.val; + else if (data?.big_num) + return data.big_num.val; + else if (data?.bulk_error) + return bulk_string_content(data.bulk_error); + else if (data?.verbatim_string) + return bulk_string_content(data.verbatim_string); + else if (data?.map_) { + local res = b"{"; + local first = True; + local i = 0; + while (i < data.map_.num_elements) { + if (!first) + res += b", "; + res += stringify(data.map_.raw_data[i]); + res += b": "; + res += stringify(data.map_.raw_data[i + 1]); + i += 2; + first = False; + } + res += b"}"; + return res; + } else if (data?.set_) { + local res = b"("; + local first = True; + for (ele in data.set_.elements) { + if (!first) + res += b", "; + res += stringify(ele); + first = False; + } + res += b")"; + return res; + } else if (data?.push) { + local res = b"["; + local first = True; + for (ele in data.push.elements) { + if (!first) + res += b", "; + res += stringify(ele); first = False; } res += b"]"; return res; } - return Null; + throw "unknown RESP type"; } # Gets the server reply in a simpler form diff --git a/src/analyzer/protocol/redis/resp.spicy b/src/analyzer/protocol/redis/resp.spicy index 2fb73277dc..dfc7d990bb 100644 --- a/src/analyzer/protocol/redis/resp.spicy +++ b/src/analyzer/protocol/redis/resp.spicy @@ -149,7 +149,7 @@ type SimpleString = unit(is_error: bool) { }; type Integer = unit { - int: RedisBytes &convert=$$.to_int(10); + val: RedisBytes; }; type BulkString = unit(is_error: bool) { @@ -178,11 +178,10 @@ type Boolean = unit { }; type Double = unit { - val: RedisBytes &convert=$$.to_real(); + val: RedisBytes; }; type BigNum = unit { - # Big num can be very big so leave it in bytes. val: RedisBytes; }; diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output index 60f704875f..6458393b86 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output @@ -1,6 +1,6 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -Got published data!, [value=] -Got published data!, [value=] +Got published data!, [value=[subscribe, Foo, 1]] +Got published data!, [value=[psubscribe, F*, 2]] Got published data!, [value=[message, Foo, Hi:)]] Got published data!, [value=[pmessage, F*, Foo, Hi:)]] Got published data!, [value=[pmessage, F*, Foobar, Hello!]] diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output index 460df0d529..a086e7d7cc 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output @@ -1,6 +1,6 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -Got published data!, [value=] -Got published data!, [value=] +Got published data!, [value=[subscribe, Foo, 1]] +Got published data!, [value=[psubscribe, F*, 2]] Got published data!, [value=[message, Foo, Hi there :)]] Got published data!, [value=[pmessage, F*, Foo, Hi there :)]] Got published data!, [value=[pmessage, F*, FeeFooFiiFum, Hello! :)]] diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log index 11ed195257..e604657317 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/redis.log @@ -7,8 +7,8 @@ #open XXXX-XX-XX-XX-XX-XX #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p cmd.name cmd.key cmd.value success reply.value #types time string addr port addr port string string string bool string -XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 60833 127.0.0.1 6379 PUBLISH - - T - -XXXXXXXXXX.XXXXXX C4J4Th3PJpwUYZZ6gc 127.0.0.1 60837 127.0.0.1 6379 PUBLISH - - T - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 60833 127.0.0.1 6379 PUBLISH - - T 2 +XXXXXXXXXX.XXXXXX C4J4Th3PJpwUYZZ6gc 127.0.0.1 60837 127.0.0.1 6379 PUBLISH - - T 1 XXXXXXXXXX.XXXXXX CtPZjS20MLrsMUOJi2 127.0.0.1 60838 127.0.0.1 6379 SET sanity_check you_are_sane T OK XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 60831 127.0.0.1 6379 SUBSCRIBE - - - - XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 60831 127.0.0.1 6379 PSUBSCRIBE - - - - From 64443e5e5a933d2d16ab3d0a489a9afb3f8fb766 Mon Sep 17 00:00:00 2001 From: Evan Typanski Date: Thu, 26 Jun 2025 11:52:05 -0400 Subject: [PATCH 4/5] Handle more Redis RESP3 protocol pieces This passes the "minimum protocol version" along in the reply and adds support for attributes, which were added relatively recently. --- scripts/base/protocols/redis/main.zeek | 6 ++ .../base/protocols/redis/spicy-events.zeek | 7 +- src/analyzer/protocol/redis/redis.spicy | 60 +++++++++++++----- src/analyzer/protocol/redis/resp.spicy | 10 +++ .../coverage.record-fields/out.default | 4 +- .../output | 3 + .../redis.log | 12 ++++ .../output | 10 +-- .../output | 10 +-- testing/btest/Traces/redis/attr.pcap | Bin 0 -> 2314 bytes .../base/protocols/redis/attributes.zeek | 19 ++++++ .../base/protocols/redis/pubsub-resp3.zeek | 2 +- .../scripts/base/protocols/redis/pubsub.zeek | 2 +- 13 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 testing/btest/Baseline/scripts.base.protocols.redis.attributes/output create mode 100644 testing/btest/Baseline/scripts.base.protocols.redis.attributes/redis.log create mode 100644 testing/btest/Traces/redis/attr.pcap create mode 100644 testing/btest/scripts/base/protocols/redis/attributes.zeek diff --git a/scripts/base/protocols/redis/main.zeek b/scripts/base/protocols/redis/main.zeek index f214390e91..f14d49d74f 100644 --- a/scripts/base/protocols/redis/main.zeek +++ b/scripts/base/protocols/redis/main.zeek @@ -296,6 +296,12 @@ event Redis::reply(c: connection, data: ReplyData) if ( ! c?$redis_state ) make_new_state(c); + if ( data$min_protocol_version == 3 ) + { + c$redis_state$resp_version = RESP3; + c$redis_state$subscribed_mode = F; + } + if ( c$redis_state$subscribed_mode ) { event server_push(c, data); diff --git a/scripts/base/protocols/redis/spicy-events.zeek b/scripts/base/protocols/redis/spicy-events.zeek index 5eb9f6d6d6..eb950e1267 100644 --- a/scripts/base/protocols/redis/spicy-events.zeek +++ b/scripts/base/protocols/redis/spicy-events.zeek @@ -60,7 +60,12 @@ export { ## A generic Redis reply from the client. type ReplyData: record { - value: string &log &optional; + ## The RESP3 attributes applied to this, if any + attributes: string &optional; + ## The string version of the reply data + value: string &log; + ## The minimum RESP version that supports this reply type + min_protocol_version: count; }; } diff --git a/src/analyzer/protocol/redis/redis.spicy b/src/analyzer/protocol/redis/redis.spicy index 0f2c958e98..ec508a578f 100644 --- a/src/analyzer/protocol/redis/redis.spicy +++ b/src/analyzer/protocol/redis/redis.spicy @@ -402,7 +402,9 @@ public function is_hello(data: RESP::ClientData): bool { } type ReplyData = struct { - value: optional; + attributes: optional; + value: bytes; + min_protocol_version: uint8; }; public type ReplyType = enum { @@ -429,6 +431,25 @@ function bulk_string_content(bulk: RESP::BulkString): bytes { return b""; } +function stringify_map(data: RESP::Map): bytes { + local res = b"{"; + local first = True; + local i = 0; + # num_elements refers to the number of map entries, each with 2 entries + # in the raw data + while (i < data.num_elements) { + if (!first) + res += b", "; + res += stringify(data.raw_data[i * 2]); + res += b": "; + res += stringify(data.raw_data[(i * 2) + 1]); + i += 1; + first = False; + } + res += b"}"; + return res; +} + # Returns the bytes string value of this, or Null if it cannot. function stringify(data: RESP::Data): bytes { if (data?.simple_string) @@ -463,20 +484,7 @@ function stringify(data: RESP::Data): bytes { else if (data?.verbatim_string) return bulk_string_content(data.verbatim_string); else if (data?.map_) { - local res = b"{"; - local first = True; - local i = 0; - while (i < data.map_.num_elements) { - if (!first) - res += b", "; - res += stringify(data.map_.raw_data[i]); - res += b": "; - res += stringify(data.map_.raw_data[i + 1]); - i += 2; - first = False; - } - res += b"}"; - return res; + return stringify_map(data.map_); } else if (data?.set_) { local res = b"("; local first = True; @@ -506,5 +514,25 @@ function stringify(data: RESP::Data): bytes { # Gets the server reply in a simpler form public function make_server_reply(data: RESP::ServerData): ReplyData { - return [$value = stringify(data.data)]; + local min_protocol_version: uint8 = 2; + switch (data.data.ty) { + case RESP::DataType::NULL, + RESP::DataType::BOOLEAN, + RESP::DataType::DOUBLE, + RESP::DataType::BIG_NUM, + RESP::DataType::BULK_ERROR, + RESP::DataType::VERBATIM_STRING, + RESP::DataType::MAP, + RESP::DataType::SET, + RESP::DataType::PUSH: min_protocol_version = 3; + default: min_protocol_version = 2; + } + + local attributes: optional = Null; + if (data.data?.attributes) { + min_protocol_version = 3; + attributes = stringify_map(data.data.attributes); + } + + return [$attributes = attributes, $value = stringify(data.data), $min_protocol_version = min_protocol_version]; } diff --git a/src/analyzer/protocol/redis/resp.spicy b/src/analyzer/protocol/redis/resp.spicy index dfc7d990bb..6cced57ffd 100644 --- a/src/analyzer/protocol/redis/resp.spicy +++ b/src/analyzer/protocol/redis/resp.spicy @@ -89,6 +89,15 @@ public type ServerData = unit { type Data = unit(depth: uint8&) { %synchronize-after = b"\x0d\x0a"; ty: uint8 &convert=DataType($$); + + # Attributes are special, they precede the actual data + if (self.ty == DataType::ATTRIBUTE) { + attributes: Map(depth); + : uint8 &convert=DataType($$) { + self.ty = $$; + } + }; + switch (self.ty) { DataType::SIMPLE_STRING -> simple_string: SimpleString(False); DataType::SIMPLE_ERROR -> simple_error: SimpleString(True); @@ -135,6 +144,7 @@ type DataType = enum { BULK_ERROR = '!', VERBATIM_STRING = '=', MAP = '%', + ATTRIBUTE = '|', SET = '~', PUSH = '>', }; diff --git a/testing/btest/Baseline/coverage.record-fields/out.default b/testing/btest/Baseline/coverage.record-fields/out.default index 213a4bc1e0..2d7e20e22b 100644 --- a/testing/btest/Baseline/coverage.record-fields/out.default +++ b/testing/btest/Baseline/coverage.record-fields/out.default @@ -598,7 +598,9 @@ connection { conn_id { ... } * reply: record Redis::ReplyData, log=T, optional=T Redis::ReplyData { - * value: string, log=T, optional=T + * attributes: string, log=F, optional=T + * min_protocol_version: count, log=F, optional=F + * value: string, log=T, optional=F } * success: bool, log=T, optional=T * ts: time, log=T, optional=F diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.attributes/output b/testing/btest/Baseline/scripts.base.protocols.redis.attributes/output new file mode 100644 index 0000000000..deb4f9e7ab --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.redis.attributes/output @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Got data:, [1, 2, 3] +Got data:, [2039123, 9543892], with attributes:, {key-popularity: {a: 0.1923, b: 0.0012}} diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.attributes/redis.log b/testing/btest/Baseline/scripts.base.protocols.redis.attributes/redis.log new file mode 100644 index 0000000000..ef077bcd8f --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.redis.attributes/redis.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. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path redis +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p cmd.name cmd.key cmd.value success reply.value +#types time string addr port addr port string string string bool string +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 62283 127.0.0.1 6379 FAKE - - T [1, 2, 3] +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 62286 127.0.0.1 6379 FAKE2 - - T [2039123, 9543892] +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output index 6458393b86..230044f840 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub-resp3/output @@ -1,6 +1,6 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -Got published data!, [value=[subscribe, Foo, 1]] -Got published data!, [value=[psubscribe, F*, 2]] -Got published data!, [value=[message, Foo, Hi:)]] -Got published data!, [value=[pmessage, F*, Foo, Hi:)]] -Got published data!, [value=[pmessage, F*, Foobar, Hello!]] +Got published data!, [subscribe, Foo, 1] +Got published data!, [psubscribe, F*, 2] +Got published data!, [message, Foo, Hi:)] +Got published data!, [pmessage, F*, Foo, Hi:)] +Got published data!, [pmessage, F*, Foobar, Hello!] diff --git a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output index a086e7d7cc..d2a284b02f 100644 --- a/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output +++ b/testing/btest/Baseline/scripts.base.protocols.redis.pubsub/output @@ -1,6 +1,6 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -Got published data!, [value=[subscribe, Foo, 1]] -Got published data!, [value=[psubscribe, F*, 2]] -Got published data!, [value=[message, Foo, Hi there :)]] -Got published data!, [value=[pmessage, F*, Foo, Hi there :)]] -Got published data!, [value=[pmessage, F*, FeeFooFiiFum, Hello! :)]] +Got published data!, [subscribe, Foo, 1] +Got published data!, [psubscribe, F*, 2] +Got published data!, [message, Foo, Hi there :)] +Got published data!, [pmessage, F*, Foo, Hi there :)] +Got published data!, [pmessage, F*, FeeFooFiiFum, Hello! :)] diff --git a/testing/btest/Traces/redis/attr.pcap b/testing/btest/Traces/redis/attr.pcap new file mode 100644 index 0000000000000000000000000000000000000000..a94a53f72f71a06b3deb3abff18451a01fde4cfb GIT binary patch literal 2314 zcmb7^Pe>F|9LL|R%O+EUnFbS41g;Ub*6gmCyBdvuQ$Embqi`EO&e>9Up#p-bZWFadk^(CVV2S^*RH0 zXmVRB^=jG9y!YF@L|n-%(DGOXYS0j#Wuywv>3=+jz*Vkp)f4;HAdemrhj`S#T@E-Lw6r2XQ&=5W~!sqFKeRMrBTy6JhsE*X{sXZQzR>$fzm8g}Vwt+}PP+J9c zOj2!jpw=HwQ>9%=P*cNxjDV&N;(LEQ-i=wX+%V`%Q+cH(=Y1$bA4Yf|7VMSCIFcSv_BYdo!zHB+PuQ~hqQh)9`;D{6TqR+RvkKo7? z{RYm*V~Rara`A{b$(N{}D8D}w!ms(e`}#Y7&(`y0_DUbL!W@1i*=L^+w zmM>BKlA%_Z^5xNsH17s@8S^EkCkEdi`9lAv3_P=wJFe$T@s5d6T9b|wO_`bKX{`2Z zWfa^c%9ptoJ9D&aU^{Uoa~{l$`I7cr0yjqIREpP>@`X6WqyD{fz!7dSaJ{J?GhoaRbD zo+=m4_Luft?77_C*?T_Tuc^f-;6>bYYO2pT;130mG&DcW=Z4|OdI?52U<5;cS`CK| zl?5w8xW~;r&6!U?n2n)xtJFE)FS{A-P;output +# @TEST-EXEC: btest-diff output +# @TEST-EXEC: btest-diff redis.log + +# IMPORTANT: The test data was made synthetically, since real commands that +# return attributes may be version-specific. Real traffic would be better. + +@load base/protocols/redis + +event Redis::reply(c: connection, data: Redis::ReplyData) + { + if ( ! data?$attributes ) + print "Got data:", data$value; + else + print "Got data:", data$value, "with attributes:", data$attributes; + } diff --git a/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek b/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek index 59d324edbe..6d061b3d79 100644 --- a/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek +++ b/testing/btest/scripts/base/protocols/redis/pubsub-resp3.zeek @@ -12,5 +12,5 @@ event Redis::server_push(c: connection, data: Redis::ReplyData) { # The first 2 are SUBSCRIBE replies, the other 3 are message and pmessage - print "Got published data!", data; + print "Got published data!", data$value; } diff --git a/testing/btest/scripts/base/protocols/redis/pubsub.zeek b/testing/btest/scripts/base/protocols/redis/pubsub.zeek index bd79325c86..76ab16e806 100644 --- a/testing/btest/scripts/base/protocols/redis/pubsub.zeek +++ b/testing/btest/scripts/base/protocols/redis/pubsub.zeek @@ -12,5 +12,5 @@ event Redis::server_push(c: connection, data: Redis::ReplyData) { - print "Got published data!", data; + print "Got published data!", data$value; } From e7c798e526679e2728b40fa70fb0f72aac0fca6b Mon Sep 17 00:00:00 2001 From: Evan Typanski Date: Thu, 26 Jun 2025 14:08:28 -0400 Subject: [PATCH 5/5] Touchup TODOs in the Redis analyzer Also renames `KnownCommand` to `RedisCommand` to avoid conflicts. --- scripts/base/protocols/redis/main.zeek | 26 +- .../base/protocols/redis/spicy-events.zeek | 2 +- src/analyzer/protocol/redis/redis.spicy | 226 +++++++++--------- src/analyzer/protocol/redis/resp.evt | 2 +- src/analyzer/protocol/redis/resp.spicy | 13 +- .../coverage.record-fields/out.default | 2 +- 6 files changed, 131 insertions(+), 140 deletions(-) diff --git a/scripts/base/protocols/redis/main.zeek b/scripts/base/protocols/redis/main.zeek index f14d49d74f..07b6a6a61d 100644 --- a/scripts/base/protocols/redis/main.zeek +++ b/scripts/base/protocols/redis/main.zeek @@ -74,17 +74,17 @@ export { option max_pending_commands = 10000; # These commands enter subscribed mode - global enter_subscribed_mode = [KnownCommand_PSUBSCRIBE, - KnownCommand_SSUBSCRIBE, KnownCommand_SUBSCRIBE]; + global enter_subscribed_mode = [RedisCommand_PSUBSCRIBE, + RedisCommand_SSUBSCRIBE, RedisCommand_SUBSCRIBE]; # These commands exit subscribed mode - global exit_subscribed_mode = [KnownCommand_RESET, KnownCommand_QUIT]; + global exit_subscribed_mode = [RedisCommand_RESET, RedisCommand_QUIT]; # These commands don't expect a response (ever) - their replies are out of band. - global no_response_commands = [KnownCommand_PSUBSCRIBE, - KnownCommand_PUNSUBSCRIBE, KnownCommand_SSUBSCRIBE, - KnownCommand_SUBSCRIBE, KnownCommand_SUNSUBSCRIBE, - KnownCommand_UNSUBSCRIBE]; + global no_response_commands = [RedisCommand_PSUBSCRIBE, + RedisCommand_PUNSUBSCRIBE, RedisCommand_SSUBSCRIBE, + RedisCommand_SUBSCRIBE, RedisCommand_SUNSUBSCRIBE, + RedisCommand_UNSUBSCRIBE]; } redef record connection += { @@ -147,7 +147,7 @@ function is_last_interval_closed(c: connection): bool c$redis_state$no_reply_ranges[-1]?$end; } -event Redis::hello_command(c: connection, hello: HelloCommand) +event hello_command(c: connection, hello: HelloCommand) { if ( ! c?$redis_state ) make_new_state(c); @@ -156,7 +156,7 @@ event Redis::hello_command(c: connection, hello: HelloCommand) c$redis_state$resp_version = RESP3; } -event Redis::command(c: connection, cmd: Command) +event command(c: connection, cmd: Command) { if ( ! c?$redis_state ) make_new_state(c); @@ -196,7 +196,7 @@ event Redis::command(c: connection, cmd: Command) # CLIENT commands can skip a number of replies and may be used with # pipelining. We need special logic in order to track the command/reply # pairs. - if ( cmd?$known && cmd$known == KnownCommand_CLIENT ) + if ( cmd?$known && cmd$known == RedisCommand_CLIENT ) { # All 3 CLIENT commands we care about have 3 elements if ( |cmd$raw| == 3 ) @@ -291,11 +291,12 @@ function log_from(c: connection, previous_reply_num: count) } } -event Redis::reply(c: connection, data: ReplyData) +event reply(c: connection, data: ReplyData) { if ( ! c?$redis_state ) make_new_state(c); + # If the server is talking in RESP3, mark accordingly, even if we didn't see HELLO if ( data$min_protocol_version == 3 ) { c$redis_state$resp_version = RESP3; @@ -307,6 +308,7 @@ event Redis::reply(c: connection, data: ReplyData) event server_push(c, data); return; } + local previous_reply_num = c$redis_state$current_reply; c$redis_state$current_reply = reply_num(c); set_state(c, F); @@ -320,7 +322,7 @@ event Redis::reply(c: connection, data: ReplyData) clear_table(c$redis_state$skip_commands); } -event Redis::error(c: connection, data: ReplyData) +event error(c: connection, data: ReplyData) { if ( ! c?$redis_state ) make_new_state(c); diff --git a/scripts/base/protocols/redis/spicy-events.zeek b/scripts/base/protocols/redis/spicy-events.zeek index eb950e1267..8e0e6c733e 100644 --- a/scripts/base/protocols/redis/spicy-events.zeek +++ b/scripts/base/protocols/redis/spicy-events.zeek @@ -55,7 +55,7 @@ export { ## The value, if this command is known to have a value value: string &log &optional; ## The command in an enum if it was known - known: KnownCommand &optional; + known: RedisCommand &optional; }; ## A generic Redis reply from the client. diff --git a/src/analyzer/protocol/redis/redis.spicy b/src/analyzer/protocol/redis/redis.spicy index ec508a578f..c8dc9a8003 100644 --- a/src/analyzer/protocol/redis/redis.spicy +++ b/src/analyzer/protocol/redis/redis.spicy @@ -6,7 +6,7 @@ module Redis; import RESP; -public type KnownCommand = enum { +public type RedisCommand = enum { APPEND, AUTH, BITCOUNT, @@ -64,7 +64,7 @@ type Command = struct { name: bytes; key: optional; value: optional; - known: optional; + known: optional; }; # This just assumes all elements in the array is a bulk string and puts them in a vector @@ -158,44 +158,44 @@ function parse_command(raw: vector): Command { if (|raw| >= 2) { switch (*cmd) { - case KnownCommand::KEYS: + case RedisCommand::KEYS: parsed.key = raw[1]; - case KnownCommand::APPEND, - KnownCommand::BITCOUNT, - KnownCommand::BITFIELD, - KnownCommand::BITFIELD_RO, - KnownCommand::BITPOS, - KnownCommand::BLPOP, - KnownCommand::BRPOP, - KnownCommand::COPY, - KnownCommand::DECR, - KnownCommand::DECRBY, - KnownCommand::DEL, - KnownCommand::DUMP, - KnownCommand::EXISTS, - KnownCommand::EXPIRE, - KnownCommand::EXPIREAT, - KnownCommand::EXPIRETIME, - KnownCommand::GET, - KnownCommand::GETBIT, - KnownCommand::GETDEL, - KnownCommand::GETEX, - KnownCommand::GETRANGE, - KnownCommand::GETSET, - KnownCommand::HDEL, - KnownCommand::HGET, - KnownCommand::HSET, - KnownCommand::INCR, - KnownCommand::INCRBY, - KnownCommand::MGET, - KnownCommand::MOVE, - KnownCommand::MSET, - KnownCommand::PERSIST, - KnownCommand::RENAME, - KnownCommand::SET, - KnownCommand::STRLEN, - KnownCommand::TTL, - KnownCommand::TYPE: + case RedisCommand::APPEND, + RedisCommand::BITCOUNT, + RedisCommand::BITFIELD, + RedisCommand::BITFIELD_RO, + RedisCommand::BITPOS, + RedisCommand::BLPOP, + RedisCommand::BRPOP, + RedisCommand::COPY, + RedisCommand::DECR, + RedisCommand::DECRBY, + RedisCommand::DEL, + RedisCommand::DUMP, + RedisCommand::EXISTS, + RedisCommand::EXPIRE, + RedisCommand::EXPIREAT, + RedisCommand::EXPIRETIME, + RedisCommand::GET, + RedisCommand::GETBIT, + RedisCommand::GETDEL, + RedisCommand::GETEX, + RedisCommand::GETRANGE, + RedisCommand::GETSET, + RedisCommand::HDEL, + RedisCommand::HGET, + RedisCommand::HSET, + RedisCommand::INCR, + RedisCommand::INCRBY, + RedisCommand::MGET, + RedisCommand::MOVE, + RedisCommand::MSET, + RedisCommand::PERSIST, + RedisCommand::RENAME, + RedisCommand::SET, + RedisCommand::STRLEN, + RedisCommand::TTL, + RedisCommand::TYPE: parsed.key = raw[1]; default: (); } @@ -203,22 +203,22 @@ function parse_command(raw: vector): Command { if (|raw| >= 3) { switch (*cmd) { - case KnownCommand::SET, - KnownCommand::APPEND, - KnownCommand::DECRBY, - KnownCommand::EXPIRE, - KnownCommand::EXPIREAT, - KnownCommand::GETBIT, - KnownCommand::GETSET, - KnownCommand::HDEL, - KnownCommand::HGET, - KnownCommand::INCRBY, - KnownCommand::MOVE, - KnownCommand::MSET, - KnownCommand::RENAME: + case RedisCommand::SET, + RedisCommand::APPEND, + RedisCommand::DECRBY, + RedisCommand::EXPIRE, + RedisCommand::EXPIREAT, + RedisCommand::GETBIT, + RedisCommand::GETSET, + RedisCommand::HDEL, + RedisCommand::HGET, + RedisCommand::INCRBY, + RedisCommand::MOVE, + RedisCommand::MSET, + RedisCommand::RENAME: parsed.value = raw[2]; # Op first, destination second, then a list of keys. Just log dest - case KnownCommand::BITOP: parsed.key = raw[2]; + case RedisCommand::BITOP: parsed.key = raw[2]; default: (); } } @@ -226,7 +226,7 @@ function parse_command(raw: vector): Command { if (|raw| >= 4) { switch (*cmd) { # timeout, numkeys, then key - case KnownCommand::BLMPOP: parsed.key = raw[3]; + case RedisCommand::BLMPOP: parsed.key = raw[3]; default: (); } } @@ -234,60 +234,60 @@ function parse_command(raw: vector): Command { return parsed; } -function command_from(cmd_bytes: bytes): optional { - local cmd: optional = Null; +function command_from(cmd_bytes: bytes): optional { + local cmd: optional = Null; switch (cmd_bytes.lower()) { - case b"append": cmd = KnownCommand::APPEND; - case b"auth": cmd = KnownCommand::AUTH; - case b"bitcount": cmd = KnownCommand::BITCOUNT; - case b"bitfield": cmd = KnownCommand::BITFIELD; - case b"bitfield_ro": cmd = KnownCommand::BITFIELD_RO; - case b"bitop": cmd = KnownCommand::BITOP; - case b"bitpos": cmd = KnownCommand::BITPOS; - case b"blmpop": cmd = KnownCommand::BLMPOP; - case b"blpop": cmd = KnownCommand::BLPOP; - case b"brpop": cmd = KnownCommand::BRPOP; - case b"client": cmd = KnownCommand::CLIENT; - case b"copy": cmd = KnownCommand::COPY; - case b"decr": cmd = KnownCommand::DECR; - case b"decrby": cmd = KnownCommand::DECRBY; - case b"del": cmd = KnownCommand::DEL; - case b"dump": cmd = KnownCommand::DUMP; - case b"exists": cmd = KnownCommand::EXISTS; - case b"expire": cmd = KnownCommand::EXPIRE; - case b"expireat": cmd = KnownCommand::EXPIREAT; - case b"expiretime": cmd = KnownCommand::EXPIRETIME; - case b"expiretime": cmd = KnownCommand::EXPIRETIME; - case b"get": cmd = KnownCommand::GET; - case b"getbit": cmd = KnownCommand::GETBIT; - case b"getdel": cmd = KnownCommand::GETDEL; - case b"getex": cmd = KnownCommand::GETEX; - case b"getrange": cmd = KnownCommand::GETRANGE; - case b"getset": cmd = KnownCommand::GETSET; - case b"hdel": cmd = KnownCommand::HDEL; - case b"hello": cmd = KnownCommand::HELLO; - case b"hget": cmd = KnownCommand::HGET; - case b"hset": cmd = KnownCommand::HSET; - case b"incr": cmd = KnownCommand::INCR; - case b"incrby": cmd = KnownCommand::INCRBY; - case b"keys": cmd = KnownCommand::KEYS; - case b"mget": cmd = KnownCommand::MGET; - case b"move": cmd = KnownCommand::MOVE; - case b"mset": cmd = KnownCommand::MSET; - case b"persist": cmd = KnownCommand::PERSIST; - case b"psubscribe": cmd = KnownCommand::PSUBSCRIBE; - case b"punsubscribe": cmd = KnownCommand::PUNSUBSCRIBE; - case b"quit": cmd = KnownCommand::QUIT; - case b"rename": cmd = KnownCommand::RENAME; - case b"reset": cmd = KnownCommand::RESET; - case b"set": cmd = KnownCommand::SET; - case b"strlen": cmd = KnownCommand::STRLEN; - case b"ssubscribe": cmd = KnownCommand::SSUBSCRIBE; - case b"subscribe": cmd = KnownCommand::SUBSCRIBE; - case b"sunsubscribe": cmd = KnownCommand::SUNSUBSCRIBE; - case b"ttl": cmd = KnownCommand::TTL; - case b"type": cmd = KnownCommand::TYPE; - case b"unsubscribe": cmd = KnownCommand::UNSUBSCRIBE; + case b"append": cmd = RedisCommand::APPEND; + case b"auth": cmd = RedisCommand::AUTH; + case b"bitcount": cmd = RedisCommand::BITCOUNT; + case b"bitfield": cmd = RedisCommand::BITFIELD; + case b"bitfield_ro": cmd = RedisCommand::BITFIELD_RO; + case b"bitop": cmd = RedisCommand::BITOP; + case b"bitpos": cmd = RedisCommand::BITPOS; + case b"blmpop": cmd = RedisCommand::BLMPOP; + case b"blpop": cmd = RedisCommand::BLPOP; + case b"brpop": cmd = RedisCommand::BRPOP; + case b"client": cmd = RedisCommand::CLIENT; + case b"copy": cmd = RedisCommand::COPY; + case b"decr": cmd = RedisCommand::DECR; + case b"decrby": cmd = RedisCommand::DECRBY; + case b"del": cmd = RedisCommand::DEL; + case b"dump": cmd = RedisCommand::DUMP; + case b"exists": cmd = RedisCommand::EXISTS; + case b"expire": cmd = RedisCommand::EXPIRE; + case b"expireat": cmd = RedisCommand::EXPIREAT; + case b"expiretime": cmd = RedisCommand::EXPIRETIME; + case b"expiretime": cmd = RedisCommand::EXPIRETIME; + case b"get": cmd = RedisCommand::GET; + case b"getbit": cmd = RedisCommand::GETBIT; + case b"getdel": cmd = RedisCommand::GETDEL; + case b"getex": cmd = RedisCommand::GETEX; + case b"getrange": cmd = RedisCommand::GETRANGE; + case b"getset": cmd = RedisCommand::GETSET; + case b"hdel": cmd = RedisCommand::HDEL; + case b"hello": cmd = RedisCommand::HELLO; + case b"hget": cmd = RedisCommand::HGET; + case b"hset": cmd = RedisCommand::HSET; + case b"incr": cmd = RedisCommand::INCR; + case b"incrby": cmd = RedisCommand::INCRBY; + case b"keys": cmd = RedisCommand::KEYS; + case b"mget": cmd = RedisCommand::MGET; + case b"move": cmd = RedisCommand::MOVE; + case b"mset": cmd = RedisCommand::MSET; + case b"persist": cmd = RedisCommand::PERSIST; + case b"psubscribe": cmd = RedisCommand::PSUBSCRIBE; + case b"punsubscribe": cmd = RedisCommand::PUNSUBSCRIBE; + case b"quit": cmd = RedisCommand::QUIT; + case b"rename": cmd = RedisCommand::RENAME; + case b"reset": cmd = RedisCommand::RESET; + case b"set": cmd = RedisCommand::SET; + case b"strlen": cmd = RedisCommand::STRLEN; + case b"ssubscribe": cmd = RedisCommand::SSUBSCRIBE; + case b"subscribe": cmd = RedisCommand::SUBSCRIBE; + case b"sunsubscribe": cmd = RedisCommand::SUNSUBSCRIBE; + case b"ttl": cmd = RedisCommand::TTL; + case b"type": cmd = RedisCommand::TYPE; + case b"unsubscribe": cmd = RedisCommand::UNSUBSCRIBE; default: cmd = Null; } @@ -352,7 +352,7 @@ public function make_set(command: Command): Set { } public function is_set(data: RESP::ClientData): bool { - return data.command.known && *(data.command.known) == KnownCommand::SET && data.command.key && data.command.value; + return data.command.known && *(data.command.known) == RedisCommand::SET && data.command.key && data.command.value; } type Get = struct { @@ -365,7 +365,7 @@ public function make_get(command: Command): Get { } public function is_get(data: RESP::ClientData): bool { - return data.command.known && *(data.command.known) == KnownCommand::GET && |data.command.raw| >= 2; + return data.command.known && *(data.command.known) == RedisCommand::GET && |data.command.raw| >= 2; } type Auth = struct { @@ -383,7 +383,7 @@ public function make_auth(command: Command): Auth { } public function is_auth(data: RESP::ClientData): bool { - return data.command.known && *(data.command.known) == KnownCommand::AUTH && |data.command.raw| >= 2; + return data.command.known && *(data.command.known) == RedisCommand::AUTH && |data.command.raw| >= 2; } type Hello = struct { @@ -398,7 +398,7 @@ public function make_hello(command: Command): Hello { } public function is_hello(data: RESP::ClientData): bool { - return data.command.known && *(data.command.known) == KnownCommand::HELLO; + return data.command.known && *(data.command.known) == RedisCommand::HELLO; } type ReplyData = struct { @@ -431,7 +431,7 @@ function bulk_string_content(bulk: RESP::BulkString): bytes { return b""; } -function stringify_map(data: RESP::Map): bytes { +function stringify_map(data: RESP::Map&): bytes { local res = b"{"; local first = True; local i = 0; @@ -451,7 +451,7 @@ function stringify_map(data: RESP::Map): bytes { } # Returns the bytes string value of this, or Null if it cannot. -function stringify(data: RESP::Data): bytes { +function stringify(data: RESP::Data&): bytes { if (data?.simple_string) return data.simple_string.content; else if (data?.simple_error) diff --git a/src/analyzer/protocol/redis/resp.evt b/src/analyzer/protocol/redis/resp.evt index c84ab36084..c197ecd4f4 100644 --- a/src/analyzer/protocol/redis/resp.evt +++ b/src/analyzer/protocol/redis/resp.evt @@ -7,7 +7,7 @@ protocol analyzer Redis over TCP: import RESP; import Redis; -export Redis::KnownCommand; +export Redis::RedisCommand; on RESP::ClientData if ( Redis::is_set(self) ) -> event Redis::set_command($conn, Redis::make_set(self.command)); on RESP::ClientData if ( Redis::is_get(self) ) -> event Redis::get_command($conn, Redis::make_get(self.command).key); diff --git a/src/analyzer/protocol/redis/resp.spicy b/src/analyzer/protocol/redis/resp.spicy index 6cced57ffd..76899dca36 100644 --- a/src/analyzer/protocol/redis/resp.spicy +++ b/src/analyzer/protocol/redis/resp.spicy @@ -116,7 +116,7 @@ type Data = unit(depth: uint8&) { DataType::MAP -> map_: Map(depth); DataType::SET -> set_: Set(depth); # "Push events are encoded similarly to arrays, differing only in their - # first byte" - TODO: can probably make it more obvious, though + # first byte" DataType::PUSH -> push: Array(depth); }; @@ -197,22 +197,11 @@ type BigNum = unit { type Map = unit(depth: uint8&) { num_elements: RedisBytes &convert=$$.to_uint(10); - # TODO: How can I make this into a map? Alternatively, how can I do this better? raw_data: Data(depth)[self.num_elements * 2]; - - # TODO: This is broken. See https://github.com/zeek/spicy/issues/2061 - # var key_val_pairs: vector>; - # on raw_data { - # while (local i = 0; i < self.num_elements) { - # self.key_val_pairs.push_back(($$[i], $$[i + 1])); - # i += 2; - # } - # } }; type Set = unit(depth: uint8&) { num_elements: RedisBytes &convert=$$.to_uint(10) &requires=self.num_elements <= MAX_SIZE; - # TODO: This should be a set but doesn't go in the backed C++ set elements: Data(depth)[self.num_elements]; }; diff --git a/testing/btest/Baseline/coverage.record-fields/out.default b/testing/btest/Baseline/coverage.record-fields/out.default index 2d7e20e22b..429564ff96 100644 --- a/testing/btest/Baseline/coverage.record-fields/out.default +++ b/testing/btest/Baseline/coverage.record-fields/out.default @@ -589,7 +589,7 @@ connection { * cmd: record Redis::Command, log=T, optional=F Redis::Command { * key: string, log=T, optional=T - * known: enum Redis::KnownCommand, log=F, optional=T + * known: enum Redis::RedisCommand, log=F, optional=T * name: string, log=T, optional=F * raw: vector of string, log=F, optional=F * value: string, log=T, optional=T