diff --git a/README.md b/README.md index bada4b1..e225615 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,15 @@ certcheck go run . -domain badssl.com -port 443 -format long certcheck -domain self-signed.badssl.com -insecure -format long certcheck -domain badssl.com -timeout 5s + +certcheck -domain badssl.com -warn-days 14 ``` +`-warn-days N` (default `0`, disabled) makes certcheck exit with status `2` +instead of `0` when the certificate's remaining validity is `N` days or +fewer (including already expired). Status `1` is still reserved for hard +errors such as connection or parsing failures. + ## Notes Could the same be achieved with a wrapper around `openssl` ? yes. diff --git a/main.go b/main.go index 6625330..4f908a8 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ type Config struct { Insecure bool Format OutputFormat Timeout time.Duration + WarnDays int } func main() { @@ -34,6 +35,7 @@ func main() { flag.BoolVar(&config.Insecure, "insecure", false, "Accept invalid certificate") flag.StringVar(&formatStr, "format", "short", "Output format (short|long)") flag.DurationVar(&config.Timeout, "timeout", 10*time.Second, "Connection timeout") + flag.IntVar(&config.WarnDays, "warn-days", 0, "Exit with status 2 if certificate expires within this many days (0 disables)") flag.Parse() if config.Domain == "" { @@ -62,6 +64,16 @@ func main() { } printCertificate(cert, config.Format) + + remainingDays := int(time.Until(cert.NotAfter).Hours() / 24) + os.Exit(exitCodeForExpiry(remainingDays, config.WarnDays)) +} + +func exitCodeForExpiry(remainingDays int, warnDays int) int { + if warnDays > 0 && remainingDays <= warnDays { + return 2 + } + return 0 } func getCertificate(domain string, port int, insecure bool, timeout time.Duration) (*x509.Certificate, error) { diff --git a/main_test.go b/main_test.go index 595a31d..7e12fa7 100644 --- a/main_test.go +++ b/main_test.go @@ -17,3 +17,28 @@ func TestGetCertificateTimeout(t *testing.T) { t.Fatalf("getCertificate took %s, expected it to time out quickly", elapsed) } } + +func TestExitCodeForExpiry(t *testing.T) { + tests := []struct { + name string + remainingDays int + warnDays int + want int + }{ + {"disabled always zero", 1, 0, 0}, + {"disabled even when expired", -30, 0, 0}, + {"well above threshold", 30, 5, 0}, + {"exactly at threshold", 5, 5, 2}, + {"below threshold", 3, 5, 2}, + {"expired", -10, 5, 2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := exitCodeForExpiry(tt.remainingDays, tt.warnDays) + if got != tt.want { + t.Errorf("exitCodeForExpiry(%d, %d) = %d, want %d", tt.remainingDays, tt.warnDays, got, tt.want) + } + }) + } +}