mirror of https://github.com/pulumi/pulumi.git
340 lines
10 KiB
Go
340 lines
10 KiB
Go
// Copyright 2016-2020, Pulumi Corporation.
|
|
//
|
|
// 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 analyzer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/xeipuuv/gojsonschema"
|
|
)
|
|
|
|
// LoadPolicyPackConfigFromFile loads the JSON config from a file.
|
|
func LoadPolicyPackConfigFromFile(file string) (map[string]plugin.AnalyzerPolicyConfig, error) {
|
|
b, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parsePolicyPackConfig(b)
|
|
}
|
|
|
|
// ParsePolicyPackConfigFromAPI parses the config returned from the service.
|
|
func ParsePolicyPackConfigFromAPI(config map[string]*json.RawMessage) (map[string]plugin.AnalyzerPolicyConfig, error) {
|
|
result := map[string]plugin.AnalyzerPolicyConfig{}
|
|
for k, v := range config {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
|
|
var enforcementLevel apitype.EnforcementLevel
|
|
var properties map[string]interface{}
|
|
|
|
props := make(map[string]interface{})
|
|
if err := json.Unmarshal(*v, &props); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
el, err := extractEnforcementLevel(props)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing enforcement level for %q: %w", k, err)
|
|
}
|
|
enforcementLevel = el
|
|
if len(props) > 0 {
|
|
properties = props
|
|
}
|
|
|
|
// Don't bother including empty configs.
|
|
if enforcementLevel == "" && len(properties) == 0 {
|
|
continue
|
|
}
|
|
|
|
result[k] = plugin.AnalyzerPolicyConfig{
|
|
EnforcementLevel: enforcementLevel,
|
|
Properties: properties,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parsePolicyPackConfig(b []byte) (map[string]plugin.AnalyzerPolicyConfig, error) {
|
|
result := make(map[string]plugin.AnalyzerPolicyConfig)
|
|
|
|
// Gracefully allow empty content.
|
|
if strings.TrimSpace(string(b)) == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
config := make(map[string]interface{})
|
|
if err := json.Unmarshal(b, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range config {
|
|
var enforcementLevel apitype.EnforcementLevel
|
|
var properties map[string]interface{}
|
|
switch val := v.(type) {
|
|
case string:
|
|
el := apitype.EnforcementLevel(val)
|
|
if !el.IsValid() {
|
|
return nil, fmt.Errorf("parsing enforcement level for %q: %q is not a valid enforcement level", k, val)
|
|
}
|
|
enforcementLevel = el
|
|
case map[string]interface{}:
|
|
el, err := extractEnforcementLevel(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing enforcement level for %q: %w", k, err)
|
|
}
|
|
enforcementLevel = el
|
|
if len(val) > 0 {
|
|
properties = val
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("parsing %q: %v is not a valid value; must be a string or object", k, v)
|
|
}
|
|
|
|
// Don't bother including empty configs.
|
|
if enforcementLevel == "" && len(properties) == 0 {
|
|
continue
|
|
}
|
|
|
|
result[k] = plugin.AnalyzerPolicyConfig{
|
|
EnforcementLevel: enforcementLevel,
|
|
Properties: properties,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// extractEnforcementLevel looks for "enforcementLevel" in the map, and if so, validates that it is a valid value, and
|
|
// if so, deletes it from the map and returns it.
|
|
func extractEnforcementLevel(props map[string]interface{}) (apitype.EnforcementLevel, error) {
|
|
contract.Assertf(props != nil, "props != nil")
|
|
|
|
var enforcementLevel apitype.EnforcementLevel
|
|
if unknown, ok := props["enforcementLevel"]; ok {
|
|
enforcementLevelStr, isStr := unknown.(string)
|
|
if !isStr {
|
|
return "", fmt.Errorf("%v is not a valid enforcement level; must be a string", unknown)
|
|
}
|
|
el := apitype.EnforcementLevel(enforcementLevelStr)
|
|
if !el.IsValid() {
|
|
return "", fmt.Errorf("%q is not a valid enforcement level", enforcementLevelStr)
|
|
}
|
|
enforcementLevel = el
|
|
// Remove enforcementLevel from the map.
|
|
delete(props, "enforcementLevel")
|
|
}
|
|
return enforcementLevel, nil
|
|
}
|
|
|
|
// ValidatePolicyPackConfig validates the policy pack's configuration.
|
|
func validatePolicyPackConfig(
|
|
policies []plugin.AnalyzerPolicyInfo, config map[string]plugin.AnalyzerPolicyConfig,
|
|
) ([]string, error) {
|
|
contract.Assertf(config != nil, "contract != nil")
|
|
var errors []string
|
|
for _, policy := range policies {
|
|
if policy.ConfigSchema == nil {
|
|
continue
|
|
}
|
|
var props map[string]interface{}
|
|
if c, ok := config[policy.Name]; ok {
|
|
props = c.Properties
|
|
}
|
|
if props == nil {
|
|
props = make(map[string]interface{})
|
|
}
|
|
validationErrors, err := validatePolicyConfig(*policy.ConfigSchema, props)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, validationError := range validationErrors {
|
|
errors = append(errors, fmt.Sprintf("%s: %s", policy.Name, validationError))
|
|
}
|
|
}
|
|
return errors, nil
|
|
}
|
|
|
|
// validatePolicyConfig validates an individual policy's configuration.
|
|
func validatePolicyConfig(schema plugin.AnalyzerPolicyConfigSchema, config map[string]interface{}) ([]string, error) {
|
|
var errors []string
|
|
schemaLoader := gojsonschema.NewGoLoader(convertSchema(schema))
|
|
documentLoader := gojsonschema.NewGoLoader(config)
|
|
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !result.Valid() {
|
|
for _, err := range result.Errors() {
|
|
// Root errors are prefixed with "(root):" (e.g. "(root): foo is required"),
|
|
// but that's just noise for our purposes, so we trim it from the message.
|
|
msg := strings.TrimPrefix(err.String(), "(root): ")
|
|
errors = append(errors, msg)
|
|
}
|
|
}
|
|
return errors, nil
|
|
}
|
|
|
|
// ValidatePolicyPackConfig validates a policy pack configuration against the specified config schema.
|
|
func ValidatePolicyPackConfig(schemaMap map[string]apitype.PolicyConfigSchema,
|
|
config map[string]*json.RawMessage,
|
|
) (err error) {
|
|
for property, schema := range schemaMap {
|
|
schemaLoader := gojsonschema.NewGoLoader(schema)
|
|
|
|
// If the config for this property is nil, we override it with an empty
|
|
// json struct to ensure the config is not missing any required properties.
|
|
propertyConfig := config[property]
|
|
if propertyConfig == nil {
|
|
temp := json.RawMessage([]byte(`{}`))
|
|
propertyConfig = &temp
|
|
}
|
|
configLoader := gojsonschema.NewBytesLoader(*propertyConfig)
|
|
result, err := gojsonschema.Validate(schemaLoader, configLoader)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to validate schema: %w", err)
|
|
}
|
|
|
|
// If the result is invalid, we need to gather the errors to return to the user.
|
|
if !result.Valid() {
|
|
resultErrs := make([]string, len(result.Errors()))
|
|
for i, e := range result.Errors() {
|
|
resultErrs[i] = e.Description()
|
|
}
|
|
msg := "policy pack configuration is invalid: " + strings.Join(resultErrs, ", ")
|
|
return errors.New(msg)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func convertSchema(schema plugin.AnalyzerPolicyConfigSchema) plugin.JSONSchema {
|
|
result := plugin.JSONSchema{}
|
|
result["type"] = "object"
|
|
if len(schema.Properties) > 0 {
|
|
result["properties"] = schema.Properties
|
|
}
|
|
if len(schema.Required) > 0 {
|
|
result["required"] = schema.Required
|
|
}
|
|
return result
|
|
}
|
|
|
|
// createConfigWithDefaults returns a new map filled-in with defaults from the policy metadata.
|
|
func createConfigWithDefaults(policies []plugin.AnalyzerPolicyInfo) map[string]plugin.AnalyzerPolicyConfig {
|
|
result := make(map[string]plugin.AnalyzerPolicyConfig)
|
|
|
|
// Prepare the resulting config with all defaults from the policy metadata.
|
|
for _, policy := range policies {
|
|
var props map[string]interface{}
|
|
|
|
// Set default values from the schema.
|
|
if policy.ConfigSchema != nil {
|
|
for k, v := range policy.ConfigSchema.Properties {
|
|
if val, ok := v["default"]; ok {
|
|
if props == nil {
|
|
props = make(map[string]interface{})
|
|
}
|
|
props[k] = val
|
|
}
|
|
}
|
|
}
|
|
|
|
result[policy.Name] = plugin.AnalyzerPolicyConfig{
|
|
EnforcementLevel: policy.EnforcementLevel,
|
|
Properties: props,
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ReconcilePolicyPackConfig takes metadata about each policy containing default values and config schema, and
|
|
// reconciles this with the given config to produce a new config that has all default values filled-in and then sets
|
|
// configured values.
|
|
func ReconcilePolicyPackConfig(
|
|
policies []plugin.AnalyzerPolicyInfo,
|
|
initialConfig map[string]plugin.AnalyzerPolicyConfig,
|
|
config map[string]plugin.AnalyzerPolicyConfig,
|
|
) (map[string]plugin.AnalyzerPolicyConfig, []string, error) {
|
|
// Prepare the resulting config with all defaults from the policy metadata.
|
|
result := createConfigWithDefaults(policies)
|
|
contract.Assertf(result != nil, "result != nil")
|
|
|
|
// Apply initial config supplied programmatically.
|
|
if initialConfig != nil {
|
|
result = applyConfig(result, initialConfig)
|
|
}
|
|
|
|
// Apply additional config from API or CLI.
|
|
if config != nil {
|
|
result = applyConfig(result, config)
|
|
}
|
|
|
|
// Validate the resulting config.
|
|
validationErrors, err := validatePolicyPackConfig(policies, result)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(validationErrors) > 0 {
|
|
return nil, validationErrors, nil
|
|
}
|
|
return result, nil, nil
|
|
}
|
|
|
|
func applyConfig(result map[string]plugin.AnalyzerPolicyConfig,
|
|
configToApply map[string]plugin.AnalyzerPolicyConfig,
|
|
) map[string]plugin.AnalyzerPolicyConfig {
|
|
// Apply anything that applies to "all" policies.
|
|
if all, hasAll := configToApply["all"]; hasAll && all.EnforcementLevel.IsValid() {
|
|
for k, v := range result {
|
|
result[k] = plugin.AnalyzerPolicyConfig{
|
|
EnforcementLevel: all.EnforcementLevel,
|
|
Properties: v.Properties,
|
|
}
|
|
}
|
|
}
|
|
// Apply policy level config.
|
|
for policy, givenConfig := range configToApply {
|
|
var enforcementLevel apitype.EnforcementLevel
|
|
var properties map[string]interface{}
|
|
|
|
if resultConfig, hasResultConfig := result[policy]; hasResultConfig {
|
|
enforcementLevel = resultConfig.EnforcementLevel
|
|
properties = resultConfig.Properties
|
|
}
|
|
|
|
if givenConfig.EnforcementLevel.IsValid() {
|
|
enforcementLevel = givenConfig.EnforcementLevel
|
|
}
|
|
if len(givenConfig.Properties) > 0 && properties == nil {
|
|
properties = make(map[string]interface{})
|
|
}
|
|
for k, v := range givenConfig.Properties {
|
|
properties[k] = v
|
|
}
|
|
result[policy] = plugin.AnalyzerPolicyConfig{
|
|
EnforcementLevel: enforcementLevel,
|
|
Properties: properties,
|
|
}
|
|
}
|
|
return result
|
|
}
|