385 lines
11 KiB
Go
385 lines
11 KiB
Go
// 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 := "<mount>"
|
|
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
|
|
}
|