pkg/gcv/result.go (247 lines of code) (raw):
// Copyright 2020 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 gcv
import (
"encoding/json"
"fmt"
"reflect"
"github.com/GoogleCloudPlatform/config-validator/pkg/api/validator"
"github.com/GoogleCloudPlatform/config-validator/pkg/gcv/configs"
cftypes "github.com/open-policy-agent/frameworks/constraint/pkg/types"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
ConstraintKey = "constraint"
)
// Result is the result of reviewing an individual resource
type Result struct {
// The name of the resource as given to Config Validator
Name string
// InputResource is the resource as given to Config Validator. This may be a
// CAI Asset or a Terraform Resource Change.
InputResource map[string]interface{}
// ReviewResource is the resource sent to Constraint Framework for review.
// This may be a CAI Asset, K8S resource, or Terraform Resource Change.
ReviewResource map[string]interface{}
// ConstraintViolations are the constraints that were not satisfied during review.
ConstraintViolations []ConstraintViolation
}
// NewResult creates a Result from the provided CF Response.
func NewResult(
target, name string,
inputResource map[string]interface{},
reviewResource map[string]interface{},
responses *cftypes.Responses) (*Result, error) {
cfResponse, found := responses.ByTarget[target]
if !found {
return nil, errors.Errorf("No response for target %s", target)
}
result := &Result{
Name: name,
InputResource: inputResource,
ReviewResource: reviewResource,
ConstraintViolations: make([]ConstraintViolation, len(cfResponse.Results)),
}
for idx, cfResult := range cfResponse.Results {
for k := range cfResult.Metadata {
if k == ConstraintKey {
return nil, errors.Errorf("constraint template metadata contains reserved key %s", ConstraintKey)
}
}
severity, found, err := unstructured.NestedString(cfResult.Constraint.Object, "spec", "severity")
if err != nil || !found {
severity = ""
}
result.ConstraintViolations[idx] = ConstraintViolation{
Message: cfResult.Msg,
Metadata: cfResult.Metadata,
Constraint: cfResult.Constraint,
Severity: severity,
}
}
return result, nil
}
// ConstraintViolations represents an unsatisfied constraint
type ConstraintViolation struct {
// Message is a human readable message for the violation
Message string
// Metadata is the metadata returned by the constraint check
Metadata map[string]interface{}
// Constraint is the K8S resource of the constraint that triggered the violation
Constraint *unstructured.Unstructured
// Constraint Severity
Severity string
}
// ToInsights returns the result represented as a slice of insights.
func (r *Result) ToInsights() []*Insight {
if len(r.ConstraintViolations) == 0 {
return nil
}
insights := make([]*Insight, len(r.ConstraintViolations))
for idx, cv := range r.ConstraintViolations {
i := &Insight{
Description: cv.Message,
TargetResources: []string{r.Name},
InsightSubtype: cv.name(),
Content: map[string]interface{}{
"resource": r.InputResource,
"metadata": cv.metadata(nil),
},
Category: "SECURITY",
}
insights[idx] = i
}
return insights
}
func (r *Result) ToViolations() ([]*validator.Violation, error) {
auxMetadata := map[string]interface{}{}
ancestryPath, found, err := unstructured.NestedString(r.InputResource, ancestryPathKey)
if err != nil {
return nil, errors.Wrapf(err, "error getting ancestry path from %v", r.InputResource)
}
if found {
auxMetadata[ancestryPathKey] = ancestryPath
}
var violations []*validator.Violation
for _, rv := range r.ConstraintViolations {
violation, err := rv.toViolation(r.Name, auxMetadata)
if err != nil {
return nil, errors.Wrapf(err, "failed to convert result")
}
violations = append(violations, violation)
}
return violations, nil
}
func (cv *ConstraintViolation) metadata(auxMetadata map[string]interface{}) map[string]interface{} {
labels := cv.Constraint.GetLabels()
if labels == nil {
labels = map[string]string{}
}
annotations := cv.Constraint.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
params, found, err := unstructured.NestedMap(cv.Constraint.Object, "spec", "parameters")
if err != nil {
panic(fmt.Sprintf(
"constraint has invalid schema (%#v), should have already been validated, "+
" .spec.parameters got schema error on access: %s", cv.Constraint.Object, err))
}
if !found {
params = map[string]interface{}{}
}
metadata := map[string]interface{}{
ConstraintKey: map[string]interface{}{
"labels": labels,
"annotations": annotations,
"parameters": params,
},
}
for k, v := range auxMetadata {
metadata[k] = v
}
for k, v := range cv.Metadata {
metadata[k] = v
}
return metadata
}
// name returns the name for the constraint, this is given as "[Kind].[Name]" to uniquely identify which template and
// constraint the violation came from.
func (cv *ConstraintViolation) name() string {
name := cv.Constraint.GetName()
ans := cv.Constraint.GetAnnotations()
if ans != nil {
if originalName, ok := ans[configs.OriginalName]; ok {
name = originalName
}
}
return fmt.Sprintf("%s.%s", cv.Constraint.GetKind(), name)
}
// toViolation converts the constriant to a violation.
func (cv *ConstraintViolation) toViolation(name string, auxMetadata map[string]interface{}) (*validator.Violation, error) {
metadataJson, err := json.Marshal(cv.metadata(auxMetadata))
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal result metadata %v to json", cv.Metadata)
}
metadata := &structpb.Value{}
if err := protojson.Unmarshal(metadataJson, metadata); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal json %s into structpb", string(metadataJson))
}
// Extract the object fields if they exists.
var apiVersion string
if constraintAPIVersion, ok := cv.Constraint.Object["apiVersion"]; ok {
apiVersion = fmt.Sprintf("%s", constraintAPIVersion)
}
var kind string
if constraintKind, ok := cv.Constraint.Object["kind"]; ok {
kind = fmt.Sprintf("%s", constraintKind)
}
var pbMetadata *structpb.Value
if constraintMetadata, ok := cv.Constraint.Object["metadata"]; ok {
if pbMetadata, err = convertToProtoVal(constraintMetadata); err != nil {
return nil, errors.Wrapf(err, "failed to convert constraint metadata into structpb.Value")
}
}
var pbSpec *structpb.Value
if constraintSpec, ok := cv.Constraint.Object["spec"]; ok {
if pbSpec, err = convertToProtoVal(constraintSpec); err != nil {
return nil, errors.Wrapf(err, "failed to convert constraint spec into structpb.Value")
}
}
// Build the ConstraintConfig proto.
constraintConfig := &validator.Constraint{
ApiVersion: apiVersion,
Kind: kind,
Metadata: pbMetadata,
Spec: pbSpec,
}
return &validator.Violation{
Constraint: cv.name(),
ConstraintConfig: constraintConfig,
Resource: name,
Message: cv.Message,
Metadata: metadata,
Severity: cv.Severity,
}, nil
}
type convertFailed struct {
err error
}
// convertToProtoVal converts an interface into a proto struct value.
func convertToProtoVal(from interface{}) (val *structpb.Value, err error) {
defer func() {
if x := recover(); x != nil {
convFail, ok := x.(*convertFailed)
if !ok {
panic(x)
}
val = nil
err = errors.Errorf("failed to convert proto val: %s", convFail.err)
}
}()
val = convertToProtoValInternal(from)
return
}
func convertToProtoValInternal(from interface{}) *structpb.Value {
if from == nil {
return nil
}
switch val := from.(type) {
case map[string]interface{}:
fields := map[string]*structpb.Value{}
for k, v := range val {
fields[k] = convertToProtoValInternal(v)
}
return &structpb.Value{
Kind: &structpb.Value_StructValue{
StructValue: &structpb.Struct{
Fields: fields,
},
}}
case []interface{}:
vals := make([]*structpb.Value, len(val))
for idx, v := range val {
vals[idx] = convertToProtoValInternal(v)
}
return &structpb.Value{
Kind: &structpb.Value_ListValue{
ListValue: &structpb.ListValue{Values: vals},
},
}
case string:
return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: val}}
case int:
return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(val)}}
case int64:
return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(val)}}
case float64:
return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: val}}
case float32:
return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(val)}}
case bool:
return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: val}}
default:
panic(&convertFailed{errors.Errorf("Unhandled type %v (%s)", from, reflect.TypeOf(from).String())})
}
}