github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/resource/analyzer/config.go (about)

     1  // Copyright 2016-2020, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package analyzer
    16  
    17  import (
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"strings"
    23  
    24  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    25  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
    26  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    27  	"github.com/xeipuuv/gojsonschema"
    28  )
    29  
    30  // LoadPolicyPackConfigFromFile loads the JSON config from a file.
    31  func LoadPolicyPackConfigFromFile(file string) (map[string]plugin.AnalyzerPolicyConfig, error) {
    32  	b, err := ioutil.ReadFile(file)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  	return parsePolicyPackConfig(b)
    37  }
    38  
    39  // ParsePolicyPackConfigFromAPI parses the config returned from the service.
    40  func ParsePolicyPackConfigFromAPI(config map[string]*json.RawMessage) (map[string]plugin.AnalyzerPolicyConfig, error) {
    41  	result := map[string]plugin.AnalyzerPolicyConfig{}
    42  	for k, v := range config {
    43  		if v == nil {
    44  			continue
    45  		}
    46  
    47  		var enforcementLevel apitype.EnforcementLevel
    48  		var properties map[string]interface{}
    49  
    50  		props := make(map[string]interface{})
    51  		if err := json.Unmarshal(*v, &props); err != nil {
    52  			return nil, err
    53  		}
    54  
    55  		el, err := extractEnforcementLevel(props)
    56  		if err != nil {
    57  			return nil, fmt.Errorf("parsing enforcement level for %q: %w", k, err)
    58  		}
    59  		enforcementLevel = el
    60  		if len(props) > 0 {
    61  			properties = props
    62  		}
    63  
    64  		// Don't bother including empty configs.
    65  		if enforcementLevel == "" && len(properties) == 0 {
    66  			continue
    67  		}
    68  
    69  		result[k] = plugin.AnalyzerPolicyConfig{
    70  			EnforcementLevel: enforcementLevel,
    71  			Properties:       properties,
    72  		}
    73  	}
    74  	return result, nil
    75  }
    76  
    77  func parsePolicyPackConfig(b []byte) (map[string]plugin.AnalyzerPolicyConfig, error) {
    78  	result := make(map[string]plugin.AnalyzerPolicyConfig)
    79  
    80  	// Gracefully allow empty content.
    81  	if strings.TrimSpace(string(b)) == "" {
    82  		return nil, nil
    83  	}
    84  
    85  	config := make(map[string]interface{})
    86  	if err := json.Unmarshal(b, &config); err != nil {
    87  		return nil, err
    88  	}
    89  	for k, v := range config {
    90  		var enforcementLevel apitype.EnforcementLevel
    91  		var properties map[string]interface{}
    92  		switch val := v.(type) {
    93  		case string:
    94  			el := apitype.EnforcementLevel(val)
    95  			if !el.IsValid() {
    96  				return nil, fmt.Errorf("parsing enforcement level for %q: %q is not a valid enforcement level", k, val)
    97  			}
    98  			enforcementLevel = el
    99  		case map[string]interface{}:
   100  			el, err := extractEnforcementLevel(val)
   101  			if err != nil {
   102  				return nil, fmt.Errorf("parsing enforcement level for %q: %w", k, err)
   103  			}
   104  			enforcementLevel = el
   105  			if len(val) > 0 {
   106  				properties = val
   107  			}
   108  		default:
   109  			return nil, fmt.Errorf("parsing %q: %v is not a valid value; must be a string or object", k, v)
   110  		}
   111  
   112  		// Don't bother including empty configs.
   113  		if enforcementLevel == "" && len(properties) == 0 {
   114  			continue
   115  		}
   116  
   117  		result[k] = plugin.AnalyzerPolicyConfig{
   118  			EnforcementLevel: enforcementLevel,
   119  			Properties:       properties,
   120  		}
   121  	}
   122  	return result, nil
   123  }
   124  
   125  // extractEnforcementLevel looks for "enforcementLevel" in the map, and if so, validates that it is a valid value, and
   126  // if so, deletes it from the map and returns it.
   127  func extractEnforcementLevel(props map[string]interface{}) (apitype.EnforcementLevel, error) {
   128  	contract.Assertf(props != nil, "props != nil")
   129  
   130  	var enforcementLevel apitype.EnforcementLevel
   131  	if unknown, ok := props["enforcementLevel"]; ok {
   132  		enforcementLevelStr, isStr := unknown.(string)
   133  		if !isStr {
   134  			return "", fmt.Errorf("%v is not a valid enforcement level; must be a string", unknown)
   135  		}
   136  		el := apitype.EnforcementLevel(enforcementLevelStr)
   137  		if !el.IsValid() {
   138  			return "", fmt.Errorf("%q is not a valid enforcement level", enforcementLevelStr)
   139  		}
   140  		enforcementLevel = el
   141  		// Remove enforcementLevel from the map.
   142  		delete(props, "enforcementLevel")
   143  	}
   144  	return enforcementLevel, nil
   145  }
   146  
   147  // ValidatePolicyPackConfig validates the policy pack's configuration.
   148  func validatePolicyPackConfig(
   149  	policies []plugin.AnalyzerPolicyInfo, config map[string]plugin.AnalyzerPolicyConfig) ([]string, error) {
   150  	contract.Assertf(config != nil, "contract != nil")
   151  	var errors []string
   152  	for _, policy := range policies {
   153  		if policy.ConfigSchema == nil {
   154  			continue
   155  		}
   156  		var props map[string]interface{}
   157  		if c, ok := config[policy.Name]; ok {
   158  			props = c.Properties
   159  		}
   160  		if props == nil {
   161  			props = make(map[string]interface{})
   162  		}
   163  		validationErrors, err := validatePolicyConfig(*policy.ConfigSchema, props)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		for _, validationError := range validationErrors {
   168  			errors = append(errors, fmt.Sprintf("%s: %s", policy.Name, validationError))
   169  		}
   170  	}
   171  	return errors, nil
   172  }
   173  
   174  // validatePolicyConfig validates an individual policy's configuration.
   175  func validatePolicyConfig(schema plugin.AnalyzerPolicyConfigSchema, config map[string]interface{}) ([]string, error) {
   176  	var errors []string
   177  	schemaLoader := gojsonschema.NewGoLoader(convertSchema(schema))
   178  	documentLoader := gojsonschema.NewGoLoader(config)
   179  	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	if !result.Valid() {
   184  		for _, err := range result.Errors() {
   185  			// Root errors are prefixed with "(root):" (e.g. "(root): foo is required"),
   186  			// but that's just noise for our purposes, so we trim it from the message.
   187  			msg := strings.TrimPrefix(err.String(), "(root): ")
   188  			errors = append(errors, msg)
   189  		}
   190  	}
   191  	return errors, nil
   192  }
   193  
   194  // ValidatePolicyPackConfig validates a policy pack configuration against the specified config schema.
   195  func ValidatePolicyPackConfig(schemaMap map[string]apitype.PolicyConfigSchema,
   196  	config map[string]*json.RawMessage) (err error) {
   197  	for property, schema := range schemaMap {
   198  		schemaLoader := gojsonschema.NewGoLoader(schema)
   199  
   200  		// If the config for this property is nil, we override it with an empty
   201  		// json struct to ensure the config is not missing any required properties.
   202  		propertyConfig := config[property]
   203  		if propertyConfig == nil {
   204  			temp := json.RawMessage([]byte(`{}`))
   205  			propertyConfig = &temp
   206  		}
   207  		configLoader := gojsonschema.NewBytesLoader(*propertyConfig)
   208  		result, err := gojsonschema.Validate(schemaLoader, configLoader)
   209  		if err != nil {
   210  			return fmt.Errorf("unable to validate schema: %w", err)
   211  		}
   212  
   213  		// If the result is invalid, we need to gather the errors to return to the user.
   214  		if !result.Valid() {
   215  			resultErrs := make([]string, len(result.Errors()))
   216  			for i, e := range result.Errors() {
   217  				resultErrs[i] = e.Description()
   218  			}
   219  			msg := fmt.Sprintf("policy pack configuration is invalid: %s", strings.Join(resultErrs, ", "))
   220  			return errors.New(msg)
   221  		}
   222  	}
   223  	return err
   224  }
   225  
   226  func convertSchema(schema plugin.AnalyzerPolicyConfigSchema) plugin.JSONSchema {
   227  	result := plugin.JSONSchema{}
   228  	result["type"] = "object"
   229  	if len(schema.Properties) > 0 {
   230  		result["properties"] = schema.Properties
   231  	}
   232  	if len(schema.Required) > 0 {
   233  		result["required"] = schema.Required
   234  	}
   235  	return result
   236  }
   237  
   238  // createConfigWithDefaults returns a new map filled-in with defaults from the policy metadata.
   239  func createConfigWithDefaults(policies []plugin.AnalyzerPolicyInfo) map[string]plugin.AnalyzerPolicyConfig {
   240  	result := make(map[string]plugin.AnalyzerPolicyConfig)
   241  
   242  	// Prepare the resulting config with all defaults from the policy metadata.
   243  	for _, policy := range policies {
   244  		var props map[string]interface{}
   245  
   246  		// Set default values from the schema.
   247  		if policy.ConfigSchema != nil {
   248  			for k, v := range policy.ConfigSchema.Properties {
   249  				if val, ok := v["default"]; ok {
   250  					if props == nil {
   251  						props = make(map[string]interface{})
   252  					}
   253  					props[k] = val
   254  				}
   255  			}
   256  		}
   257  
   258  		result[policy.Name] = plugin.AnalyzerPolicyConfig{
   259  			EnforcementLevel: policy.EnforcementLevel,
   260  			Properties:       props,
   261  		}
   262  	}
   263  
   264  	return result
   265  }
   266  
   267  // ReconcilePolicyPackConfig takes metadata about each policy containing default values and config schema, and
   268  // reconciles this with the given config to produce a new config that has all default values filled-in and then sets
   269  // configured values.
   270  func ReconcilePolicyPackConfig(
   271  	policies []plugin.AnalyzerPolicyInfo,
   272  	initialConfig map[string]plugin.AnalyzerPolicyConfig,
   273  	config map[string]plugin.AnalyzerPolicyConfig,
   274  ) (map[string]plugin.AnalyzerPolicyConfig, []string, error) {
   275  	// Prepare the resulting config with all defaults from the policy metadata.
   276  	result := createConfigWithDefaults(policies)
   277  	contract.Assertf(result != nil, "result != nil")
   278  
   279  	// Apply initial config supplied programmatically.
   280  	if initialConfig != nil {
   281  		result = applyConfig(result, initialConfig)
   282  	}
   283  
   284  	// Apply additional config from API or CLI.
   285  	if config != nil {
   286  		result = applyConfig(result, config)
   287  	}
   288  
   289  	// Validate the resulting config.
   290  	validationErrors, err := validatePolicyPackConfig(policies, result)
   291  	if err != nil {
   292  		return nil, nil, err
   293  	}
   294  	if len(validationErrors) > 0 {
   295  		return nil, validationErrors, nil
   296  	}
   297  	return result, nil, nil
   298  }
   299  
   300  func applyConfig(result map[string]plugin.AnalyzerPolicyConfig,
   301  	configToApply map[string]plugin.AnalyzerPolicyConfig) map[string]plugin.AnalyzerPolicyConfig {
   302  	// Apply anything that applies to "all" policies.
   303  	if all, hasAll := configToApply["all"]; hasAll && all.EnforcementLevel.IsValid() {
   304  		for k, v := range result {
   305  			result[k] = plugin.AnalyzerPolicyConfig{
   306  				EnforcementLevel: all.EnforcementLevel,
   307  				Properties:       v.Properties,
   308  			}
   309  		}
   310  	}
   311  	// Apply policy level config.
   312  	for policy, givenConfig := range configToApply {
   313  		var enforcementLevel apitype.EnforcementLevel
   314  		var properties map[string]interface{}
   315  
   316  		if resultConfig, hasResultConfig := result[policy]; hasResultConfig {
   317  			enforcementLevel = resultConfig.EnforcementLevel
   318  			properties = resultConfig.Properties
   319  		}
   320  
   321  		if givenConfig.EnforcementLevel.IsValid() {
   322  			enforcementLevel = givenConfig.EnforcementLevel
   323  		}
   324  		if len(givenConfig.Properties) > 0 && properties == nil {
   325  			properties = make(map[string]interface{})
   326  		}
   327  		for k, v := range givenConfig.Properties {
   328  			properties[k] = v
   329  		}
   330  		result[policy] = plugin.AnalyzerPolicyConfig{
   331  			EnforcementLevel: enforcementLevel,
   332  			Properties:       properties,
   333  		}
   334  	}
   335  	return result
   336  }