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 }