diff --git a/Makefile b/Makefile
index 6a46597..19f3b64 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
################################################################################
-# This Makefile generated by GoMakeGen 1.0.0 using next command:
+# This Makefile generated by GoMakeGen 1.1.0 using next command:
# gomakegen .
#
# More info: https://kaos.sh/gomakegen
@@ -17,10 +17,10 @@ all: sslcli ## Build all binaries
sslcli: ## Build sslcli binary
go build sslcli.go
-install: ## Install binaries
+install: ## Install all binaries
cp sslcli /usr/bin/sslcli
-uninstall: ## Uninstall binaries
+uninstall: ## Uninstall all binaries
rm -f /usr/bin/sslcli
git-config: ## Configure git redirects for stable import path services
@@ -28,7 +28,7 @@ git-config: ## Configure git redirects for stable import path services
deps: git-config ## Download dependencies
go get -d -v pkg.re/essentialkaos/ek.v10
- go get -d -v pkg.re/essentialkaos/sslscan.v10
+ go get -d -v pkg.re/essentialkaos/sslscan.v11
fmt: ## Format source code with gofmt
find . -name "*.go" -exec gofmt -s -w {} \;
@@ -37,9 +37,10 @@ clean: ## Remove generated files
rm -f sslcli
help: ## Show this info
- @echo -e '\nSupported targets:\n'
+ @echo -e '\n\033[1mSupported targets:\033[0m\n'
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-12s\033[0m %s\n", $$1, $$2}'
@echo -e ''
+ @echo -e '\033[90mGenerated by GoMakeGen 1.1.0\033[0m\n'
################################################################################
diff --git a/README.md b/README.md
index 3cbc25d..dc57f50 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
-Usage demo • Installation • Feature list • Usage • Build Status • Contributing • Terms of Use • License
-
@@ -9,6 +7,10 @@
+Usage demo • Installation • Feature list • Usage • Build Status • Contributing • Terms of Use • License
+
+
+
`sslcli` is command-line client for SSLLabs public API.
**IMPORTANT:** Currently, SSLLabs API doesn't provide same info as SSLLabs website.
@@ -42,14 +44,14 @@ go get -u github.com/essentialkaos/sslcli
#### From ESSENTIAL KAOS Public repo for RHEL6/CentOS6
```bash
-[sudo] yum install -y https://yum.kaos.st/6/release/x86_64/kaos-repo-9.1-0.el6.noarch.rpm
+[sudo] yum install -y https://yum.kaos.st/kaos-repo-latest.el6.noarch.rpm
[sudo] yum install sslcli
```
#### From ESSENTIAL KAOS Public repo for RHEL7/CentOS7
```bash
-[sudo] yum install -y https://yum.kaos.st/7/release/x86_64/kaos-repo-9.1-0.el7.noarch.rpm
+[sudo] yum install -y https://yum.kaos.st/kaos-repo-latest.el7.noarch.rpm
[sudo] yum install sslcli
```
@@ -92,6 +94,7 @@ Options
--avoid-cache, -c Disable cache usage
--public, -p Publish results on sslscan.com
--perfect, -P Return non-zero exit code if not A+
+ --max-left, -M duration Check expiry date (num + d/w/m/y)
--notify, -n Notify when check is done
--quiet, -q Don't show any output
--no-color, -nc Disable colors in output
@@ -109,6 +112,9 @@ Examples
sslcli -p -c google.com
Check google.com, publish results, disable cache usage
+ sslcli -M 3m -q google.com
+ Check google.com in quiet mode and return error if cert expire in 3 months
+
sslcli hosts.txt
Check all hosts defined in hosts.txt file
diff --git a/cli/cli.go b/cli/cli.go
index c36ea6b..5256760 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -9,9 +9,11 @@ package cli
import (
"errors"
+ "fmt"
"io/ioutil"
"os"
"runtime"
+ "strconv"
"strings"
"time"
@@ -19,17 +21,21 @@ import (
"pkg.re/essentialkaos/ek.v10/fmtutil"
"pkg.re/essentialkaos/ek.v10/fsutil"
"pkg.re/essentialkaos/ek.v10/options"
+ "pkg.re/essentialkaos/ek.v10/strutil"
"pkg.re/essentialkaos/ek.v10/usage"
+ "pkg.re/essentialkaos/ek.v10/usage/completion/bash"
+ "pkg.re/essentialkaos/ek.v10/usage/completion/fish"
+ "pkg.re/essentialkaos/ek.v10/usage/completion/zsh"
"pkg.re/essentialkaos/ek.v10/usage/update"
- "pkg.re/essentialkaos/sslscan.v10"
+ "pkg.re/essentialkaos/sslscan.v11"
)
// ////////////////////////////////////////////////////////////////////////////////// //
const (
APP = "SSLScan Client"
- VER = "2.3.1"
+ VER = "2.4.0"
DESC = "Command-line client for the SSL Labs API"
)
@@ -40,11 +46,14 @@ const (
OPT_AVOID_CACHE = "c:avoid-cache"
OPT_PUBLIC = "p:public"
OPT_PERFECT = "P:perfect"
+ OPT_MAX_LEFT = "M:max-left"
OPT_QUIET = "q:quiet"
OPT_NOTIFY = "n:notify"
OPT_NO_COLOR = "nc:no-color"
OPT_HELP = "h:help"
OPT_VER = "v:version"
+
+ OPT_COMPLETION = "completion"
)
const (
@@ -80,6 +89,7 @@ type EndpointCheckInfo struct {
var optMap = options.Map{
OPT_FORMAT: {},
+ OPT_MAX_LEFT: {},
OPT_DETAILED: {Type: options.BOOL},
OPT_IGNORE_MISMATCH: {Type: options.BOOL},
OPT_AVOID_CACHE: {Type: options.BOOL},
@@ -90,6 +100,8 @@ var optMap = options.Map{
OPT_NO_COLOR: {Type: options.BOOL},
OPT_HELP: {Type: options.BOOL, Alias: "u:usage"},
OPT_VER: {Type: options.BOOL, Alias: "ver"},
+
+ OPT_COMPLETION: {},
}
var gradeNumMap = map[string]float64{
@@ -107,6 +119,8 @@ var gradeNumMap = map[string]float64{
}
var api *sslscan.API
+var maxLeftToExpiry int64
+var serverMessageShown bool
// ////////////////////////////////////////////////////////////////////////////////// //
@@ -115,16 +129,21 @@ func Init() {
args, errs := options.Parse(optMap)
if len(errs) != 0 {
- fmtc.Println("{r}Arguments parsing errors:{!}")
+ printError("Arguments parsing errors:")
for _, err := range errs {
- fmtc.Printf(" {r}%v{!}\n", err)
+ printError(" %v", err)
}
os.Exit(1)
}
+ if options.Has(OPT_COMPLETION) {
+ genCompletion()
+ }
+
configureUI()
+ prepare()
if options.GetB(OPT_VER) {
showAbout()
@@ -150,6 +169,22 @@ func configureUI() {
fmtutil.SeparatorSymbol = "–"
}
+// prepare prepares utility for processing data
+func prepare() {
+ if !options.Has(OPT_MAX_LEFT) {
+ return
+ }
+
+ var err error
+
+ maxLeftToExpiry, err = parseMaxLeft(options.GetS(OPT_MAX_LEFT))
+
+ if err != nil {
+ printError(err.Error())
+ os.Exit(1)
+ }
+}
+
// process starting request processing
func process(args []string) {
var (
@@ -162,7 +197,7 @@ func process(args []string) {
if err != nil {
if !options.GetB(OPT_FORMAT) {
- fmtc.Printf("{r}%v{!}\n", err)
+ printError(err.Error())
}
os.Exit(1)
@@ -176,51 +211,38 @@ func process(args []string) {
hosts, err = readHostList(hosts[0])
if err != nil && options.GetB(OPT_FORMAT) {
- fmtc.Printf("{r}%v{!}\n", err)
+ printError(err.Error())
os.Exit(1)
}
}
- var (
- grade string
- checksInfo []*HostCheckInfo
- checkInfo *HostCheckInfo
- )
+ var grade string
+ var expiredSoon bool
+ var checksInfo []*HostCheckInfo
+ var checkInfo *HostCheckInfo
for _, host := range hosts {
-
switch {
case options.GetB(OPT_QUIET):
- grade, _ = quietCheck(host)
+ grade, expiredSoon, _ = quietCheck(host)
case options.GetB(OPT_FORMAT):
- grade, checkInfo = quietCheck(host)
+ grade, expiredSoon, checkInfo = quietCheck(host)
checksInfo = append(checksInfo, checkInfo)
default:
- grade = check(host)
+ grade, expiredSoon = check(host)
fmtc.NewLine()
}
switch {
- case options.GetB(OPT_PERFECT) && grade != "A+":
- ok = false
- case grade[:1] != "A":
+ case options.GetB(OPT_PERFECT) && grade != "A+",
+ strutil.Head(grade, 1) != "A",
+ expiredSoon:
ok = false
}
}
- if options.GetB(OPT_FORMAT) {
- switch options.GetS(OPT_FORMAT) {
- case FORMAT_TEXT:
- encodeAsText(checksInfo)
- case FORMAT_JSON:
- encodeAsJSON(checksInfo)
- case FORMAT_XML:
- encodeAsXML(checksInfo)
- case FORMAT_YAML:
- encodeAsYAML(checksInfo)
- default:
- os.Exit(1)
- }
+ if options.Has(OPT_FORMAT) {
+ renderReport(checksInfo)
}
if options.GetB(OPT_NOTIFY) {
@@ -233,7 +255,7 @@ func process(args []string) {
}
// check check some host
-func check(host string) string {
+func check(host string) (string, bool) {
var err error
var info *sslscan.AnalyzeInfo
@@ -252,20 +274,20 @@ func check(host string) string {
if err != nil {
fmtc.TPrintf("{*}%s{!} → {r}%v{!}\n", host, err)
- return "T"
+ return "T", false
}
for {
- info, err = ap.Info(false)
+ info, err = ap.Info(false, params.FromCache)
if err != nil {
fmtc.TPrintf("{*}%s{!} → {r}%v{!}\n", host, err)
- return "Err"
+ return "Err", false
}
if info.Status == sslscan.STATUS_ERROR {
fmtc.TPrintf("{*}%s{!} → {r}%s{!}\n", host, info.StatusMessage)
- return "Err"
+ return "Err", false
} else if info.Status == sslscan.STATUS_READY {
break
}
@@ -285,23 +307,29 @@ func check(host string) string {
}
}
+ expiryMessage := getExpiryMessage(ap, maxLeftToExpiry)
+
if len(info.Endpoints) == 1 {
- fmtc.TPrintf("{*}%s{!} → "+getColoredGrade(info.Endpoints[0].Grade)+"\n", host)
+ fmtc.TPrintf("{*}%s{!} → "+getColoredGrade(info.Endpoints[0].Grade)+expiryMessage+"\n", host)
} else {
- fmtc.TPrintf("{*}%s{!} → "+getColoredGrades(info.Endpoints)+"\n", host)
+ fmtc.TPrintf("{*}%s{!} → "+getColoredGrades(info.Endpoints)+expiryMessage+"\n", host)
}
if options.GetB(OPT_DETAILED) {
- printDetailedInfo(ap)
+ printDetailedInfo(ap, true)
}
lowestGrade, _ := getGrades(info.Endpoints)
- return lowestGrade
+ return lowestGrade, expiryMessage != ""
}
// showServerMessage show message from SSL Labs API
func showServerMessage() {
+ if serverMessageShown {
+ return
+ }
+
serverMessage := strings.Join(api.Info.Messages, " ")
wrappedMessage := fmtutil.Wrap(serverMessage, "", 80)
@@ -320,10 +348,12 @@ func showServerMessage() {
api.Info.NewAssessmentCoolOff,
)
fmtc.NewLine()
+
+ serverMessageShown = true
}
// quietCheck check some host without any output to console
-func quietCheck(host string) (string, *HostCheckInfo) {
+func quietCheck(host string) (string, bool, *HostCheckInfo) {
var err error
var info *sslscan.AnalyzeInfo
@@ -346,18 +376,18 @@ func quietCheck(host string) (string, *HostCheckInfo) {
ap, err := api.Analyze(host, params)
if err != nil {
- return "Err", checkInfo
+ return "Err", false, checkInfo
}
for {
- info, err = ap.Info(false)
+ info, err = ap.Info(false, params.FromCache)
if err != nil {
- return "Err", checkInfo
+ return "Err", false, checkInfo
}
if info.Status == sslscan.STATUS_ERROR {
- return "Err", checkInfo
+ return "Err", false, checkInfo
} else if info.Status == sslscan.STATUS_READY {
break
}
@@ -365,6 +395,12 @@ func quietCheck(host string) (string, *HostCheckInfo) {
time.Sleep(time.Second)
}
+ var expiredSoon bool
+
+ if maxLeftToExpiry > 0 {
+ expiredSoon = getExpiryMessage(ap, maxLeftToExpiry) != ""
+ }
+
appendEndpointsInfo(checkInfo, info.Endpoints)
lowestGrade, highestGrade := getGrades(info.Endpoints)
@@ -374,7 +410,23 @@ func quietCheck(host string) (string, *HostCheckInfo) {
checkInfo.LowestGradeNum = gradeNumMap[lowestGrade]
checkInfo.HighestGradeNum = gradeNumMap[highestGrade]
- return lowestGrade, checkInfo
+ return lowestGrade, expiredSoon, checkInfo
+}
+
+// renderReport renders report in different formats
+func renderReport(checksInfo []*HostCheckInfo) {
+ switch options.GetS(OPT_FORMAT) {
+ case FORMAT_TEXT:
+ encodeAsText(checksInfo)
+ case FORMAT_JSON:
+ encodeAsJSON(checksInfo)
+ case FORMAT_XML:
+ encodeAsXML(checksInfo)
+ case FORMAT_YAML:
+ encodeAsYAML(checksInfo)
+ default:
+ os.Exit(1)
+ }
}
// getColoredGrade return grade with color tags
@@ -445,7 +497,7 @@ func getStatusInProgress(endpoints []*sslscan.EndpointInfo) string {
}
if endpoint.StatusDetailsMessage != "" {
- return fmtc.Sprintf("#%d: %s", num, endpoint.StatusDetailsMessage)
+ return fmt.Sprintf("#%d: %s", num, endpoint.StatusDetailsMessage)
}
}
@@ -498,6 +550,28 @@ func appendEndpointsInfo(checkInfo *HostCheckInfo, endpoints []*sslscan.Endpoint
}
}
+// parseMaxLeft parses max left option value
+func parseMaxLeft(dur string) (int64, error) {
+ tm := strutil.Tail(dur, 1)
+ t := strings.Trim(dur, "dwmy")
+ ti, err := strconv.ParseInt(t, 10, 64)
+
+ if err != nil {
+ return -1, fmt.Errorf("Invalid value for --max-left option: %s", dur)
+ }
+
+ switch strings.ToLower(tm) {
+ case "w":
+ return ti * 604800, nil
+ case "m":
+ return ti * 2592000, nil
+ case "y":
+ return ti * 31536000, nil
+ default:
+ return ti * 86400, nil
+ }
+}
+
// getNormGrade return grade or error
func getNormGrade(grade string) string {
switch grade {
@@ -508,9 +582,20 @@ func getNormGrade(grade string) string {
}
}
+// printError prints error message to console
+func printError(f string, a ...interface{}) {
+ fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...)
+}
+
// ////////////////////////////////////////////////////////////////////////////////// //
+// showUsage prints usage info
func showUsage() {
+ genUsage().Render()
+}
+
+// genUsage generates usage info
+func genUsage() *usage.Info {
info := usage.NewInfo("", "host…")
info.AddOption(OPT_FORMAT, "Output result in different formats", "text|json|yaml|xml")
@@ -519,6 +604,7 @@ func showUsage() {
info.AddOption(OPT_AVOID_CACHE, "Disable cache usage")
info.AddOption(OPT_PUBLIC, "Publish results on sslscan.com")
info.AddOption(OPT_PERFECT, "Return non-zero exit code if not A+")
+ info.AddOption(OPT_MAX_LEFT, "Check expiry date {s-}(num + d/w/m/y){!}", "duration")
info.AddOption(OPT_NOTIFY, "Notify when check is done")
info.AddOption(OPT_QUIET, "Don't show any output")
info.AddOption(OPT_NO_COLOR, "Disable colors in output")
@@ -528,11 +614,31 @@ func showUsage() {
info.AddExample("google.com", "Check google.com")
info.AddExample("-P google.com", "Check google.com and return zero exit code only if result is perfect (A+)")
info.AddExample("-p -c google.com", "Check google.com, publish results, disable cache usage")
+ info.AddExample("-M 3m -q google.com", "Check google.com in quiet mode and return error if cert expire in 3 months")
info.AddExample("hosts.txt", "Check all hosts defined in hosts.txt file")
- info.Render()
+ return info
+}
+
+// genCompletion generates completion for different shells
+func genCompletion() {
+ info := genUsage()
+
+ switch options.GetS(OPT_COMPLETION) {
+ case "bash":
+ fmt.Printf(bash.Generate(info, "sslcli"))
+ case "fish":
+ fmt.Printf(fish.Generate(info, "sslcli"))
+ case "zsh":
+ fmt.Printf(zsh.Generate(info, optMap, "sslcli"))
+ default:
+ os.Exit(1)
+ }
+
+ os.Exit(0)
}
+// showAbout prints info about version
func showAbout() {
about := &usage.About{
App: APP,
diff --git a/cli/details.go b/cli/details.go
index 527c9f7..25a10be 100644
--- a/cli/details.go
+++ b/cli/details.go
@@ -2,12 +2,13 @@ package cli
// ////////////////////////////////////////////////////////////////////////////////// //
// //
-// Copyright (c) 2009-2018 ESSENTIAL KAOS //
+// Copyright (c) 2009-2019 ESSENTIAL KAOS //
// Apache License, Version 2.0 //
// //
// ////////////////////////////////////////////////////////////////////////////////// //
import (
+ "fmt"
"strings"
"time"
@@ -18,7 +19,7 @@ import (
"pkg.re/essentialkaos/ek.v10/strutil"
"pkg.re/essentialkaos/ek.v10/timeutil"
- "pkg.re/essentialkaos/sslscan.v10"
+ "pkg.re/essentialkaos/sslscan.v11"
)
// ////////////////////////////////////////////////////////////////////////////////// //
@@ -57,16 +58,16 @@ var isWeakForwardSecrecy bool
// ////////////////////////////////////////////////////////////////////////////////// //
// printDetailedInfo fetches and prints detailed info for all endpoints
-func printDetailedInfo(ap *sslscan.AnalyzeProgress) {
- info, err := ap.Info(true)
+func printDetailedInfo(ap *sslscan.AnalyzeProgress, fromCache bool) {
+ info, err := ap.Info(true, fromCache)
if err != nil {
- fmtc.Printf("\n{r}Can't fetch full analyze info: %v{!}\n\n", err)
+ printError("\nCan't fetch full analyze info: %v\n", err)
return
}
if strings.ToUpper(info.Status) != "READY" {
- fmtc.Printf("\n{r}%s{!}\n\n", info.StatusMessage)
+ printError("\n%s\n", info.StatusMessage)
return
}
@@ -1481,3 +1482,30 @@ func getTrustInfo(certID string, endpoints []*sslscan.EndpointInfo) (map[string]
return result, true
}
+
+// getExpiryMessage returns message if cert is expired in given period
+func getExpiryMessage(ap *sslscan.AnalyzeProgress, dur int64) string {
+ if dur <= 0 {
+ return ""
+ }
+
+ info, err := ap.Info(true, true)
+
+ if err != nil || strings.ToUpper(info.Status) != "READY" || len(info.Certs) == 0 {
+ return ""
+ }
+
+ cert := info.Certs[0]
+ validUntilDate := time.Unix(cert.NotAfter/1000, 0)
+
+ if validUntilDate.Unix()-time.Now().Unix() > dur {
+ return ""
+ }
+
+ validDays := (validUntilDate.Unix() - time.Now().Unix()) / 86400
+
+ return fmt.Sprintf(
+ " {r}(expires in %s){!}",
+ pluralize.Pluralize(int(validDays), "day", "days"),
+ )
+}
diff --git a/common/sslcli.spec b/common/sslcli.spec
index 32ecf71..4e2df0d 100644
--- a/common/sslcli.spec
+++ b/common/sslcli.spec
@@ -44,7 +44,7 @@
Summary: Pretty awesome command-line client for public SSLLabs API
Name: sslcli
-Version: 2.3.1
+Version: 2.4.0
Release: 0%{?dist}
Group: Applications/System
License: EKOL
@@ -54,7 +54,7 @@ Source0: https://source.kaos.st/%{name}/%{name}-%{version}.tar.bz2
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
-BuildRequires: golang >= 1.11
+BuildRequires: golang >= 1.12
Provides: %{name} = %{version}-%{release}
@@ -76,12 +76,39 @@ go build src/github.com/essentialkaos/sslcli/%{name}.go
rm -rf %{buildroot}
install -dm 755 %{buildroot}%{_bindir}
-
install -pm 755 %{name} %{buildroot}%{_bindir}/
%clean
rm -rf %{buildroot}
+%post
+if [[ -d %{_sysconfdir}/bash_completion.d ]] ; then
+ %{name} --completion=bash 1> %{_sysconfdir}/bash_completion.d/%{name} 2>/dev/null
+fi
+
+if [[ -d %{_datarootdir}/fish/vendor_completions.d ]] ; then
+ %{name} --completion=fish 1> %{_datarootdir}/fish/vendor_completions.d/%{name}.fish 2>/dev/null
+fi
+
+if [[ -d %{_datadir}/zsh/site-functions ]] ; then
+ %{name} --completion=zsh 1> %{_datadir}/zsh/site-functions/_%{name} 2>/dev/null
+fi
+
+%postun
+if [[ $1 == 0 ]] ; then
+ if [[ -f %{_sysconfdir}/bash_completion.d/%{name} ]] ; then
+ rm -f %{_sysconfdir}/bash_completion.d/%{name} &>/dev/null || :
+ fi
+
+ if [[ -f %{_datarootdir}/fish/vendor_completions.d/%{name}.fish ]] ; then
+ rm -f %{_datarootdir}/fish/vendor_completions.d/%{name}.fish &>/dev/null || :
+ fi
+
+ if [[ -f %{_datadir}/zsh/site-functions/_%{name} ]] ; then
+ rm -f %{_datadir}/zsh/site-functions/_%{name} &>/dev/null || :
+ fi
+fi
+
################################################################################
%files
@@ -92,6 +119,11 @@ rm -rf %{buildroot}
################################################################################
%changelog
+* Tue Jul 09 2019 Anton Novojilov - 2.4.0-0
+- Added '--max-left/-M' for checking certificate expiry date
+- Added completions for bash, fish and zsh
+- Minor improvements
+
* Mon Jun 03 2019 Anton Novojilov - 2.3.1-0
- Updated for compatibility with the latest version of SSLLabs API