Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add actionsgen #303

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the linked PR is merged, we can change to os.Args[1] here.


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 }}
Loading