github.com/DataDog/datadog-agent/pkg/security/secl@v0.55.0-devel.0.20240517055856-10c4965fea94/compiler/generators/accessors/doc/doc.go (about) 1 // Unless explicitly stated otherwise all files in this repository are licensed 2 // under the Apache License Version 2.0. 3 // This product includes software developed at Datadog (https://www.datadoghq.com/). 4 // Copyright 2016-present Datadog, Inc. 5 6 // Package doc holds doc related files 7 package doc 8 9 import ( 10 "encoding/json" 11 "fmt" 12 "go/ast" 13 "os" 14 "path/filepath" 15 "regexp" 16 "sort" 17 "strings" 18 19 "golang.org/x/tools/go/packages" 20 21 "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/generators/accessors/common" 22 ) 23 24 const ( 25 generateConstantsAnnotationPrefix = "// generate_constants:" 26 SECLDocForLength = "SECLDoc[length] Definition:`Length of the corresponding string`" // SECLDocForLength defines SECL doc for length 27 ) 28 29 type documentation struct { 30 Types []eventType `json:"event_types"` 31 PropertiesDoc []propertyDocumentation `json:"properties_doc"` 32 Constants []constants `json:"constants"` 33 } 34 35 type eventType struct { 36 Name string `json:"name"` 37 Definition string `json:"definition"` 38 Type string `json:"type"` 39 FromAgentVersion string `json:"from_agent_version"` 40 Experimental bool `json:"experimental"` 41 Properties []eventTypeProperty `json:"properties"` 42 } 43 44 type eventTypeProperty struct { 45 Name string `json:"name"` 46 Definition string `json:"definition"` 47 DocLink string `json:"property_doc_link"` 48 PropertyKey string `json:"-"` 49 } 50 51 type constants struct { 52 Name string `json:"name"` 53 Link string `json:"link"` 54 Description string `json:"description"` 55 All []constant `json:"all"` 56 } 57 58 type constant struct { 59 Name string `json:"name"` 60 Architecture string `json:"architecture"` 61 } 62 63 type example struct { 64 Expression string `json:"expression"` 65 Description string `json:"description"` 66 } 67 68 type propertyDocumentation struct { 69 Name string `json:"name"` 70 Link string `json:"link"` 71 Type string `json:"type"` 72 Doc string `json:"definition"` 73 Prefixes []string `json:"prefixes"` 74 Constants string `json:"constants"` 75 ConstantsLink string `json:"constants_link"` 76 Examples []example `json:"examples"` 77 IsUniqueEventProperty bool `json:"-"` 78 } 79 80 func translateFieldType(rt string) string { 81 switch rt { 82 case "net.IPNet", "net.IP": 83 return "IP/CIDR" 84 } 85 return rt 86 } 87 88 // GenerateDocJSON generates the SECL json documentation file to the provided outputPath 89 func GenerateDocJSON(module *common.Module, seclModelPath, outputPath string) error { 90 // parse constants 91 consts, err := parseConstants(seclModelPath, module.BuildTags) 92 if err != nil { 93 return fmt.Errorf("couldn't generate documentation for constants: %w", err) 94 } 95 96 kinds := make(map[string][]eventTypeProperty) 97 cachedDocumentation := make(map[string]*propertyDocumentation) 98 99 for name, field := range module.Fields { 100 if field.GettersOnly { 101 continue 102 } 103 104 var propertyKey string 105 var propertySuffix string 106 var propertyDefinition string 107 if strings.HasPrefix(field.Alias, field.AliasPrefix) { 108 propertySuffix = strings.TrimPrefix(field.Alias, field.AliasPrefix) 109 propertyKey = field.Struct + propertySuffix 110 propertySuffix = strings.TrimPrefix(propertySuffix, ".") 111 } else { 112 propertyKey = field.Alias 113 propertySuffix = field.Alias 114 } 115 116 if propertyDoc, exists := cachedDocumentation[propertyKey]; !exists { 117 definition, constantsName, examples := parseSECLDocWithSuffix(field.CommentText, propertySuffix) 118 if definition == "" { 119 return fmt.Errorf("failed to parse SECL documentation for field '%s' (name:%s psuffix:%s, pkey:%s, alias:%s, aliasprefix:%s)\n%+v", name, field.Name, propertySuffix, propertyKey, field.Alias, field.AliasPrefix, field) 120 } 121 122 var constsLink string 123 if len(constantsName) > 0 { 124 var found bool 125 for _, constantList := range consts { 126 if constantList.Name == constantsName { 127 found = true 128 constsLink = constantList.Link 129 } 130 } 131 if !found { 132 return fmt.Errorf("couldn't generate documentation for %s: unknown constant name %s", name, constantsName) 133 } 134 } 135 136 var propertyDoc = &propertyDocumentation{ 137 Name: name, 138 Link: strings.ReplaceAll(name, ".", "-") + "-doc", 139 Type: translateFieldType(field.ReturnType), 140 Doc: strings.TrimSpace(definition), 141 Prefixes: []string{field.AliasPrefix}, 142 Constants: constantsName, 143 ConstantsLink: constsLink, 144 Examples: make([]example, 0), // force the serialization of an empty array 145 IsUniqueEventProperty: true, 146 } 147 propertyDoc.Examples = append(propertyDoc.Examples, examples...) 148 propertyDefinition = propertyDoc.Doc 149 cachedDocumentation[propertyKey] = propertyDoc 150 } else if propertyDoc.IsUniqueEventProperty { 151 propertyDoc.IsUniqueEventProperty = false 152 fieldSuffix := strings.TrimPrefix(field.Alias, field.AliasPrefix) 153 propertyDoc.Name = "*" + fieldSuffix 154 propertyDoc.Link = "common-" + strings.ReplaceAll(strings.ToLower(propertyKey), ".", "-") + "-doc" 155 propertyDoc.Prefixes = append(propertyDoc.Prefixes, field.AliasPrefix) 156 propertyDefinition = propertyDoc.Doc 157 } else { 158 propertyDoc.Prefixes = append(propertyDoc.Prefixes, field.AliasPrefix) 159 propertyDefinition = propertyDoc.Doc 160 } 161 162 kinds[field.Event] = append(kinds[field.Event], eventTypeProperty{ 163 Name: name, 164 Definition: propertyDefinition, 165 PropertyKey: propertyKey, 166 }) 167 } 168 169 eventTypes := make([]eventType, 0) 170 for name, properties := range kinds { 171 for i := 0; i < len(properties); i++ { 172 property := &properties[i] 173 if propertyDoc, exists := cachedDocumentation[property.PropertyKey]; exists { 174 property.DocLink = propertyDoc.Link 175 sort.Slice(propertyDoc.Prefixes, func(i, j int) bool { 176 return propertyDoc.Prefixes[i] < propertyDoc.Prefixes[j] 177 }) 178 } 179 } 180 181 sort.Slice(properties, func(i, j int) bool { 182 return properties[i].Name < properties[j].Name 183 }) 184 185 info := extractVersionAndDefinition(module.EventTypes[name]) 186 eventTypes = append(eventTypes, eventType{ 187 Name: name, 188 Definition: info.Definition, 189 Type: info.Type, 190 FromAgentVersion: info.FromAgentVersion, 191 Experimental: info.Experimental, 192 Properties: properties, 193 }) 194 } 195 196 // for stability 197 sort.Slice(eventTypes, func(i, j int) bool { 198 return eventTypes[i].Name < eventTypes[j].Name 199 }) 200 201 // force the serialization of an empty array 202 propertiesDoc := make([]propertyDocumentation, 0) 203 for _, ex := range cachedDocumentation { 204 propertiesDoc = append(propertiesDoc, *ex) 205 } 206 sort.Slice(propertiesDoc, func(i, j int) bool { 207 if propertiesDoc[i].Name != propertiesDoc[j].Name { 208 return propertiesDoc[i].Name < propertiesDoc[j].Name 209 } 210 return propertiesDoc[i].Link < propertiesDoc[j].Link 211 }) 212 213 doc := documentation{ 214 Types: eventTypes, 215 PropertiesDoc: propertiesDoc, 216 Constants: consts, 217 } 218 219 res, err := json.MarshalIndent(doc, "", " ") 220 if err != nil { 221 return err 222 } 223 224 return os.WriteFile(outputPath, res, 0644) 225 } 226 227 func mergeConstants(one []constants, two []constants) []constants { 228 var output []constants 229 230 // add constants from one to output 231 output = append(output, one...) 232 233 // check if the constants from two should be appended or merged 234 for _, constsTwo := range two { 235 shouldAppendConsts := true 236 for i, existingConsts := range output { 237 if existingConsts.Name == constsTwo.Name { 238 shouldAppendConsts = false 239 240 // merge architecture if necessary 241 for _, constTwo := range constsTwo.All { 242 shouldAppendConst := true 243 for j, existingConst := range existingConsts.All { 244 if constTwo.Name == existingConst.Name { 245 shouldAppendConst = false 246 247 if len(constTwo.Architecture) > 0 && constTwo.Architecture != "all" && len(existingConst.Architecture) > 0 && existingConst.Architecture != "all" { 248 existingConst.Architecture += ", " + constTwo.Architecture 249 } 250 output[i].All[j].Architecture = existingConst.Architecture 251 } 252 } 253 if shouldAppendConst { 254 output[i].All = append(output[i].All, constTwo) 255 } 256 } 257 break 258 } 259 } 260 if shouldAppendConsts { 261 output = append(output, constsTwo) 262 } 263 } 264 return output 265 } 266 267 func parseArchFromFilepath(filepath string) (string, error) { 268 switch { 269 case strings.HasSuffix(filepath, "common.go") || strings.HasSuffix(filepath, "linux.go"): 270 return "all", nil 271 case strings.HasSuffix(filepath, "amd64.go"): 272 return "amd64", nil 273 case strings.HasSuffix(filepath, "arm.go"): 274 return "arm", nil 275 case strings.HasSuffix(filepath, "arm64.go"): 276 return "arm64", nil 277 default: 278 return "", fmt.Errorf("couldn't parse architecture from filepath: %s", filepath) 279 } 280 } 281 282 var nonLinkCharactersRegex = regexp.MustCompile(`(?:^[^a-z0-9]+)|(?:[^a-z0-9-]+)|(?:[^a-z0-9]+$)`) 283 284 func constsLinkFromName(constName string) string { 285 return nonLinkCharactersRegex.ReplaceAllString(strings.ReplaceAll(strings.ToLower(strings.TrimSpace(constName)), " ", "-"), "") 286 } 287 288 func parseConstantsFile(filepath string, tags []string) ([]constants, error) { 289 // extract architecture from filename 290 arch, err := parseArchFromFilepath(filepath) 291 if err != nil { 292 return nil, err 293 } 294 295 // generate constants 296 var output []constants 297 cfg := packages.Config{ 298 Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedImports, 299 BuildFlags: []string{"-mod=mod", fmt.Sprintf("-tags=%s", tags)}, 300 } 301 302 pkgs, err := packages.Load(&cfg, filepath) 303 if err != nil { 304 return nil, fmt.Errorf("load error:%w", err) 305 } 306 307 if len(pkgs) == 0 || len(pkgs[0].Syntax) == 0 { 308 return nil, fmt.Errorf("couldn't parse constant file") 309 } 310 311 pkg := pkgs[0] 312 astFile := pkg.Syntax[0] 313 for _, decl := range astFile.Decls { 314 if decl, ok := decl.(*ast.GenDecl); ok { 315 for _, s := range decl.Specs { 316 var consts constants 317 val, ok := s.(*ast.ValueSpec) 318 if !ok { 319 continue 320 } 321 322 // check if this ValueSpec has a generate_commands annotation 323 if val.Doc == nil { 324 continue 325 } 326 for _, comment := range val.Doc.List { 327 if !strings.HasPrefix(comment.Text, generateConstantsAnnotationPrefix) { 328 continue 329 } 330 331 name, description, found := strings.Cut(strings.TrimPrefix(comment.Text, generateConstantsAnnotationPrefix), ",") 332 if !found { 333 continue 334 } 335 consts.Name = name 336 consts.Link = constsLinkFromName(name) 337 consts.Description = description 338 break 339 } 340 if len(consts.Name) == 0 || len(consts.Description) == 0 { 341 continue 342 } 343 344 // extract list of keys 345 if len(val.Values) < 1 { 346 continue 347 } 348 values, ok := val.Values[0].(*ast.CompositeLit) 349 if !ok { 350 continue 351 } 352 for _, value := range values.Elts { 353 valueExpr, ok := value.(*ast.KeyValueExpr) 354 if !ok { 355 continue 356 } 357 358 // create new constant entry from valueExpr 359 consts.All = append(consts.All, constant{ 360 Name: strings.Trim(valueExpr.Key.(*ast.BasicLit).Value, "\""), 361 Architecture: arch, 362 }) 363 } 364 365 output = append(output, consts) 366 } 367 } 368 } 369 370 return output, nil 371 } 372 373 func parseConstants(path string, tags []string) ([]constants, error) { 374 var output []constants 375 for _, filename := range []string{"consts_common.go", "consts_linux.go", "consts_linux_amd64.go", "consts_linux_arm.go", "consts_linux_arm64.go"} { 376 consts, err := parseConstantsFile(filepath.Join(path, filename), tags) 377 if err != nil { 378 return nil, err 379 } 380 381 output = mergeConstants(output, consts) 382 } 383 384 // sort the list of constants 385 sort.Slice(output, func(i int, j int) bool { 386 return output[i].Name < output[j].Name 387 }) 388 return output, nil 389 } 390 391 var ( 392 minVersionRE = regexp.MustCompile(`^\[(?P<version>(\w|\.|\s)*)\]\s*\[(?P<type>\w+)\]\s*(\[(?P<experimental>Experimental)\])?\s*(?P<def>.*)`) 393 minVersionREIndex = minVersionRE.SubexpIndex("version") 394 typeREIndex = minVersionRE.SubexpIndex("type") 395 experimentalREIndex = minVersionRE.SubexpIndex("experimental") 396 definitionREIndex = minVersionRE.SubexpIndex("def") 397 ) 398 399 type eventTypeInfo struct { 400 Definition string 401 Type string 402 Experimental bool 403 FromAgentVersion string 404 } 405 406 func extractVersionAndDefinition(evtType *common.EventTypeMetadata) eventTypeInfo { 407 var comment string 408 if evtType != nil { 409 comment = evtType.Doc 410 } 411 trimmed := strings.TrimSpace(comment) 412 413 if matches := minVersionRE.FindStringSubmatch(trimmed); matches != nil { 414 return eventTypeInfo{ 415 Definition: strings.TrimSpace(matches[definitionREIndex]), 416 Type: strings.TrimSpace(matches[typeREIndex]), 417 Experimental: matches[experimentalREIndex] != "", 418 FromAgentVersion: strings.TrimSpace(matches[minVersionREIndex]), 419 } 420 } 421 422 return eventTypeInfo{ 423 Definition: trimmed, 424 } 425 } 426 427 var ( 428 seclDocRE = regexp.MustCompile(`SECLDoc\[((?:[a-z0-9_]+\.?)*[a-z0-9_]+)\]\s*Definition:\s*\x60([^\x60]+)\x60\s*(?:Constants:\x60([^\x60]+)\x60\s*)?(?:Example:\s*\x60([^\x60]+)\x60\s*(?:Description:\s*\x60([^\x60]+)\x60\s*)?)*`) 429 examplesRE = regexp.MustCompile(`Example:\s*\x60([^\x60]+)\x60\s*(?:Description:\s*\x60([^\x60]+)\x60\s*)?`) 430 ) 431 432 func parseSECLDocWithSuffix(comment string, wantedSuffix string) (string, string, []example) { 433 trimmed := strings.TrimSpace(comment) 434 435 for _, match := range seclDocRE.FindAllStringSubmatchIndex(trimmed, -1) { 436 matchedSubString := trimmed[match[0]:match[1]] 437 seclSuffix := trimmed[match[2]:match[3]] 438 if seclSuffix != wantedSuffix { 439 continue 440 } 441 442 definition := trimmed[match[4]:match[5]] 443 var constants string 444 if match[6] != -1 && match[7] != -1 { 445 constants = trimmed[match[6]:match[7]] 446 } 447 448 var examples []example 449 for _, exampleMatch := range examplesRE.FindAllStringSubmatchIndex(matchedSubString, -1) { 450 expr := matchedSubString[exampleMatch[2]:exampleMatch[3]] 451 var desc string 452 if exampleMatch[4] != -1 && exampleMatch[5] != -1 { 453 desc = matchedSubString[exampleMatch[4]:exampleMatch[5]] 454 } 455 examples = append(examples, example{Expression: expr, Description: desc}) 456 } 457 458 return strings.TrimSpace(definition), strings.TrimSpace(constants), examples 459 } 460 461 return "", "", nil 462 }