Skip to content

Commit a8e5e32

Browse files
committed
Redesign CLI around -string and -detect
Positional args mixed secret bodies with stray tokens and forced users to remember an implicit join. Stdin is now the default (cat-like), -string passes inline text, and -detect runs HasSecrets and exits 1 on a hit. Renamed -min-entropy and -min-submatch to shorter -entropy and -strength.
1 parent e5b4f7c commit a8e5e32

11 files changed

Lines changed: 112 additions & 56 deletions

File tree

README.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,7 @@ Hide secrets in text.
44

55
## Usage
66

7-
Pass text as an argument:
8-
9-
```
10-
$ redact 'user=alice token=ghp_abcdefghijklmnopqrstuvwxyz0123456789 status=ok'
11-
user=alice token=**************************************** status=ok
12-
```
13-
14-
Or pipe a file:
7+
Pipe a file:
158

169
```
1710
$ redact < ~/.aws/credentials
@@ -20,17 +13,34 @@ aws_access_key_id = ********************
2013
aws_secret_access_key = ****************************************
2114
```
2215

16+
Pass text inline with `-string`:
17+
18+
```
19+
$ redact -string 'user=alice token=ghp_abcdefghijklmnopqrstuvwxyz0123456789 status=ok'
20+
user=alice token=**************************************** status=ok
21+
```
22+
2323
Custom mask character:
2424

2525
```
26-
$ redact --mask=# 'token: ghp_abcdefghijklmnopqrstuvwxyz0123456789'
26+
$ redact -mask=# -string 'token: ghp_abcdefghijklmnopqrstuvwxyz0123456789'
2727
token: ########################################
2828
```
2929

3030
Catch a wider net of unknown-looking values in a config file:
3131

3232
```
33-
$ redact --min-entropy=2.0 < config.yaml
33+
$ redact -entropy=2.0 < config.yaml
34+
```
35+
36+
Check for secrets without printing anything. Exits 1 if found, 0 if not:
37+
38+
```
39+
$ redact -detect -string 'token=ghp_abcdefghijklmnopqrstuvwxyz0123456789' && echo clean || echo dirty
40+
dirty
41+
42+
$ redact -detect -string 'log_level=debug' && echo clean || echo dirty
43+
clean
3444
```
3545

3646
## Use as a package
@@ -39,6 +49,10 @@ $ redact --min-entropy=2.0 < config.yaml
3949
import "github.com/inancgumus/redact"
4050

4151
clean := redact.String(text, redact.DefaultOptions)
52+
53+
if redact.HasSecrets(text, redact.DefaultOptions) {
54+
// text contains at least one detected secret
55+
}
4256
```
4357

4458
Tune the defaults:

cmd/redact/main.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
// Command redact reads text from positional args or stdin and writes a
2-
// copy with detected secrets replaced by a placeholder.
1+
// Command redact reads text from -string or stdin and writes a copy with
2+
// detected secrets replaced by a mask character.
33
package main
44

55
import (
6-
"bufio"
76
"errors"
87
"flag"
98
"fmt"
109
"io"
1110
"os"
12-
"strings"
1311

1412
"github.com/inancgumus/redact"
1513
)
@@ -25,18 +23,22 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
2523
fs := flag.NewFlagSet("redact", flag.ContinueOnError)
2624
fs.SetOutput(stderr)
2725
fs.Usage = func() {
28-
_, _ = fmt.Fprintln(stderr, "usage: redact [flags] [text...]")
29-
_, _ = fmt.Fprintln(stderr, "reads text from args; if none, reads from stdin.")
26+
_, _ = fmt.Fprintln(stderr, "usage: redact [flags]")
27+
_, _ = fmt.Fprintln(stderr, "reads -string if set, otherwise stdin.")
3028
fs.PrintDefaults()
3129
}
3230

3331
opts := redact.DefaultOptions
3432
fs.Var((*runeFlag)(&opts.Mask), "mask",
3533
"character repeated for each byte of a secret")
36-
fs.Float64Var(&opts.MinEntropy, "min-entropy", opts.MinEntropy,
34+
fs.Float64Var(&opts.MinEntropy, "entropy", opts.MinEntropy,
3735
"how random a value must look to be redacted (lower = redacts more)")
38-
fs.IntVar(&opts.MinSubmatch, "min-submatch", opts.MinSubmatch,
36+
fs.IntVar(&opts.MinSubmatch, "strength", opts.MinSubmatch,
3937
"how strong a match must be to redact unknown secrets (higher = redacts less)")
38+
detect := fs.Bool("detect", false,
39+
"exit 1 if input contains secrets, 0 otherwise; no output")
40+
var input string
41+
fs.StringVar(&input, "string", "", "redact this text instead of reading stdin")
4042

4143
if err := fs.Parse(args); err != nil {
4244
if errors.Is(err, flag.ErrHelp) {
@@ -45,27 +47,40 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
4547
return err
4648
}
4749

48-
if rest := fs.Args(); len(rest) > 0 {
49-
_, err := io.WriteString(stdout, redact.String(strings.Join(rest, " "), opts))
50-
return err
50+
if fs.NArg() > 0 {
51+
return fmt.Errorf("unexpected positional args; use -string for inline text")
5152
}
5253

53-
// Peek so empty stdin prints usage instead of silently exiting.
54-
// The peeked byte stays buffered for ReadAll.
55-
br := bufio.NewReader(stdin)
56-
if _, err := br.Peek(1); err != nil {
57-
fs.Usage()
58-
return nil
54+
text := input
55+
if !isStringFlagSet(fs) {
56+
buf, err := io.ReadAll(stdin)
57+
if err != nil {
58+
return fmt.Errorf("read stdin: %w", err)
59+
}
60+
text = string(buf)
5961
}
6062

61-
buf, err := io.ReadAll(br)
62-
if err != nil {
63-
return fmt.Errorf("read stdin: %w", err)
63+
if *detect {
64+
if redact.HasSecrets(text, opts) {
65+
os.Exit(1)
66+
}
67+
return nil
6468
}
65-
_, err = io.WriteString(stdout, redact.String(string(buf), opts))
69+
70+
_, err := io.WriteString(stdout, redact.String(text, opts))
6671
return err
6772
}
6873

74+
func isStringFlagSet(fs *flag.FlagSet) bool {
75+
var set bool
76+
fs.Visit(func(f *flag.Flag) {
77+
if f.Name == "string" {
78+
set = true
79+
}
80+
})
81+
return set
82+
}
83+
6984
// runeFlag adapts a *rune to the flag.Value interface so the mask flag
7085
// accepts exactly one character.
7186
type runeFlag rune
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Redacts a secret passed as a positional arg.
2-
exec ./redact AKIAIOSFODNN7EXAMPLE
1+
# Redacts a secret passed via -string.
2+
exec ./redact -string AKIAIOSFODNN7EXAMPLE
33
! stdout 'AKIAIOSFODNN7EXAMPLE'
44
stdout '\*+'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# -detect exits 1 when -string contains a secret.
2+
! exec ./redact -detect -string AKIAIOSFODNN7EXAMPLE
3+
! stdout .
4+
5+
# -detect exits 0 on plain prose.
6+
exec ./redact -detect -string 'the quick brown fox'
7+
! stdout .
8+
9+
# -detect reads stdin and exits 1 when a secret is found.
10+
stdin secret.txt
11+
! exec ./redact -detect
12+
! stdout .
13+
14+
# -detect reads stdin and exits 0 when no secret is found.
15+
stdin clean.txt
16+
exec ./redact -detect
17+
! stdout .
18+
19+
# Options propagate: raising --entropy hides generic matches.
20+
exec ./redact -detect --entropy=10.0 -string 'api_key="aB3kPq9xZ7mNvR2tYuIo"'
21+
! stdout .
22+
23+
-- secret.txt --
24+
ghp_abcdefghijklmnopqrstuvwxyz0123456789
25+
-- clean.txt --
26+
log_level=debug
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# High --min-entropy suppresses the generic detector.
2-
exec ./redact --min-entropy=10 'api_key="abcdefghij1234567890"'
2+
exec ./redact --entropy=10 -string 'api_key="abcdefghij1234567890"'
33
! stdout '\*'
44
stdout 'abcdefghij1234567890'
55

66
# Low --min-entropy redacts aggressively.
7-
exec ./redact --min-entropy=1 'api_key="abcdefghij1234567890"'
7+
exec ./redact --entropy=1 -string 'api_key="abcdefghij1234567890"'
88
stdout '\*+'
99
! stdout 'abcdefghij1234567890'

cmd/redact/testdata/scripts/min_submatch.txtar

Lines changed: 0 additions & 8 deletions
This file was deleted.

cmd/redact/testdata/scripts/multi_args.txtar

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
# No args and empty stdin prints usage to stderr and exits 0.
1+
# Empty stdin produces empty output, exits 0 (cat-like).
22
exec ./redact
3-
stderr 'usage: redact'
4-
stderr '-mask'
5-
stderr '-min-entropy'
6-
stderr '-min-submatch'
73
! stdout .
4+
! stderr .
85

9-
# Explicit -h also prints usage and exits 0.
6+
# Explicit -h prints usage and exits 0.
107
exec ./redact -h
118
stderr 'usage: redact'
129
! stdout .
10+
11+
# Positional args are rejected.
12+
! exec ./redact some-text
13+
stderr 'unexpected positional args'
14+
15+
# Empty -string yields empty output.
16+
exec ./redact -string ''
17+
! stdout .
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Custom mask character via --mask.
2-
exec ./redact '--mask=#' AKIAIOSFODNN7EXAMPLE
2+
exec ./redact '--mask=#' -string AKIAIOSFODNN7EXAMPLE
33
stdout '^####################$'
44
! stdout 'AKIAIOSFODNN7EXAMPLE'
55
! stdout '\*'
66

77
# Rejects multi-character mask.
8-
! exec ./redact '--mask=ab' AKIAIOSFODNN7EXAMPLE
8+
! exec ./redact '--mask=ab' -string AKIAIOSFODNN7EXAMPLE
99
stderr 'mask must be one character'

cmd/redact/testdata/scripts/preserves.txtar

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Plain prose passes through unchanged.
2-
exec ./redact 'the quick brown fox'
2+
exec ./redact -string 'the quick brown fox'
33
stdout '^the quick brown fox$'
44
! stdout '\*'
55

0 commit comments

Comments
 (0)