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 }