Day 20 — regexp
Learning Objectives
- Pre-compile regex patterns at startup — never inside a hot path
- Use named capture groups for structured log parsing
- Build the pattern-matching foundation for WAF rules (Day 69-71)
Key Concepts
// WRONG: re-compiles on every call — O(n) compilation per request
func isEmail(s string) bool {
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`, s)
return matched
}
// RIGHT: compile once at package init
var emailRe = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
func isEmail(s string) bool {
return emailRe.MatchString(s)
}
// Named capture groups — extract structured data
var logRe = regexp.MustCompile(
`^(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<bytes>\d+)`,
)
Exercise: Nginx Log Parser + PII Detector
package main
import (
"bufio"
"fmt"
"regexp"
"strings"
)
// Pre-compiled patterns — NEVER compile inside a loop or handler
var (
// Nginx combined log format
nginxRe = regexp.MustCompile(
`^(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>[^"]+) HTTP/[^"]*" (?P<status>\d+) (?P<bytes>\d+)`,
)
// WAF-relevant patterns (will expand in Week 10)
sqlKeywordsRe = regexp.MustCompile(`(?i)\b(UNION|SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)\b`)
xssPatternRe = regexp.MustCompile(`(?i)(<script|javascript:|on\w+=|<iframe)`)
pathTraversalRe = regexp.MustCompile(`(\.\.[\\/]|%2e%2e)`)
// PII patterns — for log scrubbing (Day 79)
emailRe = regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`)
creditCardRe = regexp.MustCompile(`\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b`)
)
type LogEntry struct {
IP string
Time string
Method string
Path string
Status string
Bytes string
Flags []string
}
func parseNginxLog(line string) (*LogEntry, bool) {
m := nginxRe.FindStringSubmatch(line)
if m == nil { return nil, false }
names := nginxRe.SubexpNames()
entry := &LogEntry{}
for i, name := range names {
switch name {
case "ip": entry.IP = m[i]
case "time": entry.Time = m[i]
case "method": entry.Method = m[i]
case "path": entry.Path = m[i]
case "status": entry.Status = m[i]
case "bytes": entry.Bytes = m[i]
}
}
// Scan path for attack patterns
if sqlKeywordsRe.MatchString(entry.Path) {
entry.Flags = append(entry.Flags, "SQL_INJECTION")
}
if xssPatternRe.MatchString(entry.Path) {
entry.Flags = append(entry.Flags, "XSS")
}
if pathTraversalRe.MatchString(entry.Path) {
entry.Flags = append(entry.Flags, "PATH_TRAVERSAL")
}
return entry, true
}
// MaskPII replaces PII patterns with redacted markers
func MaskPII(s string) string {
s = emailRe.ReplaceAllString(s, "[EMAIL]")
s = creditCardRe.ReplaceAllString(s, "[CARD]")
return s
}
func main() {
logs := `
192.168.1.1 - - [10/Jan/2025:10:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234
10.0.0.1 - - [10/Jan/2025:10:00:01 +0000] "GET /api/data?id=1 UNION SELECT * FROM users-- HTTP/1.1" 200 0
203.0.113.5 - - [10/Jan/2025:10:00:02 +0000] "POST /login?email=alice@example.com HTTP/1.1" 200 256
172.16.0.1 - - [10/Jan/2025:10:00:03 +0000] "GET /../../etc/passwd HTTP/1.1" 404 0
`
scanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(logs)))
for scanner.Scan() {
line := MaskPII(scanner.Text())
entry, ok := parseNginxLog(line)
if !ok { continue }
flagStr := ""
if len(entry.Flags) > 0 {
flagStr = " [" + strings.Join(entry.Flags, ",") + "]"
}
fmt.Printf("%s %s %s %s%s\n", entry.IP, entry.Method, entry.Path, entry.Status, flagStr)
}
}
Real-World Context
Pre-compiled regex is non-negotiable in a WAF. At 10k rps, compiling a regex per request adds ~50µs per request = 500ms wasted CPU per second. Your WAF will have 20+ patterns; compile them once at startup in var declarations.