diff --git a/checks/host_network.yaml b/checks/host_network.yaml new file mode 100644 index 00000000..59447ece --- /dev/null +++ b/checks/host_network.yaml @@ -0,0 +1,16 @@ +name: HostNetworkSet +id: hostNetworkSet +successMessage: Host network is not configured +failureMessage: Host network should not be configured +category: Networking +controllers: + exclude: [] +target: Pod +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + hostNetwork: + not: + enum: + - true diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go index b132585b..090aa838 100644 --- a/pkg/config/exemptions.go +++ b/pkg/config/exemptions.go @@ -6,7 +6,10 @@ import ( ) // IsActionable determines whether a check is actionable given the current configuration -func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool { +func (conf Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool { + if subConfStr, ok := subConf.(string); ok { + subConf = conf.GetCategoryConfig(subConfStr) + } ruleID := GetIDFromField(subConf, ruleName) subConfRef := reflect.ValueOf(subConf) fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface() @@ -36,3 +39,29 @@ func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controlle } return true } + +func (conf Configuration) GetCategoryConfig(category string) interface{} { + if category == "Networking" { + return conf.Networking + } else if category == "Security" { + return conf.Security + } else if category == "Health Checks" { + return conf.HealthChecks + } else if category == "Resources" { + return conf.Resources + } else if category == "Images" { + return conf.Images + } + return nil +} + +func (conf Configuration) GetSeverity(category string, name string) Severity { + subConf := conf.GetCategoryConfig(category) + subConfRef := reflect.ValueOf(subConf) + fieldVal := reflect.Indirect(subConfRef).FieldByName(name).Interface() + if severity, ok := fieldVal.(Severity); ok { + return severity + } + // TODO: don't panic + panic("Unknown severity: " + category + "/" + name) +} diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go index f92f7521..d3ac89c1 100644 --- a/pkg/validator/controller.go +++ b/pkg/validator/controller.go @@ -31,11 +31,12 @@ func ValidateController(conf conf.Configuration, controller controller.Interface controllerType := controller.GetType() pod := controller.GetPodSpec() podResult := ValidatePod(conf, pod, controller.GetName(), controllerType) - return ControllerResult{ + result := ControllerResult{ Type: controllerType.String(), Name: controller.GetName(), PodResult: podResult, } + return result } // ValidateControllers validates that each deployment conforms to the Polaris config, diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index 52ed9e9f..222c6ed2 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -34,7 +34,7 @@ func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName } pv.validateSecurity(&conf, controllerName) - pv.validateNetworking(&conf, controllerName) + applyPodSchemaChecks(&conf, pod, controllerName, &pv) pRes := PodResult{ Messages: pv.messages(), @@ -83,17 +83,3 @@ func (pv *PodValidation) validateSecurity(conf *config.Configuration, controller } } } - -func (pv *PodValidation) validateNetworking(conf *config.Configuration, controllerName string) { - category := messages.CategoryNetworking - - name := "HostNetworkSet" - if conf.IsActionable(conf.Networking, name, controllerName) { - id := config.GetIDFromField(conf.Networking, name) - if pv.Pod.HostNetwork { - pv.addFailure(messages.HostNetworkFailure, conf.Networking.HostNetworkSet, category, id) - } else { - pv.addSuccess(messages.HostNetworkSuccess, category, id) - } - } -} diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go new file mode 100644 index 00000000..0cceaf2a --- /dev/null +++ b/pkg/validator/schema.go @@ -0,0 +1,125 @@ +package validator + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + packr "github.com/gobuffalo/packr/v2" + "github.com/qri-io/jsonschema" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/fairwindsops/polaris/pkg/config" + controller "github.com/fairwindsops/polaris/pkg/validator/controllers" +) + +type IncludeExcludeList struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +type Target string + +const ( + TargetContainer Target = "Container" + TargetPod Target = "Pod" +) + +type SchemaCheck struct { + Name string `yaml:"name"` + ID string `yaml:"id"` + Category string `yaml:"category"` + SuccessMessage string `yaml:"success_message"` + FailureMessage string `yaml:"failure_message"` + Controllers IncludeExcludeList `yaml:"controllers"` + Target Target `yaml:"target"` + Schema jsonschema.RootSchema `yaml:"schema"` +} + +var ( + schemaBox = (*packr.Box)(nil) + checks = map[Target][]SchemaCheck{ + TargetContainer: []SchemaCheck{}, + TargetPod: []SchemaCheck{}, + } +) + +func init() { + schemaBox = packr.New("Schemas", "../../checks") + files := schemaBox.List() + for _, file := range files { + contents, err := schemaBox.Find(file) + if err != nil { + panic(err) + } + check, err := parseCheck(contents) + if err != nil { + panic(err) + } + checks[check.Target] = append(checks[check.Target], check) + } +} + +func parseCheck(rawBytes []byte) (SchemaCheck, error) { + reader := bytes.NewReader(rawBytes) + check := SchemaCheck{} + d := yaml.NewYAMLOrJSONDecoder(reader, 4096) + for { + if err := d.Decode(&check); err != nil { + if err == io.EOF { + return check, nil + } + return check, fmt.Errorf("Decoding schema check failed: %v", err) + } + } +} + +func (check SchemaCheck) check(controller controller.Interface) (bool, error) { + pod := controller.GetPodSpec() + if check.Target == TargetPod { + return check.checkPod(pod) + } else if check.Target == TargetContainer { + for _, container := range pod.Containers { + bytes, err := json.Marshal(container) + if err != nil { + return false, err + } + errors, err := check.Schema.ValidateBytes(bytes) + if err != nil || len(errors) > 0 { + return false, err + } + } + // TODO: initcontainers + } + return true, nil +} + +func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) { + bytes, err := json.Marshal(pod) + if err != nil { + return false, err + } + errors, err := check.Schema.ValidateBytes(bytes) + return len(errors) == 0, err +} + +func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, pv *PodValidation) error { + for _, check := range checks[TargetPod] { + if !conf.IsActionable(check.Category, check.Name, controllerName) { + continue + } + severity := conf.GetSeverity(check.Category, check.Name) + passes, err := check.checkPod(pod) + if err != nil { + return err + } + if passes { + pv.addSuccess(check.SuccessMessage, check.Category, check.ID) + } else { + pv.addFailure(check.FailureMessage, severity, check.Category, check.ID) + } + } + return nil +}