Redesign docs with Apple-native theme; verify content; route CI to self-hosted runner-02

- VitePress: custom theme (SF system fonts, glass nav, soft surfaces, pill buttons,
  light/dark code blocks, refined feature cards, platform showcase + stat strip).
- Replace every emoji across docs and README with inline SVG icons.
- Verify and fix doc accuracy against actual scripts: JSON schema (category+pattern only),
  env-var configuration for json2*/import_* scripts, owasp2json CLI surface.
- Add public assets (logo.svg, favicon.svg, hero-shield.svg) and Shiki haproxy alias.
- Workflows default to self-hosted runner-02 with a configurable fallback to GitHub
  runners via the RUNS_ON repo variable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fabrizio Salmi
2026-05-01 08:07:04 +02:00
parent 4575736fed
commit 5c654b3da8
32 changed files with 1643 additions and 839 deletions

View File

@@ -1,37 +1,77 @@
import { defineConfig } from 'vitepress'
// Inline SVG icon factory for the features grid (renders inside .VPFeature .icon)
const svg = (paths: string) =>
`<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`
const icons = {
shield: svg('<path d="M12 3 4.5 5.8v6.1c0 4.7 3.3 9.1 7.5 10.4 4.2-1.3 7.5-5.7 7.5-10.4V5.8L12 3Z"/><path d="m8.5 12 2.5 2.5L15.5 9.5"/>'),
bot: svg('<rect x="4" y="7.5" width="16" height="11" rx="3"/><path d="M12 4v3.5"/><circle cx="12" cy="3.5" r="0.9" fill="currentColor" stroke="none"/><circle cx="9" cy="13" r="1.1" fill="currentColor" stroke="none"/><circle cx="15" cy="13" r="1.1" fill="currentColor" stroke="none"/><path d="M2 13.5v2M22 13.5v2"/>'),
servers: svg('<rect x="3.5" y="4" width="17" height="6" rx="1.5"/><rect x="3.5" y="14" width="17" height="6" rx="1.5"/><path d="M7 7h.01M7 17h.01"/><path d="M11 7h6M11 17h6"/>'),
refresh: svg('<path d="M3.5 12a8.5 8.5 0 0 1 14.5-6L20 8"/><path d="M20 3v5h-5"/><path d="M20.5 12a8.5 8.5 0 0 1-14.5 6L4 16"/><path d="M4 21v-5h5"/>'),
package: svg('<path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="m3.3 8 8.7 5 8.7-5"/><path d="M12 13v8"/><path d="m7.5 5.5 9 5"/>'),
puzzle: svg('<path d="M14 4.5a2 2 0 1 0-4 0V6H6a1.5 1.5 0 0 0-1.5 1.5V11h1.5a2 2 0 1 1 0 4H4.5v3.5A1.5 1.5 0 0 0 6 20h3.5v-1.5a2 2 0 1 1 4 0V20H17a1.5 1.5 0 0 0 1.5-1.5V15H20a2 2 0 1 0 0-4h-1.5V7.5A1.5 1.5 0 0 0 17 6h-3V4.5Z"/>')
}
export default defineConfig({
title: 'Patterns',
description: 'OWASP CRS and Bad Bot Detection for Web Servers',
titleTemplate: ':title — Patterns',
description: 'Automated OWASP CRS and bad-bot rules for Nginx, Apache, Traefik, and HAProxy.',
base: '/patterns/',
cleanUrls: true,
lastUpdated: true,
markdown: {
languageAlias: {
haproxy: 'apache'
},
theme: {
light: 'github-light',
dark: 'github-dark-dimmed'
}
},
head: [
['link', { rel: 'icon', href: '/patterns/favicon.ico' }]
['link', { rel: 'icon', type: 'image/svg+xml', href: '/patterns/favicon.svg' }],
['meta', { name: 'theme-color', content: '#0071e3' }],
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'default' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:title', content: 'Patterns — OWASP CRS WAF rules' }],
['meta', { property: 'og:description', content: 'Automated OWASP CRS and bad-bot rules for Nginx, Apache, Traefik, and HAProxy.' }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }]
],
themeConfig: {
logo: '/logo.svg',
logo: { src: '/logo.svg', width: 24, height: 24 },
siteTitle: 'Patterns',
// Expose icons to markdown via Vite config? Instead we expose them through home features below.
// (Used by index.md frontmatter via `icon: { svg: ... }`)
// @ts-expect-error — custom field for home page
icons,
nav: [
{ text: 'Home', link: '/' },
{ text: 'Getting Started', link: '/getting-started' },
{ text: 'Documentation', link: '/getting-started', activeMatch: '^/(getting-started|nginx|apache|traefik|haproxy|badbots|api)' },
{
text: 'Web Servers',
items: [
{ text: 'Nginx', link: '/nginx' },
{ text: 'Apache', link: '/apache' },
{ text: 'Apache (ModSecurity)', link: '/apache' },
{ text: 'Traefik', link: '/traefik' },
{ text: 'HAProxy', link: '/haproxy' }
]
},
{ text: 'Bad Bots', link: '/badbots' },
{ text: 'API', link: '/api' }
{ text: 'API', link: '/api' },
{ text: 'Releases', link: 'https://github.com/fabriziosalmi/patterns/releases' }
],
sidebar: [
{
text: 'Introduction',
items: [
{ text: 'Overview', link: '/' },
{ text: 'Getting Started', link: '/getting-started' }
]
},
@@ -45,10 +85,10 @@ export default defineConfig({
]
},
{
text: 'Features',
text: 'Reference',
items: [
{ text: 'Bad Bot Detection', link: '/badbots' },
{ text: 'API Reference', link: '/api' }
{ text: 'API & Scripts', link: '/api' }
]
}
],
@@ -59,16 +99,21 @@ export default defineConfig({
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2024-present Fabrizio Salmi'
copyright: `Copyright © 2024${new Date().getFullYear()} Fabrizio Salmi`
},
search: {
provider: 'local'
},
search: { provider: 'local' },
editLink: {
pattern: 'https://github.com/fabriziosalmi/patterns/edit/main/docs/:path',
text: 'Edit this page on GitHub'
}
},
outline: { level: [2, 3], label: 'On this page' },
docFooter: { prev: 'Previous', next: 'Next' }
}
})
// Re-export icons so the home page (index.md) can pull them via a JS hash, if needed.
export { icons }

View File

@@ -0,0 +1,63 @@
<script setup>
import IconNginx from './icons/IconNginx.vue'
import IconApache from './icons/IconApache.vue'
import IconTraefik from './icons/IconTraefik.vue'
import IconHaproxy from './icons/IconHaproxy.vue'
import IconArrow from './icons/IconArrow.vue'
import { withBase } from 'vitepress'
const platforms = [
{ name: 'Nginx', meta: 'map directives + if rules', href: '/nginx', icon: IconNginx },
{ name: 'Apache', meta: 'ModSecurity SecRule directives', href: '/apache', icon: IconApache },
{ name: 'Traefik', meta: 'Middleware TOML configuration', href: '/traefik', icon: IconTraefik },
{ name: 'HAProxy', meta: 'ACL pattern files', href: '/haproxy', icon: IconHaproxy }
]
</script>
<template>
<section class="home-section">
<div class="home-eyebrow">Integrations</div>
<h2 class="home-title">Four web servers, one source of truth</h2>
<p class="home-lede">
The same OWASP CRS rule set is converted into the native syntax of each platform &mdash;
so you get equivalent protection whether you run Nginx, Apache, Traefik, or HAProxy.
</p>
<div class="platform-grid">
<a
v-for="p in platforms"
:key="p.name"
:href="withBase(p.href)"
class="platform-card"
>
<span class="platform-icon">
<component :is="p.icon" />
</span>
<div class="platform-name">{{ p.name }}</div>
<div class="platform-meta">{{ p.meta }}</div>
<span class="platform-arrow"><IconArrow /></span>
</a>
</div>
</section>
<section class="home-section">
<div class="stats-strip">
<div class="stat-item">
<div class="stat-value">600+</div>
<div class="stat-label">OWASP CRS patterns extracted</div>
</div>
<div class="stat-item">
<div class="stat-value">Daily</div>
<div class="stat-label">Automated rule refresh</div>
</div>
<div class="stat-item">
<div class="stat-value">4</div>
<div class="stat-label">Web server backends</div>
</div>
<div class="stat-item">
<div class="stat-value">MIT</div>
<div class="stat-label">Open-source license</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5.5 20c1-4 3-9 6.5-13.5 1.5 1.5 2.5 3.5 3 5.5" />
<path d="M9 14c2-3 5-5 9-5" />
<path d="M11 17c2-2 5-3 8-3" />
<path d="M5.5 20h2" />
</svg>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14" />
<path d="m13 6 6 6-6 6" />
</svg>
</template>

View File

@@ -0,0 +1,10 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="4" y="7.5" width="16" height="11" rx="3" />
<path d="M12 4v3.5" />
<circle cx="12" cy="3.5" r="0.9" fill="currentColor" stroke="none" />
<circle cx="9" cy="13" r="1.1" fill="currentColor" stroke="none" />
<circle cx="15" cy="13" r="1.1" fill="currentColor" stroke="none" />
<path d="M2 13.5v2M22 13.5v2" />
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.09.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.36-3.37-1.36-.45-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.05 1.53 1.05.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.36-2.22-.26-4.55-1.13-4.55-5.04 0-1.11.39-2.02 1.03-2.74-.1-.26-.45-1.3.1-2.71 0 0 .84-.27 2.75 1.05A9.34 9.34 0 0 1 12 6.84c.85.004 1.71.118 2.51.34 1.91-1.32 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.74 0 3.92-2.34 4.78-4.57 5.03.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.81 0 .27.18.59.69.49C19.13 20.61 22 16.78 22 12.25 22 6.58 17.52 2 12 2Z"/>
</svg>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="5" cy="12" r="2" />
<circle cx="19" cy="6" r="2" />
<circle cx="19" cy="18" r="2" />
<path d="M7 11 17 7" />
<path d="M7 13l10 4" />
</svg>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 2.5 3.5 7v10L12 21.5 20.5 17V7L12 2.5Z" />
<path d="M9 16V8l6 8V8" />
</svg>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" />
<path d="m3.3 8 8.7 5 8.7-5" />
<path d="M12 13v8" />
<path d="m7.5 5.5 9 5" />
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 4.5a2 2 0 1 0-4 0V6H6a1.5 1.5 0 0 0-1.5 1.5V11h1.5a2 2 0 1 1 0 4H4.5v3.5A1.5 1.5 0 0 0 6 20h3.5v-1.5a2 2 0 1 1 4 0V20H17a1.5 1.5 0 0 0 1.5-1.5V15H20a2 2 0 1 0 0-4h-1.5V7.5A1.5 1.5 0 0 0 17 6h-3V4.5Z" />
</svg>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3.5 12a8.5 8.5 0 0 1 14.5-6L20 8" />
<path d="M20 3v5h-5" />
<path d="M20.5 12a8.5 8.5 0 0 1-14.5 6L4 16" />
<path d="M4 21v-5h5" />
</svg>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3.5" y="4" width="17" height="6" rx="1.5" />
<rect x="3.5" y="14" width="17" height="6" rx="1.5" />
<path d="M7 7h.01M7 17h.01" />
<path d="M11 7h6M11 17h6" />
</svg>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 3 4.5 5.8v6.1c0 4.7 3.3 9.1 7.5 10.4 4.2-1.3 7.5-5.7 7.5-10.4V5.8L12 3Z" />
<path d="m8.5 12 2.5 2.5L15.5 9.5" />
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 12h4l2-3 3 6 3-9 2 6h4" />
</svg>
</template>

View File

@@ -0,0 +1,14 @@
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import HomeShowcase from './components/HomeShowcase.vue'
import './style.css'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
'home-features-after': () => h(HomeShowcase)
})
}
} satisfies Theme

View File

@@ -0,0 +1,621 @@
/* =============================================================
Patterns Docs — Apple-native theme
System fonts, refined typography, soft surfaces, glass nav.
============================================================= */
:root {
/* Typography — system stack, identical to Apple's web properties */
--vp-font-family-base:
-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
"Helvetica Neue", "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
--vp-font-family-mono:
ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono",
Menlo, Consolas, "Liberation Mono", monospace;
/* Brand — Apple-system blue, refined */
--apple-blue: #0071e3;
--apple-blue-hover: #0077ed;
--apple-blue-active: #006edb;
--apple-cyan: #5ac8fa;
/* Light surfaces */
--vp-c-bg: #ffffff;
--vp-c-bg-alt: #fbfbfd;
--vp-c-bg-soft: #f5f5f7;
--vp-c-bg-elv: #ffffff;
/* Borders — almost invisible, like Apple */
--vp-c-divider: rgba(0, 0, 0, 0.07);
--vp-c-border: rgba(0, 0, 0, 0.09);
--vp-c-gutter: rgba(0, 0, 0, 0.06);
/* Text — Apple uses near-black, high contrast */
--vp-c-text-1: #1d1d1f;
--vp-c-text-2: #515154;
--vp-c-text-3: #86868b;
/* Brand color slots used by VitePress */
--vp-c-brand-1: var(--apple-blue);
--vp-c-brand-2: var(--apple-blue-hover);
--vp-c-brand-3: var(--apple-blue-active);
--vp-c-brand-soft: rgba(0, 113, 227, 0.10);
/* Tip / warning / etc — calmer than defaults */
--vp-c-tip-1: #0071e3;
--vp-c-tip-soft: rgba(0, 113, 227, 0.08);
--vp-c-warning-1: #b25000;
--vp-c-warning-soft: rgba(255, 159, 10, 0.10);
--vp-c-danger-1: #c53030;
--vp-c-danger-soft: rgba(255, 59, 48, 0.08);
/* Layout rhythm */
--vp-layout-max-width: 1440px;
--vp-nav-height: 60px;
/* Radii — Apple loves soft, generous corners */
--radius-xs: 6px;
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 20px;
--radius-xl: 28px;
/* Elevation — soft, low-spread shadows */
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.05);
--shadow-2: 0 4px 16px -4px rgba(0, 0, 0, 0.08), 0 2px 6px -2px rgba(0, 0, 0, 0.04);
--shadow-3: 0 16px 40px -12px rgba(0, 0, 0, 0.16), 0 4px 12px -4px rgba(0, 0, 0, 0.06);
/* Buttons / brand surface */
--vp-button-brand-border: transparent;
--vp-button-brand-text: #fff;
--vp-button-brand-bg: var(--apple-blue);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: #fff;
--vp-button-brand-hover-bg: var(--apple-blue-hover);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: #fff;
--vp-button-brand-active-bg: var(--apple-blue-active);
--vp-button-alt-border: transparent;
--vp-button-alt-text: #1d1d1f;
--vp-button-alt-bg: rgba(0, 0, 0, 0.06);
--vp-button-alt-hover-bg: rgba(0, 0, 0, 0.09);
--vp-button-alt-active-bg: rgba(0, 0, 0, 0.12);
/* Code blocks — soft surface in light, deep in dark */
--vp-code-block-bg: #f5f5f7;
--vp-code-block-color: #1d1d1f;
--vp-code-line-highlight-color: rgba(0, 0, 0, 0.05);
--vp-code-tab-bg: #f5f5f7;
--vp-code-tab-divider: var(--vp-c-divider);
}
.dark {
--vp-c-bg: #000000;
--vp-c-bg-alt: #0a0a0c;
--vp-c-bg-soft: #161618;
--vp-c-bg-elv: #1c1c1e;
--vp-c-divider: rgba(255, 255, 255, 0.08);
--vp-c-border: rgba(255, 255, 255, 0.10);
--vp-c-gutter: rgba(255, 255, 255, 0.06);
--vp-c-text-1: #f5f5f7;
--vp-c-text-2: #a1a1a6;
--vp-c-text-3: #6e6e73;
--apple-blue: #0a84ff;
--apple-blue-hover: #409cff;
--apple-blue-active: #0a84ff;
--vp-c-brand-1: var(--apple-blue);
--vp-c-brand-2: var(--apple-blue-hover);
--vp-c-brand-3: var(--apple-blue-active);
--vp-c-brand-soft: rgba(10, 132, 255, 0.16);
--vp-c-tip-1: #0a84ff;
--vp-c-tip-soft: rgba(10, 132, 255, 0.12);
--vp-c-warning-1: #ffb340;
--vp-c-warning-soft: rgba(255, 159, 10, 0.14);
--vp-c-danger-1: #ff6961;
--vp-c-danger-soft: rgba(255, 69, 58, 0.14);
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.5);
--shadow-2: 0 4px 16px -4px rgba(0, 0, 0, 0.6), 0 2px 6px -2px rgba(0, 0, 0, 0.4);
--shadow-3: 0 16px 40px -12px rgba(0, 0, 0, 0.7), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--vp-button-alt-text: #f5f5f7;
--vp-button-alt-bg: rgba(255, 255, 255, 0.10);
--vp-button-alt-hover-bg: rgba(255, 255, 255, 0.14);
--vp-button-alt-active-bg: rgba(255, 255, 255, 0.18);
--vp-code-block-bg: #161618;
--vp-code-block-color: #f5f5f7;
--vp-code-line-highlight-color: rgba(255, 255, 255, 0.06);
--vp-code-tab-bg: #161618;
--vp-code-tab-divider: var(--vp-c-divider);
}
/* ---------- Global typography ---------- */
html {
font-feature-settings: "kern", "liga", "calt", "ss01";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--vp-font-family-base);
letter-spacing: -0.01em;
}
.VPDoc h1 {
letter-spacing: -0.025em;
font-weight: 700;
font-size: 2.4rem;
line-height: 1.15;
}
.VPDoc h2 {
letter-spacing: -0.02em;
font-weight: 650;
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.VPDoc h3 {
letter-spacing: -0.015em;
font-weight: 600;
margin-top: 2rem;
}
.VPDoc p,
.VPDoc li {
font-size: 1rem;
line-height: 1.7;
color: var(--vp-c-text-1);
}
.VPDoc a:not(.header-anchor) {
text-decoration: none;
font-weight: 500;
color: var(--vp-c-brand-1);
transition: color 0.15s ease;
}
.VPDoc a:not(.header-anchor):hover {
color: var(--vp-c-brand-2);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
/* ---------- Glass navigation ---------- */
.VPNav {
background: transparent !important;
}
.VPNavBar {
background: rgba(255, 255, 255, 0.72) !important;
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
border-bottom: 1px solid var(--vp-c-divider) !important;
}
.dark .VPNavBar {
background: rgba(0, 0, 0, 0.72) !important;
}
.VPNavBar.has-sidebar .content-body {
background: transparent !important;
}
.VPNavBar .divider {
display: none;
}
.VPNavBarTitle .title {
font-weight: 600;
letter-spacing: -0.01em;
border-bottom: none !important;
}
.VPNavBarMenuLink,
.VPNavBarMenuGroup .text {
font-size: 14px !important;
font-weight: 450 !important;
color: var(--vp-c-text-1) !important;
opacity: 0.92;
}
.VPNavBarMenuLink:hover,
.VPNavBarMenuGroup:hover .text {
opacity: 1;
color: var(--vp-c-text-1) !important;
}
/* ---------- Sidebar ---------- */
.VPSidebar {
background: var(--vp-c-bg-alt) !important;
border-right: 1px solid var(--vp-c-divider) !important;
}
.VPSidebarItem.level-0 > .item > .text {
font-weight: 600 !important;
letter-spacing: -0.005em;
color: var(--vp-c-text-1) !important;
}
.VPSidebarItem .link .link-text {
font-weight: 450;
font-size: 14px;
}
.VPSidebarItem.is-active > .item .link-text {
font-weight: 600 !important;
color: var(--vp-c-brand-1) !important;
}
/* ---------- Buttons ---------- */
.VPButton {
border-radius: 980px !important;
padding: 0 22px !important;
height: 44px !important;
line-height: 44px !important;
font-weight: 500 !important;
font-size: 15px !important;
letter-spacing: -0.005em !important;
transition: background-color 0.18s ease, transform 0.12s ease, box-shadow 0.18s ease !important;
}
.VPButton.medium {
height: 40px !important;
line-height: 40px !important;
font-size: 14px !important;
padding: 0 18px !important;
}
.VPButton.brand {
box-shadow: 0 1px 2px rgba(0, 113, 227, 0.18);
}
.VPButton.brand:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0, 113, 227, 0.30);
}
.VPButton.brand:active {
transform: translateY(0);
}
/* ---------- Hero ---------- */
.VPHome .VPHero {
padding: 96px 24px 72px !important;
}
.VPHero .container {
max-width: 1120px !important;
}
.VPHero .name,
.VPHero .text {
font-weight: 700 !important;
letter-spacing: -0.04em !important;
line-height: 1.05 !important;
font-size: clamp(2.6rem, 5.4vw, 4.4rem) !important;
}
.VPHero .name .clip {
background: linear-gradient(120deg, #0a84ff 0%, #5ac8fa 50%, #0071e3 100%) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
-webkit-text-fill-color: transparent !important;
}
.VPHero .tagline {
margin-top: 22px !important;
font-size: 1.25rem !important;
font-weight: 400 !important;
line-height: 1.5 !important;
color: var(--vp-c-text-2) !important;
letter-spacing: -0.005em !important;
max-width: 720px;
}
.VPHero .actions {
margin-top: 36px !important;
gap: 12px !important;
}
.VPHero .image-container {
transform: none !important;
}
.VPHero .image-bg {
display: none !important;
}
.VPHero .image-src {
max-width: 100% !important;
filter: drop-shadow(0 30px 60px rgba(10, 132, 255, 0.18));
}
/* ---------- Features grid (default VitePress homepage) ---------- */
.VPFeatures {
padding: 8px 24px 80px !important;
}
.VPFeatures .container {
max-width: 1120px !important;
}
.VPFeature {
border: 1px solid var(--vp-c-divider) !important;
background: var(--vp-c-bg-elv) !important;
border-radius: var(--radius-lg) !important;
padding: 28px !important;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease !important;
height: 100% !important;
box-shadow: var(--shadow-1);
}
.VPFeature:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-2);
border-color: var(--vp-c-border) !important;
}
.VPFeature .icon {
width: 52px !important;
height: 52px !important;
margin-bottom: 20px !important;
background: var(--vp-c-brand-soft) !important;
border-radius: 14px !important;
font-size: 24px !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
color: var(--vp-c-brand-1);
}
.VPFeature .title {
font-size: 1.05rem !important;
font-weight: 600 !important;
letter-spacing: -0.01em !important;
margin-bottom: 8px !important;
color: var(--vp-c-text-1) !important;
}
.VPFeature .details {
font-size: 0.94rem !important;
line-height: 1.55 !important;
color: var(--vp-c-text-2) !important;
}
/* ---------- Custom blocks (tip / warning) ---------- */
.custom-block {
border-radius: var(--radius-md) !important;
border: 1px solid var(--vp-c-divider) !important;
padding: 16px 18px !important;
}
.custom-block.tip {
border-color: var(--vp-c-brand-soft) !important;
background: var(--vp-c-tip-soft) !important;
}
.custom-block.warning {
background: var(--vp-c-warning-soft) !important;
border-color: rgba(255, 159, 10, 0.18) !important;
}
.custom-block .custom-block-title {
font-weight: 600 !important;
letter-spacing: -0.005em !important;
}
/* ---------- Code blocks ---------- */
.vp-code-group .tabs {
border-radius: var(--radius-md) var(--radius-md) 0 0 !important;
}
div[class*="language-"] {
border-radius: var(--radius-md) !important;
box-shadow: var(--shadow-1);
}
div[class*="language-"] code {
font-feature-settings: "calt" 1, "liga" 0 !important;
font-size: 13.5px !important;
letter-spacing: -0.005em;
}
/* Inline code */
:not(pre) > code {
background: var(--vp-c-bg-soft) !important;
border: 1px solid var(--vp-c-divider) !important;
border-radius: 6px !important;
padding: 2px 6px !important;
font-size: 0.88em !important;
color: var(--vp-c-text-1) !important;
}
/* ---------- Tables ---------- */
.vp-doc table {
border-collapse: separate !important;
border-spacing: 0 !important;
border: 1px solid var(--vp-c-divider) !important;
border-radius: var(--radius-md) !important;
overflow: hidden !important;
display: table !important;
width: 100% !important;
margin: 24px 0 !important;
}
.vp-doc th {
background: var(--vp-c-bg-soft) !important;
font-weight: 600 !important;
letter-spacing: -0.005em !important;
font-size: 0.9rem !important;
text-align: left !important;
}
.vp-doc th,
.vp-doc td {
border: none !important;
border-bottom: 1px solid var(--vp-c-divider) !important;
padding: 12px 16px !important;
}
.vp-doc tr:last-child td {
border-bottom: none !important;
}
.vp-doc tr {
background: transparent !important;
}
/* ---------- Footer ---------- */
.VPFooter {
border-top: 1px solid var(--vp-c-divider) !important;
padding: 28px 24px !important;
background: var(--vp-c-bg-alt) !important;
}
.VPFooter .message,
.VPFooter .copyright {
font-size: 13px !important;
color: var(--vp-c-text-3) !important;
}
/* ---------- Search input ---------- */
.DocSearch-Button {
border-radius: 980px !important;
background: var(--vp-c-bg-soft) !important;
border: 1px solid var(--vp-c-divider) !important;
box-shadow: none !important;
height: 36px !important;
}
/* ---------- Subtle ambient gradient on home ---------- */
.VPHome::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 720px;
background:
radial-gradient(60% 60% at 50% 0%, rgba(10, 132, 255, 0.10) 0%, transparent 70%),
radial-gradient(40% 40% at 80% 10%, rgba(90, 200, 250, 0.08) 0%, transparent 70%);
pointer-events: none;
z-index: -1;
}
/* ---------- Custom home sections (used by HomeShowcase.vue) ---------- */
.home-section {
max-width: 1120px;
margin: 0 auto;
padding: 0 24px 96px;
}
.home-section + .home-section {
padding-top: 0;
}
.home-eyebrow {
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--vp-c-brand-1);
margin-bottom: 12px;
}
.home-title {
font-size: clamp(1.75rem, 3.2vw, 2.4rem);
font-weight: 650;
letter-spacing: -0.025em;
line-height: 1.15;
margin: 0 0 12px;
color: var(--vp-c-text-1);
}
.home-lede {
font-size: 1.1rem;
line-height: 1.55;
color: var(--vp-c-text-2);
max-width: 720px;
margin: 0 0 40px;
}
.platform-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.platform-card {
position: relative;
display: block;
padding: 24px;
border-radius: var(--radius-lg);
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-elv);
text-decoration: none;
color: inherit;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.platform-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-2);
border-color: var(--vp-c-border);
text-decoration: none !important;
}
.platform-card .platform-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.platform-card .platform-icon svg {
width: 24px;
height: 24px;
}
.platform-card .platform-name {
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.01em;
margin-bottom: 4px;
}
.platform-card .platform-meta {
font-size: 0.88rem;
color: var(--vp-c-text-3);
margin-bottom: 14px;
}
.platform-card .platform-arrow {
position: absolute;
top: 24px;
right: 24px;
color: var(--vp-c-text-3);
opacity: 0;
transform: translateX(-4px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.platform-card:hover .platform-arrow {
opacity: 1;
transform: translateX(0);
color: var(--vp-c-brand-1);
}
.platform-card .platform-arrow svg {
width: 18px;
height: 18px;
}
/* ---------- Stat strip ---------- */
.stats-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 24px;
padding: 28px 32px;
border-radius: var(--radius-lg);
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
}
.stat-item {
text-align: left;
}
.stat-value {
font-size: 2rem;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--vp-c-text-1);
line-height: 1.1;
}
.stat-label {
font-size: 0.88rem;
color: var(--vp-c-text-3);
margin-top: 4px;
}
/* ---------- Reduce motion respect ---------- */
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0s !important;
animation-duration: 0s !important;
}
}

View File

@@ -1,93 +1,100 @@
# Apache Integration
This guide explains how to integrate the WAF patterns with Apache using ModSecurity.
This guide explains how to deploy the generated rules in Apache HTTPD using the **ModSecurity** engine.
## Prerequisites
- Apache 2.4+
- ModSecurity module installed
### Install ModSecurity
- Apache HTTPD **2.4+**
- The **ModSecurity** module installed and enabled
::: code-group
```bash [Debian/Ubuntu]
```bash [Debian / Ubuntu]
sudo apt install libapache2-mod-security2
sudo a2enmod security2
```
```bash [RHEL/CentOS]
sudo yum install mod_security
```bash [RHEL / CentOS / Rocky]
sudo dnf install mod_security
```
```bash [Alpine]
sudo apk add mod_security
```
:::
## Quick Start
## Quick start
1. Download `apache_waf.zip` from [Releases](https://github.com/fabriziosalmi/patterns/releases)
2. Extract to your Apache configuration directory
3. Include the files in your Apache configuration
1. Download `apache_waf.zip` from the [latest release](https://github.com/fabriziosalmi/patterns/releases/latest).
2. Extract under your Apache config tree (e.g. `/etc/apache2/waf_patterns/apache/`).
3. Include the `.conf` files from the relevant virtual host or globally.
## Configuration Files
## Files in the archive
The Apache WAF package includes ModSecurity rules organized by attack type:
The Apache output is split by attack family, each containing standard ModSecurity `SecRule` directives.
| File | Protection Type |
|------|-----------------|
| `sqli.conf` | SQL Injection |
| `xss.conf` | Cross-Site Scripting |
| `rce.conf` | Remote Code Execution |
| `lfi.conf` | Local File Inclusion |
| `rfi.conf` | Remote File Inclusion |
| `bots.conf` | Bad Bot Detection |
| File | Protection |
|------|------------|
| `sqli.conf` | SQL injection |
| `xss.conf` | Cross-site scripting |
| `rce.conf` | Remote code execution |
| `lfi.conf` | Local file inclusion |
| `rfi.conf` | Remote file inclusion |
| `php.conf`, `java.conf`, `iis.conf`, `shells.conf` | Stack-specific exploits |
| `attack.conf`, `generic.conf`, `correlation.conf`, `evaluation.conf` | Generic anomaly detection |
| `bots.conf` | Bad-bot User-Agent rules |
## Integration
## Step 1 &mdash; Enable the engine
### Step 1: Enable ModSecurity
Create or edit `/etc/apache2/mods-enabled/security2.conf`:
In `/etc/apache2/mods-enabled/security2.conf` (or equivalent):
```apache
<IfModule security2_module>
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecDebugLogLevel 0
SecAuditEngine RelevantOnly
SecAuditLog /var/log/apache2/modsec_audit.log
SecAuditLogParts ABCDEFHZ
</IfModule>
```
### Step 2: Include WAF Rules
::: tip Run in detection mode first
Set `SecRuleEngine DetectionOnly` for the first deployment. Watch the audit log, tune false positives, then flip to `On`.
:::
Add to your Apache configuration or virtual host:
## Step 2 &mdash; Include the rules
Either include all files in one go:
```apache
<VirtualHost *:80>
<VirtualHost *:443>
ServerName example.com
# Include all WAF patterns
Include /path/to/waf_patterns/apache/*.conf
# ... other configurations ...
Include /etc/apache2/waf_patterns/apache/*.conf
# …other directives
</VirtualHost>
```
Or include specific rule sets:
…or pick the categories you want:
```apache
Include /path/to/waf_patterns/apache/sqli.conf
Include /path/to/waf_patterns/apache/xss.conf
Include /path/to/waf_patterns/apache/bots.conf
Include /etc/apache2/waf_patterns/apache/sqli.conf
Include /etc/apache2/waf_patterns/apache/xss.conf
Include /etc/apache2/waf_patterns/apache/rce.conf
Include /etc/apache2/waf_patterns/apache/bots.conf
```
### Step 3: Restart Apache
## Step 3 &mdash; Validate and restart
```bash
sudo apachectl configtest && sudo systemctl restart apache2
```
## Rule Format
## Rule format
The rules follow ModSecurity syntax:
Generated rules follow the standard ModSecurity DSL:
```apache
SecRule REQUEST_URI "@rx union.*select" \
@@ -95,68 +102,50 @@ SecRule REQUEST_URI "@rx union.*select" \
phase:2,\
deny,\
status:403,\
log,\
msg:'SQL Injection Attempt',\
severity:CRITICAL"
```
## Customization
### Adjust Severity Levels
### Detection-only mode
Modify the action from `deny` to `log` for monitoring mode:
Switch a noisy rule from blocking to logging without removing it:
```apache
SecRule REQUEST_URI "@rx pattern" \
"id:100001,\
phase:2,\
log,\
pass,\
msg:'Potential attack detected'"
SecRuleUpdateActionById 100001 "pass,log,msg:'SQLi candidate (audit only)'"
```
### Whitelist Paths
Add exceptions for specific paths:
### Whitelist a path
```apache
SecRule REQUEST_URI "@beginsWith /api/webhook" \
"id:1,\
phase:1,\
allow,\
nolog"
"id:1,phase:1,nolog,allow"
```
## Logging
ModSecurity logs are typically found at:
- `/var/log/apache2/modsec_audit.log`
- `/var/log/httpd/modsec_audit.log`
Enable detailed logging:
### Disable a single rule
```apache
SecAuditEngine RelevantOnly
SecAuditLog /var/log/apache2/modsec_audit.log
SecAuditLogParts ABCDEFHZ
SecRuleRemoveById 100001
```
## Logs
ModSecurity logs land in:
- `/var/log/apache2/modsec_audit.log` &mdash; full audit trail
- `/var/log/apache2/error.log` &mdash; rule matches and engine messages
## Testing
```bash
# Test SQL injection detection
curl -I "http://example.com/?id=1' UNION SELECT * FROM users--"
# Check Apache error log
curl -I "https://example.com/?id=1' UNION SELECT * FROM users--"
sudo tail -f /var/log/apache2/error.log
```
## Troubleshooting
### ModSecurity not loading
Ensure the module is enabled: `sudo a2enmod security2`
### Rules not triggering
Check that `SecRuleEngine` is set to `On` and rules are being included.
### Performance issues
Consider using `SecRuleRemoveById` to disable noisy rules that cause false positives.
- **Module not loading** &mdash; confirm with `apachectl -M | grep security2`. Re-enable with `sudo a2enmod security2`.
- **No rules triggering** &mdash; double-check `SecRuleEngine On` and that the include path resolves; `apachectl -S` lists the parsed config.
- **Performance regressions** &mdash; identify hot rules in the audit log and disable or scope them with `SecRuleRemoveById` / `SecRule … chain`.

View File

@@ -1,223 +1,236 @@
# API Reference
# API & Scripts Reference
This page documents the Python scripts that power the Patterns project.
Patterns is a small Python toolchain. Every script does one job and communicates with the rest through plain JSON or files on disk &mdash; no shared state, no daemon, no database.
## Core Scripts
### owasp2json.py
Fetches and parses OWASP Core Rule Set patterns from GitHub.
```bash
python owasp2json.py
```text
owasp2json.py ──▶ owasp_rules.json ──▶ json2{nginx,apache,traefik,haproxy}.py
└▶ badbots.py (independent)
```
**Output**: `owasp_rules.json`
All scripts are configured through **environment variables** (not CLI flags) except `owasp2json.py`, which has a small `argparse` interface.
**Configuration**:
- Uses environment variable `OWASP_REPO` to specify source repository
- Default: `coreruleset/coreruleset`
## Pipeline scripts
**Features**:
- Fetches latest CRS rules from GitHub
- Parses `.conf` files for regex patterns
- Extracts rule metadata (ID, severity, category)
- Outputs structured JSON for conversion scripts
### `owasp2json.py`
Fetches the OWASP Core Rule Set from GitHub and emits a flat JSON rule list.
```bash
python owasp2json.py --ref v4.0 --output owasp_rules.json
```
| Argument / env | Default | Purpose |
|----------------|---------|---------|
| `--output` | `owasp_rules.json` | Output JSON path |
| `--ref` | `v4.0` | Tag prefix to resolve (e.g. `v4.0`, `v3.3`, `dev`) |
| `--dry-run` | off | Fetch and parse without writing |
| `GITHUB_TOKEN` (env) | unset | Raises the GitHub API rate limit while iterating |
The script verifies each blob's SHA against the GitHub-reported value before parsing it.
---
### json2nginx.py
### `json2nginx.py`
Converts OWASP JSON rules to Nginx WAF configuration.
Converts `owasp_rules.json` into Nginx `map`-based rules.
```bash
python json2nginx.py
INPUT_FILE=custom.json OUTPUT_DIR=/tmp/out python json2nginx.py
```
**Input**: `owasp_rules.json`
**Output**: `waf_patterns/nginx/`
**Generated files** (in `OUTPUT_DIR`):
**Generated Files**:
| File | Purpose |
|------|---------|
| `waf_maps.conf` | Map directives (http block) |
| `waf_rules.conf` | If statements (server block) |
| `README.md` | Integration instructions |
| `waf_maps.conf` | `map` directives &mdash; include in the `http` block |
| `waf_rules.conf` | `if` rules &mdash; include in the `server` block |
| `<category>.conf` | One file per OWASP category, **for inspection only** |
| `README.md` | In-tree usage notes |
**Environment Variables**:
- `INPUT_FILE` - Path to OWASP JSON (default: `owasp_rules.json`)
- `OUTPUT_DIR` - Output directory (default: `waf_patterns/nginx`)
| Env var | Default |
|---------|---------|
| `INPUT_FILE` | `owasp_rules.json` |
| `OUTPUT_DIR` | `waf_patterns/nginx` |
---
### json2apache.py
### `json2apache.py`
Converts OWASP JSON rules to Apache ModSecurity format.
Converts `owasp_rules.json` into ModSecurity `SecRule` directives, partitioned by attack category.
```bash
python json2apache.py
```
**Input**: `owasp_rules.json`
**Output**: `waf_patterns/apache/`
**Generated files**: one `<category>.conf` per OWASP category (`sqli.conf`, `xss.conf`, `rce.conf`, `lfi.conf`, …) &mdash; each contains pure ModSecurity rules ready to `Include`.
**Generated Files**:
- Category-specific `.conf` files (sqli.conf, xss.conf, etc.)
- Each file contains ModSecurity `SecRule` directives
| Env var | Default |
|---------|---------|
| `INPUT_FILE` | `owasp_rules.json` |
| `OUTPUT_DIR` | `waf_patterns/apache` |
---
### json2traefik.py
### `json2traefik.py`
Converts OWASP JSON rules to Traefik middleware configuration.
Converts `owasp_rules.json` into a Traefik file-provider middleware.
```bash
python json2traefik.py
```
**Input**: `owasp_rules.json`
**Output**: `waf_patterns/traefik/`
**Generated files**:
**Generated Files**:
- `middleware.toml` - Traefik middleware configuration
- `README.md` - Integration instructions
- `middleware.toml` &mdash; complete WAF middleware definition
- `README.md` &mdash; in-tree integration notes
| Env var | Default |
|---------|---------|
| `INPUT_FILE` | `owasp_rules.json` |
| `OUTPUT_DIR` | `waf_patterns/traefik` |
---
### json2haproxy.py
### `json2haproxy.py`
Converts OWASP JSON rules to HAProxy ACL format.
Converts `owasp_rules.json` into HAProxy ACL files.
```bash
python json2haproxy.py
```
**Input**: `owasp_rules.json`
**Output**: `waf_patterns/haproxy/`
**Generated files**:
**Generated Files**:
- `waf.acl` - Main WAF ACL rules
- `README.md` - Integration instructions
- `waf.acl` &mdash; one regex per line, designed for `-f /etc/haproxy/waf.acl`
- `README.md` &mdash; in-tree integration notes
| Env var | Default |
|---------|---------|
| `INPUT_FILE` | `owasp_rules.json` |
| `OUTPUT_DIR` | `waf_patterns/haproxy/` |
---
### badbots.py
### `badbots.py`
Generates bad bot blocking configurations from public bot lists.
Independently fetches public bad-bot User-Agent lists and emits a `bots.*` file in each platform output directory.
```bash
python badbots.py
```
**Output**: Bot configurations in each `waf_patterns/*/` directory
**Generated files** (per platform):
**Features**:
- Fetches from multiple public bot lists
- Includes fallback sources for reliability
- Generates platform-specific configs
| Platform | File |
|----------|------|
| Nginx | `waf_patterns/nginx/bots.conf` |
| Apache | `waf_patterns/apache/bots.conf` |
| Traefik | `waf_patterns/traefik/bots.toml` |
| HAProxy | `waf_patterns/haproxy/bots.acl` |
---
| Env var | Purpose |
|---------|---------|
| `GITHUB_TOKEN` | Raises the GitHub API rate limit when fetching upstream lists |
## Import Scripts
If a remote source is unreachable, the script falls back to a bundled list.
These scripts help import existing WAF configurations.
## Import / install scripts
### import_nginx_waf.py
The `import_*.py` scripts copy generated files into a server's runtime configuration directory and (optionally) splice an `Include` line into the main config. They are configured **entirely** through environment variables.
Import Nginx WAF patterns from external sources.
### `import_nginx_waf.py`
```bash
python import_nginx_waf.py --source /path/to/external/rules
```
| Env var | Default |
|---------|---------|
| `WAF_DIR` | `waf_patterns/nginx` |
| `NGINX_WAF_DIR` | `/etc/nginx/waf/` |
| `NGINX_CONF` | `/etc/nginx/nginx.conf` |
| `BACKUP_DIR` | `/etc/nginx/waf_backup/` |
### import_apache_waf.py
### `import_apache_waf.py`
Import Apache ModSecurity rules.
| Env var | Default |
|---------|---------|
| `WAF_DIR` | `waf_patterns/apache` |
| `APACHE_WAF_DIR` | `/etc/modsecurity.d/` |
| `APACHE_CONF` | `/etc/apache2/apache2.conf` |
| `BACKUP_DIR` | `/etc/modsecurity.d/backup` |
```bash
python import_apache_waf.py --source /path/to/modsec/rules
```
### `import_traefik_waf.py`
### import_traefik_waf.py
| Env var | Default |
|---------|---------|
| `WAF_DIR` | `waf_patterns/traefik` |
| `TRAEFIK_WAF_DIR` | `/etc/traefik/waf/` |
| `TRAEFIK_DYNAMIC_CONF` | `/etc/traefik/dynamic.toml` |
| `BACKUP_DIR` | `/etc/traefik/waf_backup/` |
Import Traefik middleware configurations.
### `import_haproxy_waf.py`
```bash
python import_traefik_waf.py --source /path/to/traefik/config
```
| Env var | Default |
|---------|---------|
| `WAF_DIR` | `waf_patterns/haproxy` |
| `HAPROXY_WAF_DIR` | `/etc/haproxy/waf/` |
| `HAPROXY_CONF` | `/etc/haproxy/haproxy.cfg` |
| `BACKUP_DIR` | `/etc/haproxy/waf_backup/` |
### import_haproxy_waf.py
::: warning Privileged paths
The defaults point at system directories (`/etc/...`). Run the import scripts as root, or override every env var to point at a sandbox before running them.
:::
Import HAProxy ACL rules.
## Data format
```bash
python import_haproxy_waf.py --source /path/to/haproxy/acl
```
### `owasp_rules.json`
---
## Data Structures
### owasp_rules.json Format
A flat JSON array. Each item is a single rule with two required fields:
```json
[
{
"id": "942100",
"pattern": "(?i:union.*select)",
"category": "sqli",
"severity": "critical",
"location": "request-uri",
"description": "SQL Injection Attack Detected"
"category": "SQLI",
"pattern": "(?i:union[\\s\\S]+select)"
},
{
"category": "XSS",
"pattern": "(?i:<script[^>]*>)"
}
]
```
**Fields**:
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | OWASP CRS rule ID |
| `pattern` | string | Regex pattern |
| `category` | string | Attack category (sqli, xss, rce, etc.) |
| `severity` | string | critical, high, medium, low |
| `location` | string | Where to match (request-uri, headers, etc.) |
| `description` | string | Human-readable description |
| `category` | string | OWASP CRS category derived from the source filename (e.g. `SQLI`, `XSS`, `RCE`, `LFI`, `RFI`, `BOTS`) |
| `pattern` | string | Regex extracted from the matching `SecRule` directive |
---
The converters validate each pattern with Python's `re.compile` before emitting platform-specific output, so malformed regexes are dropped rather than propagated.
## Extending the Project
## Extending the toolchain
### Adding a New Platform
### Adding a new platform
1. Create `json2<platform>.py` based on existing converters
2. Add output directory in `waf_patterns/<platform>/`
3. Update GitHub Actions workflow
4. Add documentation in `docs/`
1. Copy one of the existing `json2<platform>.py` converters as a starting point.
2. Implement `_sanitize_pattern()` for the target syntax (escape rules differ between Nginx, Apache, HAProxy, …).
3. Emit your output under `waf_patterns/<platform>/`.
4. Add a workflow step in `.github/workflows/update_patterns.yml` to package the result.
5. Add a documentation page under `docs/`.
### Custom Pattern Sources
### Pinning a different OWASP CRS version
Modify `owasp2json.py` to add new pattern sources:
```python
SOURCES = [
"coreruleset/coreruleset",
"your-org/your-rules",
]
```bash
python owasp2json.py --ref v3.3
```
---
### Pulling rules from a fork
`owasp2json.py` hardcodes the upstream repository constant `coreruleset/coreruleset`. To target a fork, edit `GITHUB_REPO_URL` near the top of the script.
## Dependencies
Listed in `requirements.txt`:
```
requests>=2.28.0
beautifulsoup4>=4.11.0
```
Install with:
Listed in [`requirements.txt`](https://github.com/fabriziosalmi/patterns/blob/main/requirements.txt). Install with:
```bash
pip install -r requirements.txt
```
The pipeline targets Python **3.11+**.

View File

@@ -1,191 +1,172 @@
# Bad Bot Detection
This guide explains how to use the bad bot detection feature to block malicious crawlers and scrapers.
`badbots.py` generates per-platform User-Agent blocklists alongside the OWASP rules, so you can drop noisy crawlers, AI scrapers, and known abusive scanners in a single include.
## Overview
## How it works
The `badbots.py` script generates configuration files to block known malicious bots based on their User-Agent strings. It fetches bot lists from multiple public sources and generates blocking rules for each supported web server.
1. The script fetches public bot lists &mdash; including [`ai.robots.txt`](https://github.com/ai-robots-txt/ai.robots.txt) and other community-curated sources.
2. It deduplicates and normalizes the User-Agent patterns.
3. It emits one file per platform under `waf_patterns/<platform>/`.
4. The daily GitHub Actions workflow regenerates and republishes these files alongside the OWASP-derived rules.
## How It Works
If a primary source is unreachable, the script falls back to a bundled list so the build still succeeds.
1. Fetches bot lists from public sources:
- [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt)
- Various community-maintained bot lists
2. Generates blocking configurations for each platform
3. Updates configurations daily via GitHub Actions
## Generated Files
## Generated files
| Platform | File | Format |
|----------|------|--------|
| Nginx | `bots.conf` | Map directive |
| Apache | `bots.conf` | ModSecurity rules |
| Traefik | `bots.toml` | Middleware config |
| HAProxy | `bots.acl` | ACL patterns |
| Nginx | `bots.conf` | `map $http_user_agent $bad_bot` |
| Apache | `bots.conf` | ModSecurity `SecRule` directives |
| Traefik | `bots.toml` | Middleware regex replacements |
| HAProxy | `bots.acl` | One regex per line, loadable with `-f` |
## Nginx Bot Blocker
The Nginx configuration uses a map directive:
## Nginx
```nginx
# In the http block:
include /etc/nginx/waf_patterns/nginx/bots.conf;
# In any server block you want to protect:
server {
if ($bad_bot) { return 403; }
}
```
The map looks like:
```nginx
# In http block
map $http_user_agent $bad_bot {
default 0;
"~*AhrefsBot" 1;
"~*SemrushBot" 1;
"~*MJ12bot" 1;
"~*DotBot" 1;
# ... more bots
}
# In server block
if ($bad_bot) {
return 403;
"~*AhrefsBot" 1;
"~*SemrushBot" 1;
"~*MJ12bot" 1;
"~*GPTBot" 1;
#
}
```
### Integration
```nginx
http {
include /path/to/waf_patterns/nginx/bots.conf;
server {
if ($bad_bot) {
return 403;
}
}
}
```
## Apache Bot Blocker
Uses ModSecurity rules:
## Apache
```apache
SecRule REQUEST_HEADERS:User-Agent "@rx AhrefsBot" \
"id:200001,phase:1,deny,status:403,msg:'Bad Bot Blocked'"
```
## HAProxy Bot Blocker
Include the file globally or per VirtualHost:
Uses ACL rules:
```haproxy
acl bad_bot hdr_reg(User-Agent) -i -f /etc/haproxy/bots.acl
http-request deny if bad_bot
```apache
Include /etc/apache2/waf_patterns/apache/bots.conf
```
## Blocked Bot Categories
## HAProxy
The following categories of bots are blocked by default:
```haproxy
acl bad_bot hdr(User-Agent) -m reg -i -f /etc/haproxy/bots.acl
http-request deny deny_status 403 if bad_bot
```
## Traefik
```toml
[http.middlewares.bot-blocker]
# populated automatically by bots.toml
```
Reference `bot-blocker@file` from the routers you want to protect.
## What gets blocked
The default list groups User-Agent patterns into four broad categories.
### SEO and marketing crawlers
Aggressive site indexers that are usually unwelcome on production traffic:
### SEO/Marketing Crawlers
- AhrefsBot
- SemrushBot
- MJ12bot
- DotBot
- BLEXBot
### AI/ML Crawlers
- GPTBot
- ChatGPT-User
- CCBot
- Google-Extended
- Anthropic-AI
### AI training crawlers
Most are documented at [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt):
- GPTBot, ChatGPT-User
- ClaudeBot, Anthropic-AI
- Google-Extended
- CCBot, Bytespider, PerplexityBot
### General scrapers
### Scrapers
- DataForSeoBot
- PetalBot
- Bytespider
- ClaudeBot
### Malicious Bots
- Known vulnerability scanners
- Spam bots
- Content scrapers
### Malicious scanners
Public vulnerability scanners and spam bots that have no legitimate reason to crawl your origin.
::: tip Search engines are not blocked
Major search engines (Googlebot, Bingbot, DuckDuckBot, Baiduspider, YandexBot) are **not** included in the default block list &mdash; blocking them harms SEO.
:::
## Customization
### Add Custom Bots
Edit the generated file or add your own patterns:
### Add your own pattern
```nginx
# Nginx: Add to bots.conf
# Append in bots.conf
"~*MyCustomBot" 1;
```
```apache
# Apache: Add rule
SecRule REQUEST_HEADERS:User-Agent "@rx MyCustomBot" \
"id:200999,deny"
"id:200999,phase:1,deny,status:403"
```
### Whitelist Bots
### Whitelist a bot
For Nginx, allow specific bots:
For Nginx, override the match before the catch-all:
```nginx
map $http_user_agent $bad_bot {
default 0;
"~*Googlebot" 0; # Allow Google
"~*AhrefsBot" 1; # Block Ahrefs
"~*Googlebot" 0; # explicit allow
"~*AhrefsBot" 1;
}
```
### Allow All Bots for Specific Paths
### Allow bots inside a path
```nginx
location /public-api {
# Override bot blocking
if ($bad_bot) {
# Don't block here
}
location /public-api/ {
# bypass the bot rule for this path
proxy_pass http://upstream;
}
location / {
if ($bad_bot) { return 403; }
proxy_pass http://upstream;
}
```
## Generate Manually
Run the script to regenerate bot lists:
## Regenerating manually
```bash
python badbots.py
```
The script supports fallback lists if primary sources are unavailable.
The generated files end up in `waf_patterns/<platform>/`.
## Monitoring
### Log Blocked Bots
Enable logging to track blocked requests:
```nginx
if ($bad_bot) {
access_log /var/log/nginx/blocked_bots.log;
return 403;
}
```
### Analyze Bot Traffic
Track which patterns actually fire in your traffic:
```bash
# Count blocked bot requests
grep "403" /var/log/nginx/access.log | \
awk '{print $12}' | sort | uniq -c | sort -rn | head -20
# Top 20 user agents that hit a 403
awk '$9 == 403 {print $12}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
```
## Best Practices
1. **Regular Updates**: The bot lists are updated daily. Pull the latest changes or download from releases.
2. **Monitor False Positives**: Some legitimate services may use blocked User-Agents. Monitor your logs.
3. **Combine with Rate Limiting**: Use bot blocking with rate limiting for comprehensive protection.
4. **Test Before Deploying**: Verify that legitimate traffic (search engines, monitoring) is not blocked.
::: warning
Blocking search engine bots (Googlebot, Bingbot) can negatively impact SEO. The default lists do **not** block major search engines.
:::
If you see legitimate traffic in the list, add it to a whitelist and re-include `bots.conf` after your override.

View File

@@ -1,78 +1,94 @@
# Getting Started
This guide will help you get up and running with Patterns WAF configurations for your web server.
This guide walks you through installing **Patterns** and integrating the generated WAF rules into your web server.
## Prerequisites
- **Python 3.11+** (if building from source)
- **pip** (Python package installer)
- **git** (for cloning the repository)
| Requirement | Notes |
|-------------|-------|
| **Python 3.11+** | Only required when building from source. The CI workflow targets 3.11. |
| **pip** | To install the packages listed in `requirements.txt`. |
| **git** | Optional &mdash; only needed if cloning the repository. |
## Installation Options
## Two installation paths
### Option 1: Download Pre-Generated Configurations
### Option 1 &mdash; Download a pre-built release
The easiest way to get started is to download pre-built configurations:
The fastest path. A scheduled GitHub Actions workflow rebuilds every archive daily and publishes them on the [Releases page](https://github.com/fabriziosalmi/patterns/releases/latest).
1. Go to the [Releases](https://github.com/fabriziosalmi/patterns/releases) page
2. Download the ZIP file for your web server:
- `nginx_waf.zip` - Nginx configurations
- `apache_waf.zip` - Apache ModSecurity rules
- `traefik_waf.zip` - Traefik middleware
- `haproxy_waf.zip` - HAProxy ACL files
3. Extract and integrate into your server configuration
| Archive | Contains | Target |
|---------|----------|--------|
| `nginx_waf.zip` | `waf_maps.conf`, `waf_rules.conf`, `bots.conf`, category files | Nginx |
| `apache_waf.zip` | Per-category ModSecurity `.conf` files, `bots.conf` | Apache + mod_security2 |
| `traefik_waf.zip` | `middleware.toml`, `bots.toml` | Traefik (file provider) |
| `haproxy_waf.zip` | `waf.acl`, `bots.acl` | HAProxy |
### Option 2: Build from Source
Pick one, extract, then jump to the matching integration guide.
If you prefer to generate the configurations yourself:
### Option 2 &mdash; Build from source
Choose this path if you want to pin a specific OWASP CRS tag, customize the converter, or run the toolchain in your own CI:
```bash
# Clone the repository
git clone https://github.com/fabriziosalmi/patterns.git
cd patterns
# Install dependencies
pip install -r requirements.txt
# Fetch latest OWASP rules
# 1. Fetch the latest OWASP Core Rule Set into a JSON intermediate
python owasp2json.py
# Generate configurations for your platform
python json2nginx.py # For Nginx
python json2apache.py # For Apache
python json2traefik.py # For Traefik
python json2haproxy.py # For HAProxy
# 2. Convert the JSON into native rules for your platform
python json2nginx.py
python json2apache.py
python json2traefik.py
python json2haproxy.py
# Generate bad bot blockers
# 3. Generate bad-bot blocklists alongside
python badbots.py
```
## Configuration Files
::: tip GitHub API rate limits
`owasp2json.py` reads from the GitHub API. Set `GITHUB_TOKEN` in your environment to raise the rate limit when iterating locally.
:::
After running the scripts, you'll find the generated files in the `waf_patterns/` directory:
## Output layout
```
After running the converters, generated files live under `waf_patterns/`:
```text
waf_patterns/
├── nginx/ # Nginx WAF configs
├── apache/ # Apache ModSecurity rules
├── traefik/ # Traefik middleware configs
└── haproxy/ # HAProxy ACL files
├── nginx/ # waf_maps.conf, waf_rules.conf, bots.conf, per-category files
├── apache/ # sqli.conf, xss.conf, rce.conf, lfi.conf, … bots.conf
├── traefik/ # middleware.toml, bots.toml
└── haproxy/ # waf.acl, bots.acl
```
## Next Steps
## Next steps
Choose your web server to learn how to integrate the WAF configurations:
Choose your platform to wire the rules into a running server:
- [Nginx Integration](/nginx)
- [Apache Integration](/apache)
- [Traefik Integration](/traefik)
- [HAProxy Integration](/haproxy)
- [Nginx integration](/nginx)
- [Apache (ModSecurity) integration](/apache)
- [Traefik integration](/traefik)
- [HAProxy integration](/haproxy)
## Automatic Updates
For details on the bot blocklist itself, see [Bad Bot Detection](/badbots). For a reference of every script and the JSON schema that ties them together, see the [API reference](/api).
The repository includes a GitHub Actions workflow that:
- Fetches the latest OWASP CRS rules **daily**
- Regenerates all WAF configurations
- Creates a new release with updated files
## How updates flow
To get the latest rules, simply download from the [Releases](https://github.com/fabriziosalmi/patterns/releases) page or pull the latest changes if you cloned the repository.
```text
┌─────────────────────┐ daily cron ┌──────────────────────┐
│ coreruleset/ │ ───────────────▶ │ owasp2json.py │
│ coreruleset (GH) │ │ → owasp_rules.json │
└─────────────────────┘ └──────────┬───────────┘
┌─────────────────┬──────────────────┬──────┴──────────┐
▼ ▼ ▼ ▼
json2nginx.py json2apache.py json2traefik.py json2haproxy.py
│ │ │ │
▼ ▼ ▼ ▼
nginx_waf.zip apache_waf.zip traefik_waf.zip haproxy_waf.zip
(published as a GitHub Release)
```
To stay current, either download the latest archive or `git pull` and re-run the converters.

View File

@@ -1,85 +1,64 @@
# HAProxy Integration
This guide explains how to integrate the WAF patterns with HAProxy using ACL rules.
This guide explains how to plug the generated rules into HAProxy using **ACL** files.
## Quick Start
## Quick start
1. Download `haproxy_waf.zip` from [Releases](https://github.com/fabriziosalmi/patterns/releases)
2. Extract the files
3. Include the ACL files in your HAProxy configuration
1. Download `haproxy_waf.zip` from the [latest release](https://github.com/fabriziosalmi/patterns/releases/latest).
2. Drop the ACL files into `/etc/haproxy/` (or any path you prefer).
3. Reference them from a `frontend` block.
4. Reload HAProxy.
## Configuration Files
The HAProxy WAF package includes:
## Files in the archive
| File | Purpose |
|------|---------|
| `waf.acl` | Main WAF ACL rules |
| `bots.acl` | Bad bot detection ACLs |
| `waf.acl` | Pre-compiled regex patterns covering every OWASP CRS category |
| `bots.acl` | Bad-bot User-Agent patterns |
## Integration
## Step 1 &mdash; Reference the ACL files
### Step 1: Include ACL Files
In your `haproxy.cfg`, include the WAF ACL files:
The cleanest approach is to load the patterns from disk with `-f`:
```haproxy
frontend http-in
bind *:80
# Include WAF ACL rules
acl waf_block_sqli path_reg -i union.*select
acl waf_block_sqli path_reg -i insert.*into
acl waf_block_xss path_reg -i <script>
# Or include from external file
# acl waf_patterns path_reg -i -f /etc/haproxy/waf.acl
# Block matching requests
http-request deny if waf_block_sqli
http-request deny if waf_block_xss
bind *:443 ssl crt /etc/haproxy/certs/
acl waf_match path,url_dec -m reg -i -f /etc/haproxy/waf.acl
acl waf_match_q query -m reg -i -f /etc/haproxy/waf.acl
acl bad_bot hdr(User-Agent) -m reg -i -f /etc/haproxy/bots.acl
http-request deny deny_status 403 if waf_match || waf_match_q || bad_bot
default_backend servers
```
### Step 2: Include Bot Blockers
```haproxy
frontend http-in
bind *:80
# Bad bot detection
acl bad_bot hdr_reg(User-Agent) -i -f /etc/haproxy/bots.acl
http-request deny if bad_bot
default_backend servers
```
### Step 3: Reload HAProxy
## Step 2 &mdash; Validate and reload
```bash
haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
```
## ACL Rule Format
## ACL primer
HAProxy ACLs use pattern matching on various request attributes:
HAProxy ACLs match against fetch samples (path, query, headers, …) using converters and matchers:
```haproxy
# Match path
acl sqli_path path_reg -i union.*select
# Match path against a regex
acl sqli_path path -m reg -i union.*select
# Match query string
acl sqli_query url_param(id) -m reg -i union.*select
# Match a specific query parameter
acl sqli_qid url_param(id) -m reg -i union.*select
# Match headers
acl bad_referer hdr_reg(Referer) -i malicious-site\.com
# Match a request header
acl bad_ref hdr(Referer) -m reg -i malicious-site\.com
# Combined conditions
http-request deny if sqli_path OR sqli_query
# Combine with boolean operators
http-request deny if sqli_path || sqli_qid
```
## Complete Example
## A complete example
```haproxy
global
@@ -91,102 +70,75 @@ defaults
log global
option httplog
timeout connect 5s
timeout client 50s
timeout server 50s
timeout client 50s
timeout server 50s
frontend http-in
bind *:80
# WAF Rules
acl waf_sqli path_reg -i (union.*select|insert.*into|delete.*from)
acl waf_xss path_reg -i (<script|javascript:|on\w+\s*=)
acl waf_lfi path_reg -i (\.\.\/|\.\.\\)
acl waf_rce path_reg -i (;|\||`|\$\()
# Bot blocking
acl bad_bot hdr_reg(User-Agent) -i (AhrefsBot|SemrushBot|MJ12bot)
# Deny malicious requests
http-request deny deny_status 403 if waf_sqli
http-request deny deny_status 403 if waf_xss
http-request deny deny_status 403 if waf_lfi
http-request deny deny_status 403 if waf_rce
http-request deny deny_status 403 if bad_bot
# WAF
acl waf_match path,url_dec -m reg -i -f /etc/haproxy/waf.acl
acl waf_match_q query -m reg -i -f /etc/haproxy/waf.acl
acl bad_bot hdr(User-Agent) -m reg -i -f /etc/haproxy/bots.acl
# Block matching requests
http-request deny deny_status 403 if waf_match || waf_match_q || bad_bot
default_backend servers
backend servers
balance roundrobin
server server1 127.0.0.1:8080 check
server srv1 127.0.0.1:8080 check
```
## Customization
### Custom Error Pages
### Custom error response
Return a custom error page for blocked requests:
Return a styled error body instead of the default empty 403:
```haproxy
http-request deny deny_status 403 content-type text/html \
string "Access Denied" if waf_sqli
http-request deny deny_status 403 \
content-type "text/html; charset=utf-8" \
string "<h1>Blocked by WAF</h1>" if waf_match
```
### Logging Blocked Requests
Create a dedicated log for WAF blocks:
### Logging blocked requests
```haproxy
frontend http-in
# Log blocked requests
http-request set-var(txn.blocked) str(1) if waf_sqli
http-request capture var(txn.blocked) len 1
# Custom log format
log-format "%ci:%cp [%t] %ft %b/%s %Tq/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq blocked=%[var(txn.blocked)]"
http-request set-var(txn.blocked) str(1) if waf_match
http-request capture var(txn.blocked) len 1
log-format "%ci:%cp [%t] %ft %b/%s %ST %B blocked=%[var(txn.blocked)] ua=%[capture.req.hdr(0)]"
```
### Whitelist Paths
Skip WAF for specific paths:
### Per-path whitelist
```haproxy
acl is_api path_beg /api/webhook
http-request deny if waf_sqli !is_api
acl is_webhook path_beg /api/webhook
http-request deny deny_status 403 if waf_match !is_webhook
```
## Rate Limiting
Combine WAF with rate limiting:
### Combine with rate limiting
```haproxy
# Stick table for rate limiting
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
acl too_many_requests sc_http_req_rate(0) gt 100
http-request deny if too_many_requests
acl too_many sc_http_req_rate(0) gt 100
http-request deny deny_status 429 if too_many
```
## Testing
```bash
# Test SQL injection detection
curl -I "http://example.com/?id=1' UNION SELECT * FROM users--"
# Test bot blocking
curl -A "AhrefsBot" -I "http://example.com/"
# Check HAProxy stats
echo "show stat" | socat stdio /var/run/haproxy.sock
echo "show stat" | sudo socat stdio /var/run/haproxy.sock
```
## Troubleshooting
### ACLs not matching
Use `haproxy -c -f haproxy.cfg` to validate syntax. Enable debug logging to see ACL evaluation.
### Performance impact
ACL evaluation is fast, but complex regex patterns can add latency. Test with realistic traffic.
### Configuration too large
HAProxy has limits on configuration size. Consider splitting large ACL lists into multiple files.
- **ACL never matches** &mdash; run `haproxy -c -f haproxy.cfg` to validate the syntax. Use `-d` for debug output to watch ACL evaluation in real time.
- **Performance impact** &mdash; complex regex over `path,url_dec` adds per-request cost. Benchmark with realistic traffic before enabling globally.
- **Configuration too large** &mdash; the converter keeps `waf.acl` flat and grep-friendly. Split it across multiple files if you need to apply different rule subsets to different frontends.

View File

@@ -3,10 +3,10 @@ layout: home
hero:
name: Patterns
text: OWASP WAF Rules for Web Servers
tagline: Automated OWASP CRS patterns and Bad Bot detection for Nginx, Apache, Traefik, and HAProxy
text: Production-grade WAF rules, on autopilot.
tagline: Automated OWASP Core Rule Set and bad-bot patterns, converted into native configurations for Nginx, Apache, Traefik, and HAProxy &mdash; refreshed every day.
image:
src: /shield.svg
src: /hero-shield.svg
alt: Patterns
actions:
- theme: brand
@@ -17,47 +17,51 @@ hero:
link: https://github.com/fabriziosalmi/patterns
features:
- icon: 🛡️
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 4.5 5.8v6.1c0 4.7 3.3 9.1 7.5 10.4 4.2-1.3 7.5-5.7 7.5-10.4V5.8L12 3Z"/><path d="m8.5 12 2.5 2.5L15.5 9.5"/></svg>'
title: OWASP CRS Protection
details: Leverages OWASP Core Rule Set for web application firewall defense against SQLi, XSS, RCE, and LFI attacks.
- icon: 🤖
details: Defends against SQL injection, XSS, RCE, LFI, and RFI by deriving rules from the OWASP Core Rule Set &mdash; the same engine that powers ModSecurity worldwide.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="7.5" width="16" height="11" rx="3"/><path d="M12 4v3.5"/><circle cx="12" cy="3.5" r="0.9" fill="currentColor" stroke="none"/><circle cx="9" cy="13" r="1.1" fill="currentColor" stroke="none"/><circle cx="15" cy="13" r="1.1" fill="currentColor" stroke="none"/><path d="M2 13.5v2M22 13.5v2"/></svg>'
title: Bad Bot Blocking
details: Blocks known malicious bots and scrapers using regularly updated public bot lists.
- icon: ⚙️
title: Multi-Server Support
details: Generates WAF configs for Nginx, Apache, Traefik, and HAProxy with consistent protection across platforms.
- icon: 🔄
title: Daily Updates
details: GitHub Actions automatically fetch new OWASP rules daily and push updated configurations.
- icon: 📦
title: Pre-Generated Configs
details: Download ready-to-use WAF configurations from GitHub Releases without building from source.
- icon: 🧩
title: Extensible Design
details: Modular architecture makes it easy to extend support to other web servers or load balancers.
details: Curated User-Agent lists from public sources block scrapers, AI crawlers, vulnerability scanners, and SEO spam &mdash; with configurable allow-lists for legitimate bots.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="6" rx="1.5"/><rect x="3.5" y="14" width="17" height="6" rx="1.5"/><path d="M7 7h.01M7 17h.01"/><path d="M11 7h6M11 17h6"/></svg>'
title: Native Multi-Server Output
details: One source rule set, four idiomatic outputs &mdash; Nginx <code>map</code>+<code>if</code>, Apache <code>SecRule</code>, Traefik middleware TOML, and HAProxy ACL files.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 0 1 14.5-6L20 8"/><path d="M20 3v5h-5"/><path d="M20.5 12a8.5 8.5 0 0 1-14.5 6L4 16"/><path d="M4 21v-5h5"/></svg>'
title: Daily Automated Updates
details: A GitHub Actions workflow re-fetches the latest CRS release, rebuilds every backend, and publishes a fresh release archive &mdash; without manual intervention.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="m3.3 8 8.7 5 8.7-5"/><path d="M12 13v8"/><path d="m7.5 5.5 9 5"/></svg>'
title: Pre-Built Releases
details: Drop-in archives are published on every run. Skip the toolchain &mdash; download <code>nginx_waf.zip</code>, <code>apache_waf.zip</code>, <code>traefik_waf.zip</code>, or <code>haproxy_waf.zip</code>.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4.5a2 2 0 1 0-4 0V6H6a1.5 1.5 0 0 0-1.5 1.5V11h1.5a2 2 0 1 1 0 4H4.5v3.5A1.5 1.5 0 0 0 6 20h3.5v-1.5a2 2 0 1 1 4 0V20H17a1.5 1.5 0 0 0 1.5-1.5V15H20a2 2 0 1 0 0-4h-1.5V7.5A1.5 1.5 0 0 0 17 6h-3V4.5Z"/></svg>'
title: Composable & Extensible
details: Each backend is a small Python converter that consumes a single JSON intermediate. Adding a new platform is a few hundred lines &mdash; not a fork.
---
## Quick Start
<div class="home-section">
Download the latest configurations from [GitHub Releases](https://github.com/fabriziosalmi/patterns/releases) or build from source:
## Quick start
Pull the latest release archive and include it in your existing server configuration &mdash; no toolchain required.
```bash
# Pick the archive that matches your stack
curl -LO https://github.com/fabriziosalmi/patterns/releases/latest/download/nginx_waf.zip
unzip nginx_waf.zip -d /etc/nginx/waf_patterns
```
Or build from source to customize before deploying:
```bash
git clone https://github.com/fabriziosalmi/patterns.git
cd patterns
pip install -r requirements.txt
python owasp2json.py
python json2nginx.py # or json2apache.py, json2traefik.py, json2haproxy.py
python owasp2json.py # Fetch the latest OWASP CRS
python json2nginx.py # Or json2apache.py / json2traefik.py / json2haproxy.py
python badbots.py # Generate bad-bot blocklists
```
## Supported Platforms
| Platform | Config Format | Documentation |
|----------|---------------|---------------|
| **Nginx** | `.conf` files | [Read more →](/nginx) |
| **Apache** | ModSecurity rules | [Read more →](/apache) |
| **Traefik** | Middleware TOML | [Read more →](/traefik) |
| **HAProxy** | ACL files | [Read more →](/haproxy) |
::: tip Using Caddy?
Check out the [caddy-waf](https://github.com/fabriziosalmi/caddy-waf) project for Caddy-specific WAF support.
See the dedicated [caddy-waf](https://github.com/fabriziosalmi/caddy-waf) project for Caddy-specific WAF support.
:::
</div>

View File

@@ -1,131 +1,123 @@
# Nginx Integration
This guide explains how to integrate the WAF patterns into your Nginx configuration.
This guide explains how to wire the generated rules into an Nginx configuration.
## Quick Start
## Quick start
1. Download `nginx_waf.zip` from [Releases](https://github.com/fabriziosalmi/patterns/releases)
2. Extract to your Nginx configuration directory
3. Include the configuration files as shown below
1. Download `nginx_waf.zip` from the [latest release](https://github.com/fabriziosalmi/patterns/releases/latest) and extract it (e.g. into `/etc/nginx/waf_patterns/nginx/`).
2. Include `waf_maps.conf` from the `http` block.
3. Include `waf_rules.conf` from each `server` (or `location`) you want to protect.
4. Reload Nginx.
## Configuration Files
## Files in the archive
The Nginx WAF package includes:
| File | Purpose | Include Location |
| File | Purpose | Where to include |
|------|---------|------------------|
| `waf_maps.conf` | Map directives for pattern matching | `http` block |
| `waf_rules.conf` | If statements for blocking | `server` block |
| `bots.conf` | Bad bot detection maps | `http` block |
| `waf_maps.conf` | Defines `map` variables for every attack category | `http` block |
| `waf_rules.conf` | `if (...) { return 403; }` rules that consume those maps | `server` or `location` block |
| `bots.conf` | `map $http_user_agent $bad_bot` for User-Agent filtering | `http` block |
| `sqli.conf`, `xss.conf`, `rce.conf`, `lfi.conf`, … | Per-category files for inspection only | **Do not include directly** |
## Integration
::: warning Use only the two main files
The per-category `.conf` files (`attack.conf`, `xss.conf`, `sqli.conf`, …) are emitted for inspection. They contain both `map` and `if` directives, which Nginx does not allow in the same context. Always include `waf_maps.conf` + `waf_rules.conf` instead.
:::
### Step 1: Include Maps in HTTP Block
## Step 1 &mdash; Include the maps
The map directives **must** be included in the `http` context:
The `map` directives must live in the `http` context:
```nginx
http {
# Include WAF maps (pattern definitions)
include /path/to/waf_patterns/nginx/waf_maps.conf;
# Include bot detection maps
include /path/to/waf_patterns/nginx/bots.conf;
# ... other http configurations ...
include /etc/nginx/waf_patterns/nginx/waf_maps.conf;
include /etc/nginx/waf_patterns/nginx/bots.conf;
# …rest of your http config
}
```
### Step 2: Include Rules in Server Block
## Step 2 &mdash; Include the rules
The blocking rules go inside your `server` or `location` block:
Place the blocking rules inside any `server` block you want to protect:
```nginx
server {
listen 80;
listen 443 ssl http2;
server_name example.com;
# Include WAF rules
include /path/to/waf_patterns/nginx/waf_rules.conf;
# ... other server configurations ...
include /etc/nginx/waf_patterns/nginx/waf_rules.conf;
if ($bad_bot) { return 403; }
# …your locations
}
```
### Step 3: Reload Nginx
Test and reload the configuration:
## Step 3 &mdash; Validate and reload
```bash
sudo nginx -t && sudo systemctl reload nginx
```
## How It Works
## How it works
The WAF uses Nginx's `map` directive for efficient pattern matching:
The converter rewrites every OWASP regex into a `map` lookup, which Nginx evaluates with O(1) overhead per request:
```nginx
map $request_uri $waf_block_sqli {
default 0;
"~*union.*select" 1;
"~*insert.*into" 1;
"~*union[\s\S]+select" 1;
"~*insert[\s\S]+into" 1;
}
if ($waf_block_sqli) {
return 403;
}
# …elsewhere, in a server block:
if ($waf_block_sqli) { return 403; }
```
The full set of map variables is `$waf_block_<category>` &mdash; one per attack family the OWASP CRS defines.
## Customization
### Enable Logging
### Log blocked requests
To log blocked requests, edit `waf_rules.conf` and uncomment the logging lines:
Add a dedicated access log next to the deny:
```nginx
if ($waf_block_sqli) {
return 403;
access_log /var/log/nginx/waf_blocked.log;
log_format waf_block '$remote_addr - [$time_local] "$request" '
'blocked=$waf_block_sqli ua="$http_user_agent"';
server {
access_log /var/log/nginx/waf_blocked.log waf_block if=$waf_block_sqli;
include /etc/nginx/waf_patterns/nginx/waf_rules.conf;
}
```
### Whitelist Specific Paths
### Whitelist a path
Add exceptions before the WAF rules:
Skip the WAF inside specific routes by branching before the include:
```nginx
location /api/webhook {
# Skip WAF for this path
# ... your configuration ...
location = /api/webhook {
proxy_pass http://upstream;
# waf_rules.conf intentionally not included here
}
# WAF rules for other paths
include /path/to/waf_patterns/nginx/waf_rules.conf;
location / {
include /etc/nginx/waf_patterns/nginx/waf_rules.conf;
proxy_pass http://upstream;
}
```
::: warning Important
Individual category files like `attack.conf` or `xss.conf` should **not** be included directly. They contain both `map` and `if` directives which cannot be used in the same context. Always use `waf_maps.conf` + `waf_rules.conf`.
:::
## Testing
Test your WAF configuration with common attack patterns:
Probe the deployment with known-bad payloads &mdash; both should return `403`:
```bash
# Should be blocked (SQL injection)
curl -I "http://example.com/?id=1' OR '1'='1"
# Should be blocked (XSS)
curl -I "http://example.com/?q=<script>alert(1)</script>"
curl -I "https://example.com/?id=1' OR '1'='1"
curl -I "https://example.com/?q=<script>alert(1)</script>"
```
## Troubleshooting
### Configuration errors
Always run `nginx -t` before reloading to catch syntax errors.
### False positives
If legitimate requests are being blocked, check `/var/log/nginx/error.log` and consider adding path-specific exceptions.
### Performance
The map-based approach is highly efficient. For high-traffic sites, consider enabling caching for the map variables.
- **`nginx: configuration file test failed`** &mdash; you likely included a per-category file. Switch to `waf_maps.conf` + `waf_rules.conf`.
- **False positives** &mdash; check `/var/log/nginx/error.log`, identify the matching `$waf_block_*` variable, then add a `location`-scoped exemption.
- **High traffic** &mdash; the `map`-based design is already the fastest option Nginx offers; no further tuning is normally needed.

10
docs/public/favicon.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="f" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0a84ff"/>
<stop offset="1" stop-color="#0040dd"/>
</linearGradient>
</defs>
<path d="M16 2.5 4.5 6.8v8.4c0 6.5 4.6 12.5 11.5 14.3 6.9-1.8 11.5-7.8 11.5-14.3V6.8L16 2.5Z" fill="url(#f)"/>
<path d="M11 16.4l3.4 3.4 6.6-6.8" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" fill="none">
<defs>
<linearGradient id="shieldGrad" x1="40" y1="20" x2="280" y2="300" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#5ac8fa"/>
<stop offset="0.5" stop-color="#0a84ff"/>
<stop offset="1" stop-color="#0040dd"/>
</linearGradient>
<linearGradient id="shieldGlow" x1="160" y1="40" x2="160" y2="280" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff" stop-opacity="0.18"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="12" stdDeviation="18" flood-color="#0a84ff" flood-opacity="0.25"/>
</filter>
</defs>
<g filter="url(#shadow)">
<path d="M160 24 56 60v82c0 64.5 45 124 104 142 59-18 104-77.5 104-142V60L160 24Z" fill="url(#shieldGrad)"/>
<path d="M160 24 56 60v82c0 64.5 45 124 104 142 59-18 104-77.5 104-142V60L160 24Z" fill="url(#shieldGlow)"/>
</g>
<path d="M108 162 145 199 218 124" stroke="#fff" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

10
docs/public/logo.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0a84ff"/>
<stop offset="1" stop-color="#0040dd"/>
</linearGradient>
</defs>
<path d="M16 2.5 4.5 6.8v8.4c0 6.5 4.6 12.5 11.5 14.3 6.9-1.8 11.5-7.8 11.5-14.3V6.8L16 2.5Z" fill="url(#g)"/>
<path d="M11 16.4l3.4 3.4 6.6-6.8" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -1,27 +1,21 @@
# Traefik Integration
This guide explains how to integrate the WAF patterns with Traefik using middleware plugins.
This guide explains how to consume the generated WAF middleware in **Traefik v2 / v3**.
## Quick Start
## Quick start
1. Download `traefik_waf.zip` from [Releases](https://github.com/fabriziosalmi/patterns/releases)
2. Extract the files
3. Configure the middleware in your Traefik configuration
1. Download `traefik_waf.zip` from the [latest release](https://github.com/fabriziosalmi/patterns/releases/latest).
2. Drop the TOML files into your dynamic configuration directory.
3. Reference the middleware from each router that should be protected.
## Configuration Files
The Traefik WAF package includes:
## Files in the archive
| File | Purpose |
|------|---------|
| `middleware.toml` | WAF middleware configuration |
| `bots.toml` | Bad bot detection rules |
| `middleware.toml` | WAF middleware definition (regex patterns per category) |
| `bots.toml` | Bad-bot User-Agent middleware |
## Integration with File Provider
### Step 1: Enable File Provider
In your `traefik.toml` or `traefik.yml`:
## Step 1 &mdash; Enable the file provider
::: code-group
@@ -41,39 +35,31 @@ providers:
:::
### Step 2: Copy Middleware Files
Copy the WAF configuration files to your dynamic configuration directory:
## Step 2 &mdash; Drop the TOML files in
```bash
cp waf_patterns/traefik/*.toml /etc/traefik/dynamic/
sudo cp waf_patterns/traefik/*.toml /etc/traefik/dynamic/
```
### Step 3: Apply Middleware to Routes
Traefik picks them up automatically because `watch = true`.
Reference the middleware in your router configuration:
## Step 3 &mdash; Reference the middleware
::: code-group
```toml [dynamic/routes.toml]
[http.routers.my-router]
[http.routers.app]
rule = "Host(`example.com`)"
service = "my-service"
service = "app"
middlewares = ["waf-protection", "bot-blocker"]
[http.middlewares.waf-protection.plugin.waf]
# WAF configuration loaded from middleware.toml
[http.middlewares.bot-blocker.plugin.botblocker]
# Bot blocking loaded from bots.toml
```
```yaml [dynamic/routes.yml]
http:
routers:
my-router:
app:
rule: "Host(`example.com`)"
service: my-service
service: app
middlewares:
- waf-protection
- bot-blocker
@@ -81,35 +67,27 @@ http:
:::
## Integration with Docker Labels
The middleware names (`waf-protection`, `bot-blocker`) are the keys defined inside `middleware.toml` and `bots.toml`.
For Docker-based deployments:
## Docker labels
For Docker / Compose deployments, attach the middleware via labels:
```yaml
services:
my-app:
app:
image: my-app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-app.rule=Host(`example.com`)"
- "traefik.http.routers.my-app.middlewares=waf@file"
- "traefik.http.routers.app.rule=Host(`example.com`)"
- "traefik.http.routers.app.middlewares=waf-protection@file,bot-blocker@file"
```
## Middleware Configuration
The `@file` suffix tells Traefik to resolve the middleware from the file provider.
The `middleware.toml` contains regex-based blocking rules:
## Plugin compatibility
```toml
[http.middlewares.waf.plugin.rewriteHeaders]
# SQL Injection patterns
[[http.middlewares.waf.plugin.rewriteHeaders.replacements]]
regex = "(?i)union.*select"
replacement = "BLOCKED"
```
## Using with Traefik Plugins
For enhanced WAF capabilities, consider using community plugins:
`middleware.toml` is generated against Traefik's built-in middleware primitives. If you prefer a dedicated WAF plugin (e.g. one of the community plugins on [Traefik Plugins](https://plugins.traefik.io/)), you can declare it side-by-side and chain both:
```yaml
experimental:
@@ -121,25 +99,24 @@ experimental:
## Customization
### Add Custom Patterns
### Add custom patterns
Edit `middleware.toml` to add your own patterns:
Edit `middleware.toml` to extend the regex set:
```toml
[[http.middlewares.waf.plugin.rewriteHeaders.replacements]]
[[http.middlewares.waf-protection.plugin.rewriteHeaders.replacements]]
regex = "your-custom-pattern"
replacement = "BLOCKED"
```
### Logging
Enable access logs to monitor blocked requests:
Enable structured access logs to track middleware decisions:
```toml
[accessLog]
filePath = "/var/log/traefik/access.log"
format = "json"
[accessLog.fields]
[accessLog.fields.headers]
defaultMode = "keep"
@@ -148,21 +125,12 @@ Enable access logs to monitor blocked requests:
## Testing
```bash
# Test WAF detection
curl -H "Host: example.com" \
"http://localhost/?id=1' OR '1'='1"
# Check Traefik logs
curl -H "Host: example.com" "http://localhost/?id=1' OR '1'='1"
docker logs traefik 2>&1 | grep -i blocked
```
## Troubleshooting
### Middleware not loading
Check that the file provider is correctly configured and watching the right directory.
### Routes not applying middleware
Ensure the middleware name matches exactly between router and middleware definition.
### Performance considerations
Traefik's regex-based middleware can impact performance at high traffic. Monitor latency after enabling WAF rules.
- **Middleware never loads** &mdash; check that the file provider directory matches and that `watch = true`. `traefik logs -f` shows hot-reload events.
- **Router does not apply the middleware** &mdash; the middleware name must match exactly (case-sensitive) between router declaration and middleware definition.
- **Latency** &mdash; regex middleware adds per-request overhead. Profile with `traefik` access logs and consider scoping the middleware to specific routers rather than applying globally.