Skip to content

Commit 06f707c

Browse files
redact sensitive information on diagnostics collect command
. . PR changes pr changes Update CHANGELOG.next.asciidoc Co-authored-by: Michel Laterman <82832767+michel-laterman@users.noreply.github.com>
1 parent 662a07a commit 06f707c

3 files changed

Lines changed: 155 additions & 10 deletions

File tree

CHANGELOG.next.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,4 @@
186186
- Support scheduled actions and cancellation of pending actions. {issue}393[393] {pull}419[419]
187187
- Add `@metadata.input_id` and `@metadata.stream_id` when applying the inject stream processor {pull}527[527]
188188
- Add liveness endpoint, allow fleet-gateway component to report degraded state, add update time and messages to status output. {issue}390[390] {pull}569[569]
189+
- Redact sensitive information on diagnostics collect command. {issue}[241] {pull}[566]

internal/pkg/agent/cmd/diagnostics.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"io/fs"
1515
"os"
1616
"path/filepath"
17+
"reflect"
1718
"runtime"
1819
"strings"
1920
"text/tabwriter"
@@ -34,10 +35,17 @@ import (
3435
"github.com/elastic/elastic-agent/internal/pkg/config/operations"
3536
)
3637

38+
const (
39+
HUMAN = "human"
40+
JSON = "json"
41+
YAML = "yaml"
42+
REDACTED = "<REDACTED>"
43+
)
44+
3745
var diagOutputs = map[string]outputter{
38-
"human": humanDiagnosticsOutput,
39-
"json": jsonOutput,
40-
"yaml": yamlOutput,
46+
HUMAN: humanDiagnosticsOutput,
47+
JSON: jsonOutput,
48+
YAML: yamlOutput,
4149
}
4250

4351
// DiagnosticsInfo a struct to track all information related to diagnostics for the agent.
@@ -83,6 +91,7 @@ func newDiagnosticsCommand(s []string, streams *cli.IOStreams) *cobra.Command {
8391
}
8492

8593
func newDiagnosticsCollectCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command {
94+
8695
cmd := &cobra.Command{
8796
Use: "collect",
8897
Short: "Collect diagnostics information from the elastic-agent and write it to a zip archive.",
@@ -115,7 +124,7 @@ func newDiagnosticsCollectCommandWithArgs(_ []string, streams *cli.IOStreams) *c
115124
}
116125

117126
cmd.Flags().StringP("file", "f", "", "name of the output diagnostics zip archive")
118-
cmd.Flags().String("output", "yaml", "Output the collected information in either json, or yaml (default: yaml)") // replace output flag with different options
127+
cmd.Flags().String("output", YAML, "Output the collected information in either json, or yaml (default: yaml)") // replace output flag with different options
119128
cmd.Flags().Bool("pprof", false, "Collect all pprof data from all running applications.")
120129
cmd.Flags().Duration("pprof-duration", time.Second*30, "The duration to collect trace and profiling data from the debug/pprof endpoints. (default: 30s)")
121130
cmd.Flags().Duration("timeout", time.Second*30, "The timeout for the diagnostics collect command, will be either 30s or 30s+pprof-duration by default. Should be longer then pprof-duration when pprof is enabled as the command needs time to process/archive the response.")
@@ -690,16 +699,77 @@ func saveLogs(name string, logPath string, zw *zip.Writer) error {
690699

691700
// writeFile writes json or yaml data from the interface to the writer.
692701
func writeFile(w io.Writer, outputFormat string, v interface{}) error {
693-
if outputFormat == "json" {
702+
redacted, err := redact(v)
703+
if err != nil {
704+
return err
705+
}
706+
707+
if outputFormat == JSON {
694708
je := json.NewEncoder(w)
695709
je.SetIndent("", " ")
696-
return je.Encode(v)
710+
return je.Encode(redacted)
697711
}
712+
698713
ye := yaml.NewEncoder(w)
699-
err := ye.Encode(v)
714+
err = ye.Encode(redacted)
700715
return closeHandlers(err, ye)
701716
}
702717

718+
func redact(v interface{}) (map[string]interface{}, error) {
719+
redacted := map[string]interface{}{}
720+
bs, err := yaml.Marshal(v)
721+
if err != nil {
722+
return nil, fmt.Errorf("could not marshal data to redact: %w", err)
723+
}
724+
725+
err = yaml.Unmarshal(bs, &redacted)
726+
if err != nil {
727+
return nil, fmt.Errorf("could not unmarshal data to redact: %w", err)
728+
}
729+
730+
return redactMap(redacted), nil
731+
}
732+
733+
func toMapStr(v interface{}) map[string]interface{} {
734+
mm := map[string]interface{}{}
735+
m, ok := v.(map[interface{}]interface{})
736+
if !ok {
737+
return mm
738+
}
739+
740+
for k, v := range m {
741+
mm[k.(string)] = v
742+
}
743+
return mm
744+
}
745+
746+
func redactMap(m map[string]interface{}) map[string]interface{} {
747+
for k, v := range m {
748+
if v != nil && reflect.TypeOf(v).Kind() == reflect.Map {
749+
v = redactMap(toMapStr(v))
750+
}
751+
if redactKey(k) {
752+
v = REDACTED
753+
}
754+
m[k] = v
755+
}
756+
return m
757+
}
758+
759+
func redactKey(k string) bool {
760+
// "routekey" shouldn't be redacted.
761+
// Add any other exceptions here.
762+
if k == "routekey" {
763+
return false
764+
}
765+
766+
return strings.Contains(k, "certificate") ||
767+
strings.Contains(k, "passphrase") ||
768+
strings.Contains(k, "password") ||
769+
strings.Contains(k, "token") ||
770+
strings.Contains(k, "key")
771+
}
772+
703773
// closeHandlers will close all passed closers attaching any errors to the passed err and returning the result
704774
func closeHandlers(err error, closers ...io.Closer) error {
705775
var mErr *multierror.Error

internal/pkg/agent/cmd/diagnostics_test.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"testing"
1616
"time"
1717

18+
"github.com/elastic/elastic-agent-libs/transport/tlscommon"
1819
"github.com/stretchr/testify/assert"
1920
"github.com/stretchr/testify/require"
2021

@@ -30,7 +31,7 @@ var testDiagnostics = DiagnosticsInfo{
3031
BuildTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
3132
Snapshot: false,
3233
},
33-
ProcMeta: []client.ProcMeta{client.ProcMeta{
34+
ProcMeta: []client.ProcMeta{{
3435
Process: "filebeat",
3536
Name: "filebeat",
3637
Hostname: "test-host",
@@ -45,7 +46,7 @@ var testDiagnostics = DiagnosticsInfo{
4546
BinaryArchitecture: "test-architecture",
4647
RouteKey: "test",
4748
ElasticLicensed: true,
48-
}, client.ProcMeta{
49+
}, {
4950
Process: "filebeat",
5051
Name: "filebeat_monitoring",
5152
Hostname: "test-host",
@@ -60,7 +61,7 @@ var testDiagnostics = DiagnosticsInfo{
6061
BinaryArchitecture: "test-architecture",
6162
RouteKey: "test",
6263
ElasticLicensed: true,
63-
}, client.ProcMeta{
64+
}, {
6465
Name: "metricbeat",
6566
RouteKey: "test",
6667
Error: "failed to get metricbeat data",
@@ -137,3 +138,76 @@ func Test_collectEndpointSecurityLogs_noEndpointSecurity(t *testing.T) {
137138
err := collectEndpointSecurityLogs(zw, specs)
138139
assert.NoError(t, err, "collectEndpointSecurityLogs should not return an error")
139140
}
141+
142+
func Test_redact(t *testing.T) {
143+
tests := []struct {
144+
name string
145+
arg interface{}
146+
wantRedacted []string
147+
wantErr assert.ErrorAssertionFunc
148+
}{
149+
{
150+
name: "tlscommon.Config",
151+
arg: tlscommon.Config{
152+
Enabled: nil,
153+
VerificationMode: 0,
154+
Versions: nil,
155+
CipherSuites: nil,
156+
CAs: []string{"ca1", "ca2"},
157+
Certificate: tlscommon.CertificateConfig{
158+
Certificate: "Certificate",
159+
Key: "Key",
160+
Passphrase: "Passphrase",
161+
},
162+
CurveTypes: nil,
163+
Renegotiation: 0,
164+
CASha256: nil,
165+
CATrustedFingerprint: "",
166+
},
167+
wantRedacted: []string{
168+
"certificate", "key", "key_passphrase", "certificate_authorities"},
169+
},
170+
{
171+
name: "some map",
172+
arg: map[string]interface{}{
173+
"s": "sss",
174+
"some_key": "hey, a key!",
175+
"a_password": "changeme",
176+
"my_token": "a_token",
177+
"nested": map[string]string{
178+
"4242": "4242",
179+
"4242key": "4242key",
180+
"4242password": "4242password",
181+
"4242certificate": "4242certificate",
182+
},
183+
},
184+
wantRedacted: []string{
185+
"some_key", "a_password", "my_token", "4242key", "4242password", "4242certificate"},
186+
},
187+
}
188+
189+
for _, tt := range tests {
190+
t.Run(tt.name, func(t *testing.T) {
191+
got, err := redact(tt.arg)
192+
require.NoError(t, err)
193+
194+
for k, v := range got {
195+
if contains(tt.wantRedacted, k) {
196+
assert.Equal(t, v, REDACTED)
197+
} else {
198+
assert.NotEqual(t, v, REDACTED)
199+
}
200+
}
201+
})
202+
}
203+
}
204+
205+
func contains(list []string, val string) bool {
206+
for _, k := range list {
207+
if val == k {
208+
return true
209+
}
210+
}
211+
212+
return false
213+
}

0 commit comments

Comments
 (0)