Skip to content

feat(policy): match undisclosed issues by title marker and restriction - #13

Merged
plusky merged 1 commit into
mainfrom
feat/undisclosed-issue-criteria
Jul 27, 2026
Merged

feat(policy): match undisclosed issues by title marker and restriction#13
plusky merged 1 commit into
mainfrom
feat/undisclosed-issue-criteria

Conversation

@plusky

@plusky plusky commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Two new match criteria, so a policy can recognise a security issue that is
not yet public:

  • summary_contains — case-insensitive substring search over the bug's
    one-line summary. Security teams mark such issues in the title and remove
    the marker at disclosure.
  • group_restrictedtrue matches a bug readable only through at
    least one Bugzilla group, false a world-readable one. It names no groups
    on purpose: vocabularies differ per instance and a rule keyed on names
    breaks the day one is renamed.

Either signal alone is enough, so a bug that loses one stays covered by the
other. examples/policy.toml uses both as deny rules rather than
restrict with a summary capability — for an undisclosed issue the title
is itself the sensitive part, and a summary view would hand over exactly the
line that must not leak.

Fail-closed model

Implementing this surfaced that "field missing" was being treated as "field
empty". BugMeta fields are now Option, where None means the bug object
did not carry the field (absent, null, wrongly typed, or a list with an
element the parser could not read). Matcher::evaluate returns
Yes/No/Unknown instead of a bool, and classify denies on Unknown
for every action: a deny rule denies because the bug may be what it looks
for, and a granting rule may neither grant on unchecked data nor be skipped,
because skipping would fall through to a later rule or to an allowing
default_action.

That closes three fail-open paths a bool could not express:

  1. a restrict rule under default_action = "deny" is a grant, so
    "unknown matches" handed out capabilities on unreadable metadata;
  2. a granting rule matching only on unknown data short-circuited the deny
    rule that would have caught the bug had the field been readable;
  3. a groups array with unreadable elements collapsed to an empty list —
    i.e. to "world-readable", the most permissive answer available.

A definitive No still beats Unknown, so a rule another criterion ruled
out stays ruled out, and only the fields a rule actually consults can make it
undecidable. "" and [] are knowledge, not ignorance.

Verification

  • 129 tests (was 115), including one asserting every criterion reports
    Unknown on its own unreadable field, and classify-level tests for the
    restrict-under-deny-default and short-circuit cases above.
  • Exercised end to end against a live Bugzilla: a bug created inside the
    freshness window is denied by id and withheld from search (a query
    returning two bugs upstream returns none through bugwarden), a disclosed
    17-day-old security bug is served in full, and 19 of 20 ordinary bugs are
    unaffected — the rules are not over-blocking.
  • examples/policy.toml loads; a deliberately typo'd key is still rejected
    at startup.

Notes

  • docs/DESIGN.md I4 is widened to cover any undecidable rule, and its
    Matcher/BugMeta declarations are brought back in sync.
  • README's fail-closed bullet said "never as allowed" while the criteria
    table implied otherwise; both now describe the same behaviour.
  • The example drops fresh-security-group: with a group_restricted = true
    rule ahead of it, first-match-wins made it unreachable.

A security issue that is not yet public is recognisable two ways, and a
policy could express neither: its title carries a marker the security team
removes at disclosure, and it is readable only through a Bugzilla group.

Add `summary_contains`, a case-insensitive substring search over the bug's
one-line summary, and `group_restricted`, which asks whether the bug is
readable only via at least one group. The latter deliberately does not name
groups: group vocabularies differ per instance and a rule keyed on names
breaks when one is renamed. Either signal alone suffices, so a bug that
loses one is still covered by the other. Both are deny rules in the shipped
example rather than restrict-with-summary, because for such a bug the title
is itself the sensitive part.

Make unreadable metadata undecidable rather than empty. `BugMeta` fields
become `Option`, where `None` means the bug object did not carry the field —
absent, null, wrongly typed, or a list with an element the parser could not
read. `Matcher::evaluate` returns `Yes`/`No`/`Unknown` in place of a bool,
and `classify` denies on `Unknown` for every action. A deny rule denies
because the bug may be what it looks for; a granting rule may neither grant
on unchecked data nor be skipped, since skipping would fall through to a
later rule or to an allowing `default_action`.

This closes three fail-open paths that a bool could not express:

  * a `restrict` rule under `default_action = "deny"` is a grant, so
    "unknown matches" handed out capabilities on unreadable metadata;
  * a granting rule matching only on unknown data short-circuited the deny
    rule that would have caught the bug had the field been readable;
  * a group array whose elements were unreadable collapsed to an empty list,
    i.e. to "world-readable" — the most permissive answer available.

A definitive `No` still wins over `Unknown`, so a rule another criterion
ruled out stays ruled out, and only the fields a rule actually consults can
make it undecidable.

The example policy gains the two rules, drops a group-based freshness rule
that first-match-wins had made unreachable, and states the unreadable-data
behaviour once instead of per criterion.
@plusky
plusky merged commit 95088e1 into main Jul 27, 2026
10 checks passed
@plusky
plusky deleted the feat/undisclosed-issue-criteria branch July 27, 2026 16:06
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.

1 participant