// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package obfuscate
import (
"strings"
)
// redisTruncationMark is used as suffix by tracing libraries to indicate that a
// command was truncated.
const redisTruncationMark = "..."
const maxRedisNbCommands = 3
// Redis commands consisting in 2 words
var redisCompoundCommandSet = map[string]bool{
"CLIENT": true, "CLUSTER": true, "COMMAND": true, "CONFIG": true, "DEBUG": true, "SCRIPT": true}
// QuantizeRedisString returns a quantized version of a Redis query.
//
// TODO(gbbr): Refactor this method to use the tokenizer and
// remove "compactWhitespaces". This method is buggy when commands
// contain quoted strings with newlines.
func (*Obfuscator) QuantizeRedisString(query string) string {
query = compactWhitespaces(query)
var resource strings.Builder
truncated := false
nbCmds := 0
for len(query) > 0 && nbCmds < maxRedisNbCommands {
var rawLine string
// Read the next command
idx := strings.IndexByte(query, '\n')
if idx == -1 {
rawLine = query
query = ""
} else {
rawLine = query[:idx]
query = query[idx+1:]
}
line := strings.Trim(rawLine, " ")
if len(line) == 0 {
continue
}
// Parse arguments
args := strings.SplitN(line, " ", 3)
if strings.HasSuffix(args[0], redisTruncationMark) {
truncated = true
continue
}
command := strings.ToUpper(args[0])
if redisCompoundCommandSet[command] && len(args) > 1 {
if strings.HasSuffix(args[1], redisTruncationMark) {
truncated = true
continue
}
command += " " + strings.ToUpper(args[1])
}
// Write the command representation
resource.WriteByte(' ')
resource.WriteString(command)
nbCmds++
truncated = false
}
if nbCmds == maxRedisNbCommands || truncated {
resource.WriteString(" ...")
}
return strings.Trim(resource.String(), " ")
}
// ObfuscateRedisString obfuscates the given Redis command.
func (*Obfuscator) ObfuscateRedisString(rediscmd string) string {
t := newRedisTokenizer([]byte(rediscmd))
var (
str strings.Builder
cmd string
args []string
)
for {
tok, typ, done := t.scan()
switch typ {
case redisTokenCommand:
// new command starting
if cmd != "" {
// a previous command was buffered, obfuscate it
obfuscateRedisCmd(&str, cmd, args...)
str.WriteByte('\n')
}
cmd = tok
args = args[:0]
case redisTokenArgument:
args = append(args, tok)
}
if done {
// last command
obfuscateRedisCmd(&str, cmd, args...)
break
}
}
return str.String()
}
func obfuscateRedisCmd(out *strings.Builder, cmd string, args ...string) {
out.WriteString(cmd)
if len(args) == 0 {
return
}
out.WriteByte(' ')
switch strings.ToUpper(cmd) {
case "AUTH":
// Obfuscate everything after command
// • AUTH password
if len(args) > 0 {
args[0] = "?"
args = args[:1]
}
case "APPEND", "GETSET", "LPUSHX", "GEORADIUSBYMEMBER", "RPUSHX",
"SET", "SETNX", "SISMEMBER", "ZRANK", "ZREVRANK", "ZSCORE":
// Obfuscate 2nd argument:
// • APPEND key value
// • GETSET key value
// • LPUSHX key value
// • GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
// • RPUSHX key value
// • SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
// • SETNX key value
// • SISMEMBER key member
// • ZRANK key member
// • ZREVRANK key member
// • ZSCORE key member
obfuscateRedisArgN(args, 1)
case "HSETNX", "LREM", "LSET", "SETBIT", "SETEX", "PSETEX",
"SETRANGE", "ZINCRBY", "SMOVE", "RESTORE":
// Obfuscate 3rd argument:
// • HSETNX key field value
// • LREM key count value
// • LSET key index value
// • SETBIT key offset value
// • SETEX key seconds value
// • PSETEX key milliseconds value
// • SETRANGE key offset value
// • ZINCRBY key increment member
// • SMOVE source destination member
// • RESTORE key ttl serialized-value [REPLACE]
obfuscateRedisArgN(args, 2)
case "LINSERT":
// Obfuscate 4th argument:
// • LINSERT key BEFORE|AFTER pivot value
obfuscateRedisArgN(args, 3)
case "GEOHASH", "GEOPOS", "GEODIST", "LPUSH", "RPUSH", "SREM",
"ZREM", "SADD":
// Obfuscate all arguments after the first one.
// • GEOHASH key member [member ...]
// • GEOPOS key member [member ...]
// • GEODIST key member1 member2 [unit]
// • LPUSH key value [value ...]
// • RPUSH key value [value ...]
// • SREM key member [member ...]
// • ZREM key member [member ...]
// • SADD key member [member ...]
if len(args) > 1 {
args[1] = "?"
args = args[:2]
}
case "GEOADD":
// Obfuscating every 3rd argument starting from first
// • GEOADD key longitude latitude member [longitude latitude member ...]
obfuscateRedisArgsStep(args, 1, 3)
case "HSET", "HMSET":
// Every 2nd argument starting from first.
// • HSET key field value [field value ...]
// • HMSET key field value [field value ...]
obfuscateRedisArgsStep(args, 1, 2)
case "MSET", "MSETNX":
// Every 2nd argument starting from command.
// • MSET key value [key value ...]
// • MSETNX key value [key value ...]
obfuscateRedisArgsStep(args, 0, 2)
case "CONFIG":
// Obfuscate 2nd argument to SET sub-command.
// • CONFIG SET parameter value
if strings.ToUpper(args[0]) == "SET" {
obfuscateRedisArgN(args, 2)
}
case "BITFIELD":
// Obfuscate 3rd argument to SET sub-command:
// • BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
var n int
for i, arg := range args {
if strings.ToUpper(arg) == "SET" {
n = i
}
if n > 0 && i-n == 3 {
args[i] = "?"
break
}
}
case "ZADD":
// Obfuscate every 2nd argument after potential optional ones.
// • ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
var i int
loop:
for i = range args {
if i == 0 {
continue // key
}
switch args[i] {
case "NX", "XX", "CH", "INCR":
// continue
default:
break loop
}
}
obfuscateRedisArgsStep(args, i, 2)
default:
// Obfuscate nothing.
}
out.WriteString(strings.Join(args, " "))
}
// RemoveAllRedisArgs will take in a command and obfuscate all arguments following
// the command, regardless of if the command is valid Redis or not
func (*Obfuscator) RemoveAllRedisArgs(rediscmd string) string {
fullCmd := strings.Fields(rediscmd)
if len(fullCmd) == 0 {
return ""
}
cmd, args := fullCmd[0], fullCmd[1:]
var out strings.Builder
out.WriteString(cmd)
if len(args) == 0 {
return out.String()
}
out.WriteByte(' ')
switch strings.ToUpper(cmd) {
case "BITFIELD":
out.WriteString("?")
for _, a := range args {
arg := strings.ToUpper(a)
if arg == "SET" || arg == "GET" || arg == "INCRBY" {
out.WriteString(strings.Join([]string{"", a, "?"}, " "))
}
}
case "CONFIG":
arg := strings.ToUpper(args[0])
if arg == "GET" || arg == "SET" || arg == "RESETSTAT" || arg == "REWRITE" {
out.WriteString(strings.Join([]string{args[0], "?"}, " "))
} else {
out.WriteString("?")
}
default:
out.WriteString("?")
}
return out.String()
}
func obfuscateRedisArgN(args []string, n int) {
if len(args) > n {
args[n] = "?"
}
}
func obfuscateRedisArgsStep(args []string, start, step int) {
if start+step-1 >= len(args) {
// can't reach target
return
}
for i := start + step - 1; i < len(args); i += step {
args[i] = "?"
}
}