cmd/inspect: finish command implementation

This commit is contained in:
Laurence Withers 2023-04-29 10:59:15 +01:00
parent 279e9af791
commit 5e6b089971
6 changed files with 191 additions and 60 deletions

40
cmd/inspect/aurora.go Normal file
View File

@ -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)
}
}

View File

@ -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 "???"
}
}

View File

@ -2,12 +2,10 @@ package inspect
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/logrusorgru/aurora/v4"
"src.lwithers.me.uk/go/rsa/pkg/inspect" "src.lwithers.me.uk/go/rsa/pkg/inspect"
) )
@ -15,7 +13,7 @@ var (
displayedFingerprints = map[string]int{} 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 // compute max key length, for nicely aligning columns
var maxKey int var maxKey int
for _, item := range info { 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 // display loop
for _, item := range info { for _, item := range info {
fmt.Printf("════════ %s:%s ════════\n", fmt.Printf("════════ %s:%s ════════\n",
a.BrightBlue(src), a.Blue(item.Location())) aura.BrightBlue(src), aura.Blue(item.Location()))
for _, section := range item.Info() { for _, section := range item.Info() {
fmt.Println(aurora.Underline(section.Title)) fmt.Println(aura.Underline(section.Title))
for _, field := range section.Fields { 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) { switch v := field.Value.(type) {
case int: case int:
fmt.Print(a.Blue(v)) fmt.Print(aura.Blue(v))
case bool: case bool:
fmt.Print(a.Blue(v)) fmt.Print(aura.Blue(v))
case time.Time: case time.Time:
var note string var note string
switch { switch {
case strings.Contains(field.Key, "from"): case strings.Contains(field.Key, "from"):
if v.After(time.Now()) { if v.After(time.Now()) {
note = aurora.Red("not valid yet").String() note = aura.Red("not valid yet").String()
} else { } else {
note = aurora.Green("ok").String() note = aura.Green("ok").String()
} }
case strings.Contains(field.Key, "until"): case strings.Contains(field.Key, "until"):
if v.After(time.Now()) { if v.After(time.Now()) {
note = aurora.Green("ok").String() note = aura.Green("ok").String()
} else { } 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 var note string
if firstSeen { if firstSeen {
note = fmt.Sprintf("#%d %s", a.Blue(fidx), note = fmt.Sprintf("#%d %s", aura.Blue(fidx),
a.Magenta("first occurrence")) aura.Magenta("first occurrence"))
} else { } else {
note = fmt.Sprintf("#%d %s", a.Blue(fidx), note = fmt.Sprintf("#%d %s", aura.Blue(fidx),
a.Green("already seen")) aura.Green("already seen"))
} }
fmt.Printf("%v [%s]", f, note) fmt.Printf("%v [%s]", f, note)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"io"
"net" "net"
"net/url" "net/url"
"os" "os"
@ -13,14 +14,16 @@ import (
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"src.lwithers.me.uk/go/rsa/pkg/inspect" "src.lwithers.me.uk/go/rsa/pkg/inspect"
"src.lwithers.me.uk/go/stdinprompt"
) )
var ( var (
outputFormat string = "text" outputFormat string = "text"
display func(src string, info []inspect.Info) 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) { func Register(root *cobra.Command) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "inspect", Use: "inspect",
@ -39,64 +42,88 @@ func Register(root *cobra.Command) {
root.AddCommand(cmd) 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 { func Inspect(cmd *cobra.Command, args []string) error {
setupAura(false)
switch outputFormat { switch outputFormat {
case "coloured": case "coloured":
display = displayColoured setupAura(true)
display = displayText
case "text": case "text":
display = displayText display = displayText
case "json": case "json":
display = displayJSON display = displayStructured
defer displayStructuredJSON()
case "yaml": case "yaml":
display = displayYAML display = displayStructured
defer displayStructuredYAML()
default: default:
return errors.New("invalid --output format (try: coloured, text, json or yaml)") return errors.New("invalid --output format (try: coloured, text, json or yaml)")
} }
ok := true
for _, arg := range args { for _, arg := range args {
switch { switch {
case arg == "-": case arg == "-":
inspectStdin() ok = inspectStdin() && ok
case strings.HasPrefix(arg, "https://"): 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) _, _, err := net.SplitHostPort(arg)
if err == nil { if err == nil {
inspectHost(arg) ok = inspectHost(arg) && ok
break break
} }
fallthrough fallthrough
default: default:
inspectFile(arg) ok = inspectFile(arg) && ok
} }
} }
if !ok {
os.Exit(1)
}
return nil return nil
} }
func inspectStdin() { func inspectStdin() (ok bool) {
// TODO in := stdinprompt.New()
} raw, err := io.ReadAll(in)
func inspectHTTPS(addr string) {
u, err := url.Parse(addr)
if err != nil { if err != nil {
// TODO displayErr(err, "-")
return false
} }
inspectHost(u.Host) info := inspect.LoadPEM(raw)
// TODO: need to add default port 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) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
// TODO: context dial
conn, err := tls.Dial("tcp", addr, &tls.Config{ conn, err := tls.Dial("tcp", addr, &tls.Config{
// we want to skip verification so we can inspect invalid certs too // we want to skip verification so we can inspect invalid certs too
InsecureSkipVerify: true, InsecureSkipVerify: true,
@ -128,34 +155,34 @@ func inspectHost(addr string) {
MinVersion: tls.VersionTLS10, MinVersion: tls.VersionTLS10,
}) })
if err != nil { if err != nil {
// TODO displayErr(err, addr)
return false
} }
if err := conn.HandshakeContext(ctx); err != nil { if err := conn.HandshakeContext(ctx); err != nil {
// TODO displayErr(err, addr)
return false
} }
display(addr, inspect.ConnectionState(conn.ConnectionState())) display(addr, inspect.ConnectionState(conn.ConnectionState()))
return true
} }
func inspectFile(filename string) { func inspectFile(filename string) (ok bool) {
raw, err := os.ReadFile(filename) raw, err := os.ReadFile(filename)
if err != nil { if err != nil {
// TODO displayErr(err, filename)
return false
} }
info := inspect.LoadPEM(raw) info := inspect.LoadPEM(raw)
if len(info) == 0 { if len(info) == 0 {
// TODO displayErr(noPEMData, filename)
return false
} }
display(filename, info) display(filename, info)
} return true
func displayText(src string, info []inspect.Info) {
}
func displayJSON(src string, info []inspect.Info) {
} }
func displayYAML(src string, info []inspect.Info) { func displayYAML(src string, info []inspect.Info) {

4
go.mod
View File

@ -6,7 +6,9 @@ require (
github.com/logrusorgru/aurora/v4 v4.0.0 github.com/logrusorgru/aurora/v4 v4.0.0
github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-isatty v0.0.17
github.com/spf13/cobra v1.6.1 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 ( require (

7
go.sum
View File

@ -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.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 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.1 h1:Ht84EsH8yZqTXxOOXMZg1mLzUmn0ocvS1wQv9Fo4wlA=
src.lwithers.me.uk/go/rsa/pkg v1.0.0/go.mod h1:nZra8VAzQIbrQg2L6ev2BRQxvnpu7FBxfMtBrZ1ksek= 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 h1:bwBGtvyZfCxFIM14e1aYgJWlZuowKkwJx53OJlUPd0s=
src.lwithers.me.uk/go/writefile v1.0.1/go.mod h1:NahlmRCtB7kg4ai+zHZgxXdUs+MR8VqWG8mql35TsxA= src.lwithers.me.uk/go/writefile v1.0.1/go.mod h1:NahlmRCtB7kg4ai+zHZgxXdUs+MR8VqWG8mql35TsxA=