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 all 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
158 changes: 81 additions & 77 deletions magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,96 +7,100 @@
package main

import (
"bufio"
"strings"
"bufio"
"strings"

"github.com/magefile/mage/sh"
"github.com/magefile/mage/sh"
)

func Generate() error {
if err := sh.RunV("go", "mod", "vendor", "-o", ".vendor"); err != nil {
return err
}

if err := sh.RunV("go", "run", "tools/directivesgen/main.go", ".vendor/github.com/corazawaf/coraza/v3"); err != nil {
return err
}

// get all changes in content/docs
allFilesChangedNames, err := sh.Output("git", "diff", "--name-only", "content/docs")
if err != nil {
return err
}

// get those changes in content/docs that are not lastmod changes only
actuallyChangedDiff, err := sh.Output("git", "diff", "-I", "lastmod:", "content/docs")
if err != nil {
return err
}

// get the files that were not changed besides the lastmod. This is important to not to introduce massive changes
// in docs when they are actually not changed.
actuallyUnmodifiedFiles := diff(strings.Split(allFilesChangedNames, "\n"), diffToFilenames(actuallyChangedDiff))
for _, file := range actuallyUnmodifiedFiles {
if err := sh.RunV("git", "checkout", "-q", file); err != nil {
return err
}
}

return nil
if err := sh.RunV("go", "mod", "vendor", "-o", ".vendor"); err != nil {
return err
}

if err := sh.RunV("go", "run", "tools/directivesgen/main.go", ".vendor/github.com/corazawaf/coraza/v3"); err != nil {
return err
}

if err := sh.RunV("go", "run", "tools/actionsgen/main.go", ".vendor/github.com/corazawaf/coraza/v3"); err != nil {
return err
}

// get all changes in content/docs
allFilesChangedNames, err := sh.Output("git", "diff", "--name-only", "content/docs")
if err != nil {
return err
}

// get those changes in content/docs that are not lastmod changes only
actuallyChangedDiff, err := sh.Output("git", "diff", "-I", "lastmod:", "content/docs")
if err != nil {
return err
}

// get the files that were not changed besides the lastmod. This is important to not to introduce massive changes
// in docs when they are actually not changed.
actuallyUnmodifiedFiles := diff(strings.Split(allFilesChangedNames, "\n"), diffToFilenames(actuallyChangedDiff))
for _, file := range actuallyUnmodifiedFiles {
if err := sh.RunV("git", "checkout", "-q", file); err != nil {
return err
}
}

return nil
}

// diffToFilenames parses a git diff and returns the filenames that were changed
func diffToFilenames(diff string) []string {
scanner := bufio.NewScanner(strings.NewReader(diff))
filenames := make([]string, 0)

for scanner.Scan() {
line := scanner.Text()
// Check if the line starts with "diff --git"
if !strings.HasPrefix(line, "diff --git") {
continue
}

// line is "diff --git a/path/to/file b/path/to/file"
parts := strings.Split(line, " ")
if parts[2][2:] == parts[3][2:] {
filenames = append(filenames, parts[2][2:])
}
}

return filenames
scanner := bufio.NewScanner(strings.NewReader(diff))
filenames := make([]string, 0)

for scanner.Scan() {
line := scanner.Text()
// Check if the line starts with "diff --git"
if !strings.HasPrefix(line, "diff --git") {
continue
}

// line is "diff --git a/path/to/file b/path/to/file"
parts := strings.Split(line, " ")
if parts[2][2:] == parts[3][2:] {
filenames = append(filenames, parts[2][2:])
}
}

return filenames
}

// diff returns the difference between two slices of strings
func diff(list1 []string, list2 []string) []string {
var diff []string

// Loop two times, first to find slice1 strings not in slice2,
// second loop to find slice2 strings not in slice1
for i := 0; i < 2; i++ {
for _, s1 := range list1 {
found := false
for _, s2 := range list2 {
if s1 == s2 {
found = true
break
}
}
// String not found. We add it to return slice
if !found {
diff = append(diff, s1)
}
}
// Swap the slices, only if it was the first loop
if i == 0 {
list1, list2 = list2, list1
}
}

return diff
var diff []string

// Loop two times, first to find slice1 strings not in slice2,
// second loop to find slice2 strings not in slice1
for i := 0; i < 2; i++ {
for _, s1 := range list1 {
found := false
for _, s2 := range list2 {
if s1 == s2 {
found = true
break
}
}
// String not found. We add it to return slice
if !found {
diff = append(diff, s1)
}
}
// Swap the slices, only if it was the first loop
if i == 0 {
list1, list2 = list2, list1
}
}

return diff
}

func Test() error {
return sh.RunV("go", "test", "./...")
return sh.RunV("go", "test", "./...")
}
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
}
Loading
Loading