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 the parameter that receives untrusted input at the system boundary:

  processRequest()
    -> input as sanitized String
    <- result as String: input

The RECEIVING function cleans the parameter the moment it is entered, at run time, on every route into it: a detected threat (an injection, a script) arrives as an UNSET String and is logged. The compiler guarantees the cleaning step exists - the author cannot forget it - but it does not track the value afterwards.

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 declaration guard 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
    -> content as String
    <- result as String: "Processed: " + content

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 neutralises injection and script payloads on entry. 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 declaration guards. 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: String()

      if content?
        result: "Processed: " + content

    // Complete defense-in-depth pipeline
    processRequest()
      // Layer 1: sanitized entry point - cleaned on entry, at run time
      -> input as sanitized String
      <- result as String: String()

      // 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 ===

      //A rejected request leaves the result unset
      invalidResult <- processRequest("not-email")
      if not invalidResult?
        stdout.println("Bad format caught by constrained type")

      // === LAYERS SUMMARY ===

      stdout.println("Defense in depth layers:")
      stdout.println("  1. sanitized — cleans input on entry")
      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

E08120 — The processing layer is pure, and a pure scope may not use mutation operators such as '+=' - not even on its return variable. Assign the newly computed value with ':' instead. See ek9 -h E08120 for details.

Incorrect:

        result += "Processed: " + content

Correct:

        result: "Processed: " + content
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; the compiler guarantees each layer is present, and the sanitizing and constraint checks run on every call.

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