Skip to content

Declare which plugin columns are credentials, and which only look like it - #22

Merged
mastacontrola merged 1 commit into
mainfrom
declare-plugin-credentials
Aug 21, 2026
Merged

Declare which plugin columns are credentials, and which only look like it#22
mastacontrola merged 1 commit into
mainfrom
declare-plugin-credentials

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

Four bundled plugins carry a friendly key matching Redaction::CREDENTIAL_PATTERN and none of them said what it was.

Two are real credentials the API was emitting. pushbullet and slack both put their class in $validClasses via their own api hooks, so every list the API returned carried the token in clear to any caller holding pushbullet.view / slack.view. Holding either token is being able to post as that account. Both go in the always tier — nothing reads them back, the same reasoning as ldap.bindPwd.

One is a product key. windowskey.key is the same kind of value as core's host.productKey, so it goes in the tier core puts that one in: the ordinary tier, stripped from API lists and kept on a direct single-entity GET. The plugin's own pages are unaffected — they read the model, and the list grid is served by the web tier at ?node=windowskey&sub=list, not by the API emitter that strips.

Two only look like credentials, declared through the new exempt bucket:

field what it actually is
capone.key the DMI string capone matches an image on — the form calls it "Key to match", and the unauthenticated capone endpoint posts it in the clear on every lookup
windowskeyassociation.windowskeyID an integer foreign key, matching only because "key" is in the plugin's name

Core cannot answer either on a plugin's behalf: the bundled plugins are a fetched artifact (ADR 0009), so a core entry naming a plugin class fails on any tree that has not fetched them — a fresh clone, or CI. That is why the exempt bucket exists.

Depends on

fogproject's side of the seam (API_SENSITIVE_FIELDS gaining the exempt bucket). Without it the bucket is not passed to the event and the two exemptions are ignored — which leaves those fields redacted, the safe direction. The credential declarations work either way.

Verified

  • tests/run-all.sh here: 7 passed, 0 failed.
  • Against fogproject's tests/audit-redaction-coverage.test.php with the seam in place: 66 checks, 0 failures, and four mutations confirmed the gate catches each declaration being removed or pointed at a renamed column.

🤖 Generated with Claude Code

…e it

Four models carry a friendly key matching Redaction::CREDENTIAL_PATTERN and
none of them said what it was, so fogproject's coverage gate had nothing to
check them against. Two are real credentials that the API was handing out:

  pushbullet.token and slack.token. Both classes are in $validClasses --
  their own api hooks put them there -- so every list the API returned
  carried the token in clear to any caller holding pushbullet.view or
  slack.view. Holding either is being able to post as that account. Both go
  in the 'always' tier, for the same reason ldap.bindPwd is there: nothing
  reads them back, only the web tier sends them, and it does so through the
  model.

  windowskey.key is a Windows product key, the same kind of thing as core's
  host.productKey, so it goes in the tier core puts that one in -- the
  ordinary tier, stripped from API lists and kept on a direct single-entity
  GET. That stops a bulk dump of every key an install holds without
  breaking a caller entitled to one. The plugin's pages are unaffected: they
  read the model, and the list grid is served by the web tier at
  ?node=windowskey&sub=list, not by the API emitter that strips.

The other two only look like credentials, and say so through the 'exempt'
bucket the hook now carries:

  capone.key is the DMI string capone matches an image on -- the edit form
  calls it "Key to match" -- and the unauthenticated capone endpoint posts
  it in the clear on every lookup. Redacting it would protect a value the
  protocol publishes anyway while blanking the column that says which rule
  an audit row changed.

  windowskeyassociation.windowskeyID is an integer foreign key that matches
  only because the word "key" is in the plugin's name.

Core cannot hold either answer on a plugin's behalf: the bundled plugins are
a fetched artifact (ADR 0009), so a core entry naming a plugin class fails
on any tree that has not fetched them, including a fresh clone and CI. The
'exempt' bucket exists so the plugin can answer for its own model.

Needs fogproject's side of the seam: without it the exempt bucket is not
passed to the event and these two declarations are ignored -- which leaves
them redacted, the safe direction.

Co-Authored-By: Claude <noreply@anthropic.com>
@mastacontrola
mastacontrola merged commit 545f1e5 into main Aug 21, 2026
2 checks passed
@mastacontrola
mastacontrola deleted the declare-plugin-credentials branch August 21, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants