github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/rego/metadata.go (about)

     1  package rego
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/khulnasoft-lab/defsec/pkg/framework"
    11  	"github.com/khulnasoft-lab/defsec/pkg/providers"
    12  	"github.com/khulnasoft-lab/defsec/pkg/scan"
    13  	"github.com/khulnasoft-lab/defsec/pkg/severity"
    14  	defsecTypes "github.com/khulnasoft-lab/defsec/pkg/types"
    15  	"github.com/mitchellh/mapstructure"
    16  	"github.com/open-policy-agent/opa/ast"
    17  	"github.com/open-policy-agent/opa/rego"
    18  	"github.com/open-policy-agent/opa/util"
    19  )
    20  
    21  type StaticMetadata struct {
    22  	ID                 string
    23  	AVDID              string
    24  	Title              string
    25  	ShortCode          string
    26  	Description        string
    27  	Severity           string
    28  	RecommendedActions string
    29  	PrimaryURL         string
    30  	References         []string
    31  	InputOptions       InputOptions
    32  	Package            string
    33  	Frameworks         map[framework.Framework][]string
    34  	Provider           string
    35  	Service            string
    36  	Library            bool
    37  	CloudFormation     *scan.EngineMetadata
    38  	Terraform          *scan.EngineMetadata
    39  }
    40  
    41  type InputOptions struct {
    42  	Combined  bool
    43  	Selectors []Selector
    44  }
    45  
    46  type Selector struct {
    47  	Type     string
    48  	Subtypes []SubType
    49  }
    50  
    51  type SubType struct {
    52  	Group     string
    53  	Version   string
    54  	Kind      string
    55  	Namespace string
    56  	Service   string // only for cloud
    57  	Provider  string // only for cloud
    58  }
    59  
    60  func (m StaticMetadata) ToRule() scan.Rule {
    61  
    62  	provider := "generic"
    63  	if m.Provider != "" {
    64  		provider = m.Provider
    65  	} else if len(m.InputOptions.Selectors) > 0 {
    66  		provider = m.InputOptions.Selectors[0].Type
    67  	}
    68  	service := "general"
    69  	if m.Service != "" {
    70  		service = m.Service
    71  	}
    72  
    73  	return scan.Rule{
    74  		AVDID:          m.AVDID,
    75  		Aliases:        []string{m.ID},
    76  		ShortCode:      m.ShortCode,
    77  		Summary:        m.Title,
    78  		Explanation:    m.Description,
    79  		Impact:         "",
    80  		Resolution:     m.RecommendedActions,
    81  		Provider:       providers.Provider(provider),
    82  		Service:        service,
    83  		Links:          m.References,
    84  		Severity:       severity.Severity(m.Severity),
    85  		RegoPackage:    m.Package,
    86  		Frameworks:     m.Frameworks,
    87  		CloudFormation: m.CloudFormation,
    88  		Terraform:      m.Terraform,
    89  	}
    90  }
    91  
    92  type MetadataRetriever struct {
    93  	compiler *ast.Compiler
    94  }
    95  
    96  func NewMetadataRetriever(compiler *ast.Compiler) *MetadataRetriever {
    97  	return &MetadataRetriever{
    98  		compiler: compiler,
    99  	}
   100  }
   101  
   102  func (m *MetadataRetriever) findPackageAnnotation(module *ast.Module) *ast.Annotations {
   103  	annotationSet := m.compiler.GetAnnotationSet()
   104  	if annotationSet == nil {
   105  		return nil
   106  	}
   107  	for _, annotation := range annotationSet.Flatten() {
   108  		if annotation.GetPackage().Path.String() != module.Package.Path.String() || annotation.Annotations.Scope != "package" {
   109  			continue
   110  		}
   111  		return annotation.Annotations
   112  	}
   113  	return nil
   114  }
   115  
   116  func (m *MetadataRetriever) RetrieveMetadata(ctx context.Context, module *ast.Module, inputs ...Input) (*StaticMetadata, error) {
   117  
   118  	metadata := StaticMetadata{
   119  		ID:           "N/A",
   120  		Title:        "N/A",
   121  		Severity:     "UNKNOWN",
   122  		Description:  fmt.Sprintf("Rego module: %s", module.Package.Path.String()),
   123  		Package:      module.Package.Path.String(),
   124  		InputOptions: m.queryInputOptions(ctx, module),
   125  		Frameworks:   make(map[framework.Framework][]string),
   126  	}
   127  
   128  	// read metadata from official rego annotations if possible
   129  	if annotation := m.findPackageAnnotation(module); annotation != nil {
   130  		if err := m.fromAnnotation(&metadata, annotation); err != nil {
   131  			return nil, err
   132  		}
   133  		return &metadata, nil
   134  	}
   135  
   136  	// otherwise, try to read metadata from the rego module itself - we used to do this before annotations were a thing
   137  	namespace := getModuleNamespace(module)
   138  	metadataQuery := fmt.Sprintf("data.%s.__rego_metadata__", namespace)
   139  
   140  	options := []func(*rego.Rego){
   141  		rego.Query(metadataQuery),
   142  		rego.Compiler(m.compiler),
   143  		rego.Capabilities(nil),
   144  	}
   145  	// support dynamic metadata fields
   146  	for _, in := range inputs {
   147  		options = append(options, rego.Input(in.Contents))
   148  	}
   149  
   150  	instance := rego.New(options...)
   151  	set, err := instance.Eval(ctx)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	// no metadata supplied
   157  	if set == nil {
   158  		return &metadata, nil
   159  	}
   160  
   161  	if len(set) != 1 {
   162  		return nil, fmt.Errorf("failed to parse metadata: unexpected set length")
   163  	}
   164  	if len(set[0].Expressions) != 1 {
   165  		return nil, fmt.Errorf("failed to parse metadata: unexpected expression length")
   166  	}
   167  	expression := set[0].Expressions[0]
   168  	meta, ok := expression.Value.(map[string]interface{})
   169  	if !ok {
   170  		return nil, fmt.Errorf("failed to parse metadata: not an object")
   171  	}
   172  
   173  	err = m.updateMetadata(meta, &metadata)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	return &metadata, nil
   179  }
   180  
   181  // nolint
   182  func (m *MetadataRetriever) updateMetadata(meta map[string]interface{}, metadata *StaticMetadata) error {
   183  	if raw, ok := meta["id"]; ok {
   184  		metadata.ID = fmt.Sprintf("%s", raw)
   185  	}
   186  	if raw, ok := meta["avd_id"]; ok {
   187  		metadata.AVDID = fmt.Sprintf("%s", raw)
   188  	}
   189  	if raw, ok := meta["title"]; ok {
   190  		metadata.Title = fmt.Sprintf("%s", raw)
   191  	}
   192  	if raw, ok := meta["short_code"]; ok {
   193  		metadata.ShortCode = fmt.Sprintf("%s", raw)
   194  	}
   195  	if raw, ok := meta["severity"]; ok {
   196  		metadata.Severity = strings.ToUpper(fmt.Sprintf("%s", raw))
   197  	}
   198  	if raw, ok := meta["description"]; ok {
   199  		metadata.Description = fmt.Sprintf("%s", raw)
   200  	}
   201  	if raw, ok := meta["service"]; ok {
   202  		metadata.Service = fmt.Sprintf("%s", raw)
   203  	}
   204  	if raw, ok := meta["provider"]; ok {
   205  		metadata.Provider = fmt.Sprintf("%s", raw)
   206  	}
   207  	if raw, ok := meta["library"]; ok {
   208  		if lib, ok := raw.(bool); ok {
   209  			metadata.Library = lib
   210  		}
   211  	}
   212  	if raw, ok := meta["recommended_actions"]; ok {
   213  		metadata.RecommendedActions = fmt.Sprintf("%s", raw)
   214  	}
   215  	if raw, ok := meta["recommended_action"]; ok {
   216  		metadata.RecommendedActions = fmt.Sprintf("%s", raw)
   217  	}
   218  	if raw, ok := meta["url"]; ok {
   219  		metadata.References = append(metadata.References, fmt.Sprintf("%s", raw))
   220  	}
   221  	if raw, ok := meta["frameworks"]; ok {
   222  		frameworks, ok := raw.(map[string][]string)
   223  		if !ok {
   224  			return fmt.Errorf("failed to parse framework metadata: not an object")
   225  		}
   226  		for fw, sections := range frameworks {
   227  			metadata.Frameworks[framework.Framework(fw)] = sections
   228  		}
   229  	}
   230  	if raw, ok := meta["related_resources"]; ok {
   231  		if relatedResources, ok := raw.([]interface{}); ok {
   232  			for _, relatedResource := range relatedResources {
   233  				if relatedResourceMap, ok := relatedResource.(map[string]interface{}); ok {
   234  					if raw, ok := relatedResourceMap["ref"]; ok {
   235  						metadata.References = append(metadata.References, fmt.Sprintf("%s", raw))
   236  					}
   237  				} else if relatedResourceString, ok := relatedResource.(string); ok {
   238  					metadata.References = append(metadata.References, fmt.Sprintf("%s", relatedResourceString))
   239  				}
   240  			}
   241  		}
   242  	}
   243  
   244  	var err error
   245  	if metadata.CloudFormation, err = m.getEngineMetadata("cloud_formation", meta); err != nil {
   246  		return err
   247  	}
   248  
   249  	if metadata.Terraform, err = m.getEngineMetadata("terraform", meta); err != nil {
   250  		return err
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  func (m *MetadataRetriever) getEngineMetadata(schema string, meta map[string]interface{}) (*scan.EngineMetadata, error) {
   257  	var sMap map[string]interface{}
   258  	if raw, ok := meta[schema]; ok {
   259  		sMap, ok = raw.(map[string]interface{})
   260  		if !ok {
   261  			return nil, fmt.Errorf("failed to parse %s metadata: not an object", schema)
   262  		}
   263  	}
   264  
   265  	var em scan.EngineMetadata
   266  	if val, ok := sMap["good_examples"].(string); ok {
   267  		em.GoodExamples = []string{val}
   268  	}
   269  	if val, ok := sMap["bad_examples"].(string); ok {
   270  		em.BadExamples = []string{val}
   271  	}
   272  	if val, ok := sMap["links"].(string); ok {
   273  		em.Links = []string{val}
   274  	}
   275  	if val, ok := sMap["remediation_markdown"].(string); ok {
   276  		em.RemediationMarkdown = val
   277  	}
   278  
   279  	return &em, nil
   280  }
   281  
   282  func (m *MetadataRetriever) fromAnnotation(metadata *StaticMetadata, annotation *ast.Annotations) error {
   283  	metadata.Title = annotation.Title
   284  	metadata.Description = annotation.Description
   285  	for _, resource := range annotation.RelatedResources {
   286  		if !resource.Ref.IsAbs() {
   287  			continue
   288  		}
   289  		metadata.References = append(metadata.References, resource.Ref.String())
   290  	}
   291  	if custom := annotation.Custom; custom != nil {
   292  		if err := m.updateMetadata(custom, metadata); err != nil {
   293  			return err
   294  		}
   295  	}
   296  	if len(annotation.RelatedResources) > 0 {
   297  		metadata.PrimaryURL = annotation.RelatedResources[0].Ref.String()
   298  	}
   299  	return nil
   300  }
   301  
   302  // nolint: cyclop
   303  func (m *MetadataRetriever) queryInputOptions(ctx context.Context, module *ast.Module) InputOptions {
   304  
   305  	options := InputOptions{
   306  		Combined:  false,
   307  		Selectors: nil,
   308  	}
   309  
   310  	var metadata map[string]interface{}
   311  
   312  	// read metadata from official rego annotations if possible
   313  	if annotation := m.findPackageAnnotation(module); annotation != nil && annotation.Custom != nil {
   314  		if input, ok := annotation.Custom["input"]; ok {
   315  			if mapped, ok := input.(map[string]interface{}); ok {
   316  				metadata = mapped
   317  			}
   318  		}
   319  	}
   320  
   321  	if metadata == nil {
   322  
   323  		namespace := getModuleNamespace(module)
   324  		inputOptionQuery := fmt.Sprintf("data.%s.__rego_input__", namespace)
   325  		instance := rego.New(
   326  			rego.Query(inputOptionQuery),
   327  			rego.Compiler(m.compiler),
   328  			rego.Capabilities(nil),
   329  		)
   330  		set, err := instance.Eval(ctx)
   331  		if err != nil {
   332  			return options
   333  		}
   334  
   335  		if len(set) != 1 {
   336  			return options
   337  		}
   338  		if len(set[0].Expressions) != 1 {
   339  			return options
   340  		}
   341  		expression := set[0].Expressions[0]
   342  		meta, ok := expression.Value.(map[string]interface{})
   343  		if !ok {
   344  			return options
   345  		}
   346  		metadata = meta
   347  	}
   348  
   349  	if raw, ok := metadata["combine"]; ok {
   350  		if combine, ok := raw.(bool); ok {
   351  			options.Combined = combine
   352  		}
   353  	}
   354  
   355  	if raw, ok := metadata["selector"]; ok {
   356  		if each, ok := raw.([]interface{}); ok {
   357  			for _, rawSelector := range each {
   358  				var selector Selector
   359  				if selectorMap, ok := rawSelector.(map[string]interface{}); ok {
   360  					if rawType, ok := selectorMap["type"]; ok {
   361  						selector.Type = fmt.Sprintf("%s", rawType)
   362  						// handle backward compatibility for "defsec" source type which is now "cloud"
   363  						if selector.Type == string(defsecTypes.SourceDefsec) {
   364  							selector.Type = string(defsecTypes.SourceCloud)
   365  						}
   366  					}
   367  					if subType, ok := selectorMap["subtypes"].([]interface{}); ok {
   368  						for _, subT := range subType {
   369  							if st, ok := subT.(map[string]interface{}); ok {
   370  								s := SubType{}
   371  								_ = mapstructure.Decode(st, &s)
   372  								selector.Subtypes = append(selector.Subtypes, s)
   373  							}
   374  						}
   375  					}
   376  				}
   377  				options.Selectors = append(options.Selectors, selector)
   378  			}
   379  		}
   380  	}
   381  
   382  	return options
   383  
   384  }
   385  
   386  func BuildSchemaSetFromPolicies(policies map[string]*ast.Module, paths []string, srcFS fs.FS) (*ast.SchemaSet, bool, error) {
   387  	schemaSet := ast.NewSchemaSet()
   388  	schemaSet.Put(ast.MustParseRef("schema.input"), map[string]interface{}{}) // for backwards compat only
   389  	var customFound bool
   390  	for _, policy := range policies {
   391  		for _, annotation := range policy.Annotations {
   392  			for _, ss := range annotation.Schemas {
   393  				schemaName, err := ss.Schema.Ptr()
   394  				if err != nil {
   395  					continue
   396  				}
   397  				if schemaName != "input" {
   398  					if schema, ok := SchemaMap[defsecTypes.Source(schemaName)]; ok {
   399  						customFound = true
   400  						schemaSet.Put(ast.MustParseRef(ss.Schema.String()), util.MustUnmarshalJSON([]byte(schema)))
   401  					} else {
   402  						b, err := findSchemaInFS(paths, srcFS, schemaName)
   403  						if err != nil {
   404  							return schemaSet, true, err
   405  						}
   406  						if b != nil {
   407  							customFound = true
   408  							schemaSet.Put(ast.MustParseRef(ss.Schema.String()), util.MustUnmarshalJSON(b))
   409  						}
   410  					}
   411  				}
   412  			}
   413  		}
   414  	}
   415  
   416  	return schemaSet, customFound, nil
   417  }
   418  
   419  // findSchemaInFS tries to find the schema anywhere in the specified FS
   420  func findSchemaInFS(paths []string, srcFS fs.FS, schemaName string) ([]byte, error) {
   421  	var schema []byte
   422  	for _, path := range paths {
   423  		if err := fs.WalkDir(srcFS, sanitisePath(path), func(path string, info fs.DirEntry, err error) error {
   424  			if err != nil {
   425  				return err
   426  			}
   427  			if info.IsDir() {
   428  				return nil
   429  			}
   430  			if !isJSONFile(info.Name()) {
   431  				return nil
   432  			}
   433  			if info.Name() == schemaName+".json" {
   434  				schema, err = fs.ReadFile(srcFS, filepath.ToSlash(path))
   435  				if err != nil {
   436  					return err
   437  				}
   438  				return nil
   439  			}
   440  			return nil
   441  		}); err != nil {
   442  			return nil, err
   443  		}
   444  	}
   445  	return schema, nil
   446  }