Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d512c4fec3 | ||
|
|
6f30fa13e3 |
@@ -1,27 +1,38 @@
|
|||||||
# Nolimit Security — releases
|
# Nolimit Security
|
||||||
|
|
||||||
This repository holds released builds only. The source lives in a private
|

|
||||||
repository; this one exists so a WordPress site can check for and download an
|
|
||||||
update without needing any credentials.
|
A firewall, login protection, country filtering, honeytraps, malware and
|
||||||
|
vulnerability scanning, and an optional AI analyst for WordPress and
|
||||||
|
WooCommerce — with **nothing leaving the site**. No cloud account, no
|
||||||
|
telemetry, no paid tier.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Download `nolimit-security.zip` from the latest release, then in wp-admin:
|
1. Download `nolimit-security.zip` from the [latest release](https://gitea.nolimit.dk/kw/nolimit-security-dist/releases/latest).
|
||||||
**Plugins → Add New → Upload Plugin**.
|
2. **Plugins → Add New → Upload Plugin**, then activate.
|
||||||
|
3. Put your own address on the allow-list before you switch anything to
|
||||||
|
*refuse* — [the setup guide](docs/SETUP.md) says where, and why first.
|
||||||
|
|
||||||
After the first install the plugin updates itself from here — it reads the
|
Later versions arrive in **Dashboard → Updates** like any other plugin.
|
||||||
latest release on this repository and offers it on Dashboard → Updates like any
|
|
||||||
other plugin.
|
|
||||||
|
|
||||||
## What it is
|
## Documentation
|
||||||
|
|
||||||
Firewall, login protection with two-factor, country filtering, honeytraps,
|
- [Overview](docs/README.md) — what it does, and what it deliberately does not
|
||||||
malware and vulnerability scanning, and an AI analyst that watches the site for
|
- [Setting it up, and knowing it works](docs/SETUP.md) — the few settings that
|
||||||
you. It works on any WordPress site and knows about WooCommerce's checkout, My
|
need a decision, how to check each protection is live, and the three ways
|
||||||
Account and store API where they are present.
|
back in if you lock yourself out
|
||||||
|
- [Hooks and integration](docs/CONTRACT.md) — for developers: what the plugin
|
||||||
|
offers to other plugins and what it asks of them
|
||||||
|
|
||||||
No telemetry. Nothing is sent anywhere, and there is no account to create.
|
## More of it
|
||||||
|
|
||||||
Part of the **Nolimit** family, which shares one menu in wp-admin: Nolimit
|
| | |
|
||||||
Security, Nolimit Cache, Nolimit Assistant, Nolimit Discounts and Nolimit
|
|---|---|
|
||||||
Subscriptions.
|
|  | **Firewall.** Refuses requests before WordPress loads, with a watching mode for switching it on in front of a shop that is already taking money. |
|
||||||
|
|  | **Activity.** Every decision it made, with the rule that made it, so "why was this customer blocked?" has an answer. |
|
||||||
|
|  | **Login.** Lockouts that stop a guesser without stopping a customer who forgot a password, and two-factor with recovery codes. |
|
||||||
|
|  | **Scanner.** Core checksums, malware signatures and a vulnerability check, run in the background. |
|
||||||
|
|  | **AI analyst.** Off until you give it a key, and graded from "answers when asked" to "acts on what you have ticked". |
|
||||||
|
|
||||||
|
Requires WordPress 5.8+ and PHP 7.4+. WooCommerce is not required.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,144 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Nolimit Security
|
||||||
|
|
||||||
|
Firewall, login protection, country filtering, honeytraps, malware and
|
||||||
|
vulnerability scanning, and an AI analyst that watches the site for you.
|
||||||
|
|
||||||
|
The point of it is that **nothing leaves the site**. There is no cloud account,
|
||||||
|
no telemetry and no address sent anywhere. The threat lists are compiled
|
||||||
|
locally and matched locally; the AI analyst runs against a key you supply and
|
||||||
|
can be switched off without disabling anything else.
|
||||||
|
|
||||||
|
Current version: **1.4.7**. Requires WordPress 5.8+ and PHP 7.4+. WooCommerce
|
||||||
|
is not required, and where it is installed its checkout, My Account and store
|
||||||
|
API paths are protected too.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Firewall** | Refuses requests before WordPress loads, where it can. Rate limits per address, filters by country, and scans the request body for attack patterns. Verified crawlers are exempt — Googlebot at full speed looks exactly like a scanner to a counter, and falling out of the index is worse than the scan. |
|
||||||
|
| **Login protection** | Lockouts, two-factor with recovery codes, password quality, XML-RPC control. |
|
||||||
|
| **Honeytraps** | Paths no human visits. One request is enough to know. |
|
||||||
|
| **Scanning** | File hashes against WordPress core, signature matching for known malware, and a vulnerability check against installed plugin and theme versions. |
|
||||||
|
| **AI analyst** | Reads the findings and the log and says what it thinks is going on, on a schedule. Optional, off until a key is supplied, and it never acts without being asked unless you have ticked the actions it may take. |
|
||||||
|
|
||||||
|
## What it does not do
|
||||||
|
|
||||||
|
- **No telemetry.** Not a ping, not a count, not an anonymised anything. The
|
||||||
|
only network traffic is fetching public threat lists and vulnerability data,
|
||||||
|
and calling the AI provider if you configured one.
|
||||||
|
- **No cloud account.** There is nothing to sign up for.
|
||||||
|
- **No paid tier.** There is no feature behind a licence check, because there
|
||||||
|
is no licence check.
|
||||||
|
- **It is not translated.** The interface is English, on every site.
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
- **Installing it and knowing it works** — [SETUP.md](SETUP.md)
|
||||||
|
- **Every hook it offers and asks for, for developers** — [CONTRACT.md](CONTRACT.md)
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# Setting it up, and knowing it is working
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Download `nolimit-security.zip` from the latest release in this repository and
|
||||||
|
install it the ordinary way (**Plugins → Add New → Upload Plugin**). Once it is
|
||||||
|
installed, WordPress offers later versions in **Dashboard → Updates** like any
|
||||||
|
other plugin; there is nothing else to configure.
|
||||||
|
|
||||||
|
On activation the plugin creates its tables and schedules its jobs. There is no
|
||||||
|
wizard, and the firewall does nothing you have not looked at first.
|
||||||
|
|
||||||
|
## The settings that actually require a decision
|
||||||
|
|
||||||
|
There are ten groups and over a hundred settings. Almost all of them have a
|
||||||
|
default that is right. These are the ones worth reading before the site takes
|
||||||
|
money.
|
||||||
|
|
||||||
|
### The allow-list — do this first
|
||||||
|
|
||||||
|
**Security → Firewall → Blocks → Never blocked.** Put your own address in it.
|
||||||
|
It is checked **before every other rule**, so it is how you get back in when
|
||||||
|
something goes wrong, and it is the only thing that works when a rule has
|
||||||
|
locked you out of wp-admin. Find your address on the **Overview** screen
|
||||||
|
(*Your connection*) rather than guessing — a site behind Cloudflare does not
|
||||||
|
see what you think it sees.
|
||||||
|
|
||||||
|
An allow-listed address is also never reported as suspicious to other plugins,
|
||||||
|
which is the behaviour you want: a verdict that outlives the allow-list takes
|
||||||
|
back the thing the allow-list is for.
|
||||||
|
|
||||||
|
### Watching mode before refusing
|
||||||
|
|
||||||
|
**Security → Firewall → Rules → What to do when a rule matches.** *Record it
|
||||||
|
and let it through* logs what would have been refused without refusing it. Run
|
||||||
|
a day in watching mode on a shop with real traffic and read **Activity** before
|
||||||
|
switching to *Refuse the request*. The cost of being wrong here is a customer
|
||||||
|
who cannot check out.
|
||||||
|
|
||||||
|
### Rate limiting — 240 requests per 60 seconds
|
||||||
|
|
||||||
|
**Security → Firewall → Rate limiting.** Counted per **address**, not per path.
|
||||||
|
A shared office or a school network is one address as far as this counter is
|
||||||
|
concerned, so raise the limit rather than lower it if a real visitor
|
||||||
|
complains. Verified crawlers are exempt.
|
||||||
|
|
||||||
|
### Countries — off by default, and read the mode twice
|
||||||
|
|
||||||
|
**Security → Firewall → Countries.** *Block* refuses the countries in the list.
|
||||||
|
*Allow* refuses everything **not** in it. The second is the one that locks out
|
||||||
|
a customer on holiday, so decide which question you are answering before you
|
||||||
|
fill in the list. Putting checkout outside the scope is usually right.
|
||||||
|
|
||||||
|
### Failed sign-ins and two-factor
|
||||||
|
|
||||||
|
**Security → Login.** The lockout length is shown to the person who is locked
|
||||||
|
out, so it is written in plain words. Two-factor is enrolled by each user on
|
||||||
|
their own profile; there is deliberately no way to enrol it on somebody else's
|
||||||
|
behalf, because a second factor another person set up is not a second factor.
|
||||||
|
|
||||||
|
### The secrets
|
||||||
|
|
||||||
|
The AI provider key, the alert webhook address and a vulnerability-data key
|
||||||
|
(optional) are kept in a separate, non-autoloaded option, so they are not on
|
||||||
|
the row WordPress reads on every request. The screens show a mask rather than
|
||||||
|
the value, and submitting the mask unchanged leaves the stored key alone.
|
||||||
|
|
||||||
|
None of them is required. With no AI key the analyst is simply absent; with no
|
||||||
|
webhook nothing is posted anywhere.
|
||||||
|
|
||||||
|
## How to tell it is working
|
||||||
|
|
||||||
|
Do not read "no errors". Read these.
|
||||||
|
|
||||||
|
| Check | Where | What a working site shows |
|
||||||
|
|---|---|---|
|
||||||
|
| The firewall is actually running | **Overview → This site's protection** | *Firewall: blocking*, and a named state on every row. |
|
||||||
|
| It is seeing traffic | **Overview → Requests blocked**, and **Activity** | A non-zero number on any site with visitors. **Zero means two things** — a quiet site and a broken counter — so compare it with **Firewall → Blocks**, which is written by a different path. |
|
||||||
|
| Blocking works end to end | **Firewall → Blocks**: add a test address that is not yours, then visit from it | A refusal page, and the block appearing in **Activity**. If the page loads normally, the early file is not installed or a cache is serving around it. |
|
||||||
|
| It runs before the rest of WordPress | **Overview → Runs before other plugins** | *yes*. This is the half that loads first, so it is also the half a cache plugin can quietly replace. Switch it on under **Firewall → Rules**. |
|
||||||
|
| The scanner has run | **Scanner → Findings** | A *Last scanned* timestamp. A scan that has never run and a scan that found nothing both read "no findings", so look for the time. |
|
||||||
|
| The AI analyst is configured | **AI analyst** | A model name it actually fetched from the provider, not just a saved key. A wrong key is stored exactly as happily as a right one. |
|
||||||
|
|
||||||
|
## If you are locked out
|
||||||
|
|
||||||
|
Three ways back in, in the order to try them. They are also written on
|
||||||
|
**Settings → When something has gone wrong**.
|
||||||
|
|
||||||
|
1. **The kill switch.** Add this line to `wp-config.php` over FTP, SSH or a
|
||||||
|
file manager. It switches off every blocking decision and leaves the screens
|
||||||
|
working, so the setting that caused the lockout can be found and changed.
|
||||||
|
Nothing in the database can override it.
|
||||||
|
|
||||||
|
```php
|
||||||
|
define( 'NLSEC_DISABLE', true );
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **WP-CLI.** `wp nlsec` is registered as a command and reaches the same
|
||||||
|
things from the shell:
|
||||||
|
|
||||||
|
```
|
||||||
|
wp nlsec status
|
||||||
|
wp nlsec unblock 203.0.113.9 --allow
|
||||||
|
wp nlsec panic # every blocking feature off at once
|
||||||
|
wp nlsec 2fa reset username # clear a lost second factor
|
||||||
|
wp nlsec verify # do all the rules still compile?
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Settings → Buttons → Remove every automatic block**, once you are back in.
|
||||||
Reference in New Issue
Block a user