8.1 KiB
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_filterson anlsec_name. Yours to hook. - Answers —
add_filteron a name a consumer applies. We supply the value. - Asks —
apply_filterson awpmgr_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
nullas clean. - Do not act on
severitywithout also handlingexempt. - 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:
hrefinstead ofurl— the card draws, the button goes nowhere- no
colour— falls back to a neutral grey linesas a numeric list instead oflabel => value— values with no labelsnameinstead oftitle— a card without atitleis 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.