coraza: ship rules-catalog.json generated from bundled CRS at build time
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,16 @@ RUN git clone --depth 1 --branch "${CORAZA_SPOA_VERSION}" \
|
|||||||
&& go mod download \
|
&& go mod download \
|
||||||
&& CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/coraza-spoa .
|
&& CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/coraza-spoa .
|
||||||
|
|
||||||
|
# Catalog extractor: walks the bundled CRS at build time and emits
|
||||||
|
# rules-catalog.json so WHP's UI can render rule metadata without parsing
|
||||||
|
# .conf files at runtime. Uses the SAME coraza-coreruleset version pin as
|
||||||
|
# the coraza-spoa binary above (drift between the two would mislabel rules).
|
||||||
|
FROM repo.anhonesthost.net/cloud-hosting-platform/golang:1.25 AS catalog
|
||||||
|
WORKDIR /src
|
||||||
|
COPY catalog-extractor/ .
|
||||||
|
RUN go build -trimpath -o /out/catalog-extractor . \
|
||||||
|
&& /out/catalog-extractor > /out/rules-catalog.json
|
||||||
|
|
||||||
# Distroless runtime: no shell, no package manager, no /tmp by default —
|
# Distroless runtime: no shell, no package manager, no /tmp by default —
|
||||||
# smallest attack surface for an exposed service. Audit log directory is
|
# smallest attack surface for an exposed service. Audit log directory is
|
||||||
# bind-mounted; coraza-spoa writes to it via direct file I/O (no shell needed).
|
# bind-mounted; coraza-spoa writes to it via direct file I/O (no shell needed).
|
||||||
@@ -41,6 +51,7 @@ COPY config.yaml /etc/coraza-spoa/config.yaml
|
|||||||
COPY overrides.conf /etc/coraza/overrides.conf
|
COPY overrides.conf /etc/coraza/overrides.conf
|
||||||
COPY local-overrides.conf /etc/coraza/local-overrides.conf
|
COPY local-overrides.conf /etc/coraza/local-overrides.conf
|
||||||
COPY host-exceptions/ /etc/coraza/host-exceptions/
|
COPY host-exceptions/ /etc/coraza/host-exceptions/
|
||||||
|
COPY --from=catalog /out/rules-catalog.json /etc/coraza/rules-catalog.json
|
||||||
|
|
||||||
# Audit log directory — bind-mount /var/log/coraza:/var/log/coraza from host
|
# Audit log directory — bind-mount /var/log/coraza:/var/log/coraza from host
|
||||||
# so logs persist across container restarts and AI Monitor can tail them.
|
# so logs persist across container restarts and AI Monitor can tail them.
|
||||||
|
|||||||
7
coraza-spoa/catalog-extractor/go.mod
Normal file
7
coraza-spoa/catalog-extractor/go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module catalog-extractor
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require github.com/corazawaf/coraza-coreruleset/v4 v4.25.0
|
||||||
|
|
||||||
|
require github.com/magefile/mage v1.17.0 // indirect
|
||||||
12
coraza-spoa/catalog-extractor/go.sum
Normal file
12
coraza-spoa/catalog-extractor/go.sum
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
github.com/corazawaf/coraza-coreruleset/v4 v4.25.0 h1:tqFO1lfVpTiyWtlN618OXpZMfw+nnN0Q4///W5W+/HM=
|
||||||
|
github.com/corazawaf/coraza-coreruleset/v4 v4.25.0/go.mod h1:nRuGXITxOPvsLF2VxaTB7pYok8QB8BitX3ZenXcUryY=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
|
||||||
|
github.com/magefile/mage v1.17.0/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
80
coraza-spoa/catalog-extractor/main.go
Normal file
80
coraza-spoa/catalog-extractor/main.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
crs "github.com/corazawaf/coraza-coreruleset/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
File string `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
idRe = regexp.MustCompile(`(?i)\bid:'?(\d+)'?`)
|
||||||
|
msgRe = regexp.MustCompile(`(?i)\bmsg:'([^']+)'`)
|
||||||
|
severityRe = regexp.MustCompile(`(?i)\bseverity:'?([A-Z]+)'?`)
|
||||||
|
tagRe = regexp.MustCompile(`(?i)\btag:'([^']+)'`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out := []Rule{}
|
||||||
|
err := fs.WalkDir(crs.FS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(path, ".conf") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := fs.ReadFile(crs.FS, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Coalesce backslash-continuation lines so id/msg/etc on the same
|
||||||
|
// logical rule are visible to the per-line scanner.
|
||||||
|
text := regexp.MustCompile(`\\\s*\n\s*`).ReplaceAllString(string(b), " ")
|
||||||
|
for _, line := range strings.Split(text, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "SecRule") && !strings.HasPrefix(line, "SecAction") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m := idRe.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, _ := strconv.Atoi(m[1])
|
||||||
|
r := Rule{ID: id, File: path}
|
||||||
|
if mm := msgRe.FindStringSubmatch(line); mm != nil {
|
||||||
|
r.Msg = mm[1]
|
||||||
|
}
|
||||||
|
if mm := severityRe.FindStringSubmatch(line); mm != nil {
|
||||||
|
r.Severity = strings.ToLower(mm[1])
|
||||||
|
}
|
||||||
|
for _, mm := range tagRe.FindAllStringSubmatch(line, -1) {
|
||||||
|
r.Tags = append(r.Tags, mm[1])
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err := enc.Encode(out); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user