diff --git a/README.md b/README.md index 81c00d4..7d7744d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # CRL Inspector [![GoReportCard example](https://goreportcard.com/badge/github.com/pimg/certguard)](https://goreportcard.com/report/github.com/pimg/certguard) ![CI tests](https://github.com/pimg/certguard/actions/workflows/build.yml/badge.svg) + A Terminal User Interface (TUI) for inspecting Certificate Revocation Lists (CRL's) diff --git a/go.mod b/go.mod index a75577f..5c283c7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.9.1 + github.com/kr/pretty v0.3.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 ) @@ -16,6 +17,7 @@ require ( github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -26,6 +28,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.6 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index ebce9e8..7472124 100644 --- a/go.sum +++ b/go.sum @@ -11,10 +11,15 @@ github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -32,12 +37,15 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= diff --git a/internal/ports/models/base.go b/internal/ports/models/base.go index 007b409..71504cd 100644 --- a/internal/ports/models/base.go +++ b/internal/ports/models/base.go @@ -2,7 +2,6 @@ package models import ( "crypto/x509" - "fmt" "strings" "github.com/charmbracelet/bubbles/help" @@ -87,6 +86,7 @@ type BaseModel struct { help help.Model styles *styles.Styles input InputModel + list ListModel crl *x509.RevocationList err error width int @@ -131,7 +131,7 @@ func (m BaseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case messages.CRLResponseMsg: m.state = listView m.title = titles[listView] - m.crl = msg.RevocationList + m.list = NewListModel(msg.RevocationList) } // state specific actions @@ -145,6 +145,15 @@ func (m BaseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input = inputModel.(InputModel) cmd = append(cmd, inputCmd) } + case listView: + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.help.Width = msg.Width + default: + listModel, listCmd := m.list.Update(msg) + m.list = listModel.(ListModel) + cmd = append(cmd, listCmd) + } case baseView: switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -175,9 +184,10 @@ func (m BaseModel) View() string { return lipgloss.JoinVertical(lipgloss.Top, title, inputBox) + lipgloss.Place(m.width, m.height-height, lipgloss.Left, lipgloss.Bottom, helpMenu) case listView: title := m.styles.Title.Render(m.title) - crlInfo := m.styles.Text.Render(fmt.Sprintf("CRL Issuer: %s, \nUpdated at: %s, \nNext update: %s", m.crl.Issuer.String(), m.crl.ThisUpdate.String(), m.crl.NextUpdate.String())) - helpMenu := helpView - return lipgloss.JoinVertical(lipgloss.Top, title, crlInfo, helpMenu) + listInfo := m.list.View() + helpMenu := m.list.help.View(&listKeys) + height := strings.Count(listInfo, "\n") + strings.Count(title, "\n") + return lipgloss.JoinVertical(lipgloss.Top, title, listInfo) + lipgloss.Place(m.width, m.height-height, lipgloss.Left, lipgloss.Bottom, helpMenu) default: title := m.styles.Title.Render(m.title) if m.err != nil { diff --git a/internal/ports/models/list.go b/internal/ports/models/list.go new file mode 100644 index 0000000..e3ed84a --- /dev/null +++ b/internal/ports/models/list.go @@ -0,0 +1,91 @@ +package models + +import ( + "crypto/x509" + "fmt" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/pimg/certguard/internal/ports/models/styles" +) + +// keyMap defines a set of keybindings. To work for help it must satisfy +// key.Map. It could also very easily be a map[string]key.Binding. +type listKeyMap struct { + Back key.Binding + Quit key.Binding +} + +// ShortHelp returns keybindings to be shown in the mini help view. It's part +// of the key.Map interface. +func (k *listKeyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Back, k.Quit} +} + +// FullHelp returns keybindings for the expanded help view. It's part of the +// key.Map interface. +func (k *listKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Back, k.Quit}, + } +} + +var listKeys = listKeyMap{ + Back: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "back to main view"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "ctrl+c"), + key.WithHelp("q", "quit"), + ), +} + +type ListModel struct { + keys listKeyMap + help help.Model + styles *styles.Styles + crl *x509.RevocationList +} + +func NewListModel(crl *x509.RevocationList) ListModel { + return ListModel{ + keys: listKeys, + help: help.New(), + styles: styles.DefaultStyles(), + crl: crl, + } +} + +func (l ListModel) Init() tea.Cmd { + return nil +} + +func (l ListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return l, nil +} + +func (l ListModel) View() string { + + //mainInfo := map[string]string{ + // "CRL Issuer": l.crl.Issuer.String(), + // "Upated at": l.crl.ThisUpdate.String(), + // "Next update": l.crl.NextUpdate.String(), + // "No of revoked Certificates": fmt.Sprintf("%d", len(l.crl.RevokedCertificateEntries)), + //} + + //var renderedMainInfo string + //for k, v := range mainInfo { + // renderedMainInfo = renderedMainInfo + "\n" + fmt.Sprintf("%s:\t\t%s", k, v) + //} + issuer := fmt.Sprintf("CRL Issuer : %s", l.crl.Issuer) + updatedAt := fmt.Sprintf("Updated At : %s", l.crl.ThisUpdate) + nextUpdate := fmt.Sprintf("Next Update : %s", l.crl.NextUpdate) + revokedCertCount := fmt.Sprintf("Revoked Certificates: %d", len(l.crl.RevokedCertificateEntries)) + + crlInfo := l.styles.Text.Render( + fmt.Sprintf("%s\n%s\n%s\n%s", issuer, updatedAt, nextUpdate, revokedCertCount), + ) + return crlInfo +} diff --git a/internal/ports/models/styles/styles.go b/internal/ports/models/styles/styles.go index b2aee09..153f1e3 100644 --- a/internal/ports/models/styles/styles.go +++ b/internal/ports/models/styles/styles.go @@ -20,6 +20,6 @@ func DefaultStyles() *Styles { PaddingLeft(2), Background: lipgloss.NewStyle().Background(lipgloss.Color("#282828")), ErrorMessages: lipgloss.NewStyle().Background(lipgloss.Color("#FB4934")).BorderForeground(lipgloss.Color("#FB4934")).BorderStyle(lipgloss.NormalBorder()).Width(80).Padding(1), - Text: lipgloss.NewStyle().Foreground(lipgloss.Color("#B8BB26")).Padding(1), + Text: lipgloss.NewStyle().Foreground(lipgloss.Color("#B8BB26")).Padding(1).Width(80), } }