-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrules_api.go
More file actions
517 lines (438 loc) · 13.9 KB
/
rules_api.go
File metadata and controls
517 lines (438 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
// Rule management API endpoints
// rulesListHandler handles GET /_waf/rules - list all rules with pagination and filtering
func (w *WAF) rulesListHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse query parameters
query := r.URL.Query()
// Pagination
limit := 50
if l := query.Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 1000 {
limit = parsed
}
}
offset := 0
if o := query.Get("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
// Filters
var enabled *bool
if e := query.Get("enabled"); e != "" {
if e == "true" {
t := true
enabled = &t
} else if e == "false" {
f := false
enabled = &f
}
}
action := query.Get("action")
if action != "" && !isValidAction(action) {
http.Error(rw, "Invalid action parameter", http.StatusBadRequest)
return
}
// Query database
rules, total, err := w.ruleDB.ListRules(enabled, action, limit, offset)
if err != nil {
http.Error(rw, fmt.Sprintf("Failed to list rules: %v", err), http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"rules": rules,
"total": total,
"limit": limit,
"offset": offset,
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(response)
}
// ruleGetHandler handles GET /_waf/rules/{id} - get a specific rule
func (w *WAF) ruleGetHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ruleID := extractRuleIDFromPath(r.URL.Path, "/_waf/rules/")
if ruleID == "" {
http.Error(rw, "Rule ID is required", http.StatusBadRequest)
return
}
rule, err := w.ruleDB.GetRule(ruleID)
if err != nil {
if strings.Contains(err.Error(), "not found") {
http.Error(rw, "Rule not found", http.StatusNotFound)
} else {
http.Error(rw, fmt.Sprintf("Failed to get rule: %v", err), http.StatusInternalServerError)
}
return
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(rule)
}
// ruleCreateHandler handles POST /_waf/rules - create a new rule
func (w *WAF) ruleCreateHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var rule RuleRecord
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
http.Error(rw, "Invalid JSON payload", http.StatusBadRequest)
return
}
// Validate required fields
if rule.RuleID == "" || rule.Name == "" || rule.Pattern == "" {
http.Error(rw, "rule_id, name, and pattern are required", http.StatusBadRequest)
return
}
// Set defaults
if rule.Action == "" {
rule.Action = "block"
}
if !isValidAction(rule.Action) {
http.Error(rw, "Invalid action value", http.StatusBadRequest)
return
}
// Create rule in database
if err := w.ruleDB.CreateRule(&rule); err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
http.Error(rw, "Rule ID already exists", http.StatusConflict)
} else {
http.Error(rw, fmt.Sprintf("Failed to create rule: %v", err), http.StatusInternalServerError)
}
return
}
// Reload WAF rules
if err := w.reloadRulesFromDB(); err != nil {
// Rule was created but reload failed - log error but don't fail the request
fmt.Printf("Warning: Failed to reload WAF rules after creating rule %s: %v\n", rule.RuleID, err)
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusCreated)
json.NewEncoder(rw).Encode(map[string]interface{}{
"status": "success",
"message": "Rule created successfully",
"rule": rule,
})
}
// ruleUpdateHandler handles PUT /_waf/rules/{id} - update an existing rule
func (w *WAF) ruleUpdateHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ruleID := extractRuleIDFromPath(r.URL.Path, "/_waf/rules/")
if ruleID == "" {
http.Error(rw, "Rule ID is required", http.StatusBadRequest)
return
}
var updates RuleRecord
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(rw, "Invalid JSON payload", http.StatusBadRequest)
return
}
// Validate required fields
if updates.Name == "" || updates.Pattern == "" {
http.Error(rw, "name and pattern are required", http.StatusBadRequest)
return
}
// Set defaults
if updates.Action == "" {
updates.Action = "block"
}
if !isValidAction(updates.Action) {
http.Error(rw, "Invalid action value", http.StatusBadRequest)
return
}
// Update rule in database
if err := w.ruleDB.UpdateRule(ruleID, &updates); err != nil {
if strings.Contains(err.Error(), "not found") {
http.Error(rw, "Rule not found", http.StatusNotFound)
} else {
http.Error(rw, fmt.Sprintf("Failed to update rule: %v", err), http.StatusInternalServerError)
}
return
}
// Reload WAF rules
if err := w.reloadRulesFromDB(); err != nil {
// Rule was updated but reload failed - log error but don't fail the request
fmt.Printf("Warning: Failed to reload WAF rules after updating rule %s: %v\n", ruleID, err)
}
// Get updated rule
updatedRule, err := w.ruleDB.GetRule(ruleID)
if err != nil {
http.Error(rw, fmt.Sprintf("Rule updated but failed to fetch: %v", err), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]interface{}{
"status": "success",
"message": "Rule updated successfully",
"rule": updatedRule,
})
}
// ruleDeleteHandler handles DELETE /_waf/rules/{id} - delete a rule (soft delete)
func (w *WAF) ruleDeleteHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ruleID := extractRuleIDFromPath(r.URL.Path, "/_waf/rules/")
if ruleID == "" {
http.Error(rw, "Rule ID is required", http.StatusBadRequest)
return
}
// Check for hard delete parameter
hardDelete := r.URL.Query().Get("hard") == "true"
var err error
if hardDelete {
err = w.ruleDB.HardDeleteRule(ruleID)
} else {
err = w.ruleDB.DeleteRule(ruleID)
}
if err != nil {
if strings.Contains(err.Error(), "not found") {
http.Error(rw, "Rule not found", http.StatusNotFound)
} else {
http.Error(rw, fmt.Sprintf("Failed to delete rule: %v", err), http.StatusInternalServerError)
}
return
}
// Reload WAF rules
if err := w.reloadRulesFromDB(); err != nil {
// Rule was deleted but reload failed - log error but don't fail the request
fmt.Printf("Warning: Failed to reload WAF rules after deleting rule %s: %v\n", ruleID, err)
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]interface{}{
"status": "success",
"message": fmt.Sprintf("Rule %s successfully", map[bool]string{true: "permanently deleted", false: "disabled"}[hardDelete]),
})
}
// ruleHistoryHandler handles GET /_waf/rules/{id}/history - get rule change history
func (w *WAF) ruleHistoryHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ruleID := extractRuleIDFromPath(r.URL.Path, "/_waf/rules/")
if ruleID == "" {
http.Error(rw, "Rule ID is required", http.StatusBadRequest)
return
}
// Parse limit parameter
limit := 20
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
history, err := w.ruleDB.GetRuleHistory(ruleID, limit)
if err != nil {
http.Error(rw, fmt.Sprintf("Failed to get rule history: %v", err), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]interface{}{
"rule_id": ruleID,
"history": history,
"limit": limit,
})
}
// rulesStatsHandler handles GET /_waf/rules/stats - get rules database statistics
func (w *WAF) rulesStatsHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
stats, err := w.ruleDB.GetStats()
if err != nil {
http.Error(rw, fmt.Sprintf("Failed to get rules stats: %v", err), http.StatusInternalServerError)
return
}
// Add timestamp
stats["timestamp"] = time.Now()
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(stats)
}
// rulesExportHandler handles GET /_waf/rules/export - export rules to JSON
func (w *WAF) rulesExportHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
data, err := w.ruleDB.ExportToJSON()
if err != nil {
http.Error(rw, fmt.Sprintf("Failed to export rules: %v", err), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=waf_rules_%s.json", time.Now().Format("20060102_150405")))
rw.Write(data)
}
// rulesImportHandler handles POST /_waf/rules/import - import rules from JSON
func (w *WAF) rulesImportHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var payload struct {
Rules []WAFRule `json:"rules"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(rw, "Invalid JSON payload", http.StatusBadRequest)
return
}
if len(payload.Rules) == 0 {
http.Error(rw, "No rules provided", http.StatusBadRequest)
return
}
// Validate rules
for i, rule := range payload.Rules {
if rule.ID == "" || rule.Name == "" || rule.Pattern == "" {
http.Error(rw, fmt.Sprintf("Rule %d: id, name, and pattern are required", i), http.StatusBadRequest)
return
}
if !isValidAction(rule.Action) && rule.Action != "" {
http.Error(rw, fmt.Sprintf("Rule %d: invalid action value", i), http.StatusBadRequest)
return
}
}
// Create rules in database
created := 0
updated := 0
for _, rule := range payload.Rules {
ruleRecord := &RuleRecord{
RuleID: rule.ID,
Name: rule.Name,
Pattern: rule.Pattern,
Action: rule.Action,
RedirectURL: rule.RedirectURL,
Enabled: rule.Enabled,
Description: rule.Description,
}
// Set default action
if ruleRecord.Action == "" {
ruleRecord.Action = "block"
}
// Try to create, if exists then update
err := w.ruleDB.CreateRule(ruleRecord)
if err != nil && strings.Contains(err.Error(), "already exists") {
err = w.ruleDB.UpdateRule(rule.ID, ruleRecord)
if err == nil {
updated++
}
} else if err == nil {
created++
}
if err != nil {
http.Error(rw, fmt.Sprintf("Failed to import rule %s: %v", rule.ID, err), http.StatusInternalServerError)
return
}
}
// Reload WAF rules
if err := w.reloadRulesFromDB(); err != nil {
fmt.Printf("Warning: Failed to reload WAF rules after import: %v\n", err)
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]interface{}{
"status": "success",
"message": fmt.Sprintf("Successfully imported %d rules", len(payload.Rules)),
"created": created,
"updated": updated,
"total": len(payload.Rules),
})
}
// rulesReloadHandler handles POST /_waf/rules/reload - reload rules from database
func (w *WAF) rulesReloadHandler(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if err := w.reloadRulesFromDB(); err != nil {
http.Error(rw, fmt.Sprintf("Failed to reload rules: %v", err), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]interface{}{
"status": "success",
"message": "Rules reloaded successfully",
"timestamp": time.Now(),
})
}
// rulesRouteHandler routes requests for individual rules
func (w *WAF) rulesRouteHandler(rw http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasSuffix(path, "/history") {
// Handle /_waf/rules/{id}/history
w.ruleHistoryHandler(rw, r)
} else if path == "/_waf/rules/" {
// Handle /_waf/rules/ (create new rule)
w.ruleCreateHandler(rw, r)
} else {
// Handle /_waf/rules/{id} (get/update/delete)
switch r.Method {
case http.MethodGet:
w.ruleGetHandler(rw, r)
case http.MethodPut:
w.ruleUpdateHandler(rw, r)
case http.MethodDelete:
w.ruleDeleteHandler(rw, r)
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
}
// Helper functions
// extractRuleIDFromPath extracts rule ID from URL path
func extractRuleIDFromPath(path, prefix string) string {
if !strings.HasPrefix(path, prefix) {
return ""
}
ruleID := strings.TrimPrefix(path, prefix)
// Remove any additional path segments (for /rules/{id}/history)
if slashIndex := strings.Index(ruleID, "/"); slashIndex != -1 {
ruleID = ruleID[:slashIndex]
}
return ruleID
}
// isValidAction checks if the action is valid
func isValidAction(action string) bool {
validActions := map[string]bool{
"block": true,
"log": true,
"redirect": true,
}
return validActions[action]
}
// reloadRulesFromDB reloads rules from database into memory
func (w *WAF) reloadRulesFromDB() error {
if w.ruleDB == nil {
return fmt.Errorf("rule database not initialized")
}
rules, err := w.ruleDB.LoadRules()
if err != nil {
return fmt.Errorf("failed to load rules from database: %v", err)
}
// Update WAF rules
w.mu.Lock()
defer w.mu.Unlock()
w.rules = rules
// TODO: Recompile patterns for Vectorscan if needed
fmt.Printf("Successfully reloaded %d rules from database\n", len(rules))
return nil
}