Skip to content

Commit

Permalink
redact: allow users to redact sensitive info locally
Browse files Browse the repository at this point in the history
  • Loading branch information
joshi4 committed Oct 22, 2024
1 parent 63eb1ea commit 2f63df7
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ cli:
cli_race:
go build -race -ldflags "-X github.com/getsavvyinc/savvy-cli/config.version=$(version)" -o savvy .
cli_dev:
go build -race -ldflags "-X github.com/getsavvyinc/savvy-cli/config.version=$(version)" -tags dev -o savvy-dev .
go build -ldflags "-X github.com/getsavvyinc/savvy-cli/config.version=$(version)" -tags dev -o savvy-dev .

cli_dev_debug:
go build -race -ldflags "-X github.com/getsavvyinc/savvy-cli/config.version=$(version)" -gcflags="-N -l" -tags dev -o savvy-dev .
Expand Down
35 changes: 22 additions & 13 deletions cmd/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ import (
"github.com/getsavvyinc/savvy-cli/client"
"github.com/getsavvyinc/savvy-cli/cmd/component"
"github.com/getsavvyinc/savvy-cli/display"
"github.com/getsavvyinc/savvy-cli/redact"
"github.com/getsavvyinc/savvy-cli/server"
"github.com/getsavvyinc/savvy-cli/shell"
"github.com/getsavvyinc/savvy-cli/theme"
"github.com/muesli/cancelreader"
"github.com/spf13/cobra"
)

// historyCmd represents the history command
var historyCmd = &cobra.Command{
Use: "history",
Short: "Create a runbook from your shell history",
Long: `Create a runbook from a selection of the last 100 commands in your shell history.
Short: "Create an artifact from your shell history",
Long: `Create an artifact from a selection of the last 100 commands in your shell history.
Savvy can expand all aliases used in your shell history without running the commands.`,
Hidden: true,
Run: recordHistory,
Expand All @@ -46,7 +48,7 @@ func recordHistory(cmd *cobra.Command, _ []string) {

cl, err := client.GetLoggedInClient()
if err != nil && errors.Is(err, client.ErrInvalidClient) {
display.Error(errors.New("You must be logged in to record a runbook. Please run `savvy login`"))
display.Error(errors.New("You must be logged in to create an artifact. Please run `savvy login`"))
os.Exit(1)
} else if err != nil {
display.ErrorWithSupportCTA(err)
Expand All @@ -67,16 +69,16 @@ func recordHistory(cmd *cobra.Command, _ []string) {
gm := component.NewGenerateRunbookModel(historyCmds, cl)
p := tea.NewProgram(gm, tea.WithOutput(programOutput), tea.WithContext(gctx))
if _, err := p.Run(); err != nil {
err = fmt.Errorf("failed to generate runbook: %w", err)
err = fmt.Errorf("failed to generate artifact: %w", err)
display.ErrorWithSupportCTA(err)
os.Exit(1)
}

// ensure the bubble tea program is finished before we start the next one
logger.Debug("wait for bubbletea program", "component", "generate runbook", "status", "running")
logger.Debug("wait for bubbletea program", "component", "generate artifact", "status", "running")
cancel()
p.Wait()
logger.Debug("wait for bubbletea program", "component", "generate runbook", "status", "finished")
logger.Debug("wait for bubbletea program", "component", "generate artifact", "status", "finished")

runbook := <-gm.RunbookCh()
m, err := newDisplayCommandsModel(runbook)
Expand All @@ -86,12 +88,12 @@ func recordHistory(cmd *cobra.Command, _ []string) {

p = tea.NewProgram(m, tea.WithOutput(programOutput), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
logger.Debug("failed to display runbook", "error", err.Error())
display.Info("View and edit your runbook online at: " + runbook.URL)
logger.Debug("failed to display artifact", "error", err.Error())
display.Info("View and edit your artifact online at: " + runbook.URL)
os.Exit(1)
}
if runbook.URL != "" {
display.Success("View and edit your runbook online at: " + runbook.URL)
display.Success("View and edit your artifact online at: " + runbook.URL)
}
}

Expand All @@ -110,15 +112,15 @@ func allowUserToSelectCommands(logger *slog.Logger, history []string) []string {
form := huh.NewForm(
huh.NewGroup(
huh.NewMultiSelect[selectableCommand]().
Title("Savvy History").
Description("Press x to include/exclude commands in your Runbook. Selected Commands will NOT be executed.").
Title("Select Commands to Create an Artifact").
Description("Press x to add/remove commands").
Value(&selectedOptions).
Height(33).
Options(options...),
),
)

if err := form.Run(); err != nil {
if err := form.WithTheme(theme.New()).Run(); err != nil {
logger.Debug("failed to run form", "error", err.Error())
return nil
}
Expand Down Expand Up @@ -269,5 +271,12 @@ func selectAndExpandHistory(ctx context.Context, logger *slog.Logger) ([]*server
}).Run(); err != nil {
logger.Debug("failed to run spinner", "error", err.Error())
}
return commands, nil

redacted, err := redact.Commands(commands)
if err != nil {
logger.Debug("failed to redact commands", "error", err.Error())
return nil, err
}

return redacted, nil
}
9 changes: 8 additions & 1 deletion cmd/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/getsavvyinc/savvy-cli/cmd/component"
"github.com/getsavvyinc/savvy-cli/cmd/component/list"
"github.com/getsavvyinc/savvy-cli/display"
"github.com/getsavvyinc/savvy-cli/redact"
"github.com/getsavvyinc/savvy-cli/shell"
"github.com/muesli/cancelreader"
"github.com/muesli/termenv"
Expand Down Expand Up @@ -72,8 +73,14 @@ func runRecordCmd(cmd *cobra.Command, _ []string) {
return
}

redactedCommands, err := redact.Commands(recordedCommands)
if err != nil {
display.ErrorWithSupportCTA(err)
return
}

gctx, cancel := context.WithCancel(ctx)
gm := component.NewGenerateRunbookModel(recordedCommands, cl)
gm := component.NewGenerateRunbookModel(redactedCommands, cl)
p := tea.NewProgram(gm, tea.WithOutput(programOutput), tea.WithContext(gctx))
if _, err := p.Run(); err != nil {
err = fmt.Errorf("failed to generate runbook: %w", err)
Expand Down
61 changes: 61 additions & 0 deletions redact/redact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package redact

import (
"fmt"
"strconv"

"github.com/charmbracelet/huh"
"github.com/getsavvyinc/savvy-cli/server"
"github.com/getsavvyinc/savvy-cli/slice"
"github.com/getsavvyinc/savvy-cli/theme"
)

// Commands allows users to redact one or more commands.
// Commands returns a new slice of commands with the sensitive data redacted or removed.
//
// NOTE: It is possible for users to completely remove some commands.
func Commands(cmds []*server.RecordedCommand) ([]*server.RecordedCommand, error) {
var fs []huh.Field
description := "Replace sensitive data with <placeholders>. To remove a command, simply delete the text."
note := huh.NewNote().Title("Redact Secrets and PII").Description(description)
fs = append(fs, note)

for i, cmd := range cmds {
fs = append(fs, RedactCommand(cmd.Command, strconv.Itoa(i)))
}

customTheme := theme.New()

group := huh.NewGroup(fs...).Title("Redact Commands").WithTheme(customTheme)

if err := huh.NewForm(group).WithTheme(customTheme).Run(); err != nil {
err := fmt.Errorf("failed to run redaction form: %w", err)
return nil, err
}

for _, f := range fs {
in, ok := f.(*huh.Input)
if !ok {
continue
}
strVal, ok := in.GetValue().(string)
if !ok {
continue
}

idx, err := strconv.Atoi(in.GetKey())
if err != nil {
continue
}
cmds[idx].Command = strVal
}

redacted := slice.Filter(cmds, func(cmd *server.RecordedCommand) bool {
return cmd.Command != "" || cmd.FileInfo != nil
})
return redacted, nil
}

func RedactCommand(cmd string, key string) huh.Field {
return huh.NewInput().Value(&cmd).Key(key).WithTheme(theme.New())
}
36 changes: 36 additions & 0 deletions theme/theme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package theme

import (
catppuccin "github.com/catppuccin/go"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)

func New() *huh.Theme {
t := huh.ThemeDracula()

light := catppuccin.Latte
dark := catppuccin.Mocha
var (
subtext0 = lipgloss.AdaptiveColor{Light: light.Subtext0().Hex, Dark: dark.Subtext0().Hex}
overlay1 = lipgloss.AdaptiveColor{Light: light.Overlay1().Hex, Dark: dark.Overlay1().Hex}
)

f := &t.Focused
f.SelectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#02CF92", Dark: "#02A877"}).SetString("✓ ")
f.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ")

// light := catppuccin.Latte
// dark := catppuccin.Mocha
// green = lipgloss.AdaptiveColor{Light: light.Green().Hex, Dark: dark.Green().Hex}

t.Help.Ellipsis.Foreground(subtext0)
t.Help.ShortKey.Foreground(subtext0)
t.Help.ShortDesc.Foreground(overlay1)
t.Help.ShortSeparator.Foreground(subtext0)
t.Help.FullKey.Foreground(subtext0)
t.Help.FullDesc.Foreground(overlay1)
t.Help.FullSeparator.Foreground(subtext0)

return t
}

0 comments on commit 2f63df7

Please sign in to comment.