Skip to content

Commit 50f741a

Browse files
Harden redaction and add FATAL context to error exits
Replace createRedaction with redactField that warns and continues when fields are missing instead of panicking. Add FATAL prefix with caller file:line to checkError and command name to exitWithUsage for easier debugging. Document sensitive field redaction policy in CLAUDE.md and add test coverage table to README.
1 parent e73ecde commit 50f741a

4 files changed

Lines changed: 71 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,22 @@ make govulncheck # dependency vulnerability check
8989
make verify # all of the above
9090
```
9191

92+
## Sensitive Field Redaction
93+
94+
The `showDiff` function in `cf_targets.go` redacts sensitive fields before
95+
displaying unified diffs. Currently redacted fields:
96+
97+
- `AccessToken`
98+
- `RefreshToken`
99+
- `UAAOAuthClientSecret`
100+
101+
These are replaced with `REDACTED sha256(...)` in the diff output via
102+
`createRedaction()`. When the CF CLI's `config.json` schema changes
103+
(e.g., new fields added in `cloudfoundry/cli` `util/configv3/json_config.go`
104+
or `cf/configuration/coreconfig/config_data.go`), check whether any new
105+
fields contain tokens, secrets, passwords, or credentials and add them
106+
to the redaction list in `showDiff`.
107+
92108
## OS Abstraction
93109

94110
The `OS` interface in `cf_targets.go` wraps filesystem operations for

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,14 @@ gmake clean
212212
> functionality is now covered by the Makefile:
213213
> - Development builds: `make build`
214214
> - Release builds: `make ci-release VERSION=x.y.z`
215+
216+
## Test Coverage
217+
218+
| Package | Coverage |
219+
|---------|----------|
220+
| `cf-targets-plugin` | 72.6% |
221+
| `internal/diff` | 83.3% |
222+
| `internal/diff/lcs` | 54.3% |
223+
| **Total** | **68.1%** |
224+
225+
Last updated: 2026-03-05

cf_targets.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"flag"
99
"fmt"
1010
"path/filepath"
11+
"runtime"
1112
"strconv"
1213
"strings"
1314

@@ -252,11 +253,19 @@ func (c *TargetsPlugin) Run(cliConnection plugin.CliConnection, args []string) {
252253
}
253254
}
254255

255-
func createRedaction(jsonMap map[string]interface{}, key string) string {
256-
var valueAssertion interface{}
257-
valueAssertion = jsonMap[key]
258-
currentSum := sha256.Sum256([]byte(valueAssertion.(string)))
259-
return fmt.Sprintf("REDACTED sha256(%x)", currentSum)
256+
func redactField(jsonMap map[string]interface{}, key string) {
257+
val, ok := jsonMap[key]
258+
if !ok {
259+
fmt.Printf("Warning: expected field %q not found in config\n", key)
260+
return
261+
}
262+
str, ok := val.(string)
263+
if !ok {
264+
fmt.Printf("Warning: field %q is not a string\n", key)
265+
return
266+
}
267+
sum := sha256.Sum256([]byte(str))
268+
jsonMap[key] = fmt.Sprintf("REDACTED sha256(%x)", sum)
260269
}
261270

262271
func (c *TargetsPlugin) showDiff(targetPath string) {
@@ -273,14 +282,10 @@ func (c *TargetsPlugin) showDiff(targetPath string) {
273282
err = json.Unmarshal(targetContent, &jsonDataTarget)
274283
c.checkError(err)
275284

276-
jsonDataCurrent["AccessToken"] = createRedaction(jsonDataCurrent, "AccessToken")
277-
jsonDataTarget["AccessToken"] = createRedaction(jsonDataTarget, "AccessToken")
278-
279-
jsonDataCurrent["RefreshToken"] = createRedaction(jsonDataCurrent, "RefreshToken")
280-
jsonDataTarget["RefreshToken"] = createRedaction(jsonDataTarget, "RefreshToken")
281-
282-
jsonDataCurrent["UAAOAuthClientSecret"] = createRedaction(jsonDataCurrent, "UAAOAuthClientSecret")
283-
jsonDataTarget["UAAOAuthClientSecret"] = createRedaction(jsonDataTarget, "UAAOAuthClientSecret")
285+
for _, key := range []string{"AccessToken", "RefreshToken", "UAAOAuthClientSecret"} {
286+
redactField(jsonDataCurrent, key)
287+
redactField(jsonDataTarget, key)
288+
}
284289

285290
current, err := json.MarshalIndent(jsonDataCurrent, "", " ")
286291
c.checkError(err)
@@ -532,7 +537,8 @@ func (c *TargetsPlugin) targetPath(targetName string) string {
532537

533538
func (c *TargetsPlugin) checkError(err error) {
534539
if err != nil {
535-
fmt.Println("Error:", err)
540+
_, file, line, _ := runtime.Caller(1)
541+
fmt.Printf("FATAL: %s:%d: %v\n", filepath.Base(file), line, err)
536542
panic(1)
537543
}
538544
}
@@ -542,6 +548,7 @@ func (c *TargetsPlugin) exitWithUsage(command string) {
542548
for _, candidate := range metadata.Commands {
543549
if candidate.Name == command {
544550
fmt.Println("Usage: " + candidate.UsageDetails.Usage)
551+
fmt.Printf("FATAL: invalid syntax for command %q\n", command)
545552
panic(1)
546553
}
547554
}

cf_targets_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ var _ = Describe("TargetsPlugin", func() {
232232
})
233233
Expect(fakeOS.exitCalled).To(Equal(1))
234234
Expect(fakeOS.exitCalledWithCode).To(Equal(1))
235-
Expect(output).To(ContainSubstrings([]string{"Error:", "permission denied"}))
235+
Expect(output).To(ContainSubstrings([]string{"FATAL:", "permission denied"}))
236236
})
237237
})
238238

@@ -601,6 +601,28 @@ var _ = Describe("TargetsPlugin", func() {
601601
Expect(line).NotTo(ContainSubstring("different-refresh"))
602602
}
603603
})
604+
605+
It("warns and continues when redacted fields are missing", func() {
606+
// JSON without AccessToken, RefreshToken, or UAAOAuthClientSecret
607+
minimalJSON := []byte(`{"Target": "https://api.example.com", "ColorEnabled": "true"}`)
608+
otherJSON := []byte(`{"Target": "https://api.example.com", "ColorEnabled": "false"}`)
609+
610+
fakeOS.readfileShouldReturnMap = map[string][]byte{
611+
targetsPlugin.currentPath: minimalJSON,
612+
targetsPlugin.targetPath("minimal"): otherJSON,
613+
}
614+
615+
output := CaptureOutput(func() {
616+
targetsPlugin.showDiff(targetsPlugin.targetPath("minimal"))
617+
})
618+
// Warnings for missing fields
619+
Expect(output).To(ContainSubstrings([]string{"Warning:", "AccessToken"}))
620+
Expect(output).To(ContainSubstrings([]string{"Warning:", "RefreshToken"}))
621+
Expect(output).To(ContainSubstrings([]string{"Warning:", "UAAOAuthClientSecret"}))
622+
// Diff still runs
623+
Expect(output).To(ContainSubstrings([]string{"---", "Current"}))
624+
Expect(output).To(ContainSubstrings([]string{"+++", "Target"}))
625+
})
604626
})
605627

606628
Describe("Configuration File Manipulation", func() {

0 commit comments

Comments
 (0)