-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add logic for Kyverno policy recommendation
Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>
- Loading branch information
1 parent
ce80984
commit d0dfd5f
Showing
31 changed files
with
1,259 additions
and
210 deletions.
There are no files selected for viewing
256 changes: 256 additions & 0 deletions
256
src/admissioncontrollerpolicy/admissionControllerPolicy.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
package admissioncontrollerpolicy | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"github.com/accuknox/auto-policy-discovery/src/cluster" | ||
cfg "github.com/accuknox/auto-policy-discovery/src/config" | ||
"github.com/accuknox/auto-policy-discovery/src/libs" | ||
logger "github.com/accuknox/auto-policy-discovery/src/logging" | ||
obs "github.com/accuknox/auto-policy-discovery/src/observability" | ||
opb "github.com/accuknox/auto-policy-discovery/src/protobuf/v1/observability" | ||
wpb "github.com/accuknox/auto-policy-discovery/src/protobuf/v1/worker" | ||
"github.com/accuknox/auto-policy-discovery/src/types" | ||
"github.com/clarketm/json" | ||
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" | ||
"github.com/rs/zerolog" | ||
corev1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"path/filepath" | ||
"regexp" | ||
"sigs.k8s.io/yaml" | ||
"strings" | ||
) | ||
|
||
var log *zerolog.Logger | ||
var CfgDB types.ConfigDB | ||
|
||
type containerMountPathServiceAccountToken struct { | ||
podName string | ||
podNamespace string | ||
containerName string | ||
saTokenMountPath string | ||
} | ||
|
||
func init() { | ||
log = logger.GetInstance() | ||
} | ||
|
||
func InitAdmissionControllerPolicyDiscoveryConfiguration() { | ||
CfgDB = cfg.GetCfgDB() | ||
} | ||
|
||
func UpdateOrInsertKyvernoPolicies(kyvernoPolicies []kyvernov1.Policy) { | ||
UpdateOrInsertPolicyYamlToDB(kyvernoPolicies) | ||
} | ||
|
||
func DeleteKyvernoPolicies(kyvernoPolicies []string, namespace string, labels map[string]string) { | ||
for _, kyvernoPolicy := range kyvernoPolicies { | ||
err := libs.DeletePolicyBasedOnPolicyName(CfgDB, kyvernoPolicy, namespace, libs.LabelMapToString(labels)) | ||
if err != nil { | ||
log.Warn().Msgf("deleting policy from DB failed err=%v", err.Error()) | ||
} | ||
} | ||
} | ||
|
||
// UpdateOrInsertPolicyYamlToDB upserts admission controller policies to DB | ||
func UpdateOrInsertPolicyYamlToDB(kyvernoPolicies []kyvernov1.Policy) { | ||
|
||
var res []types.PolicyYaml | ||
for _, kyvernoPolicy := range kyvernoPolicies { | ||
jsonBytes, err := json.Marshal(kyvernoPolicy) | ||
if err != nil { | ||
log.Error().Msg(err.Error()) | ||
continue | ||
} | ||
yamlBytes, err := yaml.JSONToYAML(jsonBytes) | ||
if err != nil { | ||
log.Error().Msg(err.Error()) | ||
continue | ||
} | ||
|
||
policyYaml := types.PolicyYaml{ | ||
Type: types.PolicyTypeAdmissionController, | ||
Kind: kyvernoPolicy.TypeMeta.Kind, | ||
Name: kyvernoPolicy.ObjectMeta.Name, | ||
Namespace: kyvernoPolicy.ObjectMeta.Namespace, | ||
Cluster: cfg.GetCfgClusterName(), | ||
WorkspaceId: cfg.GetCfgWorkspaceId(), | ||
ClusterId: cfg.GetCfgClusterId(), | ||
Labels: kyvernoPolicy.Spec.Rules[0].MatchResources.Any[0].ResourceDescription.Selector.MatchLabels, | ||
Yaml: yamlBytes, | ||
} | ||
res = append(res, policyYaml) | ||
} | ||
|
||
if err := libs.UpdateOrInsertPolicyYamls(CfgDB, res); err != nil { | ||
log.Error().Msgf(err.Error()) | ||
} | ||
} | ||
|
||
func GetAdmissionControllerPolicy(namespace, clusterName, labels string) []kyvernov1.Policy { | ||
var kyvernoPolicies []kyvernov1.Policy | ||
policyYamls, err := libs.GetPolicyYamls(CfgDB, types.PolicyTypeAdmissionController) | ||
if err != nil { | ||
log.Error().Msgf("fetching policy yaml from DB failed err=%v", err.Error()) | ||
return nil | ||
} | ||
for _, policyYaml := range policyYamls { | ||
if policyYaml.Namespace == namespace && | ||
policyYaml.Cluster == clusterName && | ||
libs.LabelMapToString(policyYaml.Labels) == labels { | ||
var kyvernoPolicy kyvernov1.Policy | ||
err := yaml.Unmarshal(policyYaml.Yaml, &kyvernoPolicy) | ||
if err != nil { | ||
log.Error().Msgf("unmarshalling policy yaml failed err=%v", err.Error()) | ||
continue | ||
} | ||
kyvernoPolicies = append(kyvernoPolicies, kyvernoPolicy) | ||
} | ||
} | ||
return kyvernoPolicies | ||
} | ||
|
||
func ConvertPoliciesToWorkerResponse(policies []kyvernov1.Policy) *wpb.WorkerResponse { | ||
var response wpb.WorkerResponse | ||
|
||
for i := range policies { | ||
kyvernoPolicy := wpb.Policy{} | ||
|
||
val, err := json.Marshal(&policies[i]) | ||
if err != nil { | ||
log.Error().Msgf("kyvernoPolicy json marshal failed err=%v", err.Error()) | ||
} | ||
kyvernoPolicy.Data = val | ||
|
||
response.AdmissionControllerPolicy = append(response.AdmissionControllerPolicy, &kyvernoPolicy) | ||
} | ||
response.Res = "OK" | ||
|
||
return &response | ||
} | ||
|
||
func ShouldSATokenBeAutoMounted(namespace string, labels map[string]string) bool { | ||
client := cluster.ConnectK8sClient() | ||
|
||
podList, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{ | ||
LabelSelector: libs.LabelMapToString(labels), | ||
}) | ||
|
||
if err != nil { | ||
log.Warn().Msg(err.Error()) | ||
return true | ||
} | ||
|
||
if len(podList.Items) > 0 { | ||
// Only inspect the first pod in the list as deployment pods have same behavior | ||
pod := podList.Items[0] | ||
containersSATokenMountPath, err := getSATokenMountPath(&pod) | ||
if err != nil && strings.Contains(err.Error(), "service account token not mounted") { | ||
log.Warn().Msg(err.Error()) | ||
return false | ||
} | ||
var sumResponses []*opb.Response | ||
for _, container := range pod.Spec.Containers { | ||
sumResp, err := obs.GetSummaryData(&opb.Request{ | ||
PodName: pod.Name, | ||
NameSpace: pod.Namespace, | ||
ContainerName: container.Name, | ||
Type: "process,file", | ||
}) | ||
if err != nil { | ||
log.Warn().Msg(err.Error()) | ||
return true | ||
} | ||
log.Info().Msg(sumResp.String()) | ||
sumResponses = append(sumResponses, sumResp) | ||
} | ||
return serviceAccountTokenUsed(containersSATokenMountPath, sumResponses) | ||
} else { | ||
log.Warn().Msg("No pods found for the given labels") | ||
} | ||
|
||
return true | ||
} | ||
|
||
func getSATokenMountPath(pod *corev1.Pod) ([]containerMountPathServiceAccountToken, error) { | ||
volumes := pod.Spec.Volumes | ||
var tokenPath string | ||
var projectedVolumeName string | ||
var result = make([]containerMountPathServiceAccountToken, 0) | ||
for _, volume := range volumes { | ||
if volume.Projected != nil { | ||
for _, projectedSources := range volume.Projected.Sources { | ||
serviceAccountToken := projectedSources.ServiceAccountToken | ||
if serviceAccountToken != nil { | ||
tokenPath = serviceAccountToken.Path | ||
projectedVolumeName = volume.Name | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
if tokenPath == "" || projectedVolumeName == "" { | ||
return result, | ||
fmt.Errorf("service account token not mounted for %s in namespace %s", pod.Name, pod.Namespace) | ||
} | ||
|
||
containers := pod.Spec.Containers | ||
for _, container := range containers { | ||
volumeMounts := container.VolumeMounts | ||
for _, volumeMount := range volumeMounts { | ||
if volumeMount.Name == projectedVolumeName { | ||
result = append(result, containerMountPathServiceAccountToken{ | ||
podName: pod.Name, | ||
podNamespace: pod.Namespace, | ||
containerName: container.Name, | ||
saTokenMountPath: volumeMount.MountPath + string(filepath.Separator) + tokenPath, | ||
}) | ||
} | ||
} | ||
} | ||
return result, nil | ||
} | ||
|
||
func serviceAccountTokenUsed(containersSATokenMountPath []containerMountPathServiceAccountToken, sumResponses []*opb.Response) bool { | ||
serviceAccountTokenUsed := false | ||
for _, containerSATokenMountPath := range containersSATokenMountPath { | ||
for _, sumResp := range sumResponses { | ||
for _, fileData := range sumResp.FileData { | ||
if sumResp.ContainerName == containerSATokenMountPath.containerName { | ||
// Even if one container uses the service account token, we should allow auto mounting | ||
if matchesSATokenPath(containerSATokenMountPath.saTokenMountPath, fileData.Destination) { | ||
serviceAccountTokenUsed = true | ||
break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return serviceAccountTokenUsed | ||
} | ||
|
||
func matchesSATokenPath(saTokenPath, sumRespPath string) bool { | ||
sumRespPathParts := strings.Split(sumRespPath, string(filepath.Separator)) | ||
pattern := "..[0-9]{4}_[0-9]{2}_[0-9]{2}.*" | ||
sumRespPathPartsWithoutDoubleDot := removeMatchingElements(sumRespPathParts, pattern) | ||
sumpRespPathWithoutDoubleDot := strings.Join(sumRespPathPartsWithoutDoubleDot, string(filepath.Separator)) | ||
if strings.HasSuffix(saTokenPath, sumpRespPathWithoutDoubleDot) { | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
func removeMatchingElements(slice []string, pattern string) []string { | ||
r := regexp.MustCompile(pattern) | ||
result := make([]string, 0) | ||
|
||
for _, s := range slice { | ||
if !r.MatchString(s) { | ||
result = append(result, s) | ||
} | ||
} | ||
|
||
return result | ||
} |
Oops, something went wrong.