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:
@@ -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=
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user