pkg/safeguards/helpers.go (205 lines of code) (raw):

package safeguards import ( "context" "fmt" "io/fs" "os" "path" "path/filepath" "strings" "helm.sh/helm/v3/pkg/chartutil" constraintclient "github.com/open-policy-agent/frameworks/constraint/pkg/client" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/gatekeeper/v3/pkg/target" log "github.com/sirupsen/logrus" "golang.org/x/mod/semver" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/Azure/draft/pkg/safeguards/preprocessing" "github.com/Azure/draft/pkg/safeguards/types" ) // Given a path, will determine if it's Kustomize, Helm, a directory of manifests, or a single manifest func GetManifestFiles(manifestsPath string, opt chartutil.ReleaseOptions) ([]types.ManifestFile, error) { isDir, err := IsDirectory(manifestsPath) if err != nil { return nil, fmt.Errorf("not a valid file or directory: %w", err) } var manifestFiles []types.ManifestFile if isDir { // check if Helm or Kustomize dir if isHelm(true, manifestsPath) { return preprocessing.RenderHelmChart(false, manifestsPath, opt) } else if isKustomize(true, manifestsPath) { return preprocessing.RenderKustomizeManifest(manifestsPath) } else { manifestFiles, err = GetManifestFilesFromDir(manifestsPath) return manifestFiles, err } } else if IsYAML(manifestsPath) { // path points to a file if isHelm(false, manifestsPath) { return preprocessing.RenderHelmChart(true, manifestsPath, opt) } else if isKustomize(false, manifestsPath) { return preprocessing.RenderKustomizeManifest(manifestsPath) } else { byteContent, err := os.ReadFile(manifestsPath) if err != nil { return nil, fmt.Errorf("could not read file %s: %s", manifestsPath, err) } manifestFiles = append(manifestFiles, types.ManifestFile{ Name: path.Base(manifestsPath), ManifestContent: byteContent, }) } return manifestFiles, nil } else { return nil, fmt.Errorf("expected at least one .yaml or .yml file within given path") } } // GetManifestFilesFromDir uses filepath.Walk to retrieve a list of the manifest files within a directory of .yaml files func GetManifestFilesFromDir(p string) ([]types.ManifestFile, error) { var manifestFiles []types.ManifestFile err := filepath.Walk(p, func(walkPath string, info fs.FileInfo, err error) error { manifest := types.ManifestFile{} // skip when walkPath is just given path and also a directory if p == walkPath && info.IsDir() { return nil } if err != nil { return fmt.Errorf("error walking path %s with error: %w", walkPath, err) } if !info.IsDir() && info.Name() != "" && IsYAML(walkPath) { log.Debugf("%s is not a directory, appending to manifestFiles", info.Name()) byteContent, err := os.ReadFile(walkPath) if err != nil { return fmt.Errorf("could not read file %s: %s", walkPath, err) } manifest.Name = info.Name() manifest.ManifestContent = byteContent manifestFiles = append(manifestFiles, manifest) } else if !IsYAML(p) { log.Debugf("%s is not a manifest file, skipping...", info.Name()) } else { log.Debugf("%s is a directory, skipping...", info.Name()) } return nil }) if err != nil { return nil, fmt.Errorf("could not walk directory: %w", err) } if len(manifestFiles) == 0 { return nil, fmt.Errorf("no manifest files found within given path") } return manifestFiles, nil } // retrieves the constraint client that does all rego code related operations func getConstraintClient() (*constraintclient.Client, error) { driver, err := rego.New() if err != nil { return nil, fmt.Errorf("could not create rego driver: %w", err) } c, err := constraintclient.NewClient(constraintclient.Targets(&target.K8sValidationTarget{}), constraintclient.Driver(driver)) if err != nil { return nil, fmt.Errorf("could not create constraint client: %w", err) } return c, nil } // sorts the list of supported safeguards versions and returns the last item in the list func getLatestSafeguardsVersion() string { semver.Sort(types.SupportedVersions) return types.SupportedVersions[len(types.SupportedVersions)-1] } func updateSafeguardPaths(safeguardList *[]types.Safeguard) { for _, sg := range *safeguardList { sg.TemplatePath = fmt.Sprintf("%s/%s/%s", types.SelectedVersion, sg.Name, types.TemplateFileName) sg.ConstraintPath = fmt.Sprintf("%s/%s/%s", types.SelectedVersion, sg.Name, types.ConstraintFileName) } } // adds Safeguard_CRIP to full list of Safeguards func AddSafeguardCRIP() { fc.Safeguards = append(fc.Safeguards, types.Safeguard_CRIP) } // loads constraint templates, constraints into constraint client func loadConstraintTemplates(ctx context.Context, c *constraintclient.Client, constraintTemplates []*templates.ConstraintTemplate) error { // AddTemplate adds the template source code to OPA and registers the CRD with the client for // schema validation on calls to AddConstraint. On error, the responses return value // will still be populated so that partial results can be analyzed. for _, ct := range constraintTemplates { _, err := c.AddTemplate(ctx, ct) if err != nil { return fmt.Errorf("could not add template: %w", err) } } return nil } func loadConstraints(ctx context.Context, c *constraintclient.Client, constraints []*unstructured.Unstructured) error { // AddConstraint validates the constraint and, if valid, inserts it into OPA. // On error, the responses return value will still be populated so that // partial results can be analyzed. for _, con := range constraints { _, err := c.AddConstraint(ctx, con) if err != nil { return fmt.Errorf("could not add constraint: %w", err) } } return nil } func loadManifestObjects(ctx context.Context, c *constraintclient.Client, objects []*unstructured.Unstructured) error { // AddData inserts the provided data into OPA for every target that can handle the data. // On error, the responses return value will still be populated so that // partial results can be analyzed. for _, o := range objects { _, err := c.AddData(ctx, o) if err != nil { return fmt.Errorf("could not add data: %w", err) } } return nil } // IsDirectory determines if a file represented by path is a directory or not func IsDirectory(path string) (bool, error) { fileInfo, err := os.Stat(path) if err != nil { return false, err } return fileInfo.IsDir(), nil } // IsYAML determines if a file is of the YAML extension or not func IsYAML(path string) bool { return filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" } // getObjectViolations executes validation on manifests based on loaded constraint templates and returns a map of manifest name to list of objectViolations func getObjectViolations(ctx context.Context, c *constraintclient.Client, objects []*unstructured.Unstructured) (map[string][]string, error) { // Review makes sure the provided object satisfies all stored constraints. // On error, the responses return value will still be populated so that // partial results can be analyzed. var results = make(map[string][]string) // map of object name to slice of objectViolations for _, o := range objects { objectViolations := []string{} log.Debugf("Reviewing %s...", o.GetName()) res, err := c.Review(ctx, o) if err != nil { return results, fmt.Errorf("could not review objects: %w", err) } for _, v := range res.ByTarget { for _, result := range v.Results { if result.Msg != "" { objectViolations = append(objectViolations, result.Msg) } } } if len(objectViolations) > 0 { results[o.GetName()] = objectViolations } } return results, nil } // Checks whether a given path is a helm directory or a path to a Helm Chart (contains/is Chart.yaml) func isHelm(isDir bool, path string) bool { var chartPaths []string // Used to define what a valid helm chart looks like. Currently, presence of Chart.yaml/.yml. if isDir { chartPaths = []string{filepath.Join(path, "Chart.yaml")} chartPaths = append(chartPaths, filepath.Join(path, "Chart.yml")) } else { if filepath.Base(path) != "Chart.yaml" && filepath.Base(path) != "Chart.yml" { return false } chartPaths = []string{path} } for _, path := range chartPaths { _, err := os.Stat(path) if err == nil { //Found the file, it's a valid helm chart return true } } return false } // IsKustomize checks whether a given path should be treated as a kustomize project func isKustomize(isDir bool, p string) bool { var err error if isDir { if _, err = os.Stat(filepath.Join(p, "kustomization.yaml")); err == nil { return true } else if _, err = os.Stat(filepath.Join(p, "kustomization.yml")); err == nil { return true } else { return false } } else { return strings.Contains(p, "kustomization.yaml") } }