Files
nolimit-security-dist/docs/CONTRACT.md
T

8.1 KiB
Raw Permalink Blame History

What this plugin offers, and what it asks for

Written from the shipped source, and checked against it in both directions on every release: a hook in the code and not in this file fails the build, and a hook in this file and not in the code fails it too.

Three roles, because they are not the same thing and add_filter is how you answer a filter, not how you consume one:

  • Offers — do_action / apply_filters on a nlsec_ name. Yours to hook.
  • Answers — add_filter on a name a consumer applies. We supply the value.
  • Asks — apply_filters on a wpmgr_ name. Somebody else supplies it.

Offers — ten hooks on our own prefix

Hook Kind Fires when Notes for a consumer
nlsec_request_blocked action Just before a request is refused The only place a refusal is observable. It fires only when somebody is being refused, so anything here runs on the path a real visitor is stuck on.
nlsec_finding_recorded action A scanner or vulnerability finding is written Carries a machine-readable code. Match on that, never on the title — a rule matching a title dies the day anybody translates one.
nlsec_scan_complete action A scan finishes Fires on a clean scan too. "No findings" and "never ran" are different states.
nlsec_privileged_login action An administrator signs in
nlsec_recovery_code_used action A two-factor recovery code is spent Worth alerting on. A spent recovery code means somebody lost their second factor, or somebody else found it.
nlsec_ai_briefing action The analyst produces a briefing
nlsec_ai_acted action The analyst acts on its own proposal
nlsec_firewall_rules filter Rules are assembled Add or remove rules.
nlsec_malware_signatures filter Signatures are assembled A pattern that does not compile is reported by NLSEC_Signatures::broken(), not turned into a warning on every file.
nlsec_public_rest_routes filter REST lockdown decides what stays public Read this one if you ship REST routes. When hardening locks the REST API down, only the prefixes in this list stay reachable to a logged-out visitor. Defaults include /wc/store, /contact-form-7, /wpcf7. A plugin whose routes a shop's front end needs should add its prefix here rather than ask the owner to switch the lockdown off. Off by default, because most modern themes use REST from the front end.
nlsec_request_reputation filter — Defined here, answered here, applied by the consumer. See below.

Answers — four

Hook Who applies it What we return
nlsec_request_reputation Any plugin that wants a verdict on a submitter An array, or the caller's null untouched if we are absent.
wpmgr_dashboard_cards shared-menu.php Our dashboard card.
wpmgr_signals Nolimit Assistant, and nothing else Our signals.
wpmgr_setup_recommendations Nolimit Assistant, and nothing else Three hardening switches for its Setup screen: hide the WordPress version and turn off pingbacks (both safe, both with an undo), and browser security headers (confirm). Each done is read from the stored setting, each apply returns true or a WP_Error, and none offers a number or a secret. We answer it and never apply it.

wpmgr_signals is collected by Nolimit Assistant alone. With that plugin deactivated our signals go to a filter nobody applies, and nothing raises an error — the answering side of a filter cannot tell whether anyone ever collected it. A collector has to live somewhere; anyone reading a family-wide signal list is reading Assistant's.

nlsec_request_reputation in full

$verdict = apply_filters( 'nlsec_request_reputation', null, $ip );

$ip is optional and defaults to the current request. null back means nobody answered — read it as "nothing is known", never as "clean". The consumer keeps its own fallback and carries no hard dependency on us.

Returns:

Field
suspicious bool
exempt bool — the address is on the site owner's allowlist
reason a sentence for a human
code blocked-now, on-threat-list, blocked-recently, near-rate-limit, allowlisted, or ''
severity 0–5
measured which of allowlist, blocks, lists, log, rate actually answered
ip the address judged

Severity is pinned and asserted by the plugin's own tests, because a consumer branches on the number: blocked-now 5, on-threat-list 4, blocked-recently 2–4, near-rate-limit 2, allowlisted 0. Moving one is a red test that names who to tell.

measured is the field to wire in. "Nothing known about this address" and "I could not look" are both zeros; measured is how a caller tells them apart. If log is missing our log table was unreadable, and a clean verdict from four sources is not the same claim as one from five.

exempt is stated, not implied. suspicious => false is what a clean stranger gets too. An allowlisted address must never be refused through us, because we do not refuse it ourselves: run_main() checks the allowlist before the block list, so an allowlisted address carrying a stale block row is waved past by our own firewall.

What a consumer must not do:

  • Do not treat null as clean.
  • Do not act on severity without also handling exempt.
  • Do not check the current user's capability through us — this filter takes an arbitrary address, which need not be the requester's, so "is this a logged-in administrator" is a question only the caller can answer.
  • Do not both answer and apply this filter. A provider that also reads the filter it answers builds an infinite mutual recursion, invisible in either file because each half is three obvious lines. The only apply_filters( 'nlsec_request_reputation' ) in our shipped tree is inside the docblock above it.

wpmgr_dashboard_cards is answered and applied in this tree, and that is safe only because the two are in different files doing different jobs: class-plugin.php adds our card, shared-menu.php collects everybody's. Our callback does not re-apply. That is the line; it is one edit away from being crossed.

Emits to the family — one

Hook Kind Who listens What it carries
wpmgr_ai_usage action Nolimit Assistant ( 'nolimit-security', array( 'provider', 'model', 'input', 'output', 'purpose' ) ) — what this plugin spent on an AI call.

The point of it is that one plugin can show one honest total rather than four partial ones. It is a wpmgr_ name we emit rather than ask for.

Asks — four

Hook Who answers What we do when nobody does
wpmgr_ai_credentials Nolimit Assistant The analyst stays off. No error, no nag.
wpmgr_dashboard_cards Every family plugin Draw the cards we got.
wpmgr_menu_title Any family plugin Fall back to the built-in title.
wpmgr_menu_glyph Any family plugin Fall back to the built-in glyph.

The card contract

The shared menu reads exactly title, colour, status, headline, lines, url and action from a card. Four mistakes are easy to make:

  • href instead of url — the card draws, the button goes nowhere
  • no colour — falls back to a neutral grey
  • lines as a numeric list instead of label => value — values with no labels
  • name instead of title — a card without a title is skipped, so the whole card disappears, silently. The only one of the four that renders as absence.

WordPress core hooks we use as an interface

plugins_api — guarded on $args->slug. rest_api_init — registers nlsec/v1. Listed so a core hook can be told from an undocumented one of ours. The update filter update_plugins_{$host} is shared by every plugin that updates from the same host, so ours answers only for itself.

Internal only — not a contract

nlsec_hourly_event, nlsec_scan_event, nlsec_ai_briefing_event, nlsec_ai_triage_event, nlsec_digest_event, nlsec_surge_check are our own scheduled hooks. Nothing outside this plugin should hook them and they may be renamed without notice.