Privacy and masking
bddkit hides secrets in everything it prints or writes about a run: the failure text on the console, both reports, the event stream, the debug output on stderr, and the text of bddkit doctor and bddkit resource add. There is one set of secret values and one filter that replaces every occurrence of a value with ***.
A value enters the set in five ways.
- The step marks a parameter
secret.{key:secret}in a step declaration or a macro template matches what{key}matches, and the value joins the set when the step runs. A literal value is written***in the step text as well. The Hawk key, the AES key and the SRP password aresecret. - A variable’s name has a privacy word. A variable is sensitive when
secretis one of the_-separated words of its name, in any position and any case:access_token_secretandreg_secret_verifierare,secretary_idandsessionSecretare not. To mask a token an API returned, save it asas "login_secret"and write"Bearer <<login_secret>>"in the step that uses it. - An API resource names its sensitive headers.
authorization,cookieandset-cookieare masked by default.sensitive_headers: [x-api-key]adds to them;sensitive_headers_only: [x-api-key]replaces them. - The system under test names a field. A JSON key, form field or query parameter is sensitive when
password,secretortokenis one of its words, split on_,-and lower-to-upper case:access_tokenandrefresh-tokenare,tokenizeris not. The value is hidden where it stands; the text around it is unchanged. A string at leastmin_lengthcharacters long joins the set. - The configuration. Once
bddkit.yamlis parsed and its${…}references are expanded, the value under every key with a field word in its name (password,client_secret) and the password of every URL with credentials join the set. A secret that arrives through${DB_PASSWORD}or a.envfile is caught the same way.
Options
Section titled “Options”privacy: words: [sensitive, gdpr] # more words that make a variable name sensitive fields: [otp] # more field words for names the system under test chose min_length: 7 # shortest value that joins the set (default 7) reveal: false # true shows every valueA list given as words, fields or sensitive_headers adds to the default. words_only, fields_only and sensitive_headers_only replace it, and fields_only: [] switches that rule off. A value shorter than min_length is hidden where its position is known, but never joins the set, because hiding every 42 in the output would destroy it.
Every option has three forms, and the highest wins: flag, then environment variable, then config key, then the default.
| Config key | Flag | Environment variable |
|---|---|---|
privacy.words / words_only |
--privacy-words / --privacy-words-only |
BDDKIT_PRIVACY_WORDS / BDDKIT_PRIVACY_WORDS_ONLY |
privacy.fields / fields_only |
--privacy-fields / --privacy-fields-only |
BDDKIT_PRIVACY_FIELDS / BDDKIT_PRIVACY_FIELDS_ONLY |
privacy.min_length |
--privacy-min-length |
BDDKIT_PRIVACY_MIN_LENGTH |
privacy.reveal |
--privacy-reveal (or --reveal) |
BDDKIT_PRIVACY_REVEAL |
A list flag or variable takes comma-separated words, and an empty variable is unset. A boolean accepts true, false, 1 and 0.
Seeing everything
Section titled “Seeing everything”--reveal, privacy.reveal: true or BDDKIT_PRIVACY_REVEAL=1 turns masking off for a run, and says so on stderr. A reveal: true committed for one debugging session would turn masking off in CI too, so CI should set BDDKIT_PRIVACY_REVEAL=false, since the environment wins over the config.
The event stream’s run_started records what was in force, so a consumer can flag a run that arrived unmasked. bddkit doctor has a privacy row that states whether masking is on and which layer decided it.
What masking does not do
Section titled “What masking does not do”- It works forwards. A value is hidden from the moment it enters the set. A secret in a body that is not JSON, or under a name no field word matches, stays visible until a later step saves it into a sensitive variable.
- A field word can be too eager. With
tokenin the list,next_page_tokenis hidden wherever it appears. Usefields_onlywhen that hurts. - Database steps are not covered by
fields. A column calledpasswordinI have "users" with "password: …"is not recognised. Pass the value through a sensitive variable. - Derived values are not recognised. A hash, a signature or a ciphertext of a secret is a different string.
- Marking is by name.
_secerthides nothing. A family is hidden whole:as "secret_reg"hides every SRP variable of that login, the public ones included. - Some places hide a value only when it is long enough: a multipart text part, the message of an assertion that quotes a body, and
Print response body as "<path>". - A literal in a step’s own text reaches the event stream unmasked unless its parameter is
secret, because the stream records the step before the request that would register the value. - Images are not filtered.
- A masked assertion is harder to debug. Use
--reveal.
Upgrading from before the field words
Section titled “Upgrading from before the field words”Failure dumps changed for existing suites: Authorization, Cookie, Set-Cookie, and any JSON key, form field or query parameter with password, secret or token as a word, are now ***. Anyone who read those values from a failure needs --reveal or a fields_only list of their own.