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  }