diff --git a/internal/link/calls.go b/internal/link/calls.go index 1698df15..fa1d4740 100644 --- a/internal/link/calls.go +++ b/internal/link/calls.go @@ -468,6 +468,31 @@ func kept(args json.RawMessage) json.RawMessage { if words, ok := v.([]any); ok && len(words) > 0 { out[k] = []any{words[0], "(the rest given, not kept)"} } + case k == "command": + // The controller's own command line: its first word, never the rest, as a mesh-cli line's + // first word is. `settings set '' --node ` through the `command` verb + // put the value itself in this record, which answers anyone who may call the seat (novox/hq + // issue 397). Its words are parted by any whitespace, because the verb's own splitter parts + // them on a space, a tab or a newline, and a line written with tabs is the same line. The + // first word is capped as every other string here is: the record is written before the verb + // judges the line, so one word may be anything a caller sent, a token included. + if !isString { + out[k] = "(given, not kept)" + break + } + words := strings.Fields(s) + if len(words) == 0 { + out[k] = "" + break + } + first := words[0] + if len(first) > 120 { + first = first[:120] + "…" + } + if len(words) > 1 { + first += " (the rest given, not kept)" + } + out[k] = first case isString && len(s) <= 120: out[k] = s case isString: diff --git a/internal/link/calls_test.go b/internal/link/calls_test.go index 9c2d83fd..419d6800 100644 --- a/internal/link/calls_test.go +++ b/internal/link/calls_test.go @@ -11,6 +11,7 @@ import ( "sync" "testing" "time" + "unicode" "github.com/nats-io/nats.go" @@ -166,6 +167,55 @@ func TestACallKeepsNoSettingsOrSecrets(t *testing.T) { } } +// A command line's own words may carry a setting's value or a secret, so only its first word is kept +// — as a mesh-cli line's is (novox/hq issue 397). Each case says the whole of what is kept, because +// what matters is as much what is left out as what is there. +func TestACallKeepsNoCommandLine(t *testing.T) { + long := strings.Repeat("s3cret", 50) // one word, 300 bytes: a token, as far as this can tell + for line, want := range map[string]string{ + `settings set notes 'the operator's own passphrase' --node laptop`: "settings (the rest given, not kept)", + `settings set notes --values {"token":"s3cret"}`: "settings (the rest given, not kept)", + // Parted by a tab or a newline, which the verb's splitter reads as this line's words too. + "settings\tset\tnotes\tthe-operators-passphrase": "settings (the rest given, not kept)", + "builds\n--limit 5": "builds (the rest given, not kept)", + "builds\t--limit 5": "builds (the rest given, not kept)", + // Its first word is kept, and the spaces before it are not part of it. + ` node show laptop`: "node (the rest given, not kept)", + // A command of one word is kept whole: there is nothing after it to withhold. + `builds`: "builds", + // One word is still capped, as every other string in a kept record is. + long: long[:120] + "…", + } { + raw, err := json.Marshal(map[string]any{"command": line}) + if err != nil { + t.Fatal(err) + } + var got struct{ Command string } + if err := json.Unmarshal(kept(raw), &got); err != nil { + t.Fatal(err) + } + if got.Command != want { + t.Errorf("%q is kept as %q; want %q", line, got.Command, want) + } + // Whole words, so that "set" can be looked for although "settings" is kept, and "show" cannot + // be found in a word such as "shown". + for _, never := range []string{"set", "notes", "laptop", "show", "passphrase", "operators"} { + for _, word := range strings.FieldsFunc(got.Command, func(r rune) bool { return !unicode.IsLetter(r) }) { + if word == never { + t.Errorf("%q is kept as %q: it carries the word %q", line, got.Command, never) + } + } + } + } + + // Not a string, so its first word cannot be taken: none of it is kept. The verb refuses such a + // call, but the record is written before it judges it. + raw, _ := json.Marshal(map[string]any{"command": []string{"settings", "set", "notes", "s3cret"}}) + if got := string(kept(raw)); strings.Contains(got, "s3cret") { + t.Errorf("kept %s", got) + } +} + // Only the newest KeptCalls are kept. func TestTheLogKeepsTheNewest(t *testing.T) { l := NewCallLog()