Skip to content
Auditability & lineage

Auditability & lineage

FACE keeps two different records. The audit log records security-relevant events: who did what, and which control refused what. The lineage log records data movement: which source was read, what shape came back and where it went. Neither record contains the data itself.

Audit logging

Always on

Audit logging is always on, and that is a property of the code, not a setting. extractors.RecordAuditLog runs unconditionally, and no code path disables it. The environment variable ENFORCE_AUDIT_LOGGING gates nothing (see Compliance posture).

No record is dropped silently. Records normally go through an in-process actor. If that actor failed to start, the hook is never installed and records are written synchronously. If a send to the actor fails later, FACE switches to synchronous writes from then on, says so once in the log, and loses no record.

What a record contains

Each audit record is structured JSON with a timestamp, an event type, an actor ID, a target ID and a details map.

  • Actor and target identifiers are hashed with SHA-256 before they are written. The hash is deterministic, so an assessor can still group events by actor without the log exposing the raw identifier. The system actor SYSTEM is recorded as is.
  • Refusals are recorded. Guardrail refusals include the rule ID, severity, category, compliance tags and the action PROMPT_REJECTED. Voice-line refusals record the reason and the control that fired, but never the caller’s words. An agent email downgraded to a draft records the recipient only as a hash.

Where records go

FACE writes audit records to its standard output as structured JSON, beside its OpenTelemetry-shaped application logs. The platform’s log pipeline collects them from there.

Request logging

The unary request interceptor logs only the method name and the request size in bytes. Request bodies are never logged, because they carry prompts, data rows and document content. Error strings that reach logs on some paths are passed through a PII masker, which redacts email addresses, phone numbers, card numbers, IP and MAC addresses, and fields named like passwords, tokens, secrets, API keys, webhooks or MFA codes.

Observed data lineage

grpc/internal/lineage records observed data movement, one immutable record per event. It does not describe lineage from a schema. It records reads that actually happened.

Where it is captured

  • Every tabular connector. extractors.ConnectorForType returns every connector wrapped in a lineage decorator. All connectors are covered with no per-connector code.
  • Camera ingests. Frames pushed by a camera do not pass through the connector seam, so the ingest path records them itself: one record per ingest on every return path, failures included.
  • Deliveries. The connector does not know where its result will go, so the calling pipeline attaches the destination afterwards. It does so only on the branch where the rows actually landed.

What a record contains

A lineage record carries structure only. Every field is an identifier, a structural name, a count, a duration or a fingerprint:

Field groupContents
Kindextraction (a source was read), delivery (a result landed somewhere) or discovery (a source described its own catalogue, and no business data moved)
Source identityConnection ID, connection name, connector type, environment, runner. Taken from the connection’s labels only, never from its configuration or credentials.
What was askedA literal-free query shape plus its fingerprint
What came backColumn names and a row count
Outcomeok, empty or error. An error is kept as a closed-set error class plus a SHA-256 fingerprint of the driver message. The message itself is never stored.
DestinationKind, ID and name of the sink (delivery records only)
CorrelationRecord ID, reference to the originating extraction, trace ID, timestamp, duration

Never recorded: cell values, sample rows, result bodies, driver error text, credentials or connection configuration. The recorder is never given the result body, and no field on it could carry one. TestNoResultValuesReachAnyLineageRecord fills every value-shaped input with unique sentinel strings and asserts that none of them survives into a record or into the encoded file. Every field has a length limit, so a record’s size is bounded and measured.

Where it is stored

Lineage records are append-only Avro files in the object store, partitioned by UTC day. The object store seals every object it writes (see Encryption at rest). The cockpit’s source-activity and lineage views read this log.

What these controls do not establish

  • Retention and tamper-evidence belong to the log pipeline. FACE emits audit records to standard output. How long they are kept, who can read them and whether they are tamper-evident are properties of the log pipeline your deployment runs, not of FACE.
  • Hashing is pseudonymisation, not anonymisation. Anyone who already knows an identifier can compute its hash and find that identifier’s events.
  • Identifier hashing covers two fields. It applies to the actor and target. Details are written by the code that emits each event. They are reviewed per call site, not masked automatically.
  • Lineage records reads, not everything a person saw. The live camera detection path returns boxes to the cockpit and deliberately writes no lineage record. A 4–10 frames-per-second loop would swamp a log sized for ingests. “This camera has never been read” in the lineage view is therefore a statement about ingests.
  • A camera ingest records zero rows. A camera sends frames, not rows. The record therefore calls the ingest an extraction with a row count of zero, and carries no frame count.
  • A query fingerprint is not the query. It proves two reads had the same shape. It cannot reconstruct the values that were used.