From bdff1a4096a61ed264e24cbc658cecfdd1f4906d Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Wed, 16 Oct 2024 10:25:57 -0300 Subject: [PATCH] feat: add actionsgen Signed-off-by: Felipe Zipitria --- tools/actionsgen/main.go | 187 +++++++++++++++++++++++++++++++++++ tools/actionsgen/template.md | 44 +++++++++ 2 files changed, 231 insertions(+) create mode 100644 tools/actionsgen/main.go create mode 100644 tools/actionsgen/template.md diff --git a/tools/actionsgen/main.go b/tools/actionsgen/main.go new file mode 100644 index 00000000..800cd687 --- /dev/null +++ b/tools/actionsgen/main.go @@ -0,0 +1,187 @@ +// Copyright 2024 The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bufio" + "bytes" + _ "embed" + "fmt" + "go/ast" + "go/parser" + "go/token" + "html" + "html/template" + "log" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" +) + +type Page struct { + LastModification string + Actions []Action +} + +type Action struct { + Name string + ActionGroup string + Description string + Example string + Phases string +} + +//go:embed template.md +var contentTemplate string + +const dstFile = "./content/docs/seclang/actions.md" + +func main() { + tmpl, err := template.New("action").Parse(contentTemplate) + if err != nil { + log.Fatal(err) + } + + var files []string + + root := path.Join("../coraza", "/internal/actions") + + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return nil + } + + // get all files that are not test files + if !info.IsDir() && !strings.HasSuffix(info.Name(), "_test.go") && info.Name() != "actions.go" { + files = append(files, path) + } + + return nil + }) + + if err != nil { + log.Fatal(err) + } + + dstf, err := os.Create(dstFile) + if err != nil { + log.Fatal(err) + } + defer dstf.Close() + + page := Page{ + LastModification: time.Now().Format(time.RFC3339), + } + + for _, file := range files { + page = getActionFromFile(file, page) + } + + sort.Slice(page.Actions, func(i, j int) bool { + return page.Actions[i].Name < page.Actions[j].Name + }) + + content := bytes.Buffer{} + err = tmpl.Execute(&content, page) + if err != nil { + log.Fatal(err) + } + + _, err = dstf.WriteString(html.UnescapeString(content.String())) + if err != nil { + log.Fatal(err) + } +} + +func getActionFromFile(file string, page Page) Page { + src, err := os.ReadFile(file) + if err != nil { + log.Fatal(err) + } + fSet := token.NewFileSet() + f, err := parser.ParseFile(fSet, file, src, parser.ParseComments) + if err != nil { + log.Fatal(err) + } + + actionDoc := "" + ast.Inspect(f, func(n ast.Node) bool { + switch ty := n.(type) { + case *ast.GenDecl: + if ty.Doc.Text() != "" { + actionDoc += ty.Doc.Text() + } + case *ast.TypeSpec: + typeName := ty.Name.String() + if !strings.HasSuffix(typeName, "Fn") { + return true + } + if len(typeName) < 3 { + return true + } + + actionName := typeName[0 : len(typeName)-2] + page.Actions = append(page.Actions, parseAction(actionName, actionDoc)) + } + return true + }) + return page +} + +func parseAction(name string, doc string) Action { + var key string + var value string + var ok bool + + d := Action{ + Name: name, + } + + fieldAppenders := map[string]func(d *Action, value string){ + "Description": func(a *Action, value string) { d.Description += value }, + "Action Group": func(a *Action, value string) { d.ActionGroup += value }, + "Example": func(a *Action, value string) { d.Example += value }, + "Processing Phases": func(a *Action, value string) { d.Phases += value }, + } + + previousKey := "" + scanner := bufio.NewScanner(strings.NewReader(doc)) + for scanner.Scan() { + line := scanner.Text() + if len(strings.TrimSpace(line)) == 0 { + continue + } + + // There are two types of comments. One is a key-value pair, the other is a continuation of the previous key + // E.g. + // Action Group: Non-disruptive <= first one, key value pair + // Example: <= second one, key in a line, value in the next lines + // This action is used to generate a response. + // + if strings.HasSuffix(line, ":") { + key = line[:len(line)-1] + value = "" + } else { + key, value, ok = strings.Cut(line, ": ") + if !ok { + key = previousKey + value = " " + line + } + } + + if fn, ok := fieldAppenders[key]; ok { + fn(&d, value) + previousKey = key + } else if previousKey != "" { + fieldAppenders[previousKey](&d, value) + } else { + log.Fatalf("unknown field %q", key) + } + } + return d +} diff --git a/tools/actionsgen/template.md b/tools/actionsgen/template.md new file mode 100644 index 00000000..53ab271b --- /dev/null +++ b/tools/actionsgen/template.md @@ -0,0 +1,44 @@ +--- +title: "Actions" +description: "Actions available in Coraza" +lead: "The action of a rule defines how to handle HTTP requests that have matched one or more rule conditions." +date: 2020-10-06T08:48:57+00:00 +lastmod: "{{ .LastModification }}" +draft: false +images: [] +menu: + docs: + parent: "seclang" +weight: 100 +toc: true +--- + +[//]: <> (This file is generated by tools/actionsgen. DO NOT EDIT.) + +Actions are defined as part of a `SecRule` or as parameter for `SecAction` or `SecDefaultAction`. A rule can have no or serveral actions which need to be separated by a comma. + +Actions can be categorized by how they affect overall processing: + +* **Disruptive actions** - Cause Coraza to do something. In many cases something means block transaction, but not in all. For example, the allow action is classified as a disruptive action, but it does the opposite of blocking. There can only be one disruptive action per rule (if there are multiple disruptive actions present, or inherited, only the last one will take effect), or rule chain (in a chain, a disruptive action can only appear in the first rule). +{{"{{"}}< alert icon="👉" >{{"}}"}} +Disruptive actions will NOT be executed if the `SecRuleEngine` is set to `DetectionOnly`. If you are creating exception/allowlisting rules that use the allow action, you should also add the `ctl:ruleEngine=On` action to execute the action. +{{"{{"}}< /alert >{{"}}"}} +* **Non-disruptive actions** - Do something, but that something does not and cannot affect the rule processing flow. Setting a variable, or changing its value is an example of a non-disruptive action. Non-disruptive action can appear in any rule, including each rule belonging to a chain. +* **Flow actions** - These actions affect the rule flow (for example skip or skipAfter). +* **Meta-data actions** - used to provide more information about rules. Examples include id, rev, severity and msg. +* **Data actions** - Not really actions, these are mere containers that hold data used by other actions. For example, the status action holds the status that will be used for blocking (if it takes place). + +{{ range .Actions }} +## {{ .Name }} + +**Description**: {{ .Description }} + +**Action Group**: {{ .ActionGroup }} + +{{ if .Phases }} +**Processing Phases**: {{ .Phases }} +{{ end }} + +**Example**: +{{ .Example }} +{{ end }}