From 5e6b0899711201c6ed039576740e7a87a79bf5a2 Mon Sep 17 00:00:00 2001 From: Laurence Withers Date: Sat, 29 Apr 2023 10:59:15 +0100 Subject: [PATCH] cmd/inspect: finish command implementation --- cmd/inspect/aurora.go | 40 ++++++++ cmd/inspect/display_structured.go | 69 ++++++++++++++ .../{display_coloured.go => display_text.go} | 38 +++----- cmd/inspect/inspect.go | 93 ++++++++++++------- go.mod | 4 +- go.sum | 7 +- 6 files changed, 191 insertions(+), 60 deletions(-) create mode 100644 cmd/inspect/aurora.go create mode 100644 cmd/inspect/display_structured.go rename cmd/inspect/{display_coloured.go => display_text.go} (65%) diff --git a/cmd/inspect/aurora.go b/cmd/inspect/aurora.go new file mode 100644 index 0000000..e73b51e --- /dev/null +++ b/cmd/inspect/aurora.go @@ -0,0 +1,40 @@ +package inspect + +import ( + "fmt" + "os" + "strings" + + "github.com/logrusorgru/aurora/v4" +) + +var ( + aura *aurora.Aurora +) + +func setupAura(coloured bool) { + var hyperlinks bool + if coloured { + switch os.Getenv("TERM") { + case "xterm-kitty": + hyperlinks = true + } + } + aura = aurora.New(aurora.WithColors(coloured), aurora.WithHyperlinks(hyperlinks)) +} + +func displayErr(err error, filename string) { + fmt.Fprintf(os.Stderr, "%s: ", displayFilename(filename)) + fmt.Fprintf(os.Stderr, "%v\n", aura.Red(err)) +} + +func displayFilename(filename string) aurora.Value { + switch { + case strings.HasPrefix(filename, "https://"): + return aura.Hyperlink(filename, filename) + case filename == "-": + return aura.Gray(12, "(stdin)") + default: + return aura.Blue(filename) + } +} diff --git a/cmd/inspect/display_structured.go b/cmd/inspect/display_structured.go new file mode 100644 index 0000000..d1891a0 --- /dev/null +++ b/cmd/inspect/display_structured.go @@ -0,0 +1,69 @@ +package inspect + +import ( + "encoding/json" + "fmt" + "os" + + "gopkg.in/yaml.v3" + "src.lwithers.me.uk/go/rsa/pkg/inspect" +) + +var ( + displayStructures []displayStructure +) + +func displayStructured(src string, info []inspect.Info) { + for _, item := range info { + displayStructures = append(displayStructures, displayStructure{ + Source: src, + Type: displayInfoType(item.Type()), + Location: item.Location(), + Info: item.Info(), + }) + } +} + +func displayStructuredJSON() { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(displayStructures); err != nil { + fmt.Fprintln(os.Stderr, "failed to encode as JSON: %v\n", aura.Red(err)) + os.Exit(1) + } +} + +func displayStructuredYAML() { + enc := yaml.NewEncoder(os.Stdout) + enc.SetIndent(2) + if err := enc.Encode(displayStructures); err != nil { + fmt.Fprintln(os.Stderr, "failed to encode as YAML: %v\n", aura.Red(err)) + os.Exit(2) + } +} + +type displayStructure struct { + Source string `json:"source" yaml:"source"` + Type string `json:"type" yaml:"type"` + Location string `json:"location" yaml:"location"` + Info []inspect.Section `json:"info" yaml:"info"` +} + +func displayInfoType(t inspect.Type) string { + switch t { + case inspect.TypePrivateKey: + return "private_key" + case inspect.TypePublicKey: + return "public_key" + case inspect.TypeX509Certificate: + return "certificate" + case inspect.TypeX509CRL: + return "crl" + case inspect.TypeX509CSR: + return "csr" + case inspect.TypeTLSConnectionState: + return "tls_state" + default: + return "???" + } +} diff --git a/cmd/inspect/display_coloured.go b/cmd/inspect/display_text.go similarity index 65% rename from cmd/inspect/display_coloured.go rename to cmd/inspect/display_text.go index 5b9022d..4fbc30b 100644 --- a/cmd/inspect/display_coloured.go +++ b/cmd/inspect/display_text.go @@ -2,12 +2,10 @@ package inspect import ( "fmt" - "os" "strings" "time" "unicode/utf8" - "github.com/logrusorgru/aurora/v4" "src.lwithers.me.uk/go/rsa/pkg/inspect" ) @@ -15,7 +13,7 @@ var ( displayedFingerprints = map[string]int{} ) -func displayColoured(src string, info []inspect.Info) { +func displayText(src string, info []inspect.Info) { // compute max key length, for nicely aligning columns var maxKey int for _, item := range info { @@ -29,43 +27,35 @@ func displayColoured(src string, info []inspect.Info) { } } - // output options - var auroraHyper bool - switch os.Getenv("TERM") { - case "xterm-kitty": - auroraHyper = true - } - a := aurora.New(aurora.WithColors(true), aurora.WithHyperlinks(auroraHyper)) - // display loop for _, item := range info { fmt.Printf("════════ %s:%s ════════\n", - a.BrightBlue(src), a.Blue(item.Location())) + aura.BrightBlue(src), aura.Blue(item.Location())) for _, section := range item.Info() { - fmt.Println(aurora.Underline(section.Title)) + fmt.Println(aura.Underline(section.Title)) for _, field := range section.Fields { - fmt.Printf(" %*s: ", maxKey, a.Yellow(field.Key)) + fmt.Printf(" %*s: ", maxKey, aura.Yellow(field.Key)) switch v := field.Value.(type) { case int: - fmt.Print(a.Blue(v)) + fmt.Print(aura.Blue(v)) case bool: - fmt.Print(a.Blue(v)) + fmt.Print(aura.Blue(v)) case time.Time: var note string switch { case strings.Contains(field.Key, "from"): if v.After(time.Now()) { - note = aurora.Red("not valid yet").String() + note = aura.Red("not valid yet").String() } else { - note = aurora.Green("ok").String() + note = aura.Green("ok").String() } case strings.Contains(field.Key, "until"): if v.After(time.Now()) { - note = aurora.Green("ok").String() + note = aura.Green("ok").String() } else { - note = aurora.Red("expired").String() + note = aura.Red("expired").String() } } @@ -91,11 +81,11 @@ func displayColoured(src string, info []inspect.Info) { var note string if firstSeen { - note = fmt.Sprintf("#%d %s", a.Blue(fidx), - a.Magenta("first occurrence")) + note = fmt.Sprintf("#%d %s", aura.Blue(fidx), + aura.Magenta("first occurrence")) } else { - note = fmt.Sprintf("#%d %s", a.Blue(fidx), - a.Green("already seen")) + note = fmt.Sprintf("#%d %s", aura.Blue(fidx), + aura.Green("already seen")) } fmt.Printf("%v [%s]", f, note) diff --git a/cmd/inspect/inspect.go b/cmd/inspect/inspect.go index 4814fca..415a20f 100644 --- a/cmd/inspect/inspect.go +++ b/cmd/inspect/inspect.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "errors" + "io" "net" "net/url" "os" @@ -13,14 +14,16 @@ import ( "github.com/mattn/go-isatty" "github.com/spf13/cobra" "src.lwithers.me.uk/go/rsa/pkg/inspect" + "src.lwithers.me.uk/go/stdinprompt" ) var ( outputFormat string = "text" display func(src string, info []inspect.Info) + noPEMData = errors.New("no PEM data found") ) -// Register the "keygen" subcommand. +// Register the "inspect" subcommand. func Register(root *cobra.Command) { cmd := &cobra.Command{ Use: "inspect", @@ -39,64 +42,88 @@ func Register(root *cobra.Command) { root.AddCommand(cmd) } -// Keygen will generate a new RSA private key and save it to a file. +// Inspect PEM data from files or TLS handshakes. func Inspect(cmd *cobra.Command, args []string) error { + setupAura(false) switch outputFormat { case "coloured": - display = displayColoured + setupAura(true) + display = displayText case "text": display = displayText case "json": - display = displayJSON + display = displayStructured + defer displayStructuredJSON() case "yaml": - display = displayYAML + display = displayStructured + defer displayStructuredYAML() default: return errors.New("invalid --output format (try: coloured, text, json or yaml)") } + ok := true for _, arg := range args { switch { case arg == "-": - inspectStdin() + ok = inspectStdin() && ok case strings.HasPrefix(arg, "https://"): - inspectHTTPS(arg) + ok = inspectHTTPS(arg) && ok - case strings.IndexByte(arg, ':') != 0: + case strings.IndexByte(arg, '/') == -1 && strings.IndexByte(arg, ':') != -1: _, _, err := net.SplitHostPort(arg) if err == nil { - inspectHost(arg) + ok = inspectHost(arg) && ok break } fallthrough default: - inspectFile(arg) + ok = inspectFile(arg) && ok } } + if !ok { + os.Exit(1) + } return nil } -func inspectStdin() { - // TODO -} - -func inspectHTTPS(addr string) { - u, err := url.Parse(addr) +func inspectStdin() (ok bool) { + in := stdinprompt.New() + raw, err := io.ReadAll(in) if err != nil { - // TODO + displayErr(err, "-") + return false } - inspectHost(u.Host) - // TODO: need to add default port + info := inspect.LoadPEM(raw) + if len(info) == 0 { + displayErr(noPEMData, "-") + return false + } + + display("(stdin)", info) + return true } -func inspectHost(addr string) { +func inspectHTTPS(addr string) (ok bool) { + u, err := url.Parse(addr) + if err != nil { + displayErr(err, addr) + return false + } + + addr = u.Host + if _, _, err := net.SplitHostPort(addr); err != nil { + addr += ":443" + } + return inspectHost(addr) +} + +func inspectHost(addr string) (ok bool) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - // TODO: context dial - conn, err := tls.Dial("tcp", addr, &tls.Config{ // we want to skip verification so we can inspect invalid certs too InsecureSkipVerify: true, @@ -128,34 +155,34 @@ func inspectHost(addr string) { MinVersion: tls.VersionTLS10, }) if err != nil { - // TODO + displayErr(err, addr) + return false } if err := conn.HandshakeContext(ctx); err != nil { - // TODO + displayErr(err, addr) + return false } display(addr, inspect.ConnectionState(conn.ConnectionState())) + return true } -func inspectFile(filename string) { +func inspectFile(filename string) (ok bool) { raw, err := os.ReadFile(filename) if err != nil { - // TODO + displayErr(err, filename) + return false } info := inspect.LoadPEM(raw) if len(info) == 0 { - // TODO + displayErr(noPEMData, filename) + return false } display(filename, info) -} - -func displayText(src string, info []inspect.Info) { -} - -func displayJSON(src string, info []inspect.Info) { + return true } func displayYAML(src string, info []inspect.Info) { diff --git a/go.mod b/go.mod index 7bad87a..f6f64ed 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require ( github.com/logrusorgru/aurora/v4 v4.0.0 github.com/mattn/go-isatty v0.0.17 github.com/spf13/cobra v1.6.1 - src.lwithers.me.uk/go/rsa/pkg v1.0.0 + gopkg.in/yaml.v3 v3.0.1 + src.lwithers.me.uk/go/rsa/pkg v1.0.1 + src.lwithers.me.uk/go/stdinprompt v1.0.0 ) require ( diff --git a/go.sum b/go.sum index 385b012..6d47af7 100644 --- a/go.sum +++ b/go.sum @@ -17,10 +17,13 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -src.lwithers.me.uk/go/rsa/pkg v1.0.0 h1:JDJDlml1GfJE/jyiqhXhqkInQRVs/t3MvsRXWNEWPVs= -src.lwithers.me.uk/go/rsa/pkg v1.0.0/go.mod h1:nZra8VAzQIbrQg2L6ev2BRQxvnpu7FBxfMtBrZ1ksek= +src.lwithers.me.uk/go/rsa/pkg v1.0.1 h1:Ht84EsH8yZqTXxOOXMZg1mLzUmn0ocvS1wQv9Fo4wlA= +src.lwithers.me.uk/go/rsa/pkg v1.0.1/go.mod h1:nZra8VAzQIbrQg2L6ev2BRQxvnpu7FBxfMtBrZ1ksek= +src.lwithers.me.uk/go/stdinprompt v1.0.0 h1:AuVloVOjwJbfl/cgJXOGCiAAXlElYFe1n+sN7vPnccY= +src.lwithers.me.uk/go/stdinprompt v1.0.0/go.mod h1:jHpqKtXU/wfnpM7SmpAXyMUbQNG3VYzFRIAed7IZ/0Y= src.lwithers.me.uk/go/writefile v1.0.1 h1:bwBGtvyZfCxFIM14e1aYgJWlZuowKkwJx53OJlUPd0s= src.lwithers.me.uk/go/writefile v1.0.1/go.mod h1:NahlmRCtB7kg4ai+zHZgxXdUs+MR8VqWG8mql35TsxA=