diff --git a/README.md b/README.md index b3c9e69f..ed7d119f 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,22 @@ accessible to this tool. Use FQDNs in order to retrieve certificates using - Optional support for ignoring expired intermediate certificates - Optional support for ignoring expiring root certificates - Optional support for ignoring expired of root certificates +- Optional support for omitting Subject Alternate Names (SANs) entries from + plugin output +- Optional support for embedding an encoded certificate metadata payload + - disabled by default to retain existing plugin behavior + - the intent is to "shuttle" a payload of certificate metadata in structured + format from the plugin, to the monitoring system and to downstream tools + (e.g., via API call) so that the payload can be retrieved, decoded, & + unmarshalled to a supported data structure for further certificate + evaluation + - see also the and + projects for the data structures + and supporting logic used in the encoding/decoding process +- Optional support for embedding an encoded certificate metadata payload *with + the original certificate chain included* in PEM encoded format + - this is not enabled by default due to the significant increase in plugin + output size ### `lscert` @@ -678,6 +694,8 @@ validation checks and any behavior changes at that time noted. | `branding` | No | `false` | No | `branding` | Toggles emission of branding details with plugin status details. This output is disabled by default. | | `h`, `help` | No | `false` | No | `h`, `help` | Show Help text along with the list of supported flags. | | `v`, `verbose` | No | `false` | No | `v`, `verbose` | Toggles emission of detailed certificate metadata. This level of output is disabled by default. | +| `payload` | No | `false` | No | `true`, `false` | Toggles emission of encoded certificate chain payload. This output is disabled by default. | +| `payload-with-full-chain` | No | `false` | No | `true`, `false` | Toggles emission of encoded certificate chain payload with the full certificate chain included. This option is disabled by default due to the significant increase in payload size. | | `omit-sans-list` | No | `false` | No | `true`, `false` | Toggles listing of SANs entries list items in certificate metadata output. This list is included by default. | | `version` | No | `false` | No | `version` | Whether to display application version and then immediately exit application. | | `c`, `age-critical` | No | 15 | No | *positive whole number of days* | The threshold for the certificate check's `CRITICAL` state. If the certificate expires before this number of days then the service check will be considered in a `CRITICAL` state. | diff --git a/cmd/check_cert/main.go b/cmd/check_cert/main.go index 068e8d4d..3ddf8a2c 100644 --- a/cmd/check_cert/main.go +++ b/cmd/check_cert/main.go @@ -30,6 +30,7 @@ func main() { // Override default section headers with our custom values. plugin.SetErrorsLabel("VALIDATION ERRORS") plugin.SetDetailedInfoLabel("VALIDATION CHECKS REPORT") + plugin.SetEncodedPayloadLabel("CERTIFICATE METADATA PAYLOAD") // defer this from the start so it is the last deferred function to run defer plugin.ReturnCheckResults() @@ -61,6 +62,14 @@ func main() { return } + // Enable this setting *after* we initialize the plugin configuration; + // Debug level is the default global logging level which our initialized + // configuration overrides (to either a user-specified value or Info as an + // app default). + if zerolog.GlobalLevel() == zerolog.DebugLevel || zerolog.GlobalLevel() == zerolog.TraceLevel { + plugin.DebugLoggingEnablePluginOutputSize() + } + // Annotate all errors (if any) with remediation advice just before ending // plugin execution. defer annotateErrors(plugin) @@ -414,6 +423,10 @@ func main() { return } + if cfg.EmitPayload || cfg.EmitPayloadWithFullChain { + addCertChainPayload(plugin, cfg, validationResults) + } + switch { case validationResults.HasFailed(): diff --git a/cmd/check_cert/paypload.go b/cmd/check_cert/paypload.go new file mode 100644 index 00000000..39cb0f99 --- /dev/null +++ b/cmd/check_cert/paypload.go @@ -0,0 +1,384 @@ +// Copyright 2024 Adam Chalkley +// +// https://github.com/atc0005/check-cert +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package main + +import ( + "crypto/x509" + "encoding/json" + "fmt" + "math" + + payload "github.com/atc0005/cert-payload" + "github.com/atc0005/check-cert/internal/certs" + "github.com/atc0005/check-cert/internal/config" + "github.com/atc0005/go-nagios" +) + +// certExpirationMetadata is a bundle of certificate expiration related +// metadata used when preparing a certificate payload for inclusion in plugin +// output. +type certExpirationMetadata struct { + validityPeriodDays int + daysRemainingTruncated int + daysRemainingPrecise float64 + certLifetimePercent int +} + +// lookupCertExpMetadata is a helper function used to lookup specific +// certificate expiration metadata values used when preparing a certificate +// payload for inclusion in plugin output. +func lookupCertExpMetadata(cert *x509.Certificate, certNumber int, certChain []*x509.Certificate) (certExpirationMetadata, error) { + if cert == nil { + return certExpirationMetadata{}, fmt.Errorf( + "cert in chain position %d of %d is nil: %w", + certNumber, + len(certChain), + certs.ErrMissingValue, + ) + } + + certLifetime, certLifeTimeErr := certs.LifeRemainingPercentageTruncated(cert) + if certLifeTimeErr != nil { + return certExpirationMetadata{}, fmt.Errorf( + "error calculating lifetime for cert %q: %w", + cert.Subject.CommonName, + certLifeTimeErr, + ) + } + + daysRemainingTruncated, expLookupErr := certs.ExpiresInDays(cert) + if expLookupErr != nil { + return certExpirationMetadata{}, fmt.Errorf( + "error calculating the number of days until the certificate %q expires: %w", + cert.Subject.CommonName, + expLookupErr, + ) + } + + daysRemainingPrecise, expLookupErrPrecise := certs.ExpiresInDaysPrecise(cert) + if expLookupErrPrecise != nil { + return certExpirationMetadata{}, fmt.Errorf( + "error calculating the number of days until the certificate %q expires: %w", + cert.Subject.CommonName, + expLookupErr, + ) + } + + validityPeriodDays, lifespanLookupErr := certs.MaxLifespanInDays(cert) + if lifespanLookupErr != nil { + return certExpirationMetadata{}, fmt.Errorf( + "error calculating the maximum lifespan in days for certificate %q: %w", + cert.Subject.CommonName, + lifespanLookupErr, + ) + } + + return certExpirationMetadata{ + certLifetimePercent: certLifetime, + daysRemainingPrecise: daysRemainingPrecise, + daysRemainingTruncated: daysRemainingTruncated, + validityPeriodDays: validityPeriodDays, + }, nil +} + +// extractExpValResult is a helper function used to extract the expiration +// validation result from a collection of previously applied certificate +// validation check results. +func extractExpValResult(validationResults certs.CertChainValidationResults) (certs.ExpirationValidationResult, error) { + var expirationValidationResult certs.ExpirationValidationResult + + for _, validationResult := range validationResults { + if expResult, ok := validationResult.(certs.ExpirationValidationResult); ok { + expirationValidationResult = expResult + break + } + } + + // Assert that we're working with a non-zero value. + if len(expirationValidationResult.CertChain()) == 0 { + // We're working with an uninitialized value; abort! + return certs.ExpirationValidationResult{}, fmt.Errorf( + "unable to extract expiration validation results"+ + " from collection of %d values: %w", + len(validationResults), + certs.ErrMissingValue, + ) + } + + return expirationValidationResult, nil +} + +// extractHostnameValResult is a helper function used to extract the expiration +// validation result from a collection of previously applied certificate +// validation check results. +func extractHostnameValResult(validationResults certs.CertChainValidationResults) (certs.HostnameValidationResult, error) { + var hostnameValidationResult certs.HostnameValidationResult + + for _, validationResult := range validationResults { + if hostnameResult, ok := validationResult.(certs.HostnameValidationResult); ok { + hostnameValidationResult = hostnameResult + break + } + } + + // Assert that we're working with a non-zero value. + if len(hostnameValidationResult.CertChain()) == 0 { + // We're working with an uninitialized value; abort! + return certs.HostnameValidationResult{}, fmt.Errorf( + "unable to extract hostname validation results"+ + " from collection of %d values: %w", + len(validationResults), + certs.ErrMissingValue, + ) + } + + return hostnameValidationResult, nil +} + +// buildCertSummary is a helper function that coordinates retrieving, +// collecting, evaluating and encoding certificate metadata as a JSON encoded +// string for inclusion in plugin output. +func buildCertSummary(cfg *config.Config, validationResults certs.CertChainValidationResults) (string, error) { + expirationValidationResult, expExtractErr := extractExpValResult(validationResults) + if expExtractErr != nil { + return "", fmt.Errorf( + "failed to generate certificate summary: %w", + expExtractErr, + ) + } + + hostnameValidationResult, hostnameExtractErr := extractHostnameValResult(validationResults) + if hostnameExtractErr != nil { + return "", fmt.Errorf( + "failed to generate certificate summary: %w", + hostnameExtractErr, + ) + } + + certsExpireAgeCritical := expirationValidationResult.AgeCriticalThreshold() + certsExpireAgeWarning := expirationValidationResult.AgeWarningThreshold() + + // Question: Should we use the customized certificate chain with any + // user-specified certificates to exclude (for whatever reason) removed so + // that we do not report on values which are problematic? + // + // Answer: No, we use the full chain so that any "downstream" reporting + // tools retrieving the certificate payload from the monitoring system can + // perform their own analysis with the full chain available for review. + certChain := expirationValidationResult.FilteredCertificateChain() + + certChainSubset := make([]payload.Certificate, 0, len(certChain)) + for certNumber, origCert := range certChain { + if origCert == nil { + return "", fmt.Errorf( + "cert in chain position %d of %d is nil: %w", + certNumber, + len(certChain), + certs.ErrMissingValue, + ) + } + + expiresText := certs.ExpirationStatus( + origCert, + certsExpireAgeCritical, + certsExpireAgeWarning, + false, + ) + + certStatus := payload.CertificateStatus{ + OK: expirationValidationResult.IsOKState(), + Expiring: expirationValidationResult.HasExpiringCerts(), + Expired: expirationValidationResult.HasExpiredCerts(), + } + + certExpMeta, lookupErr := lookupCertExpMetadata(origCert, certNumber, certChain) + if lookupErr != nil { + return "", lookupErr + } + + var SANsEntries []string + if cfg.OmitSANsEntries { + SANsEntries = nil + } else { + SANsEntries = origCert.DNSNames + } + + validityPeriodDescription := lookupValidityPeriodDescription(origCert) + + certSubset := payload.Certificate{ + Subject: origCert.Subject.String(), + CommonName: origCert.Subject.CommonName, + SANsEntries: SANsEntries, + Issuer: origCert.Issuer.String(), + IssuerShort: origCert.Issuer.CommonName, + SerialNumber: certs.FormatCertSerialNumber(origCert.SerialNumber), + IssuedOn: origCert.NotBefore, + ExpiresOn: origCert.NotAfter, + DaysRemaining: certExpMeta.daysRemainingPrecise, + DaysRemainingTruncated: certExpMeta.daysRemainingTruncated, + LifetimePercent: certExpMeta.certLifetimePercent, + ValidityPeriodDescription: validityPeriodDescription, + ValidityPeriodDays: certExpMeta.validityPeriodDays, + Summary: expiresText, + Status: certStatus, + Type: certs.ChainPosition(origCert, certChain), + } + + certChainSubset = append(certChainSubset, certSubset) + } + + hasMissingIntermediateCerts := certs.NumIntermediateCerts(certChain) == 0 + hasMultipleLeafCerts := certs.NumLeafCerts(certChain) > 1 + hasExpiredCerts := certs.HasExpiredCert(certChain) + hasHostnameMismatch := !hostnameValidationResult.IsOKState() + hasMissingSANsEntries := func(certChain []*x509.Certificate) bool { + leafCerts := certs.LeafCerts(certChain) + for _, leafCert := range leafCerts { + if len(leafCert.DNSNames) > 0 { + return false + } + } + + return true + }(certChain) + + hasSelfSignedLeaf := func(certChain []*x509.Certificate) bool { + leafCerts := certs.LeafCerts(certChain) + for _, leafCert := range leafCerts { + // NOTE: We may need to perform actual signature verification here for + // the most reliable results. + if leafCert.Issuer.String() == leafCert.Subject.String() { + return true + } + } + + return false + }(certChain) + + certChainIssues := payload.CertificateChainIssues{ + MissingIntermediateCerts: hasMissingIntermediateCerts, + MissingSANsEntries: hasMissingSANsEntries, + MultipleLeafCerts: hasMultipleLeafCerts, + // MisorderedCerts: false, // FIXME: Placeholder value + ExpiredCerts: hasExpiredCerts, + HostnameMismatch: hasHostnameMismatch, + SelfSignedLeafCert: hasSelfSignedLeaf, + } + + // Only if the user explicitly requested the full cert payload do we + // include it (due to significant payload size increase and risk of + // exceeding size constraints). + var certChainOriginal []string + switch { + case cfg.EmitPayloadWithFullChain: + pemCertChain, err := payload.CertChainToPEM(certChain) + if err != nil { + return "", fmt.Errorf("error converting original cert chain to PEM format: %w", err) + } + + certChainOriginal = pemCertChain + + default: + certChainOriginal = nil + } + + payload := payload.CertChainPayload{ + CertChainOriginal: certChainOriginal, + CertChainSubset: certChainSubset, + Server: cfg.Server, + DNSName: cfg.DNSName, + TCPPort: cfg.Port, + Issues: certChainIssues, + ServiceState: expirationValidationResult.ServiceState().Label, + } + + payloadJSON, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf( + "error marshaling cert chain payload as JSON: %w", + err, + ) + } + + return string(payloadJSON), nil +} + +// addCertChainPayload is a helper function that prepares a certificate chain +// payload as a JSON encoded value for inclusion in plugin output. +func addCertChainPayload(plugin *nagios.Plugin, cfg *config.Config, validationResults certs.CertChainValidationResults) { + certChainSummary, certSummaryErr := buildCertSummary(cfg, validationResults) + + log := cfg.Log.With().Logger() + + if certSummaryErr != nil { + log.Error(). + Err(certSummaryErr). + Msg("failed to generate cert chain summary for encoded payload") + + plugin.Errors = append(plugin.Errors, certSummaryErr) + + plugin.ExitStatusCode = nagios.StateUNKNOWNExitCode + plugin.ServiceOutput = fmt.Sprintf( + "%s: Failed to add encoded payload", + nagios.StateUNKNOWNLabel, + ) + + return + } + + // fmt.Fprintln(os.Stderr, certChainSummary) + log.Debug().Str("json_payload", certChainSummary).Msg("JSON payload before encoding") + + // NOTE: AddPayloadString will NOT return an error if empty input is + // provided. + if _, err := plugin.AddPayloadString(certChainSummary); err != nil { + log.Error(). + Err(err). + Msg("failed to add encoded payload") + + plugin.Errors = append(plugin.Errors, err) + + plugin.ExitStatusCode = nagios.StateUNKNOWNExitCode + plugin.ServiceOutput = fmt.Sprintf( + "%s: Failed to add encoded payload", + nagios.StateUNKNOWNLabel, + ) + + return + } +} + +// lookupValidityPeriodDescription is a helper function to lookup human +// readable validity period description for a certificate's maximum lifetime +// value. +func lookupValidityPeriodDescription(cert *x509.Certificate) string { + maxLifeSpanInDays, err := certs.MaxLifespanInDays(cert) + if err != nil { + return payload.ValidityPeriodUNKNOWN + } + + maxLifeSpanInTruncatedYears := int(math.Trunc(float64(maxLifeSpanInDays) / 365)) + + switch { + case maxLifeSpanInTruncatedYears >= 1: + return fmt.Sprintf("%d year", maxLifeSpanInTruncatedYears) + + default: + return fmt.Sprintf("%d days", maxLifeSpanInDays) + } +} + +// isBetween is a small helper function to determine whether a given value is +// between a specified minimum and maximum number (inclusive). +// func isBetween(val, min, max int) bool { +// if (val >= min) && (val <= max) { +// return true +// } +// +// return false +// } diff --git a/go.mod b/go.mod index b732631e..98b5700c 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,16 @@ module github.com/atc0005/check-cert go 1.20 require ( + github.com/atc0005/cert-payload v0.3.0 github.com/atc0005/go-nagios v0.17.0 github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b github.com/rs/zerolog v1.33.0 ) +// Allow for testing local changes before they're published. +// replace github.com/atc0005/cert-payload => ../cert-payload +// replace github.com/atc0005/go-nagios => ../go-nagios + require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index e510987b..79f7e2a6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/atc0005/cert-payload v0.3.0 h1:aIhir71r+Ryc7GNuQ0jxQ0iwKk8LP0wjgvSz5caJXBo= +github.com/atc0005/cert-payload v0.3.0/go.mod h1:46vw6K3bJ3zODjGW7xIilssL8r2Sg41hRAGideGH0kU= github.com/atc0005/go-nagios v0.17.0 h1:DHQbzP0HWt9kZM9xvFgI4HZ0TBY4qQN+E+Usz8MrgTw= github.com/atc0005/go-nagios v0.17.0/go.mod h1:n2RHhsrgI8xiapqkJ240dKLwMXWbWvkOPLE92x0IGaM= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= diff --git a/internal/certs/certs.go b/internal/certs/certs.go index aa580a4f..5e988485 100644 --- a/internal/certs/certs.go +++ b/internal/certs/certs.go @@ -910,6 +910,46 @@ func ExpiresInDays(cert *x509.Certificate) (int, error) { return daysRemaining, nil } +// ExpiresInDaysPrecise evaluates the given certificate and returns the number +// of days until the certificate expires as a floating point number. This +// number is rounded down. +// +// If already expired, a negative number is returned indicating how many days +// the certificate is past expiration. +// +// An error is returned if the pointer to the given certificate is nil. +func ExpiresInDaysPrecise(cert *x509.Certificate) (float64, error) { + if cert == nil { + return 0, fmt.Errorf( + "func ExpiresInDaysPrecise: unable to determine expiration: %w", + ErrMissingValue, + ) + } + + timeRemaining := time.Until(cert.NotAfter).Hours() + + // Round down to the nearest two decimal places. + daysRemaining := timeRemaining / 24 + daysRemaining = math.Floor(daysRemaining*100) / 100 + + return daysRemaining, nil +} + +// ExpiresInHours evaluates the given certificate and returns the number of +// hours until the certificate expires as a floating point number. +// +// An error is returned if the pointer to the given certificate is nil. +func ExpiresInHours(cert *x509.Certificate) (float64, error) { + if cert == nil { + return 0, fmt.Errorf( + "func ExpiresInHours: unable to determine expiration: %w", + ErrMissingValue, + ) + } + + return time.Until(cert.NotAfter).Hours(), nil +} + // MaxLifespan returns the maximum lifespan for a given certificate from the // date it was issued until the time it is scheduled to expire. func MaxLifespan(cert *x509.Certificate) (time.Duration, error) { @@ -1391,6 +1431,29 @@ func ChainPosition(cert *x509.Certificate, certChain []*x509.Certificate) string } +// SANsEntriesLine provides a formatted list of SANs entries for a given +// certificate if present, "none" if none are available or if requested a +// brief message indicating that they have been explicitly omitted. +func SANsEntriesLine(cert *x509.Certificate, omitSANsEntries bool) string { + switch { + case omitSANsEntries && len(cert.DNSNames) > 0: + return fmt.Sprintf( + "SANs entries (%d): Omitted by request", + len(cert.DNSNames), + ) + + case len(cert.DNSNames) > 0: + return fmt.Sprintf( + "SANs entries (%d): %s", + len(cert.DNSNames), + cert.DNSNames, + ) + + default: + return "SANs entries: None" + } +} + // GenerateCertChainReport receives the current certificate chain status // generates a formatted report suitable for display on the console or // (potentially) via Microsoft Teams provided suitable conversion is performed @@ -1409,26 +1472,6 @@ func GenerateCertChainReport( certsTotal := len(certChain) - sansEntriesLine := func(cert *x509.Certificate) string { - switch { - case omitSANsEntries && len(cert.DNSNames) > 0: - return fmt.Sprintf( - "SANs entries (%d): Omitted by request", - len(cert.DNSNames), - ) - - case len(cert.DNSNames) > 0: - return fmt.Sprintf( - "SANs entries (%d): %s", - len(cert.DNSNames), - cert.DNSNames, - ) - - default: - return "SANs entries: None" - } - } - for idx, certificate := range certChain { certPosition := ChainPosition(certificate, certChain) @@ -1509,7 +1552,7 @@ func GenerateCertChainReport( nagios.CheckOutputEOL, certificate.Subject, nagios.CheckOutputEOL, - sansEntriesLine(certificate), + SANsEntriesLine(certificate, omitSANsEntries), nagios.CheckOutputEOL, textutils.BytesToDelimitedHexStr(certificate.SubjectKeyId, ":"), nagios.CheckOutputEOL, @@ -1549,7 +1592,7 @@ func GenerateCertChainReport( nagios.CheckOutputEOL, certificate.Subject, nagios.CheckOutputEOL, - sansEntriesLine(certificate), + SANsEntriesLine(certificate, omitSANsEntries), nagios.CheckOutputEOL, certificate.Issuer, nagios.CheckOutputEOL, diff --git a/internal/certs/validation-expiration.go b/internal/certs/validation-expiration.go index 205b2aa2..f5afd7c9 100644 --- a/internal/certs/validation-expiration.go +++ b/internal/certs/validation-expiration.go @@ -88,9 +88,10 @@ type ExpirationValidationResult struct { priorityModifier int // ageWarningThreshold is the specified age threshold for when - // certificates in the chain with an expiration less than this value are - // considered to be in a WARNING state. This value is calculated based on - // user specified threshold in days. + // certificates in the chain with an expiration less than this value (but + // greater than the CRITICAL threshold) are considered to be in a WARNING + // state. This value is calculated based on user specified threshold in + // days. ageWarningThreshold time.Time // ageCriticalThreshold is the specified age threshold for when @@ -824,3 +825,18 @@ func (evr ExpirationValidationResult) ValidationStatus(certChain []*x509.Certifi return "successful" } } + +// AgeWarningThreshold returns the value of the warning threshold based on the +// user specified value in days. Certificates in the chain with an expiration +// less than this value (but greater than the CRITICAL threshold) are +// considered to be in a WARNING state. +func (evr ExpirationValidationResult) AgeWarningThreshold() time.Time { + return evr.ageWarningThreshold +} + +// AgeCriticalThreshold returns the value of the CRITICAL threshold based on +// the user specified value in days. Certificates in the chain with an +// expiration less than this value are considered to be in a CRITICAL state. +func (evr ExpirationValidationResult) AgeCriticalThreshold() time.Time { + return evr.ageCriticalThreshold +} diff --git a/internal/config/config.go b/internal/config/config.go index 3b5cc241..2d88a5e3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -296,6 +296,16 @@ type Config struct { // their own branding output. EmitBranding bool + // EmitPayload controls whether an encoded certificate chain payload is + // included at the bottom of plugin output. + EmitPayload bool + + // EmitPayloadWithFullChain controls whether an encoded certificate chain + // payload (included at the bottom of plugin output) also bundles the full + // original certificate chain. If enabled this significantly increases the + // payload size. + EmitPayloadWithFullChain bool + // VerboseOutput controls whether detailed certificate metadata is emitted // along with standard certificate details. VerboseOutput bool diff --git a/internal/config/constants.go b/internal/config/constants.go index 67d6279a..16595792 100644 --- a/internal/config/constants.go +++ b/internal/config/constants.go @@ -42,6 +42,8 @@ const ( certExpireAgeWarningFlagHelp string = "The number of days remaining before certificate expiration when this application will will flag the NotAfter certificate field as a WARNING state." certExpireAgeCriticalFlagHelp string = "The number of days remaining before certificate expiration when this application will will flag the NotAfter certificate field as a CRITICAL state." brandingFlagHelp string = "Toggles emission of branding details with plugin status details. This output is disabled by default." + payloadFlagHelp string = "Toggles emission of encoded certificate chain payload. This output is disabled by default." + payloadWithFullChainFlagHelp string = "Toggles emission of encoded certificate chain payload with the full certificate chain included. This option is disabled by default due to the significant increase in payload size." verboseOutputFlagHelp string = "Toggles emission of detailed certificate metadata. This level of output is disabled by default." omitSANsEntriesFlagHelp string = "Toggles listing of SANs entries list items in certificate metadata output. This list is included by default." showHostsWithClosedPortsFlagHelp string = "Toggles listing all host port scan results, even for hosts without any specified ports in an open state." @@ -86,17 +88,19 @@ const ( IgnoreExpiringIntermediateCertificatesFlag string = "ignore-expiring-intermediate-certs" IgnoreExpiringRootCertificatesFlag string = "ignore-expiring-root-certs" - VersionFlagLong string = "version" - OmitSANsEntriesFlagLong string = "omit-sans-list" - VerboseFlagLong string = "verbose" - VerboseFlagShort string = "v" - BrandingFlag string = "branding" - ServerFlagLong string = "server" - ServerFlagShort string = "s" - PortFlagLong string = "port" - PortFlagShort string = "p" - DNSNameFlagLong string = "dns-name" - DNSNameFlagShort string = "dn" + VersionFlagLong string = "version" + OmitSANsEntriesFlagLong string = "omit-sans-list" + VerboseFlagLong string = "verbose" + VerboseFlagShort string = "v" + BrandingFlag string = "branding" + PayloadFlag string = "payload" + PayloadWithFullChainFlag string = "payload-with-full-chain" + ServerFlagLong string = "server" + ServerFlagShort string = "s" + PortFlagLong string = "port" + PortFlagShort string = "p" + DNSNameFlagLong string = "dns-name" + DNSNameFlagShort string = "dn" // Flags used for specifying a list of keywords used to explicitly ignore // or apply validation check results when determining final plugin state. @@ -171,6 +175,8 @@ const ( defaultEmitCertText bool = false defaultFilename string = "" // inspector, plugin; potentially deprecated defaultBranding bool = false + defaultPayload bool = false + defaultPayloadWithFullChain bool = false defaultVerboseOutput bool = false defaultOmitSANsEntriesList bool = false defaultDisplayVersionAndExit bool = false diff --git a/internal/config/flags.go b/internal/config/flags.go index 369695d5..871b8918 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -58,6 +58,8 @@ func (c *Config) handleFlagsConfig(appType AppType) { appDescription = "Nagios plugin used to monitor & perform validation checks of certificate chains." + flag.BoolVar(&c.EmitPayload, PayloadFlag, defaultPayload, payloadFlagHelp) + flag.BoolVar(&c.EmitPayloadWithFullChain, PayloadWithFullChainFlag, defaultPayloadWithFullChain, payloadWithFullChainFlagHelp) flag.BoolVar(&c.EmitBranding, BrandingFlag, defaultBranding, brandingFlagHelp) flag.BoolVar( &c.IgnoreHostnameVerificationFailureIfEmptySANsList, diff --git a/vendor/github.com/atc0005/cert-payload/.gitignore b/vendor/github.com/atc0005/cert-payload/.gitignore new file mode 100644 index 00000000..2d551e7f --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/.gitignore @@ -0,0 +1,35 @@ +# Copyright 2024 Adam Chalkley +# +# https://github.com/atc0005/cert-payload +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Ignore local Visual Studio Code settings +.vscode/ + +# Ignore local "scratch" directory of temporary files +/scratch + +# Go workspace file +go.work +go.work.sum + +# env file +.env diff --git a/vendor/github.com/atc0005/cert-payload/.golangci.yml b/vendor/github.com/atc0005/cert-payload/.golangci.yml new file mode 100644 index 00000000..2b755868 --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/.golangci.yml @@ -0,0 +1,32 @@ +# Copyright 2024 Adam Chalkley +# +# https://github.com/atc0005/cert-payload +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +issues: + # equivalent CLI flag: --exclude-use-default + # + # see: + # atc0005/todo#29 + # golangci-lint/golangci-lint#1249 + # golangci-lint/golangci-lint#413 + exclude-use-default: false + +linters: + enable: + - dogsled + - dupl + - goconst + - gocritic + - gofmt + - goimports + - gosec + - govet + - misspell + - prealloc + - revive + - staticcheck + - stylecheck + - unconvert diff --git a/vendor/github.com/atc0005/cert-payload/.markdownlint.yml b/vendor/github.com/atc0005/cert-payload/.markdownlint.yml new file mode 100644 index 00000000..03b1ace5 --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/.markdownlint.yml @@ -0,0 +1,22 @@ +# Copyright 2024 Adam Chalkley +# +# https://github.com/atc0005/cert-payload +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +# https://github.com/igorshubovych/markdownlint-cli#configuration +# https://github.com/DavidAnson/markdownlint#optionsconfig + +# Setting the special default rule to true or false includes/excludes all +# rules by default. +"default": true + +# We know that line lengths will be long in the main README file, so don't +# report those cases. +"MD013": false + +# Don't complain if sub-heading names are duplicated since this is a common +# practice in CHANGELOG.md (e.g., "Fixed"). +"MD024": + "siblings_only": true diff --git a/vendor/github.com/atc0005/cert-payload/CHANGELOG.md b/vendor/github.com/atc0005/cert-payload/CHANGELOG.md new file mode 100644 index 00000000..136f686b --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +## Overview + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a +Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Please [open an issue](https://github.com/atc0005/cert-payload/issues) for any +deviations that you spot; I'm still learning!. + +## Types of changes + +The following types of changes will be recorded in this file: + +- `Added` for new features. +- `Changed` for changes in existing functionality. +- `Deprecated` for soon-to-be removed features. +- `Removed` for now removed features. +- `Fixed` for any bug fixes. +- `Security` in case of vulnerabilities. + +## [Unreleased] + +- placeholder + +## [v0.3.0] - 2024-11-06 + +### Changed + +- (GH-23) Update `ValidityPeriod*` constants +- (GH-24) Clarify fields which may not be populated + +## [v0.2.0] - 2024-11-04 + +### Added + +- (GH-21) Add `CertificateChainIssues.MissingSANsEntries` + +## [v0.1.0] - 2024-11-03 + +### Added + +Initial package state + +Add current code used in `atc0005/check-cert` prototype to be used when +generating an encoded certificate chain metadata payload for inclusion in +plugin output. + +[Unreleased]: https://github.com/atc0005/cert-payload/compare/v0.3.0...HEAD +[v0.3.0]: https://github.com/atc0005/cert-payload/releases/tag/v0.3.0 +[v0.2.0]: https://github.com/atc0005/cert-payload/releases/tag/v0.2.0 +[v0.1.0]: https://github.com/atc0005/cert-payload/releases/tag/v0.1.0 diff --git a/vendor/github.com/atc0005/cert-payload/LICENSE b/vendor/github.com/atc0005/cert-payload/LICENSE new file mode 100644 index 00000000..fa6ae0b3 --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Adam Chalkley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/atc0005/cert-payload/Makefile b/vendor/github.com/atc0005/cert-payload/Makefile new file mode 100644 index 00000000..9ce7bd6b --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/Makefile @@ -0,0 +1,115 @@ +# Copyright 2024 Adam Chalkley +# +# https://github.com/atc0005/cert-payload +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +# REFERENCES +# +# https://github.com/golangci/golangci-lint#install +# https://github.com/golangci/golangci-lint/releases/latest + +SHELL = /bin/bash + +BUILDCMD = go build -mod=vendor ./... +GOCLEANCMD = go clean -mod=vendor ./... +GITCLEANCMD = git clean -xfd +CHECKSUMCMD = sha256sum -b + +.DEFAULT_GOAL := help + + ########################################################################## + # Targets will not work properly if a file with the same name is ever + # created in this directory. We explicitly declare our targets to be phony + # by making them a prerequisite of the special target .PHONY + ########################################################################## + +.PHONY: help +## help: prints this help message +help: + @echo "Usage:" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +.PHONY: lintinstall +## lintinstall: install common linting tools +# https://github.com/golang/go/issues/30515#issuecomment-582044819 +lintinstall: + @echo "Installing linting tools" + + @export PATH="${PATH}:$(go env GOPATH)/bin" + + @echo "Installing latest stable staticcheck version via go install command ..." + go install honnef.co/go/tools/cmd/staticcheck@latest + staticcheck --version + + @echo Installing latest stable golangci-lint version per official installation script ... + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin + golangci-lint --version + + @echo "Finished updating linting tools" + +.PHONY: linting +## linting: runs common linting checks +linting: + @echo "Running linting tools ..." + + @echo "Running go vet ..." + @go vet -mod=vendor $(shell go list -mod=vendor ./... | grep -v /vendor/) + + @echo "Running golangci-lint ..." + @golangci-lint --version + @golangci-lint run + + @echo "Running staticcheck ..." + @staticcheck --version + @staticcheck $(shell go list -mod=vendor ./... | grep -v /vendor/) + + @echo "Finished running linting checks" + +.PHONY: gotests +## gotests: runs go test recursively, verbosely +gotests: + @echo "Running go tests ..." + @go test -mod=vendor ./... + @echo "Finished running go tests" + +.PHONY: goclean +## goclean: removes local build artifacts, temporary files, etc +goclean: + @echo "Removing object files and cached files ..." + @$(GOCLEANCMD) + +.PHONY: clean +## clean: alias for goclean +clean: goclean + +.PHONY: gitclean +## gitclean: WARNING - recursively cleans working tree by removing non-versioned files +gitclean: + @echo "Removing non-versioned files ..." + @$(GITCLEANCMD) + +.PHONY: pristine +## pristine: run goclean and gitclean to remove local changes +pristine: goclean gitclean + +.PHONY: all +# https://stackoverflow.com/questions/3267145/makefile-execute-another-target +## all: run all applicable build steps +all: clean build + @echo "Completed build process ..." + +.PHONY: quick +## quick: alias for build recipe +quick: clean build + @echo "Completed tasks for quick build" + +.PHONY: build +## build: ensure that packages build +build: + @echo "Building packages ..." + + $(BUILDCMD) + + @echo "Completed build tasks" diff --git a/vendor/github.com/atc0005/cert-payload/README.md b/vendor/github.com/atc0005/cert-payload/README.md new file mode 100644 index 00000000..f8fe3881 --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/README.md @@ -0,0 +1,125 @@ + +# cert-payload + +Support for managing embedded JSON payloads generated by a plugin from the +atc0005/check-cert project + +[![Latest Release](https://img.shields.io/github/release/atc0005/cert-payload.svg?style=flat-square)](https://github.com/atc0005/cert-payload/releases/latest) +[![Go Reference](https://pkg.go.dev/badge/github.com/atc0005/cert-payload.svg)](https://pkg.go.dev/github.com/atc0005/cert-payload) +[![go.mod Go version](https://img.shields.io/github/go-mod/go-version/atc0005/cert-payload)](https://github.com/atc0005/cert-payload) +[![Lint and Build](https://github.com/atc0005/cert-payload/actions/workflows/lint-and-build.yml/badge.svg)](https://github.com/atc0005/cert-payload/actions/workflows/lint-and-build.yml) +[![Project Analysis](https://github.com/atc0005/cert-payload/actions/workflows/project-analysis.yml/badge.svg)](https://github.com/atc0005/cert-payload/actions/workflows/project-analysis.yml) + + +## Table of contents + +- [Overview](#overview) +- [Status](#status) +- [Features](#features) +- [Changelog](#changelog) +- [Examples](#examples) +- [License](#license) +- [Used by](#used-by) +- [References](#references) + +## Overview + +This package provides support and functionality for managing embedded JSON +payloads generated by a plugin from the `atc0005/check-cert` project. + +## Status + +While attempts are made to provide stability, this codebase is subject to +change without notice and may break client code that depends on it. You are +encouraged to [vendor](#references) this package if you find it useful until +such time that the API is considered stable. + +## Features + +- placeholder + +## Changelog + +See the [`CHANGELOG.md`](CHANGELOG.md) file for the changes associated with +each release of this application. Changes that have been merged to `master`, +but not yet an official release may also be noted in the file under the +`Unreleased` section. A helpful link to the Git commit history since the last +official release is also provided for further review. + +## Examples + +Add this line to your imports like so: + +```golang +package main + +import ( + "fmt" + "log" + "os" + + payload "github.com/atc0005/cert-payload" +) +``` + +and pull in a specific version of this library that you'd like to use. + +```console +go get github.com/atc0005/cert-payload@v0.1.0 +``` + +Alternatively, you can use the latest stable tag available to get started: + +```console +go get github.com/atc0005/cert-payload@latest +``` + +See for specific examples. + +See for +projects that are using this library. + +## License + +From the [LICENSE](LICENSE) file: + +```license +MIT License + +Copyright (c) 2024 Adam Chalkley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Used by + +See the Known importers lists below for a dynamically updated list of projects +using either this library or the original project. + +- [Known importers (pkg.go.dev)](https://pkg.go.dev/github.com/atc0005/cert-payload?tab=importedby) + +## References + +- +- + +See also the [Used by](#used-by) section for projects known to be using this +package. Please +[report](https://github.com/atc0005/cert-payload/issues/new/choose) any +additional projects that we've missed! diff --git a/vendor/github.com/atc0005/cert-payload/chain-export.go b/vendor/github.com/atc0005/cert-payload/chain-export.go new file mode 100644 index 00000000..21ba5c37 --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/chain-export.go @@ -0,0 +1,82 @@ +// Copyright 2024 Adam Chalkley +// +// https://github.com/atc0005/cert-payload +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package payload + +import ( + "bytes" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" +) + +// CertChainToJSON encodes the certificate chain in PEM format and then +// marshals the PEM-encoded certificates to JSON. An error is returned if an +// invalid cert chain is provided or if the marshaling process fails. +func CertChainToJSON(certChain []*x509.Certificate) ([]byte, error) { + if len(certChain) == 0 { + return nil, fmt.Errorf( + "failed to encode cert chain: %w", + ErrMissingValue, + ) + } + + pemCerts, err := CertChainToPEM(certChain) + if err != nil { + return nil, fmt.Errorf( + "failed to encode cert chain: %w", + err, + ) + } + + certChainJSON, err := json.Marshal(pemCerts) + if err != nil { + return nil, fmt.Errorf( + "error marshaling JSON: %w", + err, + ) + } + + return certChainJSON, nil +} + +// CertChainToPEM encodes the certificate chain in PEM format as a slice of +// string values. An error is returned if an invalid cert chain is provided. +func CertChainToPEM(certChain []*x509.Certificate) ([]string, error) { + if len(certChain) == 0 { + return nil, fmt.Errorf( + "failed to encode cert chain: %w", + ErrMissingValue, + ) + } + + pemCerts := make([]string, 0, len(certChain)) + + var buf bytes.Buffer + for i, cert := range certChain { + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + + err := pem.Encode(&buf, pemBlock) + if err != nil { + return nil, fmt.Errorf( + "failed to encode cert %d in PEM format: %w", + i, + err, + ) + } + + pemCerts = append(pemCerts, buf.String()) + + buf.Reset() + } + + return pemCerts, nil +} diff --git a/vendor/github.com/atc0005/cert-payload/doc.go b/vendor/github.com/atc0005/cert-payload/doc.go new file mode 100644 index 00000000..f3ab60be --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/doc.go @@ -0,0 +1,19 @@ +// Copyright 2024 Adam Chalkley +// +// https://github.com/atc0005/cert-payload +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +// Package payload provides support for managing embedded JSON payloads +// generated by the `check_cert` plugin within this project. +// +// See our [GitHub repo]: +// +// - to review documentation (including examples) +// - for the latest code +// - to file an issue or submit improvements for review and potential +// inclusion into the project +// +// [GitHub repo]: https://github.com/atc0005/cert-payload +package payload diff --git a/vendor/github.com/atc0005/cert-payload/payload.go b/vendor/github.com/atc0005/cert-payload/payload.go new file mode 100644 index 00000000..e8602a76 --- /dev/null +++ b/vendor/github.com/atc0005/cert-payload/payload.go @@ -0,0 +1,243 @@ +// Copyright 2024 Adam Chalkley +// +// https://github.com/atc0005/cert-payload +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package payload + +import ( + "errors" + "time" +) + +// Nagios plugin/service check state "labels". These values are used (where +// applicable) by the CertChainPayload `ServiceState` field. +const ( + StateOKLabel string = "OK" + StateWARNINGLabel string = "WARNING" + StateCRITICALLabel string = "CRITICAL" + StateUNKNOWNLabel string = "UNKNOWN" + StateDEPENDENTLabel string = "DEPENDENT" +) + +// Validity period keywords intended as human readable output. +// +// Common historical certificate lifetimes: +// +// - 5 year (1825 days, 60 months) +// - 3 year (1185 days, 39 months) +// - 2 year (825 days, 27 months) +// - 1 year (398 days, 13 months) +// +// See also: +// +// - https://www.sectigo.com/knowledge-base/detail/TLS-SSL-Certificate-Lifespan-History-2-3-and-5-year-validity/kA01N000000zFKp +// - https://support.sectigo.com/Com_KnowledgeDetailPage?Id=kA03l000000o6cv +// - https://www.digicert.com/faq/public-trust-and-certificates/how-long-are-tls-ssl-certificate-validity-periods +// - https://docs.digicert.com/en/whats-new/change-log/older-changes/change-log--2023.html#certcentral--changes-to-multi-year-plan-coverage +// - https://knowledge.digicert.com/quovadis/ssl-certificates/ssl-general-topics/maximum-validity-changes-for-tls-ssl-to-drop-to-825-days-in-q1-2018 +// - https://chromium.googlesource.com/chromium/src/+/666712ff6c7ba7aa5da380bc0a617b637c9232b3/net/docs/certificate_lifetimes.md +// - https://www.entrust.com/blog/2017/03/maximum-certificate-lifetime-drops-to-825-days-in-2018 +const ( + ValidityPeriod1Year string = "1 year" + ValidityPeriod90Days string = "90 days" + ValidityPeriod45Days string = "45 days" + ValidityPeriodUNKNOWN string = "UNKNOWN" +) + +var ( + // ErrMissingValue indicates that an expected value was missing. + ErrMissingValue = errors.New("missing expected value") +) + +// CertificateStatus is the overall status of a certificate. +// +// - no problems (ok) +// - expired +// - expiring (based on given threshold values) +// - revoked (not yet supported) +// +// TODO: Any useful status values to borrow here? +// They have `Active`, `Revoked` and then a `Pending*` variation for both. +// https://developers.cloudflare.com/ssl/reference/certificate-statuses/#client-certificates +type CertificateStatus struct { + OK bool `json:"status_ok"` // No observed issues; shouldn't this be calculated? + Expiring bool `json:"status_expiring"` // Based on given monitoring thresholds + Expired bool `json:"status_expired"` // Based on certificate NotAfter field + + // This is a feature to add later + // RevokedPerCRL bool `json:"status_revoked_per_crl"` // Based on CRL or OCSP check? + // RevokedPerOCSP bool `json:"status_revoked_per_ocsp"` // Based on CRL or OCSP check? + // ? +} + +// Certificate is a subset of the metadata for an evaluated certificate. +type Certificate struct { + // Subject is the full subject value for a certificate. This is intended + // for (non-cryptographic) comparison purposes. + Subject string `json:"subject"` + + // CommonName is the short subject value of a certificate. This is + // intended for display purposes. + CommonName string `json:"common_name"` + + // SANsEntries is the full list of Subject Alternate Names for a + // certificate. + SANsEntries []string `json:"sans_entries"` + + // Issuer is the full CommonName of the signing certificate. This is + // intended for (non-cryptographic) comparison purposes. + Issuer string `json:"issuer"` + + // IssuerShort is the short CommonName of the signing certificate. This is + // intended for display purposes. + IssuerShort string `json:"issuer_short"` + + // SerialNumber is the serial number for a certificate in hex format with + // a colon inserted after each two digits. + // + // For example, `77:BD:0D:6C:DB:36:F9:1A:EA:21:0F:C4:F0:58:D3:0D`. + SerialNumber string `json:"serial_number"` + + // IssuedOn is a RFC3389 time value for when a certificate is first + // valid or usable. + IssuedOn time.Time `json:"not_before"` + + // ExpiresOn is a RFC3389 time value for when the certificate expires. + ExpiresOn time.Time `json:"not_after"` + + // DaysRemaining is the number of days remaining for a certificate in two + // digit decimal precision. + DaysRemaining float64 `json:"days_remaining"` + + // DaysRemainingTruncated is the number of days remaining for a + // certificate as a whole number rounded down. + // + // For example, if five and a half days remain then this value would be + // `5`. + DaysRemainingTruncated int `json:"days_remaining_truncated"` + + // LifetimePercent is percentage of life remaining for a certificate. + // + // For example, if 43% life is remaining for a cert (a rounded value) this + // field would be set to `43`. + LifetimePercent int `json:"lifetime_remaining_percent"` + + // ValidityPeriodDescription is the human readable value such as "90 days" + // or "1 year". + ValidityPeriodDescription string `json:"validity_period_description"` + + // ValidityPeriodDays is the number of total days a certificate is valid + // for using `Not Before` & `Not After` as the starting & ending range. + ValidityPeriodDays int `json:"validity_period_days"` + + // human readable summary such as, `[OK] 1199d 2h remaining (43%)` + Summary string `json:"summary"` + + // Status is the overall status of the certificate. + Status CertificateStatus `json:"status"` + + // Type indicates the type of certificate (leaf, intermediate or root). + Type string `json:"type"` +} + +// CertificateChainIssues is an aggregated collection of problems detected for +// the certificate chain. +type CertificateChainIssues struct { + // MissingIntermediateCerts indicates that intermediate certificates are + // missing from the certificate chain. + MissingIntermediateCerts bool `json:"missing_intermediate_certs"` + + // MissingSANsEntries indicates that SANs entries are missing from a leaf + // certificate within the certificates chain. + MissingSANsEntries bool `json:"missing_sans_entries"` + + // MultipleLeafCerts indicates that there are more than the single + // permitted leaf certificate in the certificate chain. + MultipleLeafCerts bool `json:"multiple_leaf_certs"` + + // MisorderedCerts indicates that certificates in the chain are out of the + // expected order. + // + // E.g., instead of leaf, intermediate(s), root (technically not best + // practice) the chain has something like leaf, root, intermediate(s) or + // intermediates and then leaf. + // MisorderedCerts bool `json:"misordered_certs"` + + // ExpiredCerts indicates that there are one or more expired certificates + // in the certificate chain. + ExpiredCerts bool `json:"expired_certs"` + + // HostnameMismatch indicates that the name or IP Address used to + // establish a connection to a certificate-enabled service does not match + // the list of valid host names honored by the leaf certificate. + // + // Historically the Common Name (CN) field was searched in addition to the + // Subject Alternate Names (SANs) field for a match, but this practice is + // deprecated and many clients (e.g., web browsers) no longer support + // this. + HostnameMismatch bool `json:"hostname_mismatch"` + + // SelfSignedLeafCert indicates that the leaf certificate is self-signed. + // This is fairly common for development/test environments but is not best + // practice for certificates used outside of temporary / lab environments. + SelfSignedLeafCert bool `json:"self_signed_leaf_cert"` + + // SelfSignedIntermediateCerts indicates that an intermediate certificate + // in the chain is self-signed. + // + // NOTE: This is unlikely to occur in practice, so we're likely not going + // to keep this field. + // + // SelfSignedIntermediateCerts bool `json:"self_signed_intermediate_certs"` + + // This is a later TODO item. + // RevokedCerts bool `json:"revoked_certs"` +} + +// CertChainPayload is the "parent" data structure which represents the +// information to be encoded as a payload and later decoded for use in +// reporting (and other) tools. +type CertChainPayload struct { + // CertChainOriginal is the original certificate chain entries encoded in + // PEM format. + // + // Due to size constraints this field may not be populated if the user did + // not explicitly opt into bundling the full certificate chain. + CertChainOriginal []string `json:"cert_chain_original"` + + // CertChainSubset is a customized subset of the original certificate + // chain metadata. This field should always be populated. + CertChainSubset []Certificate `json:"cert_chain_subset"` + + // Server is the FQDN or IP Address specified to the plugin which was used + // to retrieve the certificate chain. + // + // TODO: Considering making this a struct with fields for resolved IP + // Address and original CLI flag value (often a FQDN, but just as often a + // fixed IP Address). + Server string `json:"server"` + + // A fully-qualified domain name or IP Address in the Subject Alternate + // Names (SANs) list for the leaf certificate. + // + // Depending on how the check_cert plugin was called this value may not be + // set (e.g., the `server` flag is sufficient if specifying a valid FQDN + // associated with the leaf certificate). + DNSName string `json:"dns_name"` + + // TCPPort is the TCP port of the remote certificate-enabled service. This + // is usually 443 (HTTPS) or 636 (LDAPS). + TCPPort int `json:"tcp_port"` + + // Issues is an aggregated collection of problems detected for the + // certificate chain. + Issues CertificateChainIssues `json:"cert_chain_issues"` + + // ServiceState is the monitoring system's evaluated state for the service + // check performed against a given certificate chain (e.g., OK, CRITICAL, + // WARNING, UNKNOWN). + ServiceState string `json:"service_state"` +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 11e52524..d4a63772 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,6 @@ +# github.com/atc0005/cert-payload v0.3.0 +## explicit; go 1.19 +github.com/atc0005/cert-payload # github.com/atc0005/go-nagios v0.17.0 ## explicit; go 1.19 github.com/atc0005/go-nagios