$ complex

$ cat privacy.md

Information flow control

A label on a field is the whole privacy policy. The compiler carries it everywhere the value goes and refuses every path to a page, an attribute, a branch, a loop or a log line. The only way across is explicit, and every crossing is listed.

$ cat model.complex

model Citizen {
  name:   Text
  city:   Text
  email:  Text @personal
  bsn:    Text @secret
}

Three labels, ordered: public < personal < secret. Anything computed from labelled data carries the highest label of its inputs. A secret can only leave through masked(); personal data only through reveal().

$ complex check leak.complex

This does not compile. Four leaks, four errors, each with a line number and a fix:

page "/citizen/:id" (id: Int) {
  let c = Citizen[id]
  log "viewed " + c.email        // error: Personal<Text> cannot be written to the log
  view {
    p { c.bsn }                    // error: Secret<Text> cannot be shown in a view
    if c.bsn == "999990019" { }    // error: cannot branch on secret data
    a(href: "/x?e=" + c.email) { } // error: Personal<Text> cannot be placed in 'href'
  }
}

$ cat fixed.complex

page "/citizen/:id" (id: Int) {
  let c = Citizen[id]
  log "viewed citizen " + id
  view {
    p { c.name }
    p { masked(c.bsn, 3) }          // ok: explicit, and listed by `complex audit`
    p { reveal(c.email) }           // ok: explicit, and listed by `complex audit`
  }
}

$ complex audit site

The audit is not a document someone wrote. It is derived from the source by the compiler, so it cannot drift from what the code does:

$ complex audit site

labelled data
  Citizen.email                personal
  Citizen.bsn                  secret

declassifications (2)
  privacy.complex:62:21  masked()  secret -> public
      p { masked(c.bsn, 3) }
  privacy.complex:63:21  reveal()  personal -> public
      p { reveal(c.email) }

result: no labelled value reaches a sink except through the declassifications above

$ ls /citizen

Live, from this very model:

Or by id: /citizen/1, /citizen/2, /citizen/3. Try /citizen/9 for a 404.