pkg/gcptarget/gcptarget.go (299 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package gcptarget is a constraint framework target for config-validator to use for integrating with the opa constraint framework. package gcptarget import ( "encoding/json" "errors" "fmt" "log" "regexp" "strings" "github.com/GoogleCloudPlatform/config-validator/pkg/api/validator" asset2 "github.com/GoogleCloudPlatform/config-validator/pkg/asset" "github.com/open-policy-agent/frameworks/constraint/pkg/core/constraints" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" "github.com/open-policy-agent/frameworks/constraint/pkg/types" "google.golang.org/protobuf/encoding/protojson" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Name is the target name for GCPTarget const Name = "validation.gcp.forsetisecurity.org" // GCPTarget is the constraint framework target for CAI asset data type GCPTarget struct { } var _ handler.TargetHandler = &GCPTarget{} // New returns a new GCPTarget func New() *GCPTarget { return &GCPTarget{} } // ToMatcher converts .spec.match in mutators to Matcher. func (h *GCPTarget) ToMatcher(constraint *unstructured.Unstructured) (constraints.Matcher, error) { match, ok, err := unstructured.NestedMap(constraint.Object, "spec", "match") if err != nil { return nil, fmt.Errorf("unable to get spec.match: %w", err) } if !ok { return &matcher{ancestries: []string{"**"}, excludedAncestries: []string{}}, nil } include, ok, err := unstructured.NestedStringSlice(match, "ancestries") if err != nil { return nil, fmt.Errorf("unable to get string slice from spec.match.ancestries: %w", err) } if !ok { include, ok, err = unstructured.NestedStringSlice(match, "target") if err != nil { return nil, fmt.Errorf("unable to get string slice from spec.match.target: %w", err) } if !ok { include = []string{"**"} } } exclude, ok, err := unstructured.NestedStringSlice(match, "excludedAncestries") if err != nil { return nil, fmt.Errorf("unable to get string slice from spec.match.excludedAncestries: %w", err) } if !ok { exclude, ok, err = unstructured.NestedStringSlice(match, "exclude") if err != nil { return nil, fmt.Errorf("unable to get string slice from spec.match.exclude: %w", err) } if !ok { exclude = []string{} } } return &matcher{ ancestries: include, excludedAncestries: exclude, }, nil } // MatchSchema implements client.MatchSchemaProvider func (g *GCPTarget) MatchSchema() apiextensions.JSONSchemaProps { return apiextensions.JSONSchemaProps{ Type: "object", Properties: map[string]apiextensions.JSONSchemaProps{ "target": { Type: "array", Items: &apiextensions.JSONSchemaPropsOrArray{ Schema: &apiextensions.JSONSchemaProps{ Type: "string", }, }, }, "exclude": { Type: "array", Items: &apiextensions.JSONSchemaPropsOrArray{ Schema: &apiextensions.JSONSchemaProps{ Type: "string", }, }, }, "ancestries": { Type: "array", Items: &apiextensions.JSONSchemaPropsOrArray{ Schema: &apiextensions.JSONSchemaProps{ Type: "string", }, }, }, "excludedAncestries": { Type: "array", Items: &apiextensions.JSONSchemaPropsOrArray{ Schema: &apiextensions.JSONSchemaProps{ Type: "string", }, }, }, }, } } // GetName implements handler.TargetHandler func (g *GCPTarget) GetName() string { return Name } // ProcessData implements handler.TargetHandler func (g *GCPTarget) ProcessData(obj interface{}) (bool, []string, interface{}, error) { return false, nil, nil, errors.New("storing data for referential constraint eval is not supported at this time.") } // HandleReview implements handler.TargetHandler func (g *GCPTarget) HandleReview(obj interface{}) (bool, interface{}, error) { switch asset := obj.(type) { case *validator.Asset: return g.handleAsset(asset) case map[string]interface{}: if _, found, err := unstructured.NestedString(asset, "name"); !found || err != nil { return false, nil, err } if _, found, err := unstructured.NestedString(asset, "asset_type"); !found || err != nil { return false, nil, err } if _, found, err := unstructured.NestedString(asset, "ancestry_path"); !found || err != nil { return false, nil, err } _, foundResource, err := unstructured.NestedMap(asset, "resource") if err != nil { return false, nil, err } _, foundIam, err := unstructured.NestedMap(asset, "iam_policy") if err != nil { return false, nil, err } foundOrgPolicy := false if asset["org_policy"] != nil { foundOrgPolicy = true } foundV2OrgPolicy := false if asset["v2_org_policies"] != nil { foundV2OrgPolicy = true } _, foundAccessPolicy, err := unstructured.NestedMap(asset, "access_policy") if err != nil { return false, nil, err } _, foundAcessLevel, err := unstructured.NestedMap(asset, "access_level") if err != nil { return false, nil, err } _, foundServicePerimeter, err := unstructured.NestedMap(asset, "service_perimeter") if err != nil { return false, nil, err } if !foundIam && !foundResource && !foundOrgPolicy && !foundV2OrgPolicy && !foundAccessPolicy && !foundAcessLevel && !foundServicePerimeter { return false, nil, nil } resourceTypes := 0 if foundResource { resourceTypes++ } if foundIam { resourceTypes++ } if foundOrgPolicy { resourceTypes++ } if foundV2OrgPolicy { resourceTypes++ } if foundAccessPolicy { resourceTypes++ } if foundAcessLevel { resourceTypes++ } if foundServicePerimeter { resourceTypes++ } if resourceTypes > 1 { return false, nil, fmt.Errorf("malformed asset has more than one of: resource, iam policy, org policy, access context policy: %v", asset) } return true, asset, nil } return false, nil, nil } // handleAsset handles input from CAI assets as received via the gRPC interface. func (g *GCPTarget) handleAsset(asset *validator.Asset) (bool, interface{}, error) { if asset.Resource == nil { return false, nil, fmt.Errorf("CAI asset's resource field is nil %s", asset) } asset2.CleanStructValue(asset.Resource.Data) m := &protojson.MarshalOptions{ UseProtoNames: true, } buf, err := m.Marshal(asset) if err != nil { return false, nil, fmt.Errorf("marshalling to json with asset %s: %v. %w", asset.Name, asset, err) } var f interface{} if err := json.Unmarshal(buf, &f); err != nil { return false, nil, fmt.Errorf("marshalling from json with asset %s: %v. %w", asset.Name, asset, err) } return true, f, nil } // HandleViolation implements handler.TargetHandler func (g *GCPTarget) HandleViolation(result *types.Result) error { return nil } /* cases organizations/* organizations/[0-9]+/* organizations/[0-9]+/folders/* organizations/[0-9]+/folders/[0-9]+/* organizations/[0-9]+/folders/[0-9]+/projects/* organizations/[0-9]+/folders/[0-9]+/projects/[0-9]+ folders/* folders/[0-9]+/* folders/[0-9]+/projects/* folders/[0-9]+/projects/[0-9]+ projects/* projects/[0-9]+ */ const ( organization = "organizations" folder = "folders" project = "projects" ) const ( stateStart = "stateStart" stateFolder = "stateFolder" stateProject = "stateProject" ) var numberRegex = regexp.MustCompile(`^[0-9]+\*{0,2}$`) // From https://cloud.google.com/resource-manager/docs/creating-managing-projects: // The project ID must be a unique string of 6 to 30 lowercase letters, digits, or hyphens. It must start with a letter, and cannot have a trailing hyphen. var projectIDRegex = regexp.MustCompile(`^[a-z][a-z0-9-]{5,27}[a-z0-9]$`) // checkPathGlob func checkPathGlob(expression string) error { // check for path components / numbers parts := strings.Split(expression, "/") state := stateStart for i := 0; i < len(parts); i++ { item := parts[i] switch { case item == organization: if state != stateStart { return fmt.Errorf("unexpected %s element %d in %s", item, i, expression) } state = stateFolder case item == folder: if state != stateStart && state != stateFolder { return fmt.Errorf("unexpected %s element %d in %s", item, i, expression) } state = stateFolder case item == project: state = stateProject case item == "*": case item == "**": case item == "unknown": case numberRegex.MatchString(item): case state == stateProject && projectIDRegex.MatchString(item): default: return fmt.Errorf("unexpected item %s element %d in %s", item, i, expression) } } return nil } func checkPathGlobs(rs []string) error { for idx, r := range rs { if err := checkPathGlob(r); err != nil { return fmt.Errorf("idx [%d]: %w", idx, err) } } return nil } // ValidateConstraint implements handler.TargetHandler func (g *GCPTarget) ValidateConstraint(constraint *unstructured.Unstructured) error { ancestries, ancestriesFound, ancestriesErr := unstructured.NestedStringSlice(constraint.Object, "spec", "match", "ancestries") targets, targetsFound, targetsErr := unstructured.NestedStringSlice(constraint.Object, "spec", "match", "target") if ancestriesFound && targetsFound { return errors.New("only one of spec.match.ancestries and spec.match.target can be specified") } else if ancestriesFound { if ancestriesErr != nil { return fmt.Errorf("invalid spec.match.ancestries: %s", ancestriesErr) } if ancestriesErr := checkPathGlobs(ancestries); ancestriesErr != nil { return fmt.Errorf("invalid glob in spec.match.ancestries: %w", ancestriesErr) } } else if targetsFound { // TODO b/232980918: replace with zapLogger.Warn log.Print( "spec.match.target is deprecated and will be removed in a future release. Use spec.match.ancestries instead", ) if targetsErr != nil { return fmt.Errorf("invalid spec.match.target: %s", targetsErr) } if targetsErr := checkPathGlobs(targets); targetsErr != nil { return fmt.Errorf("invalid glob in spec.match.target: %w", targetsErr) } } excludedAncestries, excludedAncestriesFound, excludedAncestriesErr := unstructured.NestedStringSlice(constraint.Object, "spec", "match", "excludedAncestries") excludes, excludesFound, excludesErr := unstructured.NestedStringSlice(constraint.Object, "spec", "match", "exclude") if excludedAncestriesFound && excludesFound { return errors.New("only one of spec.match.excludedAncestries and spec.match.exclude can be specified") } else if excludedAncestriesFound { if excludedAncestriesErr != nil { return fmt.Errorf("invalid spec.match.excludedAncestries: %s", excludedAncestriesErr) } if excludedAncestriesErr := checkPathGlobs(excludedAncestries); excludedAncestriesErr != nil { return fmt.Errorf("invalid glob in spec.match.excludedAncestries: %w", excludedAncestriesErr) } } else if excludesFound { // TODO b/232980918: replace with zapLogger.Warn log.Print( "spec.match.exclude is deprecated and will be removed in a future release. Use spec.match.excludedAncestries instead", ) if excludesErr != nil { return fmt.Errorf("invalid spec.match.exclude: %s", excludesErr) } if excludesErr := checkPathGlobs(excludes); excludesErr != nil { return fmt.Errorf("invalid glob in spec.match.exclude: %w", excludesErr) } } return nil }