// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "encoding/json" "fmt" "os" "strings" "github.com/hashicorp/vault/command/healthcheck" "github.com/ghodss/yaml" "github.com/mitchellh/cli" "github.com/posener/complete" "github.com/ryanuber/columnize" ) const ( pkiRetOK int = iota pkiRetUsage pkiRetInformational pkiRetWarning pkiRetCritical pkiRetInvalidVersion pkiRetInsufficientPermissions ) var ( _ cli.Command = (*PKIHealthCheckCommand)(nil) _ cli.CommandAutocomplete = (*PKIHealthCheckCommand)(nil) // Ensure the above return codes match (outside of OK/Usage) the values in // the healthcheck package. _ = pkiRetInformational == int(healthcheck.ResultInformational) _ = pkiRetWarning == int(healthcheck.ResultWarning) _ = pkiRetCritical == int(healthcheck.ResultCritical) _ = pkiRetInvalidVersion == int(healthcheck.ResultInvalidVersion) _ = pkiRetInsufficientPermissions == int(healthcheck.ResultInsufficientPermissions) ) type PKIHealthCheckCommand struct { *BaseCommand flagConfig string flagReturnIndicator string flagDefaultDisabled bool flagList bool } func (c *PKIHealthCheckCommand) Synopsis() string { return "Check a PKI Secrets Engine mount's health and operational status" } func (c *PKIHealthCheckCommand) Help() string { helpText := ` Usage: vault pki health-check [options] MOUNT Reports status of the specified mount against best practices and pending failures. This is an informative command and not all recommendations will apply to all mounts; consider using a configuration file to tune the executed health checks. To check the pki-root mount with default configuration: $ vault pki health-check pki-root To specify a configuration: $ vault pki health-check -health-config=mycorp-root.json /pki-root Return codes indicate failure type: 0 - Everything is good. 1 - Usage error (check CLI parameters). 2 - Informational message from a health check. 3 - Warning message from a health check. 4 - Critical message from a health check. 5 - A version mismatch between health check and Vault Server occurred, preventing one or more health checks from being run. 6 - A permission denied message was returned from Vault Server for one or more health checks. For more detailed information, refer to the online documentation about the vault pki health-check command. ` + c.Flags().Help() return strings.TrimSpace(helpText) } func (c *PKIHealthCheckCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) f := set.NewFlagSet("Command Options") f.StringVar(&StringVar{ Name: "health-config", Target: &c.flagConfig, Default: "", EnvVar: "", Usage: "Path to JSON configuration file to modify health check execution and parameters.", }) f.StringVar(&StringVar{ Name: "return-indicator", Target: &c.flagReturnIndicator, Default: "default", EnvVar: "", Completion: complete.PredictSet("default", "informational", "warning", "critical", "permission"), Usage: `Behavior of the return value: - permission, for exiting with a non-zero code when the tool lacks permissions or has a version mismatch with the server; - critical, for exiting with a non-zero code when a check returns a critical status in addition to the above; - warning, for exiting with a non-zero status when a check returns a warning status in addition to the above; - informational, for exiting with a non-zero status when a check returns an informational status in addition to the above; - default, for the default behavior based on severity of message and only returning a zero exit status when all checks have passed and no execution errors have occurred. `, }) f.BoolVar(&BoolVar{ Name: "default-disabled", Target: &c.flagDefaultDisabled, Default: false, EnvVar: "", Usage: `When specified, results in all health checks being disabled by default unless enabled by the configuration file explicitly.`, }) f.BoolVar(&BoolVar{ Name: "list", Target: &c.flagList, Default: false, EnvVar: "", Usage: `When specified, no health checks are run, but all known health checks are printed.`, }) return set } func (c *PKIHealthCheckCommand) isValidRetIndicator() bool { switch c.flagReturnIndicator { case "", "default", "informational", "warning", "critical", "permission": return true default: return false } } func (c *PKIHealthCheckCommand) AutocompleteArgs() complete.Predictor { // Return an anything predictor here, similar to `vault write`. We // don't know what values are valid for the mount path. return complete.PredictAnything } func (c *PKIHealthCheckCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } func (c *PKIHealthCheckCommand) Run(args []string) int { // Parse and validate the arguments. f := c.Flags() if err := f.Parse(args); err != nil { c.UI.Error(err.Error()) return pkiRetUsage } args = f.Args() if !c.flagList && len(args) < 1 { c.UI.Error("Not enough arguments (expected mount path, got nothing)") return pkiRetUsage } else if !c.flagList && len(args) > 1 { c.UI.Error(fmt.Sprintf("Too many arguments (expected only mount path, got %d arguments)", len(args))) for _, arg := range args { if strings.HasPrefix(arg, "-") { c.UI.Warn(fmt.Sprintf("Options (%v) must be specified before positional arguments (%v)", arg, args[0])) break } } return pkiRetUsage } if !c.isValidRetIndicator() { c.UI.Error(fmt.Sprintf("Invalid flag -return-indicator=%v; known options are default, informational, warning, critical, and permission", c.flagReturnIndicator)) return pkiRetUsage } // Setup the client and the executor. client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return pkiRetUsage } // When listing is enabled, we lack an argument here, but do not contact // the server at all, so we're safe to use a hard-coded default here. pkiPath := "" if len(args) == 1 { pkiPath = args[0] } mount := sanitizePath(pkiPath) executor := healthcheck.NewExecutor(client, mount) executor.AddCheck(healthcheck.NewCAValidityPeriodCheck()) executor.AddCheck(healthcheck.NewCRLValidityPeriodCheck()) executor.AddCheck(healthcheck.NewHardwareBackedRootCheck()) executor.AddCheck(healthcheck.NewRootIssuedLeavesCheck()) executor.AddCheck(healthcheck.NewRoleAllowsLocalhostCheck()) executor.AddCheck(healthcheck.NewRoleAllowsGlobWildcardsCheck()) executor.AddCheck(healthcheck.NewRoleNoStoreFalseCheck()) executor.AddCheck(healthcheck.NewAuditVisibilityCheck()) executor.AddCheck(healthcheck.NewAllowIfModifiedSinceCheck()) executor.AddCheck(healthcheck.NewEnableAutoTidyCheck()) executor.AddCheck(healthcheck.NewTidyLastRunCheck()) executor.AddCheck(healthcheck.NewTooManyCertsCheck()) executor.AddCheck(healthcheck.NewEnableAcmeIssuance()) executor.AddCheck(healthcheck.NewAllowAcmeHeaders()) if c.flagDefaultDisabled { executor.DefaultEnabled = false } // Handle listing, if necessary. if c.flagList { uiFormat := Format(c.UI) if uiFormat == "yaml" { c.UI.Error("YAML output format is not supported by the --list command") return pkiRetUsage } if uiFormat != "json" { c.UI.Output("Default health check config:") } config := map[string]map[string]interface{}{} for _, checker := range executor.Checkers { config[checker.Name()] = checker.DefaultConfig() } marshaled, err := json.MarshalIndent(config, "", " ") if err != nil { c.UI.Error(fmt.Sprintf("Failed to marshal default config for check: %v", err)) return pkiRetUsage } c.UI.Output(string(marshaled)) return pkiRetOK } // Handle config merging. external_config := map[string]interface{}{} if c.flagConfig != "" { contents, err := os.Open(c.flagConfig) if err != nil { c.UI.Error(fmt.Sprintf("Failed to read configuration file %v: %v", c.flagConfig, err)) return pkiRetUsage } decoder := json.NewDecoder(contents) decoder.UseNumber() // Use json.Number instead of float64 values as we are decoding to an interface{}. if err := decoder.Decode(&external_config); err != nil { c.UI.Error(fmt.Sprintf("Failed to parse configuration file %v: %v", c.flagConfig, err)) return pkiRetUsage } } if err := executor.BuildConfig(external_config); err != nil { c.UI.Error(fmt.Sprintf("Failed to build health check configuration: %v", err)) return pkiRetUsage } // Run the health checks. results, err := executor.Execute() if err != nil { c.UI.Error(fmt.Sprintf("Failed to run health check: %v", err)) return pkiRetUsage } // Display the output. if err := c.outputResults(executor, results); err != nil { c.UI.Error(fmt.Sprintf("Failed to render results for display: %v", err)) } // Select an appropriate return code. return c.selectRetCode(results) } func (c *PKIHealthCheckCommand) outputResults(e *healthcheck.Executor, results map[string][]*healthcheck.Result) error { switch Format(c.UI) { case "", "table": return c.outputResultsTable(e, results) case "json": return c.outputResultsJSON(results) case "yaml": return c.outputResultsYAML(results) default: return fmt.Errorf("unknown output format: %v", Format(c.UI)) } } func (c *PKIHealthCheckCommand) outputResultsTable(e *healthcheck.Executor, results map[string][]*healthcheck.Result) error { // Iterate in checker order to ensure stable output. for _, checker := range e.Checkers { if !checker.IsEnabled() { continue } scanner := checker.Name() findings := results[scanner] c.UI.Output(scanner) c.UI.Output(strings.Repeat("-", len(scanner))) data := []string{"status" + hopeDelim + "endpoint" + hopeDelim + "message"} for _, finding := range findings { row := []string{ finding.StatusDisplay, finding.Endpoint, finding.Message, } data = append(data, strings.Join(row, hopeDelim)) } c.UI.Output(tableOutput(data, &columnize.Config{ Delim: hopeDelim, })) c.UI.Output("\n") } return nil } func (c *PKIHealthCheckCommand) outputResultsJSON(results map[string][]*healthcheck.Result) error { bytes, err := json.MarshalIndent(results, "", " ") if err != nil { return err } c.UI.Output(string(bytes)) return nil } func (c *PKIHealthCheckCommand) outputResultsYAML(results map[string][]*healthcheck.Result) error { bytes, err := yaml.Marshal(results) if err != nil { return err } c.UI.Output(string(bytes)) return nil } func (c *PKIHealthCheckCommand) selectRetCode(results map[string][]*healthcheck.Result) int { var highestResult healthcheck.ResultStatus = healthcheck.ResultNotApplicable for _, findings := range results { for _, finding := range findings { if finding.Status > highestResult { highestResult = finding.Status } } } cutOff := healthcheck.ResultInformational switch c.flagReturnIndicator { case "", "default", "informational": case "permission": cutOff = healthcheck.ResultInvalidVersion case "critical": cutOff = healthcheck.ResultCritical case "warning": cutOff = healthcheck.ResultWarning } if highestResult >= cutOff { return int(highestResult) } return pkiRetOK }