How do I build defense in depth in EK9?

← Security and Sanitization · Ref: Q272

Defense in depth means applying multiple security layers so that if one layer fails, the next catches the problem. EK9's security features compose naturally into a layered pipeline.

LAYER 1: SANITIZED ENTRY POINT

Mark untrusted input at the system boundary:

  processRequest()
    -> input as sanitized String

The compiler tracks this input through all subsequent operations.

LAYER 2: STRUCTURAL VALIDATION

Use a constrained type to validate the format:

  ValidEmail as String constrain as
    matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

Validate untrusted input with the fallible factory ValidEmail().of(value): it produces an unset object on a constraint failure (a bare ValidEmail(value) instead ASSERTS validity and Panics on a set invalid value).

LAYER 3: GUARD CHECK

Use a guard expression to verify validation succeeded:

  safeCopy <- String(input)
  if email <- ValidEmail().of(safeCopy)
    // Only proceeds with structurally valid data

LAYER 4: BUSINESS VALIDATION

Use require for domain-specific rules:

  require length $email > 5
  require $email contains "@company.com"

Failed require throws, preventing invalid business data.

LAYER 5: PURE PROCESSING

Process validated data in a pure function:

  pureTransform() as pure
    -> data as String
    <- result as String?

Purity prevents side effects and mutation during processing.

LAYER 6: OUTPUT GUARD

Guard the output before use:

  if result <- pureTransform(cleanData)
    stdout.println(result)

Ensures processing produced a valid result.

Each layer catches a different failure mode: Layer 1 catches missing sanitization at compile time. Layer 2 catches malformed input. Layer 3 prevents processing unset values. Layer 4 enforces business rules. Layer 5 prevents side effects. Layer 6 catches processing failures.

See Q54 for pure functions. See Q74 for guard expressions. See Q215 for sanitized parameters. See Q218 for security best practices. See Q257 for constrained types. See Q269 for input validation. See Q273 for purity as security boundary.

Example

defines module qa.security.defenseindepth

  defines type

    // Layer 2: Structural validation
    ValidEmail as String constrain as
      matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

  defines function

    // Layer 5: Pure processing
    pureTransform() as pure
      -> content as String
      <- result as String: "Empty"

      if content?
        result: "Processed: " + content

    // Complete defense-in-depth pipeline
    processRequest()
      // Layer 1: sanitized entry point
      -> input as sanitized String
      <- result as String: "Rejected"

      // Layer 2+3: Copy, validate via the fallible factory, and guard
      safeCopy <- String(input)
      if email <- ValidEmail().of(safeCopy)

        // Layer 4: Business validation
        require length $email > 5

        // Layer 5: Pure processing
        if processed <- pureTransform($email)

          // Layer 6: Output guard
          result: processed

  defines program

    DefenseInDepthDemo()
      stdout <- Stdout()

      // === FULL PIPELINE ===

      validResult <- processRequest("user@example.com")
      if validResult?
        stdout.println(validResult)

      // === LAYER 2 CATCHES BAD FORMAT ===

      invalidResult <- processRequest("not-email")
      if invalidResult?
        stdout.println("Should not reach here")
      else
        stdout.println("Bad format caught by constrained type")

      // === LAYERS SUMMARY ===

      stdout.println("Defense in depth layers:")
      stdout.println("  1. sanitized — marks untrusted input")
      stdout.println("  2. Constrained type — validates format")
      stdout.println("  3. Guard — checks construction")
      stdout.println("  4. require — business rules")
      stdout.println("  5. Pure — no side effects")
      stdout.println("  6. Output guard — validates result")

Common mistakes

E50001 — Renaming the variable means later references to 'validResult' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

validResultXYZ <- processRequest("user@example.com")

Correct:

validResult <- processRequest("user@example.com")

E50001 — Renaming the variable means later references to 'invalidResult' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

invalidResultXYZ <- processRequest("not-email")

Correct:

invalidResult <- processRequest("not-email")
Other ways to ask this
  • How do EK9 security features work together?
  • How do I create a layered security pipeline in EK9?
  • How do I combine sanitized, constrained, require, and pure in EK9?

Coming from another language?

Java: defense in depth requires manual layering of OWASP ESAPI, Bean Validation, Spring Security. Python: no compile-time enforcement, runtime-only layers. Rust: ownership model provides some layers but no taint tracking. Go: manual error checking at each layer. Kotlin: null safety as one layer, but no sanitization or purity. EK9: sanitized, constrained types, guards, require, and pure compose into a natural defense-in-depth pipeline with compile-time enforcement.

Keywords: depth, migrate, security, combine, defense, compile, protect, pipeline, multi, safe, layer, stack, chain