0 | ||| Masking and hiding of sensitive values in log output.
 1 | |||
 2 | ||| `Masked a` and `Hidden a` mark a value as sensitive in its type: their
 3 | ||| `Loggable` instances redact or omit it, so the value cannot reach a log
 4 | ||| through its instance. The wrapped value stays available through
 5 | ||| `reveal`, which keeps every deliberate unmasking grep-auditable.
 6 | module Log4Types.Core.Masking
 7 |
 8 | import Data.List
 9 | import Data.String
10 | import Log4Types.Core.Value
11 | import Log4Types.Core.Renderer
12 | import Log4Types.Core.Loggable
13 |
14 | %default total
15 |
16 | ||| Wrap a value to redact it in log output.
17 | |||
18 | ||| The wrapped value remains accessible via `reveal`; only the `Loggable`
19 | ||| instance redacts it.
20 | public export
21 | record Masked (a : Type) where
22 |   constructor MkMasked
23 |   reveal : a
24 |
25 | ||| Wrap a value to omit it from log output entirely.
26 | |||
27 | ||| The wrapped value remains accessible via `reveal`; the `Loggable`
28 | ||| instance produces no output for it.
29 | public export
30 | record Hidden (a : Type) where
31 |   constructor MkHidden
32 |   reveal : a
33 |
34 | ||| A policy describing how to render a masked value.
35 | public export
36 | record MaskPolicy (a : Type) where
37 |   constructor MkMaskPolicy
38 |   mask : a -> String
39 |
40 | ||| Default policy: render as "***", ignoring the value.
41 | public export
42 | defaultMask : MaskPolicy a
43 | defaultMask = MkMaskPolicy (const "***")
44 |
45 | maskChar : Char
46 | maskChar = '*'
47 |
48 | ||| Policy: keep the last `n` characters and replace each earlier one with
49 | ||| "*". A value no longer than `n` is masked entirely as "***", so the
50 | ||| policy never reveals a whole value.
51 | public export
52 | lastN : Nat -> MaskPolicy String
53 | lastN n = MkMaskPolicy $ \s =>
54 |   let hidden = length s `minus` n
55 |   in if hidden == 0
56 |        then defaultMask.mask s
57 |        else replicate hidden maskChar ++ substr hidden n s
58 |
59 | ||| Policy: keep the domain of an email address and replace each character
60 | ||| of its local part with "*", so "alice@example.com" renders as
61 | ||| "*****@example.com". The split is at the last "@"; a value without one
62 | ||| is masked entirely as "***".
63 | public export
64 | emailDomainOnly : MaskPolicy String
65 | emailDomainOnly = MkMaskPolicy $ \s =>
66 |   case break (== '@') (reverse (unpack s)) of
67 |     (domain, _ :: local) => replicate (length local) maskChar ++ "@" ++ pack (reverse domain)
68 |     (_, [])              => defaultMask.mask s
69 |
70 | ||| Renders a masked value through `defaultMask`, whatever its type.
71 | public export
72 | Loggable (Masked a) where
73 |   logFields renderer m acc = renderer.addField "value" (StrVal (defaultMask.mask m.reveal)) acc
74 |   logShow m = defaultMask.mask m.reveal
75 |
76 | ||| Renders nothing for a hidden value.
77 | public export
78 | Loggable (Hidden a) where
79 |   logFields _ _ acc = acc
80 |   logShow _ = ""
81 |
82 | ||| Add a field rendered by an explicit masking policy, for a per-field
83 | ||| policy in a hand-written instance such as the last four digits of a
84 | ||| card number.
85 | public export
86 | addMaskedField
87 |   :  LogRenderer r
88 |   -> (name : String)
89 |   -> (policy : MaskPolicy a)
90 |   -> a
91 |   -> r -> r
92 | addMaskedField renderer name policy v =
93 |   renderer.addField name (StrVal (policy.mask v))
94 |