Analyze TUI (#3172)

* Setup TUI entrypoint

* Setup key type selector and form pages

* Add basic confirmation component

* Add basic list selector for analyzer type

* Add form page

* Remove quit confirmation

* Add styles

* Add input text redaction

* Add log file input to form

* Fix some bugs and race conditions

* Remove unused code

* Fix filtering bug
This commit is contained in:
Miccah
2024-08-05 15:00:46 -07:00
committed by GitHub
parent b8cbb4dc72
commit 59fccbcf3f
10 changed files with 571 additions and 272 deletions
-6
View File
@@ -708,8 +708,6 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/depaware v0.0.0-20240804103531-585336c3e1b3 h1:1wRadBXZ4ddC71FYpzDTS3weuj0IOgmeptZtgTDGU7g=
github.com/tailscale/depaware v0.0.0-20240804103531-585336c3e1b3/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME=
@@ -914,8 +912,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -927,8 +923,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+1 -50
View File
@@ -37,57 +37,8 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/version"
)
// usageTemplate is a copy of kingpin.DefaultUsageTemplate with a minor change
// to not list all flattened commands. This is required to hide all of the
// analyze sub-commands from the main help.
const usageTemplate = `{{define "FormatCommand" -}}
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
{{end -}}
{{define "FormatCommands" -}}
{{range .Commands -}}
{{if not .Hidden -}}
{{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
{{.Help|Wrap 4}}
{{end -}}
{{end -}}
{{end -}}
{{define "FormatUsage" -}}
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
{{if .Help}}
{{.Help|Wrap 0 -}}
{{end -}}
{{end -}}
{{if .Context.SelectedCommand -}}
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}}
{{ else -}}
usage: {{.App.Name}}{{template "FormatUsage" .App}}
{{end}}
{{if .Context.Flags -}}
Flags:
{{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
{{end -}}
{{if .Context.Args -}}
Args:
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
{{end -}}
{{if .Context.SelectedCommand -}}
{{if len .Context.SelectedCommand.Commands -}}
Subcommands:
{{template "FormatCommands" .Context.SelectedCommand}}
{{end -}}
{{else if .App.Commands -}}
Commands:
{{template "FormatCommands" .App}}
{{end -}}
`
var (
cli = kingpin.New("TruffleHog", "TruffleHog is a tool for finding credentials.").UsageTemplate(usageTemplate)
cli = kingpin.New("TruffleHog", "TruffleHog is a tool for finding credentials.")
cmd string
debug = cli.Flag("debug", "Run in debug mode.").Bool()
trace = cli.Flag("trace", "Run in trace mode.").Bool()
+26
View File
@@ -60,6 +60,32 @@ const (
FullAccess string = "full_access"
)
// Sorted list of all available analyzers. Used for valid sub-commands and TUI
// selection. TODO: Change slice type to Analyzer interface when all available
// analyzers implement it.
var AvailableAnalyzers = []string{
"airbrake",
"asana",
"bitbucket",
"github",
"gitlab",
"huggingface",
"mailchimp",
"mailgun",
"mysql",
"openai",
"opsgenie",
"postgres",
"postman",
"sendgrid",
"shopify",
"slack",
"sourcegraph",
"square",
"stripe",
"twilio",
}
type PermissionStatus struct {
Value bool
IsError bool
+59 -208
View File
@@ -1,6 +1,9 @@
package analyzer
import (
"fmt"
"strings"
"github.com/alecthomas/kingpin/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake"
@@ -24,227 +27,75 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/stripe"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/twilio"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/tui"
)
var (
// TODO: Add list of supported key types.
list *kingpin.CmdClause
showAll *bool
log *bool
githubScan *kingpin.CmdClause
githubKey *string
sendgridScan *kingpin.CmdClause
sendgridKey *string
openAIScan *kingpin.CmdClause
openaiKey *string
postgresScan *kingpin.CmdClause
postgresConnectionStr *string
mysqlScan *kingpin.CmdClause
mysqlConnectionStr *string
// mongodbScan *kingpin.CmdClause
// mongodbConnectionStr *string
slackScan *kingpin.CmdClause
slackKey *string
twilioScan *kingpin.CmdClause
twilioKey *string
airbrakeScan *kingpin.CmdClause
airbrakeKey *string
huggingfaceScan *kingpin.CmdClause
huggingfaceKey *string
stripeScan *kingpin.CmdClause
stripeKey *string
gitlabScan *kingpin.CmdClause
gitlabKey *string
mailchimpScan *kingpin.CmdClause
mailchimpKey *string
// mandrillScan *kingpin.CmdClause
// mandrillKey *string
postmanScan *kingpin.CmdClause
postmanKey *string
bitbucketScan *kingpin.CmdClause
bitbucketKey *string
asanaScan *kingpin.CmdClause
asanaKey *string
mailgunScan *kingpin.CmdClause
mailgunKey *string
squareScan *kingpin.CmdClause
squareKey *string
sourcegraphScan *kingpin.CmdClause
sourcegraphKey *string
shopifyScan *kingpin.CmdClause
shopifyKey *string
shopifyStoreURL *string
opsgenieScan *kingpin.CmdClause
opsgenieKey *string
analyzeKeyType *string
)
func Command(app *kingpin.Application) *kingpin.CmdClause {
// TODO: Add list of supported key types.
cli := app.Command("analyze", "Analyze API keys for fine-grained permissions information").Hidden()
list = cli.Command("list", "List supported API providers")
showAll = cli.Flag("show-all", "Show all data, including permissions not available to this account + publicly-available data related to this account.").Default("false").Bool()
log = cli.Flag("log", "Log all HTTP requests sent during analysis to a file").Default("false").Bool()
cli := app.Command("analyze", "Analyze API keys for fine-grained permissions information.").Hidden()
githubScan = cli.Command("github", "Scan a GitHub API key")
githubKey = githubScan.Arg("key", "GitHub Key.").Required().String()
sendgridScan = cli.Command("sendgrid", "Scan a Sendgrid API key")
sendgridKey = sendgridScan.Arg("key", "Sendgrid Key.").Required().String()
openAIScan = cli.Command("openai", "Scan an OpenAI API key")
openaiKey = openAIScan.Arg("key", "OpenAI Key.").Required().String()
postgresScan = cli.Command("postgres", "Scan a Postgres connection string")
postgresConnectionStr = postgresScan.Arg("connection-string", "Postgres Connection String. As a reference, here's an example: postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]").Required().String()
mysqlScan = cli.Command("mysql", "Scan a MySQL connection string")
mysqlConnectionStr = mysqlScan.Arg("connection-string", "MySQL Connection String. As a reference, here's an example: mysql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]").Required().String()
// mongodbScan = cli.Command("mongodb", "Scan a MongoDB connection string")
// mongodbConnectionStr = mongodbScan.Arg("connection-string", "MongoDB Connection String. As a reference, here's an example: mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[defaultauthdb][?options]]").Required().String()
slackScan = cli.Command("slack", "Scan a Slack API key")
slackKey = slackScan.Arg("key", "Slack Key.").Required().String()
twilioScan = cli.Command("twilio", "Scan a Twilio API key")
twilioKey = twilioScan.Arg("key", "Twilio API Key SID & Secret (ex: keySID:keySecret).").Required().String()
airbrakeScan = cli.Command("airbrake", "Scan an Airbrake User Key or Token")
airbrakeKey = airbrakeScan.Arg("key", "Airbrake User Key or Token.").Required().String()
huggingfaceScan = cli.Command("huggingface", "Scan a Huggingface API key")
huggingfaceKey = huggingfaceScan.Arg("key", "Huggingface Key.").Required().String()
stripeScan = cli.Command("stripe", "Scan a Stripe API key")
stripeKey = stripeScan.Arg("key", "Stripe Key.").Required().String()
gitlabScan = cli.Command("gitlab", "Scan a GitLab API key")
gitlabKey = gitlabScan.Arg("key", "GitLab Key.").Required().String()
mailchimpScan = cli.Command("mailchimp", "Scan a Mailchimp API key")
mailchimpKey = mailchimpScan.Arg("key", "Mailchimp Key.").Required().String()
// mandrillScan = cli.Command("mandrill", "Scan a Mandrill API key")
// mandrillKey = mandrillScan.Arg("key", "Mandril Key.").Required().String()
postmanScan = cli.Command("postman", "Scan a Postman API key")
postmanKey = postmanScan.Arg("key", "Postman Key.").Required().String()
bitbucketScan = cli.Command("bitbucket", "Scan a Bitbucket Access Token")
bitbucketKey = bitbucketScan.Arg("key", "Bitbucket Access Token.").Required().String()
asanaScan = cli.Command("asana", "Scan an Asana API key")
asanaKey = asanaScan.Arg("key", "Asana Key.").Required().String()
mailgunScan = cli.Command("mailgun", "Scan a Mailgun API key")
mailgunKey = mailgunScan.Arg("key", "Mailgun Key.").Required().String()
squareScan = cli.Command("square", "Scan a Square API key")
squareKey = squareScan.Arg("key", "Square Key.").Required().String()
sourcegraphScan = cli.Command("sourcegraph", "Scan a Sourcegraph Access Token")
sourcegraphKey = sourcegraphScan.Arg("key", "Sourcegraph Access Token.").Required().String()
shopifyScan = cli.Command("shopify", "Scan a Shopify API key")
shopifyKey = shopifyScan.Arg("key", "Shopify Key.").Required().String()
shopifyStoreURL = shopifyScan.Arg("store-url", "Shopify Store Domain (ex: 22297c-c6.myshopify.com).").Required().String()
opsgenieScan = cli.Command("opsgenie", "Scan an Opsgenie API key")
opsgenieKey = opsgenieScan.Arg("key", "Opsgenie Key.").Required().String()
keyTypeHelp := fmt.Sprintf(
"Type of key to analyze. Omit to interactively choose. Available key types: %s",
strings.Join(analyzers.AvailableAnalyzers, ", "),
)
analyzeKeyType = cli.Arg("key-type", keyTypeHelp).Enum(analyzers.AvailableAnalyzers...)
return cli
}
func Run(cmd string) {
// Initialize configuration
cfg := &config.Config{
LoggingEnabled: *log,
ShowAll: *showAll,
keyType, secretInfo, err := tui.Run(*analyzeKeyType)
if err != nil {
// TODO: Log error.
return
}
switch cmd {
case list.FullCommand():
panic("todo")
case githubScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("github")
github.AnalyzeAndPrintPermissions(cfg, *githubKey)
case sendgridScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("sendgrid")
sendgrid.AnalyzeAndPrintPermissions(cfg, *sendgridKey)
case openAIScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("openai")
openai.AnalyzeAndPrintPermissions(cfg, *openaiKey)
case postgresScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("postgres")
postgres.AnalyzeAndPrintPermissions(cfg, *postgresConnectionStr)
case mysqlScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("mysql")
mysql.AnalyzeAndPrintPermissions(cfg, *mysqlConnectionStr)
case slackScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("slack")
slack.AnalyzeAndPrintPermissions(cfg, *slackKey)
case twilioScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("twilio")
twilio.AnalyzeAndPrintPermissions(cfg, *twilioKey)
case airbrakeScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("airbrake")
airbrake.AnalyzeAndPrintPermissions(cfg, *airbrakeKey)
case huggingfaceScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("huggingface")
huggingface.AnalyzeAndPrintPermissions(cfg, *huggingfaceKey)
case stripeScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("stripe")
stripe.AnalyzeAndPrintPermissions(cfg, *stripeKey)
case gitlabScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("gitlab")
gitlab.AnalyzeAndPrintPermissions(cfg, *gitlabKey)
case mailchimpScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("mailchimp")
mailchimp.AnalyzeAndPrintPermissions(cfg, *mailchimpKey)
case postmanScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("postman")
postman.AnalyzeAndPrintPermissions(cfg, *postmanKey)
case bitbucketScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("bitbucket")
bitbucket.AnalyzeAndPrintPermissions(cfg, *bitbucketKey)
case asanaScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("asana")
asana.AnalyzeAndPrintPermissions(cfg, *asanaKey)
case mailgunScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("mailgun")
mailgun.AnalyzeAndPrintPermissions(cfg, *mailgunKey)
case squareScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("square")
square.AnalyzeAndPrintPermissions(cfg, *squareKey)
case sourcegraphScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("sourcegraph")
sourcegraph.AnalyzeAndPrintPermissions(cfg, *sourcegraphKey)
case shopifyScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("shopify")
shopify.AnalyzeAndPrintPermissions(cfg, *shopifyKey, *shopifyStoreURL)
case opsgenieScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("opsgenie")
opsgenie.AnalyzeAndPrintPermissions(cfg, *opsgenieKey)
if secretInfo.Cfg == nil {
secretInfo.Cfg = &config.Config{}
}
switch keyType {
case "github":
github.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "sendgrid":
sendgrid.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "openai":
openai.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "postgres":
postgres.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mysql":
mysql.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "slack":
slack.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "twilio":
twilio.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "airbrake":
airbrake.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "huggingface":
huggingface.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "stripe":
stripe.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "gitlab":
gitlab.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mailchimp":
mailchimp.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "postman":
postman.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "bitbucket":
bitbucket.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "asana":
asana.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "mailgun":
mailgun.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "square":
square.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "sourcegraph":
sourcegraph.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "shopify":
shopify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"], secretInfo.Parts["url"])
case "opsgenie":
opsgenie.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
}
}
+100
View File
@@ -0,0 +1,100 @@
package tui
import (
"fmt"
"slices"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles"
)
type FormPage struct {
Common *common.Common
KeyType string
form textinputs.Model
}
func NewFormPage(c *common.Common, keyType string) FormPage {
inputs := []textinputs.InputConfig{{
Label: "Secret",
Key: "key",
Required: true,
RedactInput: true,
}}
if keyType == "shopify" {
inputs = append(inputs, textinputs.InputConfig{
Label: "Shopify URL",
Key: "url",
Required: true,
})
}
inputs = append(inputs, textinputs.InputConfig{
Label: "Log file",
Help: "Log HTTP requests that analysis performs to this file",
Key: "log_file",
})
form := textinputs.New(inputs).
SetHeader(titleStyle.Render(fmt.Sprintf("Configuring %s analyzer", keyType))).
SetFooter("⚠️ Running TruffleHog Analyze will send a lot of requests ⚠️").
SetSubmitMsg("Run TruffleHog Analyze")
return FormPage{
Common: c,
KeyType: keyType,
form: form,
}
}
func (FormPage) Init() tea.Cmd {
return nil
}
func (ui FormPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// TODO: Check form focus.
if msg, ok := msg.(tea.KeyMsg); ok {
switch {
case key.Matches(msg, ui.Common.KeyMap.Back):
return ui.PrevPage()
}
}
if _, ok := msg.(textinputs.SelectNextMsg); ok {
values := make(map[string]string)
for k, v := range ui.form.GetInputs() {
values[k] = v.Value
}
secretInfoCmd := func() tea.Msg {
// TODO: Set Config
logFile := values["log_file"]
cfg := config.Config{
LogFile: logFile,
LoggingEnabled: logFile != "",
}
return SecretInfo{Cfg: &cfg, Parts: values}
}
return nil, secretInfoCmd
}
form, cmd := ui.form.Update(msg)
ui.form = form.(textinputs.Model)
return ui, cmd
}
func (ui FormPage) View() string {
return styles.AppStyle.Render(ui.form.View())
}
func (ui FormPage) PrevPage() (tea.Model, tea.Cmd) {
page := NewKeyTypePage(ui.Common)
// Select what was previously selected.
index, ok := slices.BinarySearch(analyzers.AvailableAnalyzers, ui.KeyType)
if !ok {
// Should be impossible.
index = 0
}
page.list.Select(index)
return page, nil
}
+88
View File
@@ -0,0 +1,88 @@
package tui
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles"
)
var (
selectedItemStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(lipgloss.AdaptiveColor{Dark: styles.Colors["sprout"], Light: styles.Colors["bronze"]}).
Foreground(lipgloss.AdaptiveColor{Dark: styles.Colors["sprout"], Light: styles.Colors["fern"]}).
Padding(0, 0, 0, 1)
titleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFDF5")).
Background(lipgloss.Color(styles.Colors["bronze"])).
Padding(0, 1)
)
type KeyTypePage struct {
Common *common.Common
list list.Model
}
func (ui KeyTypePage) Init() tea.Cmd {
return nil
}
func NewKeyTypePage(c *common.Common) KeyTypePage {
items := make([]list.Item, len(analyzers.AvailableAnalyzers))
for i, analyzerType := range analyzers.AvailableAnalyzers {
items[i] = KeyTypeItem(analyzerType)
}
delegate := list.NewDefaultDelegate()
delegate.ShowDescription = false
delegate.SetSpacing(0)
delegate.Styles.SelectedTitle = selectedItemStyle
list := list.New(items, delegate, c.Width, c.Height)
list.Title = "Select an analyzer type"
list.SetShowStatusBar(false)
list.Styles.Title = titleStyle
return KeyTypePage{
Common: c,
list: list,
}
}
func (ui KeyTypePage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !ui.list.SettingFilter() {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, ui.Common.KeyMap.Back):
return nil, tea.Quit
case key.Matches(msg, ui.Common.KeyMap.Select):
chosen := string(ui.list.SelectedItem().(KeyTypeItem))
return NewFormPage(ui.Common, chosen), SetKeyTypeCmd(chosen)
}
}
}
var cmd tea.Cmd
ui.list, cmd = ui.list.Update(msg)
return ui, cmd
}
func (ui KeyTypePage) View() string {
return styles.AppStyle.Render(ui.list.View())
}
func (ui KeyTypePage) NextPage(keyType string) (tea.Model, tea.Cmd) {
return NewFormPage(ui.Common, keyType), SetKeyTypeCmd(keyType)
}
type KeyTypeItem string
func (i KeyTypeItem) ID() string { return string(i) }
func (i KeyTypeItem) Title() string { return string(i) }
func (i KeyTypeItem) Description() string { return "" }
func (i KeyTypeItem) FilterValue() string { return string(i) }
+114
View File
@@ -0,0 +1,114 @@
package tui
import (
"errors"
"fmt"
"slices"
tea "github.com/charmbracelet/bubbletea"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/keymap"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles"
)
// TUI is the main TUI model.
type TUI struct {
keyType string
secretInfo *SecretInfo
common *common.Common
model tea.Model
}
type SecretInfo struct {
Parts map[string]string
Cfg *config.Config
}
var AbortError error = errors.New("command aborted")
func Run(keyType string) (string, *SecretInfo, error) {
// If a keyType is provided, make sure it's in the list of AvailableAnalyzers.
if keyType != "" {
if _, ok := slices.BinarySearch(analyzers.AvailableAnalyzers, keyType); !ok {
return "", nil, fmt.Errorf("Unrecognized command %q", keyType)
}
}
t := &TUI{
keyType: keyType,
common: &common.Common{
KeyMap: keymap.DefaultKeyMap(),
},
}
if _, err := tea.NewProgram(t).Run(); err != nil {
return "", nil, err
}
if t.secretInfo == nil {
return "", nil, AbortError
}
return t.keyType, t.secretInfo, nil
}
func (ui *TUI) Init() tea.Cmd {
return nil
}
func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := msg.(tea.WindowSizeMsg); ok {
ui.SetSize(msg)
}
// Always be able to force quit.
if msg, ok := msg.(tea.KeyMsg); ok && msg.Type.String() == "ctrl+c" {
return ui, tea.Quit
}
switch m := msg.(type) {
case SetKeyTypeMsg:
ui.keyType = string(m)
case SecretInfo:
ui.secretInfo = &m
return ui, tea.Quit
}
if ui.model == nil {
return ui, nil
}
var cmd tea.Cmd
ui.model, cmd = ui.model.Update(msg)
return ui, cmd
}
func (ui *TUI) View() string {
if ui.model == nil {
return "Loading..."
}
return ui.model.View()
}
func (ui *TUI) SetSize(msg tea.WindowSizeMsg) {
h, v := styles.AppStyle.GetFrameSize()
h, v = msg.Width-h, msg.Height-v
ui.common.SetSize(h, v)
if ui.model != nil {
return
}
// Set the model only after we have size information.
// TODO: Responsive pages.
if ui.keyType == "" {
ui.model = NewKeyTypePage(ui.common)
} else {
ui.model = NewFormPage(ui.common, ui.keyType)
}
}
type SetKeyTypeMsg string
func SetKeyTypeCmd(keyType string) tea.Cmd {
return func() tea.Msg {
return SetKeyTypeMsg(keyType)
}
}
+115
View File
@@ -0,0 +1,115 @@
package confirm
import (
"fmt"
"unicode"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/common"
)
type Confirm struct {
common common.Common
message string
affirmativeChoice string
negativeChoice string
affirmativeUpdate func() (tea.Model, tea.Cmd)
negativeUpdate func() (tea.Model, tea.Cmd)
choice bool
}
type Msg struct {
Choice bool
Message string
}
type ConfirmOpt func(*Confirm)
func WithAffirmativeMessage(msg string) ConfirmOpt {
return func(c *Confirm) { c.affirmativeChoice = msg }
}
func WithNegativeMessage(msg string) ConfirmOpt {
return func(c *Confirm) { c.negativeChoice = msg }
}
func WithDefault(choice bool) ConfirmOpt {
return func(c *Confirm) { c.choice = choice }
}
func WithAffirmativeTransition(m tea.Model, cmd tea.Cmd) ConfirmOpt {
return func(c *Confirm) {
c.affirmativeUpdate = func() (tea.Model, tea.Cmd) {
return m, cmd
}
}
}
func WithNegativeTransition(m tea.Model, cmd tea.Cmd) ConfirmOpt {
return func(c *Confirm) {
c.negativeUpdate = func() (tea.Model, tea.Cmd) {
return m, cmd
}
}
}
func New(c common.Common, msg string, opts ...ConfirmOpt) Confirm {
confirm := Confirm{
common: c,
message: msg,
affirmativeChoice: "Yes",
negativeChoice: "Cancel",
affirmativeUpdate: func() (tea.Model, tea.Cmd) { return nil, nil },
negativeUpdate: func() (tea.Model, tea.Cmd) { return nil, nil },
}
for _, opt := range opts {
opt(&confirm)
}
return confirm
}
func (Confirm) Init() tea.Cmd {
return nil
}
func (c Confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
keyMsg, ok := msg.(tea.KeyMsg)
if !ok {
return c, nil
}
switch {
case key.Matches(keyMsg, c.common.KeyMap.Left) || keyMatchesFirstChar(keyMsg, c.negativeChoice):
c.choice = false
case key.Matches(keyMsg, c.common.KeyMap.Right) || keyMatchesFirstChar(keyMsg, c.affirmativeChoice):
c.choice = true
case key.Matches(keyMsg, c.common.KeyMap.Select):
model, cmd := c.negativeUpdate()
if c.choice {
model, cmd = c.affirmativeUpdate()
}
return model, cmd
}
return c, nil
}
func (c Confirm) View() string {
var affirmative, negative string
if c.choice {
affirmative = fmt.Sprintf("[ %s ]", c.affirmativeChoice)
negative = fmt.Sprintf(" %s ", c.negativeChoice)
} else {
affirmative = fmt.Sprintf(" %s ", c.affirmativeChoice)
negative = fmt.Sprintf("[ %s ]", c.negativeChoice)
}
return fmt.Sprintf("%s\t%s\t%s", c.message, negative, affirmative)
}
func keyMatchesFirstChar(msg tea.KeyMsg, s string) bool {
if s == "" {
return false
}
firstChar := rune(s[0])
return msg.String() == string(unicode.ToLower(firstChar))
}
+44 -8
View File
@@ -19,8 +19,6 @@ var (
// cursorStyle = focusedStyle.Copy()
// cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
focusedButton = focusedStyle.Copy().Render("[ Next ]")
blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Next"))
focusedSkipButton = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("[ Run with defaults ]")
blurredSkipButton = fmt.Sprintf("[ %s ]", lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("Run with defaults"))
)
@@ -37,6 +35,9 @@ type Model struct {
configs []InputConfig
// cursorMode cursor.Mode
skipButton bool
submitMsg string
header string
footer string
}
type InputConfig struct {
@@ -45,6 +46,7 @@ type InputConfig struct {
Help string
Required bool
Placeholder string
RedactInput bool
}
type Input struct {
@@ -80,7 +82,8 @@ func (m Model) GetLabels() map[string]string {
func New(config []InputConfig) Model {
m := Model{
inputs: make([]textinput.Model, len(config)),
inputs: make([]textinput.Model, len(config)),
submitMsg: "Next",
}
for i, conf := range config {
@@ -171,6 +174,10 @@ func (m *Model) updateInputs(msg tea.Msg) tea.Cmd {
func (m Model) View() string {
var b strings.Builder
if m.header != "" {
fmt.Fprintf(&b, "%s\n\n", m.header)
}
if m.skipButton {
button := &blurredSkipButton
if m.focusIndex == -1 {
@@ -184,18 +191,32 @@ func (m Model) View() string {
b.WriteString(m.GetLabel(m.configs[i]))
}
b.WriteString(m.inputs[i].View())
input := m.inputs[i]
if val := input.Value(); len(val) > 4 && m.configs[i].RedactInput {
if len(val) > 10 {
// start***end
input.SetValue(val[:4] + strings.Repeat("*", len(val)-8) + val[len(val)-4:])
} else {
// start***
input.SetValue(val[:4] + strings.Repeat("*", len(val)-4))
}
}
b.WriteString(input.View())
b.WriteRune('\n')
if i < len(m.inputs)-1 {
b.WriteRune('\n')
}
}
button := &blurredButton
if m.focusIndex == len(m.inputs) {
button = &focusedButton
if m.footer != "" {
fmt.Fprintf(&b, "\n\n%s", m.footer)
}
fmt.Fprintf(&b, "\n\n%s\n\n", *button)
button := blurredStyle.Render(m.submitMsg)
if m.focusIndex == len(m.inputs) {
button = focusedStyle.Render(fmt.Sprintf("[ %s ]", m.submitMsg))
}
fmt.Fprintf(&b, "\n\n%s\n\n", button)
return b.String()
}
@@ -227,6 +248,21 @@ func (m Model) SetSkip(skip bool) Model {
return m
}
func (m Model) SetSubmitMsg(msg string) Model {
m.submitMsg = msg
return m
}
func (m Model) SetFooter(foot string) Model {
m.footer = foot
return m
}
func (m Model) SetHeader(head string) Model {
m.header = head
return m
}
func (m *Model) unfocusInput(index int) {
m.inputs[index].Blur()
m.inputs[index].PromptStyle = noStyle
+24
View File
@@ -6,6 +6,8 @@ import "github.com/charmbracelet/bubbles/key"
type KeyMap struct {
Quit key.Binding
CmdQuit key.Binding
Left key.Binding
Right key.Binding
Up key.Binding
Down key.Binding
UpDown key.Binding
@@ -84,6 +86,28 @@ func DefaultKeyMap() *KeyMap {
),
)
km.Left = key.NewBinding(
key.WithKeys(
"left",
"h",
),
key.WithHelp(
"←/h",
"left",
),
)
km.Right = key.NewBinding(
key.WithKeys(
"right",
"l",
),
key.WithHelp(
"→/l",
"right",
),
)
km.LeftRight = key.NewBinding(
key.WithKeys(
"left",