pkg/analyzer/psaEvaluator.go (164 lines of code) (raw):
package analyzer
import (
"bytes"
"errors"
"fmt"
"io"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/scheme"
api "k8s.io/pod-security-admission/api"
policy "k8s.io/pod-security-admission/policy"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
yaml "gopkg.in/yaml.v3"
)
type PsaEvaluator interface {
Evaluate(stream []byte, level string) (AnalyzerResponse, error)
}
func NewPsaEvaluator() PsaEvaluator {
return &psaEvaluator{}
}
type psaEvaluator struct {
}
func (e *psaEvaluator) Evaluate(stream []byte, levelString string) (AnalyzerResponse, error) {
var allowed bool
var obj runtime.Object
var gKV *schema.GroupVersionKind
var err error
var response AnalyzerResponse
var latest api.Version
var level api.Level
yamlDecoder := yaml.NewDecoder(bytes.NewReader(stream))
k8sDecode := scheme.Codecs.UniversalDeserializer().Decode
response.Allowed = true
//TODO: Accept PSS version as a parameter
latest, err = api.ParseVersion("latest")
if err != nil {
panic(err)
}
level, err = api.ParseLevel(levelString)
if err != nil {
panic(err)
}
levelVersion := api.LevelVersion{
Level: level,
Version: latest,
}
for {
var node yaml.Node
err = yamlDecoder.Decode(&node)
if errors.Is(err, io.EOF) {
response.AnalysisStatus = "eof"
break
}
if err != nil {
response.AnalysisStatus = "error"
panic(err)
}
content, err := yaml.Marshal(&node)
if err != nil {
response.AnalysisStatus = "error"
panic(err)
}
// prepare yaml document for evaluation
obj, gKV, err = k8sDecode(content, nil, nil)
if err != nil {
fmt.Printf("Non standard k8s node found\n")
response.AnalysisStatus = "error"
//TODO: Take into consideration break parameter flag
continue
}
//process response
allowed, err = e.evaluate(obj, gKV, levelVersion)
if err != nil {
response.AnalysisStatus = "error"
panic(err)
}
response.Allowed = response.Allowed && allowed
}
if response.AnalysisStatus == "error" {
return response, err
}
return response, nil
}
func (e *psaEvaluator) evaluate(obj runtime.Object, gKV *schema.GroupVersionKind, levelVersion api.LevelVersion) (bool, error) {
var podMetadata v1.ObjectMeta
var podSpec corev1.PodSpec
var name string
evaluator, _ := policy.NewEvaluator(policy.DefaultChecks())
// TODO: Defer return true or false after whole document evaluation depending on configuration
// f.e.: You may want to consider that including non evaluable versions should render the level as privileged
// or you may just skip them depending on command line parameters
switch gKV.Kind {
case "Pod":
pod := obj.(*corev1.Pod)
name = pod.ObjectMeta.Name
podMetadata = pod.ObjectMeta
podSpec = pod.Spec
case "Deployment":
if gKV.Group+gKV.Version != "appsv1" {
fmt.Printf(gKV.Group+"."+gKV.Version+" not evaluable for kind: %v\n", gKV.Kind)
return true, nil
}
deployment := obj.(*appsv1.Deployment)
name = deployment.ObjectMeta.Name
podMetadata = deployment.ObjectMeta
podSpec = deployment.Spec.Template.Spec
case "DaemonSet":
if gKV.Group+gKV.Version != "appsv1" {
fmt.Printf("Version "+gKV.Version+" not evaluable for kind: %v\n", gKV.Kind)
return true, nil
}
daemonset := obj.(*appsv1.DaemonSet)
name = daemonset.ObjectMeta.Name
podMetadata = daemonset.ObjectMeta
podSpec = daemonset.Spec.Template.Spec
case "ReplicaSet":
if gKV.Group+gKV.Version != "appsv1" {
fmt.Printf("Version "+gKV.Version+" not evaluable for kind: %v\n", gKV.Kind)
return true, nil
}
replicaset := obj.(*appsv1.ReplicaSet)
name = replicaset.ObjectMeta.Name
podMetadata = replicaset.ObjectMeta
podSpec = replicaset.Spec.Template.Spec
case "StatefulSet":
if gKV.Group+gKV.Version != "appsv1" {
fmt.Printf("Version "+gKV.Version+" not evaluable for kind: %v\n", gKV.Kind)
return true, nil
}
statefulset := obj.(*appsv1.StatefulSet)
name = statefulset.ObjectMeta.Name
podMetadata = statefulset.ObjectMeta
podSpec = statefulset.Spec.Template.Spec
case "Job":
if gKV.Group+gKV.Version != "batchv1" {
fmt.Printf("Version "+gKV.Version+" not evaluable for kind: %v\n", gKV.Kind)
return true, nil
}
job := obj.(*batchv1.Job)
name = job.ObjectMeta.Name
podMetadata = job.ObjectMeta
podSpec = job.Spec.Template.Spec
case "CronJob":
if gKV.Group+gKV.Version != "batchv1" {
fmt.Printf("Version "+gKV.Version+" not evaluable for kind: %v\n", gKV.Kind)
return true, nil
}
cronJob := obj.(*batchv1.CronJob)
name = cronJob.ObjectMeta.Name
podMetadata = cronJob.ObjectMeta
podSpec = cronJob.Spec.JobTemplate.Spec.Template.Spec
default:
//TODO: optional log message on verbose output
fmt.Printf("Kind not evaluable: %v\n", gKV.Kind)
return true, nil
}
fmt.Printf("%v %v\n", gKV.Kind, name)
// Evaluate
allowed := true
results := evaluator.EvaluatePod(levelVersion, &podMetadata, &podSpec)
//TODO: optional log message on verbose output
fmt.Printf(" PSS level %v %v\n", levelVersion.Level, levelVersion.Version)
for i := range results {
if !results[i].Allowed {
fmt.Printf(" Check %v failed: %v\n", i, results[i].ForbiddenReason)
fmt.Printf(" %v\n", results[i].ForbiddenDetail)
allowed = false
}
}
//TODO: make error return error
return allowed, nil
}