Frontend Labs
⋯

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.


Generated from week3.md · 8 Oct 2026 16:36