Skip to content

Commit

Permalink
feat: add actionsgen
Browse files Browse the repository at this point in the history
Signed-off-by: Felipe Zipitria <felipe.zipitria@owasp.org>
  • Loading branch information
fzipi committed Oct 16, 2024
1 parent be34c21 commit bdff1a4
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 0 deletions.
187 changes: 187 additions & 0 deletions tools/actionsgen/main.go
Original file line number Diff line number Diff line change
@@ -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
}
44 changes: 44 additions & 0 deletions tools/actionsgen/template.md
Original file line number Diff line number Diff line change
@@ -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 }}

0 comments on commit bdff1a4

Please sign in to comment.