Skip to content

Commit 0f3d46b

Browse files
authored
feat(obfuscation/redis): Reach feature parity on redis obfuscation [APMSP-2668] (#1632)
# What does this PR do? - make remove_all_reids_args public (used in testing) - fix HSET with multiple key/values obfuscation - port quantize_redis_string # Motivation Making libdatadog’s obfuscation feature equivalent to the Agent’s implementation. # Additional Notes # How to test the change? Co-authored-by: oscar.ledauphin <oscar.ledauphin@datadoghq.com>
1 parent b70ded5 commit 0f3d46b

1 file changed

Lines changed: 168 additions & 5 deletions

File tree

libdd-trace-obfuscation/src/redis.rs

Lines changed: 168 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,61 @@
33

44
use crate::redis_tokenizer::{RedisTokenType, RedisTokenizer};
55

6+
const REDIS_TRUNCATION_MARK: &str = "...";
7+
const MAX_REDIS_NB_COMMANDS: usize = 3;
8+
9+
/// Returns a quantized version of a Redis query, keeping only up to 3 command names.
10+
pub fn quantize_redis_string(query: &str) -> String {
11+
let mut commands: Vec<String> = Vec::with_capacity(MAX_REDIS_NB_COMMANDS);
12+
let mut truncated = false;
13+
14+
for raw_line in query.lines() {
15+
if commands.len() >= MAX_REDIS_NB_COMMANDS {
16+
break;
17+
}
18+
19+
let line = raw_line.trim();
20+
if line.is_empty() {
21+
continue;
22+
}
23+
24+
let mut tokens = line.split_whitespace();
25+
let Some(first) = tokens.next() else { continue };
26+
27+
if first.ends_with(REDIS_TRUNCATION_MARK) {
28+
truncated = true;
29+
continue;
30+
}
31+
32+
let cmd = first.to_ascii_uppercase();
33+
let command = match cmd.as_bytes() {
34+
b"CLIENT" | b"CLUSTER" | b"COMMAND" | b"CONFIG" | b"DEBUG" | b"SCRIPT" => {
35+
match tokens.next() {
36+
Some(sub) if sub.ends_with(REDIS_TRUNCATION_MARK) => {
37+
truncated = true;
38+
continue;
39+
}
40+
Some(sub) => format!("{cmd} {}", sub.to_ascii_uppercase()),
41+
None => cmd,
42+
}
43+
}
44+
_ => cmd,
45+
};
46+
47+
commands.push(command);
48+
truncated = false;
49+
}
50+
51+
let mut result = commands.join(" ");
52+
if commands.len() == MAX_REDIS_NB_COMMANDS || truncated {
53+
if !result.is_empty() {
54+
result.push(' ');
55+
}
56+
result.push_str("...");
57+
}
58+
result
59+
}
60+
661
pub fn obfuscate_redis_string(cmd: &str) -> String {
762
let mut tokenizer = RedisTokenizer::new(cmd);
863
let s = &mut String::new();
@@ -79,8 +134,8 @@ fn obfuscate_redis_cmd<'a>(str: &mut String, cmd: &'a str, mut args: Vec<&'a str
79134
// • ZSCORE key member
80135
args = obfuscate_redis_args_n(args, 1);
81136
}
82-
b"HSET" | b"HSETNX" | b"LREM" | b"LSET" | b"SETBIT" | b"SETEX" | b"PSETEX"
83-
| b"SETRANGE" | b"ZINCRBY" | b"SMOVE" | b"RESTORE" => {
137+
b"HSETNX" | b"LREM" | b"LSET" | b"SETBIT" | b"SETEX" | b"PSETEX" | b"SETRANGE"
138+
| b"ZINCRBY" | b"SMOVE" | b"RESTORE" => {
84139
// Obfuscate 3rd argument:
85140
// • HSET key field value
86141
// • HSETNX key field value
@@ -120,7 +175,7 @@ fn obfuscate_redis_cmd<'a>(str: &mut String, cmd: &'a str, mut args: Vec<&'a str
120175
// • GEOADD key longitude latitude member [longitude latitude member ...]
121176
args = obfuscate_redis_args_step(args, 1, 3)
122177
}
123-
b"HMSET" => {
178+
b"HMSET" | b"HSET" => {
124179
// Every 2nd argument starting from first.
125180
// • HMSET key field value [field value ...]
126181
args = obfuscate_redis_args_step(args, 1, 2)
@@ -196,7 +251,7 @@ fn obfuscate_redis_args_step(mut args: Vec<&str>, start: usize, step: usize) ->
196251
args
197252
}
198253

199-
pub(crate) fn remove_all_redis_args(redis_cmd: &str) -> String {
254+
pub fn remove_all_redis_args(redis_cmd: &str) -> String {
200255
let mut redis_cmd_iter = redis_cmd.split_whitespace().peekable();
201256
let mut obfuscated_cmd = String::new();
202257

@@ -267,7 +322,110 @@ fn ascii_uppercase<'a>(s: &str, dest: &'a mut [u8]) -> Option<&'a [u8]> {
267322
mod tests {
268323
use duplicate::duplicate_item;
269324

270-
use super::{obfuscate_redis_string, remove_all_redis_args};
325+
use super::{obfuscate_redis_string, quantize_redis_string, remove_all_redis_args};
326+
327+
#[duplicate_item(
328+
[
329+
test_name [test_quantize_redis_string_client]
330+
input ["CLIENT"]
331+
expected ["CLIENT"];
332+
]
333+
[
334+
test_name [test_quantize_redis_string_client_list]
335+
input ["CLIENT LIST"]
336+
expected ["CLIENT LIST"];
337+
]
338+
[
339+
test_name [test_quantize_redis_string_get_lowercase]
340+
input ["get my_key"]
341+
expected ["GET"];
342+
]
343+
[
344+
test_name [test_quantize_redis_string_set]
345+
input ["SET le_key le_value"]
346+
expected ["SET"];
347+
]
348+
[
349+
test_name [test_quantize_redis_string_set_with_newlines]
350+
input ["\n\n \nSET foo bar \n \n\n "]
351+
expected ["SET"];
352+
]
353+
[
354+
test_name [test_quantize_redis_string_config_set]
355+
input ["CONFIG SET parameter value"]
356+
expected ["CONFIG SET"];
357+
]
358+
[
359+
test_name [test_quantize_redis_string_two_cmds]
360+
input ["SET toto tata \n \n EXPIRE toto 15 "]
361+
expected ["SET EXPIRE"];
362+
]
363+
[
364+
test_name [test_quantize_redis_string_mset]
365+
input ["MSET toto tata toto tata toto tata \n "]
366+
expected ["MSET"];
367+
]
368+
[
369+
test_name [test_quantize_redis_string_max_cmds]
370+
input ["MULTI\nSET k1 v1\nSET k2 v2\nSET k3 v3\nSET k4 v4\nDEL to_del\nEXEC"]
371+
expected ["MULTI SET SET ..."];
372+
]
373+
[
374+
test_name [test_quantize_redis_string_truncation_first]
375+
input ["GET..."]
376+
expected ["..."];
377+
]
378+
[
379+
test_name [test_quantize_redis_string_truncation_arg]
380+
input ["GET k..."]
381+
expected ["GET"];
382+
]
383+
[
384+
test_name [test_quantize_redis_string_truncation_third]
385+
input ["GET k1\nGET k2\nG..."]
386+
expected ["GET GET ..."];
387+
]
388+
[
389+
test_name [test_quantize_redis_string_truncation_after_max]
390+
input ["GET k1\nGET k2\nDEL k3\nGET k..."]
391+
expected ["GET GET DEL ..."];
392+
]
393+
[
394+
test_name [test_quantize_redis_string_truncation_hdel]
395+
input ["GET k1\nGET k2\nHDEL k3 a\nG..."]
396+
expected ["GET GET HDEL ..."];
397+
]
398+
[
399+
test_name [test_quantize_redis_string_truncation_mid]
400+
input ["GET k...\nDEL k2\nMS..."]
401+
expected ["GET DEL ..."];
402+
]
403+
[
404+
test_name [test_quantize_redis_string_truncation_early]
405+
input ["GET k...\nDE...\nMS..."]
406+
expected ["GET ..."];
407+
]
408+
[
409+
test_name [test_quantize_redis_string_truncation_then_cmd]
410+
input ["GET k1\nDE...\nGET k2"]
411+
expected ["GET GET"];
412+
]
413+
[
414+
test_name [test_quantize_redis_string_truncation_complex]
415+
input ["GET k1\nDE...\nGET k2\nHDEL k3 a\nGET k4\nDEL k5"]
416+
expected ["GET GET HDEL ..."];
417+
]
418+
[
419+
test_name [test_quantize_redis_string_unknown]
420+
input ["UNKNOWN 123"]
421+
expected ["UNKNOWN"];
422+
]
423+
)]
424+
#[test]
425+
fn test_name() {
426+
let result = quantize_redis_string(input);
427+
assert_eq!(result, expected);
428+
}
271429

272430
#[duplicate_item(
273431
[
@@ -684,6 +842,11 @@ SET k v
684842
expected [r#"CONFIG command
685843
SET k ?"#];
686844
]
845+
[
846+
test_name [test_obfuscate_redis_string_67]
847+
input ["HSET key field value field value"]
848+
expected ["HSET key field ? field ?"];
849+
]
687850
)]
688851
#[test]
689852
fn test_name() {

0 commit comments

Comments
 (0)