Skip to content

Commit e9594a7

Browse files
feat: add WAF rule catalog, policy system, and structured KDL generation
Introduces proper WAF configuration management with a rule catalog of ~60 OWASP CRS-inspired rules, project-scoped policies with per-rule overrides, and structured KDL output replacing the unstructured security map toggles. - Migration: waf_rules, waf_policies, waf_policy_rule_overrides tables - Schemas: WafRule, WafPolicy, WafPolicyRuleOverride with validations - Built-in rules: 8 categories (sqli, xss, lfi, rfi, rce, scanner, protocol, data_leak) - Context: CRUD + get_effective_rules/1 merging policy defaults with overrides - KDL generator: structured waf { category { rule } } blocks with fallback - LiveViews: rule browser, policy CRUD, effective rules with override dropdowns - Service integration: waf_policy_id FK, dropdown in new/edit, display in show - 35 new tests covering schemas, context, effective rules, and cascades
1 parent ce154f4 commit e9594a7

22 files changed

Lines changed: 2941 additions & 10 deletions

File tree

lib/sentinel_cp/services/kdl_generator.ex

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ defmodule SentinelCp.Services.KdlGenerator do
1717
InternalCa
1818
}
1919

20+
alias SentinelCp.Waf
21+
alias SentinelCp.Waf.WafPolicy
22+
2023
@doc """
2124
Generates KDL configuration for a project from its services and config.
2225
@@ -38,6 +41,7 @@ defmodule SentinelCp.Services.KdlGenerator do
3841
upstream_groups = Services.list_upstream_groups(project_id)
3942
certificates = Services.list_certificates(project_id)
4043
auth_policies = Services.list_auth_policies(project_id)
44+
waf_policies = Waf.list_policies(project_id)
4145
trust_stores = Services.list_trust_stores(project_id)
4246
internal_ca = Services.get_internal_ca(project_id)
4347

@@ -68,7 +72,8 @@ defmodule SentinelCp.Services.KdlGenerator do
6872
middleware_chains,
6973
trust_stores,
7074
internal_ca,
71-
plugin_chains
75+
plugin_chains,
76+
waf_policies
7277
)
7378

7479
{:ok, kdl}
@@ -146,7 +151,8 @@ defmodule SentinelCp.Services.KdlGenerator do
146151
middleware_chains \\ %{},
147152
trust_stores \\ [],
148153
internal_ca \\ nil,
149-
plugin_chains \\ %{}
154+
plugin_chains \\ %{},
155+
waf_policies \\ []
150156
) do
151157
# Build lookup maps
152158
group_map =
@@ -161,6 +167,10 @@ defmodule SentinelCp.Services.KdlGenerator do
161167
auth_policies
162168
|> Enum.into(%{}, fn a -> {a.id, a} end)
163169

170+
waf_policy_map =
171+
waf_policies
172+
|> Enum.into(%{}, fn w -> {w.id, w} end)
173+
164174
trust_store_map =
165175
trust_stores
166176
|> Enum.into(%{}, fn t -> {t.id, t} end)
@@ -201,7 +211,8 @@ defmodule SentinelCp.Services.KdlGenerator do
201211
auth_policy_map,
202212
middleware_chains,
203213
internal_ca,
204-
plugin_chains
214+
plugin_chains,
215+
waf_policy_map
205216
),
206217
build_rate_limits(services)
207218
]
@@ -344,14 +355,25 @@ defmodule SentinelCp.Services.KdlGenerator do
344355
auth_policy_map,
345356
middleware_chains,
346357
internal_ca,
347-
plugin_chains
358+
plugin_chains,
359+
waf_policy_map
348360
) do
349361
route_blocks =
350362
services
351363
|> Enum.map(fn service ->
352364
chain = Map.get(middleware_chains, service.id, [])
353365
plugins = Map.get(plugin_chains, service.id, [])
354-
build_route(service, group_map, cert_map, auth_policy_map, chain, internal_ca, plugins)
366+
367+
build_route(
368+
service,
369+
group_map,
370+
cert_map,
371+
auth_policy_map,
372+
chain,
373+
internal_ca,
374+
plugins,
375+
waf_policy_map
376+
)
355377
end)
356378
|> Enum.intersperse([""])
357379

@@ -365,7 +387,8 @@ defmodule SentinelCp.Services.KdlGenerator do
365387
auth_policy_map,
366388
middleware_chain,
367389
internal_ca,
368-
plugin_chain
390+
plugin_chain,
391+
waf_policy_map
369392
) do
370393
lines = [" route #{inspect(service.route_path)} {"]
371394

@@ -411,7 +434,15 @@ defmodule SentinelCp.Services.KdlGenerator do
411434
lines = lines ++ build_path_rewrite_block(service.path_rewrite)
412435
lines = lines ++ build_tls_ref(service.certificate_id, cert_map)
413436
lines = lines ++ build_auth_block(service.auth_policy_id, auth_policy_map, internal_ca)
414-
lines = lines ++ build_security_block(service.security)
437+
438+
lines =
439+
lines ++
440+
build_waf_block(
441+
Map.get(service, :waf_policy_id),
442+
waf_policy_map,
443+
service.security
444+
)
445+
415446
lines = lines ++ build_request_transform_block(service.request_transform)
416447
lines = lines ++ build_response_transform_block(service.response_transform)
417448
lines = lines ++ build_traffic_split_block(service.traffic_split, group_map)
@@ -543,6 +574,59 @@ defmodule SentinelCp.Services.KdlGenerator do
543574
build_nested_map_block(sec, "security", " ")
544575
end
545576

577+
# WAF block: when a policy is bound, emit structured waf {} block.
578+
# When no policy but legacy security map exists, fall back to build_security_block.
579+
defp build_waf_block(nil, _waf_policy_map, security_map) do
580+
build_security_block(security_map)
581+
end
582+
583+
defp build_waf_block(waf_policy_id, waf_policy_map, _security_map) do
584+
case Map.get(waf_policy_map, waf_policy_id) do
585+
nil ->
586+
[]
587+
588+
%WafPolicy{} = policy ->
589+
effective_rules = Waf.get_effective_rules(policy)
590+
591+
lines = [" waf {"]
592+
lines = lines ++ [" mode #{inspect(policy.mode)}"]
593+
lines = lines ++ [" sensitivity #{inspect(policy.sensitivity)}"]
594+
595+
lines =
596+
if policy.max_body_size,
597+
do: lines ++ [" max_body_size #{policy.max_body_size}"],
598+
else: lines
599+
600+
lines =
601+
if policy.max_header_size,
602+
do: lines ++ [" max_header_size #{policy.max_header_size}"],
603+
else: lines
604+
605+
lines =
606+
if policy.max_uri_length,
607+
do: lines ++ [" max_uri_length #{policy.max_uri_length}"],
608+
else: lines
609+
610+
# Group effective rules by category
611+
by_category =
612+
effective_rules
613+
|> Enum.group_by(fn {rule, _action} -> rule.category end)
614+
|> Enum.sort_by(fn {cat, _} -> cat end)
615+
616+
category_lines =
617+
Enum.flat_map(by_category, fn {category, rules_with_actions} ->
618+
rule_lines =
619+
Enum.map(rules_with_actions, fn {rule, action} ->
620+
" rule #{inspect(rule.rule_id)} action=#{inspect(action)}"
621+
end)
622+
623+
[" category #{inspect(category)} {"] ++ rule_lines ++ [" }"]
624+
end)
625+
626+
lines ++ category_lines ++ [" }"]
627+
end
628+
end
629+
546630
defp build_request_transform_block(rt) when rt == %{} or rt == nil, do: []
547631

548632
defp build_request_transform_block(rt) do

lib/sentinel_cp/services/service.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ defmodule SentinelCp.Services.Service do
4646
belongs_to :upstream_group, SentinelCp.Services.UpstreamGroup
4747
belongs_to :certificate, SentinelCp.Services.Certificate
4848
belongs_to :auth_policy, SentinelCp.Services.AuthPolicy
49+
belongs_to :waf_policy, SentinelCp.Waf.WafPolicy
4950
belongs_to :openapi_spec, SentinelCp.Services.OpenApiSpec
5051

5152
has_many :service_middlewares, SentinelCp.Services.ServiceMiddleware
@@ -91,6 +92,7 @@ defmodule SentinelCp.Services.Service do
9192
:upstream_group_id,
9293
:certificate_id,
9394
:auth_policy_id,
95+
:waf_policy_id,
9496
:openapi_spec_id,
9597
:openapi_path,
9698
:project_id
@@ -142,6 +144,7 @@ defmodule SentinelCp.Services.Service do
142144
:upstream_group_id,
143145
:certificate_id,
144146
:auth_policy_id,
147+
:waf_policy_id,
145148
:openapi_spec_id,
146149
:openapi_path
147150
])

0 commit comments

Comments
 (0)