Skip to content

Commit 8782afc

Browse files
koki-developclaude
andcommitted
feat: add --mask-secrets flag to mask sensitive information
Add a new --mask-secrets flag that automatically masks sensitive information such as API keys and tokens in the output. Matched patterns are replaced with asterisks of the same length. Supported patterns: - AWS Access Key ID - GitHub Tokens (ghp_, gho_, ghs_, ghr_) - GitLab Personal Access Tokens - Slack Tokens - JWT Tokens - Private Key Headers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent ace004a commit 8782afc

File tree

6 files changed

+150
-3
lines changed

6 files changed

+150
-3
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Flags:
6767
--list-formats print a list of supported output formats
6868
--list-langs print a list of supported languages for syntax highlighting
6969
--list-themes print a list of supported themes with preview
70+
--mask-secrets mask sensitive information (API keys, tokens)
7071
--no-resize do not resize images
7172
-p, --pretty whether to format a content pretty
7273
-M, --render-markdown render markdown
@@ -93,9 +94,27 @@ See [themes.md](./docs/themes.md) for valid themes.
9394

9495
### `-p`, `--pretty`
9596

96-
Format a content pretty.
97+
Format a content pretty.
9798
For unsupported languages, this flag is ignored.
9899

100+
### `--mask-secrets`
101+
102+
Mask sensitive information such as API keys and tokens.
103+
Matched patterns are replaced with `*` characters of the same length.
104+
105+
```console
106+
$ echo 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE' | gat --mask-secrets
107+
AWS_ACCESS_KEY_ID=********************
108+
```
109+
110+
Supported patterns:
111+
- AWS Access Key ID
112+
- GitHub Tokens (`ghp_`, `gho_`, `ghs_`, `ghr_`)
113+
- GitLab Personal Access Tokens
114+
- Slack Tokens
115+
- JWT Tokens
116+
- Private Key Headers
117+
99118
### `-M`, `--render-markdown`
100119

101120
Render markdown documents.

cmd/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ var (
3636
// --pretty
3737
flagPretty bool
3838

39+
// --mask-secrets
40+
flagMaskSecrets bool
41+
3942
// --list-langs
4043
flagListLangs bool
4144

@@ -56,6 +59,7 @@ func init() {
5659
rootCmd.Flags().BoolVar(&flagNoResize, "no-resize", false, "do not resize images")
5760

5861
rootCmd.Flags().BoolVarP(&flagPretty, "pretty", "p", false, "whether to format a content pretty")
62+
rootCmd.Flags().BoolVar(&flagMaskSecrets, "mask-secrets", false, "mask sensitive information (API keys, tokens)")
5963

6064
rootCmd.Flags().BoolVar(&flagListLangs, "list-langs", false, "print a list of supported languages for syntax highlighting")
6165
rootCmd.Flags().BoolVar(&flagListFormats, "list-formats", false, "print a list of supported output formats")

cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ var rootCmd = &cobra.Command{
5959
}
6060

6161
if len(args) == 0 {
62-
return g.Print(os.Stdout, os.Stdin, gat.WithPretty(flagPretty))
62+
return g.Print(os.Stdout, os.Stdin, gat.WithPretty(flagPretty), gat.WithMask(flagMaskSecrets))
6363
}
6464

6565
for _, filename := range args {
66-
if err := processFile(g, filename, gat.WithPretty(flagPretty), gat.WithFilename(filename)); err != nil {
66+
if err := processFile(g, filename, gat.WithPretty(flagPretty), gat.WithMask(flagMaskSecrets), gat.WithFilename(filename)); err != nil {
6767
return err
6868
}
6969
}

internal/gat/gat.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/charmbracelet/glamour"
1818
"github.com/koki-develop/gat/internal/formatters"
1919
"github.com/koki-develop/gat/internal/lexers"
20+
"github.com/koki-develop/gat/internal/masker"
2021
"github.com/koki-develop/gat/internal/prettier"
2122
"github.com/koki-develop/gat/internal/styles"
2223
"github.com/mattn/go-sixel"
@@ -76,6 +77,7 @@ func New(cfg *Config) (*Gat, error) {
7677

7778
type printOption struct {
7879
Pretty bool
80+
Mask bool
7981
Filename string
8082
}
8183

@@ -93,6 +95,12 @@ func WithFilename(name string) PrintOption {
9395
}
9496
}
9597

98+
func WithMask(m bool) PrintOption {
99+
return func(o *printOption) {
100+
o.Mask = m
101+
}
102+
}
103+
96104
func (g *Gat) Print(w io.Writer, r io.Reader, opts ...PrintOption) error {
97105
// parse options
98106
opt := &printOption{}
@@ -187,6 +195,11 @@ func (g *Gat) Print(w io.Writer, r io.Reader, opts ...PrintOption) error {
187195
}
188196
}
189197

198+
// mask sensitive information
199+
if opt.Mask {
200+
src = masker.Mask(src)
201+
}
202+
190203
// print
191204
it, err := lexer.Tokenise(nil, src)
192205
if err != nil {

internal/masker/masker.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package masker
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
var patterns = []*regexp.Regexp{
9+
// AWS Access Key ID
10+
regexp.MustCompile(`AKIA[0-9A-Z]{16}`),
11+
// GitHub Tokens (ghp_, gho_, ghs_, ghr_)
12+
regexp.MustCompile(`gh[pousr]_[a-zA-Z0-9]{36,}`),
13+
// GitLab Personal Access Token
14+
regexp.MustCompile(`glpat-[a-zA-Z0-9\-_]{20,}`),
15+
// Slack Tokens
16+
regexp.MustCompile(`xox[baprs]-[0-9a-zA-Z\-]+`),
17+
// JWT Tokens
18+
regexp.MustCompile(`eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*`),
19+
// Private Key Headers
20+
regexp.MustCompile(`-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)\s+PRIVATE\s+KEY-----`),
21+
}
22+
23+
// Mask replaces sensitive patterns in content with asterisks of the same length
24+
func Mask(content string) string {
25+
result := content
26+
for _, p := range patterns {
27+
result = p.ReplaceAllStringFunc(result, func(match string) string {
28+
return strings.Repeat("*", len(match))
29+
})
30+
}
31+
return result
32+
}

internal/masker/masker_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package masker
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestMask(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input string
14+
want string
15+
}{
16+
{
17+
name: "AWS Access Key",
18+
input: "aws_access_key_id = AKIAIOSFODNN7EXAMPLE",
19+
want: "aws_access_key_id = " + strings.Repeat("*", 20),
20+
},
21+
{
22+
name: "GitHub Personal Access Token",
23+
input: "token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
24+
want: "token: " + strings.Repeat("*", 40),
25+
},
26+
{
27+
name: "GitHub OAuth Token",
28+
input: "GITHUB_TOKEN=gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
29+
want: "GITHUB_TOKEN=" + strings.Repeat("*", 40),
30+
},
31+
{
32+
name: "GitLab Personal Access Token",
33+
input: "GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx",
34+
want: "GITLAB_TOKEN=" + strings.Repeat("*", 26),
35+
},
36+
{
37+
name: "Slack Bot Token",
38+
input: "SLACK_TOKEN=xoxb-123456789-abcdefgh",
39+
want: "SLACK_TOKEN=" + strings.Repeat("*", 23),
40+
},
41+
{
42+
name: "JWT Token",
43+
input: "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
44+
want: "Authorization: Bearer " + strings.Repeat("*", 108),
45+
},
46+
{
47+
name: "RSA Private Key Header",
48+
input: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...",
49+
want: strings.Repeat("*", 31) + "\nMIIEpAIBAAKCAQEA...",
50+
},
51+
{
52+
name: "No sensitive data",
53+
input: "const message = 'Hello World'",
54+
want: "const message = 'Hello World'",
55+
},
56+
{
57+
name: "Multiple secrets",
58+
input: "GITHUB=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nAWS=AKIAIOSFODNN7EXAMPLE",
59+
want: "GITHUB=" + strings.Repeat("*", 40) + "\nAWS=" + strings.Repeat("*", 20),
60+
},
61+
{
62+
name: "Empty string",
63+
input: "",
64+
want: "",
65+
},
66+
{
67+
name: "Preserves length",
68+
input: "KEY=AKIAIOSFODNN7EXAMPLE",
69+
want: "KEY=" + strings.Repeat("*", 20),
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
got := Mask(tt.input)
76+
assert.Equal(t, tt.want, got)
77+
})
78+
}
79+
}

0 commit comments

Comments
 (0)