Skip to content

Commit d25a794

Browse files
committed
Restore component.redactLine
1 parent dd91714 commit d25a794

2 files changed

Lines changed: 105 additions & 9 deletions

File tree

internal/component/loki/secretfilter/secretfilter.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package secretfilter
22

33
import (
44
"context"
5+
"crypto/sha1"
56
"fmt"
67
"strings"
78
"sync"
@@ -33,6 +34,8 @@ func init() {
3334
// Arguments holds values which are used to configure the secretfilter component.
3435
type Arguments struct {
3536
ForwardTo []loki.LogsReceiver `alloy:"forward_to,attr"`
37+
RedactWith string `alloy:"redact_with,attr,optional"` // Redact the secret with this string. Use $SECRET_NAME and $SECRET_HASH to include the secret name and hash
38+
PartialMask uint `alloy:"partial_mask,attr,optional"` // Show the first N characters of the secret (default: 0)
3639
OriginLabel string `alloy:"origin_label,attr,optional"` // The label name to use for tracking metrics by origin (if empty, no origin metrics are collected)
3740
}
3841

@@ -217,15 +220,8 @@ func (c *Component) processEntry(entry loki.Entry) loki.Entry {
217220
// Redact all found secrets
218221
redactedLine := entry.Line
219222
for _, finding := range findings {
220-
// Store the original secret before redaction
221-
originalSecret := finding.Secret
222-
223-
// Redact 80% of the secret
224-
//
225-
// todo(kleimkuhler): Add configuration for redaction percentage
226-
finding.Redact(20)
227-
228-
redactedLine = strings.ReplaceAll(redactedLine, originalSecret, finding.Secret)
223+
// Redact the secret using our custom redaction logic
224+
redactedLine = c.redactLine(redactedLine, finding.Secret, finding.RuleID)
229225

230226
// Record metrics for the redacted secret
231227
c.metrics.secretsRedactedTotal.Inc()
@@ -243,6 +239,41 @@ func (c *Component) processEntry(entry loki.Entry) loki.Entry {
243239
return entry
244240
}
245241

242+
func (c *Component) redactLine(line string, secret string, ruleName string) string {
243+
var redactWith = "<REDACTED-SECRET:" + ruleName + ">"
244+
if c.args.RedactWith != "" {
245+
redactWith = c.args.RedactWith
246+
redactWith = strings.ReplaceAll(redactWith, "$SECRET_NAME", ruleName)
247+
redactWith = strings.ReplaceAll(redactWith, "$SECRET_HASH", hashSecret(secret))
248+
}
249+
250+
// If partialMask is set, show the first N characters of the secret
251+
partialMask := int(c.args.PartialMask)
252+
if partialMask < 0 {
253+
partialMask = 0
254+
}
255+
runesSecret := []rune(secret)
256+
// Only do it if the secret is long enough
257+
if partialMask > 0 && len(runesSecret) >= 6 {
258+
// Show at most half of the secret
259+
if partialMask > len(runesSecret)/2 {
260+
partialMask = len(runesSecret) / 2
261+
}
262+
prefix := string(runesSecret[:partialMask])
263+
redactWith = prefix + redactWith
264+
}
265+
266+
line = strings.ReplaceAll(line, secret, redactWith)
267+
268+
return line
269+
}
270+
271+
func hashSecret(secret string) string {
272+
hasher := sha1.New()
273+
hasher.Write([]byte(secret))
274+
return fmt.Sprintf("%x", hasher.Sum(nil))
275+
}
276+
246277
// Update implements component.Component.
247278
func (c *Component) Update(args component.Arguments) error {
248279
newArgs := args.(Arguments)

internal/component/loki/secretfilter/secretfilter_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,71 @@ func TestMetricsMultipleEntries(t *testing.T) {
620620
"loki_secretfilter_secrets_redacted_by_origin"))
621621
}
622622

623+
func TestPartialMasking(t *testing.T) {
624+
// Start testing with common cases
625+
component := &Component{}
626+
component.args = Arguments{PartialMask: 4}
627+
628+
// Too short to be partially masked
629+
redacted := component.redactLine("This is a very short secret ab in a log line", "ab", "test-rule")
630+
require.Equal(t, "This is a very short secret <REDACTED-SECRET:test-rule> in a log line", redacted)
631+
632+
// Too short to be partially masked
633+
redacted = component.redactLine("This is a short secret abc12 in a log line", "abc12", "test-rule")
634+
require.Equal(t, "This is a short secret <REDACTED-SECRET:test-rule> in a log line", redacted)
635+
636+
// Will be partially masked (limited by secret length)
637+
redacted = component.redactLine("This is a longer secret abc123 in a log line", "abc123", "test-rule")
638+
require.Equal(t, "This is a longer secret abc<REDACTED-SECRET:test-rule> in a log line", redacted)
639+
640+
// Will be partially masked
641+
redacted = component.redactLine("This is a long enough secret abcd1234 in a log line", "abcd1234", "test-rule")
642+
require.Equal(t, "This is a long enough secret abcd<REDACTED-SECRET:test-rule> in a log line", redacted)
643+
644+
// Will be partially masked
645+
redacted = component.redactLine("This is the longest secret abcdef12345678 in a log line", "abcdef12345678", "test-rule")
646+
require.Equal(t, "This is the longest secret abcd<REDACTED-SECRET:test-rule> in a log line", redacted)
647+
648+
// Test with a non-ASCII character
649+
redacted = component.redactLine("This is a line with a complex secret aBc\U0001f512De\U0001f5124 in a log line", "aBc\U0001f512De\U0001f5124", "test-rule")
650+
require.Equal(t, "This is a line with a complex secret aBc\U0001f512<REDACTED-SECRET:test-rule> in a log line", redacted)
651+
652+
// Test with different secret lengths and partial masking values
653+
for partialMasking := range 20 {
654+
for secretLength := range 50 {
655+
if secretLength < 2 {
656+
continue
657+
}
658+
expectedPrefixLength := 0
659+
if secretLength >= 6 {
660+
expectedPrefixLength = min(secretLength/2, partialMasking)
661+
}
662+
checkPartialMasking(t, partialMasking, secretLength, expectedPrefixLength)
663+
}
664+
}
665+
}
666+
667+
func checkPartialMasking(t *testing.T, partialMasking int, secretLength int, expectedPrefixLength int) {
668+
component := &Component{}
669+
component.args = Arguments{PartialMask: uint(partialMasking)}
670+
671+
// Test with a simple ASCII character
672+
secret := strings.Repeat("A", secretLength)
673+
inputLog := fmt.Sprintf("This is a test with a secret %s in a log line", secret)
674+
redacted := component.redactLine(inputLog, secret, "test-rule")
675+
prefix := strings.Repeat("A", expectedPrefixLength)
676+
expectedLog := fmt.Sprintf("This is a test with a secret %s<REDACTED-SECRET:test-rule> in a log line", prefix)
677+
require.Equal(t, expectedLog, redacted)
678+
679+
// Test with a non-ASCII character
680+
secret = strings.Repeat("\U0001f512", secretLength)
681+
inputLog = fmt.Sprintf("This is a test with a secret %s in a log line", secret)
682+
redacted = component.redactLine(inputLog, secret, "test-rule")
683+
prefix = strings.Repeat("\U0001f512", expectedPrefixLength)
684+
expectedLog = fmt.Sprintf("This is a test with a secret %s<REDACTED-SECRET:test-rule> in a log line", prefix)
685+
require.Equal(t, expectedLog, redacted)
686+
}
687+
623688
// TestArgumentsUpdate validates that the secretfilter component works correctly
624689
// when its arguments are updated multiple times during runtime
625690
func TestArgumentsUpdate(t *testing.T) {

0 commit comments

Comments
 (0)