Module Algostream_infrastructure_persistence.Audit_record

One audit entry, and its canonical byte encoding.

What a record has to carry

Enough to answer "who changed what, when, and did it work" without a second lookup. In particular both body_sha256 and body_excerpt: the allocation endpoint carries its interesting value in the request body, so a record without it says a reallocation happened but not to what. The excerpt makes the common case readable; the digest still covers a body larger than the excerpt.

label and scopes are snapshotted at the time of the action, not looked up when the log is read. Relabelling or rescoping a key later must not rewrite history.

Why the encoding is defined here rather than reusing bin_prot

The chain in Audit_log hashes canonical, not a bin_prot serialisation. Coupling tamper-evidence to a serialisation library's internal layout would mean a library upgrade could silently invalidate every historical record, or — worse — quietly change what a hash commits to.

Every variable-length field is length-prefixed, and that is load-bearing rather than tidiness: without it ("ab", "c") and ("a", "bc") encode to identical bytes, and an attacker could move a field boundary without disturbing the hash.

type outcome =
  1. | Allowed
  2. | Denied of string
    (*

    "no credential", "insufficient scope", "rate limited", …

    *)
  3. | Failed of string
    (*

    the handler raised, or answered 5xx

    *)
val outcome_to_string : outcome -> string
type t = {
  1. seq : int64;
    (*

    1-based and gapless within a file; a gap is evidence, not an accident

    *)
  2. ts_ns : int64;
    (*

    wall clock — an audit trail is read against real time, unlike event time

    *)
  3. kid : string;
    (*

    "-" when anonymous

    *)
  4. label : string;
  5. scopes : string;
    (*

    comma-joined, as evaluated at the time

    *)
  6. peer : string;
    (*

    "127.0.0.1:54321"

    *)
  7. meth : string;
  8. path : string;
    (*

    the concrete path, e.g. "/api/strategies/pairs-1/stop"

    *)
  9. route : string;
    (*

    the pattern, e.g. "/api/strategies/:id/stop"

    *)
  10. params : (string * string) list;
    (*

    sorted by key before encoding

    *)
  11. body_sha256 : string;
  12. body_excerpt : string;
    (*

    first excerpt_bytes bytes of the body

    *)
  13. outcome : outcome;
  14. status : int;
}
val excerpt_bytes : int
val make : ts_ns:int64 -> kid:string -> label:string -> scopes:string -> peer:string -> meth:string -> path:string -> route:string -> params:(string * string) list -> body:string -> outcome:outcome -> status:int -> t

Build a record, hashing and truncating body and sorting params. seq is assigned by the writer, so it is 0L here.

val canonical : t -> string

The bytes the hash chain commits to. Injective: see the interface header.

val to_line : t -> string

A single line for auditctl tail and for humans. Not the hashed form.