faz
Getting Started

How faz Protects You

The five safety checks that every query passes through before it reaches your database, with examples of what each one catches.

faz wraps every query in a five-stage safety pipeline. Each stage runs in order. Any stage can block the request. If a stage blocks, the database is never touched and your assistant gets a structured error explaining exactly why.

This page walks through what each stage does, what it catches, and what it lets through. If you want the short answer: faz never executes a query you didn't authorize, even if your assistant generates one.

Why this matters

An AI assistant connected to a real database is one badly-pattern-matched suggestion away from DROP TABLE customers. Models hallucinate column names, confuse "delete the row" with "delete the table", paste in injection-shaped strings from training data, and occasionally try to be helpful by "cleaning up". The cost of one bad query depends on what the agent has access to — read-only is annoying, read-write is expensive, admin is unrecoverable.

The traditional answer is "don't connect AI to databases." faz's answer is to put a checkpoint between them. Your assistant sees a normal query interface. Your database sees only queries that have passed five independent safety checks. You get an audit trail of every query, allowed or blocked, written as structured JSONL.

The pipeline

Every query — whether it arrives via MCP, REST, or the CLI — goes through these five stages before execution.

   Query from your assistant


   ┌────────────────────────┐
   │  1. Prompt Guard       │  destructive intent in raw text
   ├────────────────────────┤
   │  2. RBAC Gate          │  per-table read / write / admin
   ├────────────────────────┤
   │  3. AST Checker        │  DDL backstop (CREATE / DROP / ALTER)
   ├────────────────────────┤
   │  4. Injection Analyser │  language-aware injection patterns
   ├────────────────────────┤
   │  5. Guardrails         │  LIMIT, timeout, size caps (rewrites)
   └────────────────────────┘


   Query executes against the database

The first four stages can return a BLOCK verdict. The fifth never blocks — it only rewrites the query to add safety bounds.

1. Prompt Guard

Catches destructive intent in raw query text before it ever reaches a parser. This stage exists for the case where free-form natural language slips into the request body — for example, an agent that pastes its user-facing instructions into a query field.

Three pattern families trigger a block:

  • Wipe / destroy / erase / purge / nuke + a table-shaped noun: "wipe all the records", "nuke the table".
  • Remove / delete all / every + records / rows / documents / entries / nodes / data: "delete all the customers", "remove every node from the graph".
  • Drop the / all + database / tables / collections / schemas: "drop the database", "drop all tables".

Read-context phrases at the start of the prompt — show, list, find, get, fetch, retrieve, search, query, display, count, how many, select — preempt the destructive match. "show me deleted records" passes. "select rows where notes contain 'wipe everything'" passes. The destructive pattern only fires when it's the actual request, not when it's incidentally mentioned inside a read.

The block message looks like this:

Destructive intent detected: 'drop the database'

2. RBAC Gate

Per-database, per-table permission check. This is the primary access-control surface; the AST Checker that follows is a backstop.

Permissions are declared in faz.yaml:

permissions:
  - database: <database>          # baseline + per-table overrides for this DB
    access: R                     # baseline for every table in this database
    tables:                       # optional per-table overrides
      <writable-table>: RW
      <audit-table>: W            # write-only — agent can append, can't read

For each query, faz extracts every table the query touches — including those reached through JOIN, MongoDB $lookup, Cypher rebound labels, and similar — then evaluates each one independently against the policy. Lookup is layered: per-table override wins over per-database baseline; an unmatched table defaults to deny.

The seven access codes are R, W, RW, RA, RWA, A, none. The full matrix is on Permission levels; the conceptual walkthrough is on Permissions.

Block messages quote the exact ask versus what was granted:

Access denied: <database>.<sensitive-table> requires permission for SELECT; policy grants none

For a federated query that touches multiple databases, the default behaviour is partial denial: denied steps are silently dropped from the request and the remaining steps continue. This keeps a multi-database ask useful even when one database is off-limits. If you want the whole request to fail when any step is denied, that's configurable.

3. AST Checker

Defense-in-depth backstop for DDL. It re-extracts every target the query touches and re-checks any whose operation is DDL — CREATE, DROP, ALTER, TRUNCATE, schema mutation. DDL passes only when the matching policy grants A (Admin). Every other access level — including RW and RWA — fails here.

Why a second check after RBAC already gated DDL? Because the cost of a wrong CREATE/DROP getting through is unbounded, and re-extracting from the parsed AST catches any future change to RBAC's extraction layer that might silently let DDL slip past.

Block message:

DDL is not permitted: <database>.<table> (CREATE / DROP / ALTER / TRUNCATE / schema mutation / etc.)

If you legitimately need DDL — running a migration, creating an index — set the database's access to A in faz.yaml. Use this sparingly. We'd suggest running schema changes outside faz with your normal migration tooling.

A (Admin) is the only level that lets DDL through. Granting A to a database means an agent error away from a DROP TABLE. Reserve it for development databases or for the brief moment you're running a migration.

4. Injection Analyser

Language-aware pattern scanner. The set of dangerous things you can put in a query depends on the query language; faz checks for each language's specific footguns.

SQL blocks: tautology injection (WHERE 1=1, OR 'a'='a'), comment-escape sequences (--, /*, #), stacked statements (...; DROP TABLE x), blind-probe functions (SLEEP, WAITFOR DELAY, BENCHMARK, PG_SLEEP), out-of-band exfiltration (LOAD_FILE, INTO OUTFILE, UTL_HTTP, DBMS_PIPE), and stored-procedure abuse (xp_cmdshell, EXEC sp_*, CALL). Also: SELECT * and queries against information_schema / pg_catalog get warnings (not blocks). Queries with subquery depth above five also warn.

MongoDB blocks: $where, $function, and $accumulator (server-side JavaScript), $out and $merge (write pipeline stages), and any value containing JavaScript-shaped patterns (function(, this., sleep(, while(, for().

Cypher blocks: dangerous APOC procedures (apoc.load.*, apoc.cypher.run, apoc.periodic.*, apoc.export.*), LOAD CSV FROM (file access), and stacked Cypher with semicolons followed by write keywords.

Elasticsearch DSL blocks: any body containing script, scripted_field, script_fields, or runtime_mappings — Painless / scripted queries can execute arbitrary code.

Vector databases, DynamoDB, and CouchDB don't have a query-language injection surface at this layer; the connector itself enforces operation-level safety.

5. Guardrails

This stage never blocks. It rewrites the query to add safety bounds before execution.

The defaults: row cap at 1000 rows, timeout at 30 seconds, max subquery depth of three (warning, not block). The defaults can be overridden per-instance with the safety: block in faz.yaml.

What rewriting looks like per language:

  • SQL: LIMIT 1000 injected if absent, capped if larger, dialect-aware (Oracle gets FETCH FIRST n ROWS ONLY).
  • MongoDB: $limit: 1000 appended to aggregation pipelines, limit: 1000 set on find operations, maxTimeMS: 30000 set on every query.
  • Cypher: LIMIT 1000 appended (only when there's a RETURN or WITH clause to attach to).
  • Elasticsearch DSL: size: 1000 set, timeout: 30s set.

Vector, DynamoDB, and CouchDB are pass-through — the connector enforces its own bounds.

Your assistant doesn't see the rewrite; the audit log does. So an agent asking for SELECT * FROM events against a 200-million-row table gets a thousand rows back, not a server kicked off the planet.

What blocking looks like

When a stage blocks, faz returns a structured envelope. Same shape whether you came in via MCP or REST:

{
  "request_id": "req_a1b2c3",
  "status": "blocked",
  "error": {
    "stage": "RBAC_GATE",
    "reason": "Access denied: <database>.<sensitive-table> requires permission for SELECT; policy grants none",
    "suggestion": null
  }
}

Your assistant sees stage and reason and can decide whether to rephrase, ask the user, or give up. The block is also written to the audit log with full context — which target was hit, which policy was in effect, when, and from which transport.

What success looks like

When all five stages pass, the response includes which stages ran:

{
  "request_id": "req_a1b2c3",
  "status": "ok",
  "data": {
    "columns": ["<column-1>", "<column-2>"],
    "rows": [...],
    "row_count": 42
  },
  "safety": {
    "stages_passed": ["PROMPT_GUARD", "RBAC_GATE", "AST_CHECKER", "INJECTION_ANALYSER", "GUARDRAILS"],
    "warnings": []
  },
  "metadata": {
    "execution_time_ms": 23.4,
    "transport": "mcp/stdio"
  }
}

The same record, with full per-stage context, gets written to .faz/audit.jsonl.

On this page