spicy-redis: Add some commands and touch up parsing

This commit is contained in:
Evan Typanski 2024-11-06 13:43:44 -05:00
parent 22bda56af3
commit f0e9f46c7c
21 changed files with 200 additions and 114 deletions

View file

@ -26,6 +26,11 @@ export {
key: string &log;
};
type AuthCommand: record {
username: string &optional;
password: string;
};
type Command: record {
## The raw command, exactly as parsed
raw: vector of string;
@ -71,15 +76,18 @@ export {
type State: record {
## Pending requests.
pending: table[count] of Info;
pending: table[count] of Info;
## Current request in the pending queue.
current_request: count &default=0;
current_request: count &default=0;
## Current response in the pending queue.
current_response: count &default=0;
## Ranges where we do not expect a response
## Each range is one or two elements, one meaning it's unbounded, two meaning
## it begins at one and ends at the second.
no_response_ranges: vector of vector of 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;
};
# Redis specifically mentions 10k commands as a good pipelining threshold, so
@ -102,6 +110,21 @@ event zeek_init() &priority=5
Analyzer::register_for_ports(Analyzer::ANALYZER_SPICY_REDIS, ports);
}
event analyzer_violation_info(atype: AllAnalyzers::Tag,
info: AnalyzerViolationInfo)
{
if ( atype == Analyzer::ANALYZER_SPICY_REDIS )
{
if ( info?$c )
{
if ( info$c?$redis_state )
{
info$c$redis_state$violation = T;
}
}
}
}
function new_redis_session(c: connection): Info
{
return Info($ts=network_time(), $uid=c$uid, $id=c$id);
@ -116,11 +139,14 @@ function make_new_state(c: connection)
function set_state(c: connection, is_orig: bool)
{
if ( ! c?$redis_state ) make_new_state(c);
if ( ! c?$redis_state )
make_new_state(c);
local current: count;
if ( is_orig ) current = c$redis_state$current_request;
else current = c$redis_state$current_response;
if ( is_orig )
current = c$redis_state$current_request;
else
current = c$redis_state$current_response;
if ( current !in c$redis_state$pending )
c$redis_state$pending[current] = new_redis_session(c);
@ -131,28 +157,24 @@ function set_state(c: connection, is_orig: bool)
# Returns true if the last interval exists and is closed
function is_last_interval_closed(c: connection): bool
{
return |c$redis_state$no_response_ranges| == 0 || |c$redis_state$no_response_ranges[|c$redis_state$no_response_ranges| - 1]| != 1;
return |c$redis_state$no_response_ranges| == 0
|| |c$redis_state$no_response_ranges[|c$redis_state$no_response_ranges| - 1]| != 1;
}
event Redis::command(c: connection, is_orig: bool, command: Command)
{
if ( ! c?$redis_state ) make_new_state(c);
if ( ! c?$redis_state )
make_new_state(c);
if ( max_pending_requests > 0 && |c$redis_state$pending| > max_pending_requests )
if ( max_pending_requests > 0
&& |c$redis_state$pending| > max_pending_requests )
{
Reporter::conn_weird("Redis_excessive_pipelining", c);
# Just spit out what we have
while ( c$redis_state$current_response < c$redis_state$current_request )
{
local cr = c$redis_state$current_response;
if ( cr in c$redis_state$pending )
{
Log::write(Redis::LOG, c$redis_state$pending[cr]);
delete c$redis_state$pending[cr];
}
++c$redis_state$current_response;
}
# Delete the current state and restart later. We'll be in a weird state, but
# really we want to abort. I don't quite get how to register this as a
# violation. :)
delete c$redis_state;
return;
}
++c$redis_state$current_request;
@ -164,9 +186,10 @@ event Redis::command(c: connection, is_orig: bool, command: Command)
if ( to_lower(command$raw[2]) == "on" )
{
# If the last range is open, close it here. Otherwise, noop
if ( |c$redis_state$no_response_ranges| > 0 )
if ( |c$redis_state$no_response_ranges| > 0 )
{
local range = c$redis_state$no_response_ranges[|c$redis_state$no_response_ranges| - 1];
local range = c$redis_state$no_response_ranges[|c$redis_state$no_response_ranges|
- 1];
if ( |range| == 1 )
{
range += c$redis_state$current_request;
@ -176,16 +199,17 @@ event Redis::command(c: connection, is_orig: bool, command: Command)
if ( to_lower(command$raw[2]) == "off" )
{
# Only add a new interval if the last one is closed
if ( is_last_interval_closed(c) )
if ( is_last_interval_closed(c) )
{
c$redis_state$no_response_ranges += vector(c$redis_state$current_request);
}
}
if ( to_lower(command$raw[2]) == "skip" )
{
if ( is_last_interval_closed(c) )
if ( is_last_interval_closed(c) )
# It skips this one and the next one
c$redis_state$no_response_ranges += vector(c$redis_state$current_request, c$redis_state$current_request + 2);
c$redis_state$no_response_ranges += vector(c$redis_state$current_request,
c$redis_state$current_request + 2);
}
}
}
@ -202,10 +226,10 @@ function response_num(c: connection): count
for ( i in c$redis_state$no_response_ranges )
{
local range = c$redis_state$no_response_ranges[i];
assert |range| >= 1;
assert | range | >= 1;
if ( |range| == 1 && resp_num > range[0] )
{} # TODO: This is necessary if not using pipelining
if ( |range| == 2 && resp_num >= range[0] && resp_num < range[1] )
{ } # TODO: This is necessary if not using pipelining
if ( |range| == 2 && resp_num >= range[0] && resp_num < range[1] )
return range[1];
}
@ -215,7 +239,8 @@ function response_num(c: connection): count
event Redis::server_data(c: connection, is_orig: bool, data: ServerData)
{
if ( ! c?$redis_state ) make_new_state(c);
if ( ! c?$redis_state )
make_new_state(c);
local previous_response_num = c$redis_state$current_response;
c$redis_state$current_response = response_num(c);
@ -232,27 +257,37 @@ event Redis::server_data(c: connection, is_orig: bool, data: ServerData)
next;
}
if ( previous_response_num in c$redis_state$pending )
if ( previous_response_num in c$redis_state$pending &&
c$redis_state$pending[previous_response_num]?$cmd )
{
Log::write(Redis::LOG, c$redis_state$pending[previous_response_num]);
delete c$redis_state$pending[previous_response_num];
}
previous_response_num += 1;
}
# Log this one
Log::write(Redis::LOG, c$redis);
delete c$redis_state$pending[c$redis_state$current_response];
# Log this one if we have the request and response
if ( c$redis?$cmd )
{
Log::write(Redis::LOG, c$redis);
delete c$redis_state$pending[c$redis_state$current_response];
}
}
hook finalize_redis(c: connection)
{
if ( c$redis_state$violation )
{
# If there's a violation, make sure everything gets deleted
delete c$redis_state;
}
# Flush all pending but incomplete request/response pairs.
if ( c?$redis_state )
if ( c?$redis_state && c$redis_state$current_response != 0 )
{
for ( r, info in c$redis_state$pending )
{
# We don't use pending elements at index 0.
if ( r == 0 ) next;
if ( r == 0 )
next;
Log::write(Redis::LOG, info);
}
}