mirror of
https://github.com/fabriziosalmi/patterns.git
synced 2026-06-11 06:54:15 -04:00
Refine home page: traffic-grid hero, editorial feature rail, real platform marks
- Replace hero shield with HeroVisual: 11-track flowing SVG of request glyphs with ~7% Apple-red blocked highlights, slow CSS-only drift, edge fade, monospace meta header and pass/blocked footer (data-driven feel). - Drop the six default feature cards. New HomeFeatureRail renders a 3x2 hairline-bordered editorial grid: numbered eyebrow + bold title + body, zero icon chrome. - Redraw platform icons as recognizable brand marks (Nginx hexagon-N, Apache feather, Traefik "Mr. Traefik" head, HAProxy load-balanced H). Showcase cards drop card chrome in favor of column dividers; hover adopts the platform's brand color via per-card --accent CSS var. - Stats strip becomes a hairline-bordered four-column rail with tabular-num values, mono sub-labels, and a Google-data-display feel. - Hero name no longer uses gradient text; pure neutral. - Code-block bg corrected for light mode. - Respects prefers-reduced-motion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
264
docs/.vitepress/theme/components/HeroVisual.vue
Normal file
264
docs/.vitepress/theme/components/HeroVisual.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<script setup lang="ts">
|
||||
// Deterministic pseudo-random sequence so SSR and client agree.
|
||||
function lcg(seed: number) {
|
||||
let n = seed
|
||||
return () => {
|
||||
n = (n * 1103515245 + 12345) % 2147483648
|
||||
return n / 2147483648
|
||||
}
|
||||
}
|
||||
|
||||
type Seg = { x: number; w: number; blocked: boolean }
|
||||
|
||||
function buildTrack(seed: number): Seg[] {
|
||||
const r = lcg(seed)
|
||||
const segs: Seg[] = []
|
||||
let x = 0
|
||||
const widths = [4, 6, 8, 10, 14, 18, 22]
|
||||
const gaps = [4, 5, 6, 8, 10]
|
||||
while (x < 480) {
|
||||
const w = widths[Math.floor(r() * widths.length)]
|
||||
const gap = gaps[Math.floor(r() * gaps.length)]
|
||||
const blocked = r() < 0.07 // ~7%
|
||||
segs.push({ x, w, blocked })
|
||||
x += w + gap
|
||||
}
|
||||
return segs
|
||||
}
|
||||
|
||||
const trackCount = 11
|
||||
const tracks = Array.from({ length: trackCount }, (_, i) => ({
|
||||
y: 18 + i * 26,
|
||||
delay: -(i * 2.7),
|
||||
duration: 36 + (i % 3) * 6, // slight variance per track
|
||||
segs: buildTrack(i + 7)
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero-visual" aria-hidden="true">
|
||||
<div class="hero-visual-frame">
|
||||
<div class="hero-visual-meta">
|
||||
<span class="meta-label">// inbound · request inspection</span>
|
||||
<span class="meta-live">
|
||||
<span class="live-dot" />
|
||||
live
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
class="hero-visual-svg"
|
||||
viewBox="0 0 480 320"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="edge-fade" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity="0" />
|
||||
<stop offset="0.08" stop-color="#fff" stop-opacity="1" />
|
||||
<stop offset="0.92" stop-color="#fff" stop-opacity="1" />
|
||||
<stop offset="1" stop-color="#fff" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<mask id="edge-mask">
|
||||
<rect x="0" y="0" width="480" height="320" fill="url(#edge-fade)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g mask="url(#edge-mask)">
|
||||
<g
|
||||
v-for="(t, i) in tracks"
|
||||
:key="i"
|
||||
:transform="`translate(0, ${t.y})`"
|
||||
>
|
||||
<g
|
||||
class="drift"
|
||||
:style="{
|
||||
'--drift-delay': `${t.delay}s`,
|
||||
'--drift-duration': `${t.duration}s`
|
||||
}"
|
||||
>
|
||||
<!-- two copies side-by-side give a seamless loop -->
|
||||
<g v-for="copy in 2" :key="copy" :transform="`translate(${(copy - 1) * 480}, 0)`">
|
||||
<template v-for="(s, j) in t.segs" :key="j">
|
||||
<rect
|
||||
:x="s.x"
|
||||
y="-2"
|
||||
:width="s.w"
|
||||
height="4"
|
||||
rx="2"
|
||||
:class="s.blocked ? 'seg-blocked' : 'seg-normal'"
|
||||
/>
|
||||
</template>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="hero-visual-foot">
|
||||
<span class="foot-stat">
|
||||
<span class="stat-num">99.93<span class="stat-unit">%</span></span>
|
||||
<span class="stat-key">pass</span>
|
||||
</span>
|
||||
<span class="foot-sep" />
|
||||
<span class="foot-stat foot-stat--blocked">
|
||||
<span class="stat-num">0.07<span class="stat-unit">%</span></span>
|
||||
<span class="stat-key">blocked</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.hero-visual-frame {
|
||||
position: relative;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background:
|
||||
linear-gradient(180deg, var(--vp-c-bg-elv), var(--vp-c-bg-alt));
|
||||
padding: 18px 18px 14px;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
||||
0 24px 60px -28px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .hero-visual-frame {
|
||||
box-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
||||
0 24px 60px -20px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.hero-visual-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.meta-live {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #34c759; /* Apple system green */
|
||||
box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.45);
|
||||
animation: live-pulse 2.4s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes live-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.45); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(52, 199, 89, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0); }
|
||||
}
|
||||
|
||||
.hero-visual-svg {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.seg-normal {
|
||||
fill: var(--vp-c-text-3);
|
||||
fill-opacity: 0.32;
|
||||
}
|
||||
.dark .seg-normal {
|
||||
fill-opacity: 0.42;
|
||||
}
|
||||
|
||||
.seg-blocked {
|
||||
fill: #ff453a; /* Apple system red */
|
||||
fill-opacity: 0.95;
|
||||
}
|
||||
|
||||
.drift {
|
||||
animation: drift var(--drift-duration, 36s) linear infinite;
|
||||
animation-delay: var(--drift-delay, 0s);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes drift {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-480px); }
|
||||
}
|
||||
|
||||
.hero-visual-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-top: 12px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.foot-stat {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.stat-key {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.foot-stat--blocked .stat-num {
|
||||
color: #ff453a;
|
||||
}
|
||||
|
||||
.foot-sep {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.drift {
|
||||
animation: none;
|
||||
}
|
||||
.live-dot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
157
docs/.vitepress/theme/components/HomeFeatureRail.vue
Normal file
157
docs/.vitepress/theme/components/HomeFeatureRail.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
const features = [
|
||||
{
|
||||
eyebrow: '01',
|
||||
title: 'OWASP CRS coverage',
|
||||
body: 'Rules for SQL injection, XSS, RCE, LFI, and RFI, derived from the same Core Rule Set behind ModSecurity.'
|
||||
},
|
||||
{
|
||||
eyebrow: '02',
|
||||
title: 'Native multi-server output',
|
||||
body: 'One source, four idiomatic backends — Nginx maps, Apache SecRule, Traefik middleware, HAProxy ACL files.'
|
||||
},
|
||||
{
|
||||
eyebrow: '03',
|
||||
title: 'Bad-bot blocking',
|
||||
body: 'Curated User-Agent lists from public sources — scrapers, AI crawlers, scanners, with allow-lists for legitimate engines.'
|
||||
},
|
||||
{
|
||||
eyebrow: '04',
|
||||
title: 'Daily automated rebuild',
|
||||
body: 'A scheduled GitHub Actions workflow re-fetches the latest CRS release and republishes every archive — no maintenance.'
|
||||
},
|
||||
{
|
||||
eyebrow: '05',
|
||||
title: 'Pre-built archives',
|
||||
body: 'Drop-in zips published on every run: nginx_waf.zip, apache_waf.zip, traefik_waf.zip, haproxy_waf.zip.'
|
||||
},
|
||||
{
|
||||
eyebrow: '06',
|
||||
title: 'Composable pipeline',
|
||||
body: 'Each backend is a small Python converter on a single JSON intermediate. Adding a platform is a few hundred lines.'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="home-rail">
|
||||
<header class="home-rail-head">
|
||||
<span class="home-eyebrow">Capabilities</span>
|
||||
<h2 class="home-title">Engineered for production</h2>
|
||||
<p class="home-lede">
|
||||
Six guarantees that turn a daily scrape of upstream rules into something your traffic can actually
|
||||
live behind — quietly, predictably, and without operator toil.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ul class="rail-grid">
|
||||
<li v-for="f in features" :key="f.title" class="rail-item">
|
||||
<span class="rail-eyebrow">{{ f.eyebrow }}</span>
|
||||
<h3 class="rail-title">{{ f.title }}</h3>
|
||||
<p class="rail-body">{{ f.body }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-rail {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 24px 80px;
|
||||
}
|
||||
|
||||
.home-rail-head {
|
||||
margin-bottom: 56px;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.rail-grid {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.rail-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.rail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.rail-item {
|
||||
position: relative;
|
||||
padding: 28px 24px 32px 0;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.rail-item:not(:nth-child(3n)) {
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
padding-right: 24px;
|
||||
}
|
||||
.rail-item:not(:nth-child(3n)) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
.rail-item:nth-child(3n+1) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.rail-item:not(:nth-child(3n)) {
|
||||
border-right: none;
|
||||
}
|
||||
.rail-item:nth-child(odd) {
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
padding-right: 24px;
|
||||
padding-left: 0;
|
||||
}
|
||||
.rail-item:nth-child(even) {
|
||||
padding-left: 24px;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.rail-item,
|
||||
.rail-item:nth-child(odd),
|
||||
.rail-item:nth-child(even) {
|
||||
border-right: none !important;
|
||||
padding: 24px 0 24px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rail-eyebrow {
|
||||
display: inline-block;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.rail-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--vp-c-text-1);
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.rail-body {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +1,62 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { withBase } from 'vitepress'
|
||||
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 }
|
||||
{ name: 'Nginx', meta: 'map directives + if rules', href: '/nginx', icon: IconNginx, accent: '#009639' },
|
||||
{ name: 'Apache', meta: 'ModSecurity SecRule directives', href: '/apache', icon: IconApache, accent: '#d22128' },
|
||||
{ name: 'Traefik', meta: 'Middleware TOML configuration', href: '/traefik', icon: IconTraefik, accent: '#24a1c1' },
|
||||
{ name: 'HAProxy', meta: 'ACL pattern files', href: '/haproxy', icon: IconHaproxy, accent: '#7cc242' }
|
||||
]
|
||||
|
||||
const stats = [
|
||||
{ value: '600+', label: 'OWASP CRS patterns', sub: 'Extracted from upstream daily' },
|
||||
{ value: 'Daily', label: 'Refresh cadence', sub: 'Scheduled GitHub Actions' },
|
||||
{ value: '4', label: 'Server backends', sub: 'Native, idiomatic output' },
|
||||
{ value: 'MIT', label: 'License', sub: 'Open-source, no vendor lock' }
|
||||
]
|
||||
</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 —
|
||||
so you get equivalent protection whether you run Nginx, Apache, Traefik, or HAProxy.
|
||||
</p>
|
||||
<section class="home-section">
|
||||
<header class="home-rail-head">
|
||||
<span class="home-eyebrow">Integrations</span>
|
||||
<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 —
|
||||
so you get equivalent protection regardless of the proxy in front of your stack.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
<div class="platform-grid">
|
||||
<a
|
||||
v-for="p in platforms"
|
||||
:key="p.name"
|
||||
:href="withBase(p.href)"
|
||||
class="platform-card"
|
||||
:style="{ '--accent': p.accent }"
|
||||
>
|
||||
<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>
|
||||
<section class="home-section home-section--stats">
|
||||
<div class="stats-rail">
|
||||
<div v-for="s in stats" :key="s.label" class="stat-cell">
|
||||
<div class="stat-value">{{ s.value }}</div>
|
||||
<div class="stat-key">{{ s.label }}</div>
|
||||
<div class="stat-sub">{{ s.sub }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<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>
|
||||
<!-- Apache-inspired feather: curved spine with tapered barbs -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M20 4c-9 1-15 8-16 17" />
|
||||
<path d="M17.5 4.7c-3 .2-5.6 1.7-7.6 4.1" />
|
||||
<path d="M14.5 6c-2.2.5-4.2 1.9-5.6 3.9" />
|
||||
<path d="M11.6 8.2c-1.6.7-3.1 2-4.1 3.6" />
|
||||
<path d="M9 11.2c-1.4.8-2.6 2-3.4 3.4" />
|
||||
<path d="M6.5 14.6c-1 .9-1.8 2-2.4 3.3" />
|
||||
<path d="M4 21h2.5" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<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>
|
||||
<!-- HAProxy-inspired stylized H with load-balanced node accents -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 4v16" />
|
||||
<path d="M19 4v16" />
|
||||
<path d="M5 12h14" />
|
||||
<circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none" />
|
||||
<circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none" />
|
||||
<path d="M5 6h2M17 6h2M5 18h2M17 18h2" stroke-opacity="0.55" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<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>
|
||||
<!-- Nginx-inspired hexagon mark with angular N -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 2.5 4 7v10l8 4.5L20 17V7L12 2.5Z" />
|
||||
<path d="M9.5 16.5V8.2l5 7.7V8.2" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +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">
|
||||
<path d="M3 12h4l2-3 3 6 3-9 2 6h4" />
|
||||
</svg>
|
||||
<!-- Traefik-inspired "Mr. Traefik" mark: rounded body with horns and eyes -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 11.5v6.5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6.5" />
|
||||
<path d="m5 11.5 2-7 2 4 1.5-3 1.5 3 1.5-3 1.5 3 2-4 2 7" />
|
||||
<circle cx="9.5" cy="14.6" r="0.85" fill="currentColor" stroke="none" />
|
||||
<circle cx="14.5" cy="14.6" r="0.85" fill="currentColor" stroke="none" />
|
||||
<path d="M10.5 17.6h3" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { h } from 'vue'
|
||||
import { h, Fragment } from 'vue'
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import HeroVisual from './components/HeroVisual.vue'
|
||||
import HomeFeatureRail from './components/HomeFeatureRail.vue'
|
||||
import HomeShowcase from './components/HomeShowcase.vue'
|
||||
import './style.css'
|
||||
|
||||
@@ -8,7 +10,9 @@ export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: () => {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'home-features-after': () => h(HomeShowcase)
|
||||
'home-hero-image': () => h(HeroVisual),
|
||||
'home-features-after': () =>
|
||||
h(Fragment, null, [h(HomeFeatureRail), h(HomeShowcase)])
|
||||
})
|
||||
}
|
||||
} satisfies Theme
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,6 @@ hero:
|
||||
name: Patterns
|
||||
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 — refreshed every day.
|
||||
image:
|
||||
src: /hero-shield.svg
|
||||
alt: Patterns
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
@@ -15,53 +12,17 @@ hero:
|
||||
- theme: alt
|
||||
text: View on GitHub
|
||||
link: https://github.com/fabriziosalmi/patterns
|
||||
|
||||
features:
|
||||
- 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: Defends against SQL injection, XSS, RCE, LFI, and RFI by deriving rules from the OWASP Core Rule Set — 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: Curated User-Agent lists from public sources block scrapers, AI crawlers, vulnerability scanners, and SEO spam — 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 — 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 — 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 — 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 — not a fork.
|
||||
---
|
||||
|
||||
<div class="home-section">
|
||||
|
||||
## Quick start
|
||||
|
||||
Pull the latest release archive and include it in your existing server configuration — 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 # Fetch the latest OWASP CRS
|
||||
python json2nginx.py # Or json2apache.py / json2traefik.py / json2haproxy.py
|
||||
python badbots.py # Generate bad-bot blocklists
|
||||
```
|
||||
|
||||
::: tip Using Caddy?
|
||||
See the dedicated [caddy-waf](https://github.com/fabriziosalmi/caddy-waf) project for Caddy-specific WAF support.
|
||||
:::
|
||||
Or build from source — full toolchain instructions in [Getting Started](/getting-started).
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user