Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ jobs:
-v "$PWD/openresty/spec:/app/spec:ro" \
forms-waf-lua-test:ci --verbose /app/spec

# The chart ships its own copy of nginx.conf, so an env declaration or a
# shared dict added in one place and not the other fails silently and only
# on Kubernetes. Shadow mode shipped missing its dict; HAPROXY_TIMEOUT was
# set by the deployment but undeclared, so Lua never saw it.
- name: Helm / nginx.conf drift
run: python3 scripts/check-helm-drift.py

# Guards the seam between config_resolver and the defense mechanisms, where
# six shipped features were silently inert.
- name: Config contract
Expand Down
2 changes: 2 additions & 0 deletions admin-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import AttackSignatureEditor from '@/pages/security/AttackSignatureEditor'
import BehavioralAnalytics from '@/pages/analytics/BehavioralAnalytics'
import ClusterStatus from '@/pages/cluster/ClusterStatus'
import ShadowMode from '@/pages/shadow/ShadowMode'
import Suppressions from '@/pages/security/Suppressions'
import { About } from '@/pages/About'
import { Users } from '@/pages/admin/Users'
import { AuthProviders } from '@/pages/admin/AuthProviders'
Expand Down Expand Up @@ -89,6 +90,7 @@ export default function App() {
<Route path="/security/attack-signatures/:id" element={<AttackSignatureEditor />} />
<Route path="/analytics/behavioral" element={<BehavioralAnalytics />} />
<Route path="/security/shadow" element={<ShadowMode />} />
<Route path="/security/suppressions" element={<Suppressions />} />
<Route path="/cluster" element={<ClusterStatus />} />
<Route path="/about" element={<About />} />
<Route path="/admin/users" element={<Users />} />
Expand Down
44 changes: 44 additions & 0 deletions admin-ui/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1504,3 +1504,47 @@ export const shadowApi = {

clear: () => request<{ cleared: boolean }>('/shadow/decisions', { method: 'DELETE' }),
}

// ---------------------------------------------------------------------------
// Rule suppressions
//
// The counterpart to shadow mode: shadow names the rule that would have
// blocked, a suppression says that rule is wrong for this scope.
// ---------------------------------------------------------------------------

export type SuppressionScope = 'global' | 'vhost' | 'endpoint'

export interface Suppression {
id: string
scope_type: SuppressionScope
scope_id?: string
flag: string
reason?: string
created_at?: number
created_by?: string
}

export interface NewSuppression {
flag: string
scope_type?: SuppressionScope
scope_id?: string
reason?: string
}

export const suppressionsApi = {
list: () =>
request<{ suppressions: Suppression[]; count: number; max?: number }>('/suppressions'),

create: (body: NewSuppression) =>
request<{ suppression: Suppression; created: boolean }>('/suppressions', {
method: 'POST',
body: JSON.stringify(body),
}),

remove: (id: string) =>
request<{ deleted: boolean; id: string }>(`/suppressions/${encodeURIComponent(id)}`, {
method: 'DELETE',
}),

clear: () => request<{ cleared: boolean }>('/suppressions', { method: 'DELETE' }),
}
180 changes: 180 additions & 0 deletions admin-ui/src/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,168 @@ export interface paths {
patch?: never;
trace?: never;
};
"/suppressions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Detections that no longer count, and where */
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Every suppression currently in force */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
suppressions: components["schemas"]["Suppression"][];
count: number;
/** @description Ceiling on entries; this list is walked on every defense node. */
max?: number;
};
};
};
};
};
put?: never;
/** Stop a detection counting for one scope */
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Exact detection flag, or a trailing "*" for a family (kw:*). This is the flag the mechanism emits, without the profile prefix that appears on recorded shadow decisions. */
flag: string;
/**
* @default global
* @enum {string}
*/
scope_type?: "global" | "vhost" | "endpoint";
/** @description Required unless scope_type is global. */
scope_id?: string;
reason?: string;
};
};
};
responses: {
/** @description Already present. The id is a digest of scope and flag, so re-adding the same suppression updates it rather than creating a duplicate. */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SuppressionCreated"];
};
};
/** @description Created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SuppressionCreated"];
};
};
/** @description Invalid input. Includes a global "*", which would disable all detection and is refused deliberately. */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
/** Remove every suppression */
delete: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Cleared */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
cleared: boolean;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/suppressions/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Remove one suppression */
delete: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Removed */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
deleted: boolean;
id: string;
};
};
};
/** @description No suppression with that id */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/shadow/summary": {
parameters: {
query?: never;
Expand Down Expand Up @@ -489,6 +651,24 @@ export interface components {
high_event_rate?: number;
};
};
Suppression: {
/** @description Digest of scope and flag, so adding the same one twice is idempotent. */
id: string;
/** @enum {string} */
scope_type: "global" | "vhost" | "endpoint";
/** @description Absent for global scope; the vhost or endpoint id otherwise. */
scope_id?: string;
/** @description Exact detection flag, or a trailing "*" to cover a family (kw:*). */
flag: string;
reason?: string;
created_at?: number;
created_by?: string;
};
SuppressionCreated: {
suppression: components["schemas"]["Suppression"];
/** @description False when the suppression already existed and was updated. */
created: boolean;
};
ShadowDecision: {
/** @description Unix time the decision was made */
ts: number;
Expand Down
2 changes: 2 additions & 0 deletions admin-ui/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Server,
Fingerprint,
EyeOff,
BellOff,
MessageSquare,
Workflow,
Target,
Expand Down Expand Up @@ -52,6 +53,7 @@ const navigation = [
name: 'Security',
children: [
{ name: 'Shadow Mode', href: '/security/shadow', icon: EyeOff },
{ name: 'Suppressions', href: '/security/suppressions', icon: BellOff },
{ name: 'Form Timing', href: '/security/timing', icon: Clock },
{ name: 'Defense Profiles', href: '/security/defense-profiles', icon: Workflow },
{ name: 'Attack Signatures', href: '/security/attack-signatures', icon: Target },
Expand Down
Loading
Loading