PHPackages                             joby/smol-sentry - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. joby/smol-sentry

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

joby/smol-sentry
================

A conceptually simple security logging and rule-enforcing system. Supports ingesting a wide variety of signals and using them to generate bot challenges and bans.

v3.1.1(1mo ago)01701MITPHPPHP &gt;=8.2CI passing

Since Mar 24Pushed 1mo agoCompare

[ Source](https://github.com/joby-lol/smol-sentry)[ Packagist](https://packagist.org/packages/joby/smol-sentry)[ RSS](/packages/joby-smol-sentry/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (18)Versions (17)Used By (1)

smolSentry
==========

[](#smolsentry)

A lightweight IP-based threat detection and mitigation library for PHP 8.2+.

Installation
------------

[](#installation)

```
composer require joby-lol/smol-sentry
```

About
-----

[](#about)

smolSentry logs suspicious activity by IP address, evaluates configurable rules, and throws exceptions when a client should be challenged or banned. It is deliberately as self-contained as possible — all state is stored in a SQLite database, which makes it easy to set up and run on nearly any server.

Basic Usage
-----------

[](#basic-usage)

```
use Joby\Smol\Query\DB;
use Joby\Smol\Sentry\Sentry;
use Joby\Smol\Sentry\BannedException;
use Joby\Smol\Sentry\ChallengedException;

$db = new DB('/path/to/sentry.db');
$sentry = Sentry::default($db);

// Run database migrations to initialize/update DB
$sentry->migrateDB();

// At the top of every request — throws if the client is banned or challenged
$sentry->resolve();

// When something suspicious happens — throws if it pushes the client over a threshold
$sentry->signal('dangerous_url', Severity::Malicious);
```

Both `resolve()` and `signal()` throw `BannedException` or `ChallengedException` when action is required. A banned client should receive a **403 Forbidden**. A challenged client should be redirected (**303 See Other**) to a CAPTCHA or similar challenge page, or served one inline with a **200**.

Releasing bans/challenges
-------------------------

[](#releasing-banschallenges)

```
// ... do some sort of bot challenge

// after passing, release all current challenge verdicts
$sentry->release();
```

Inspector
---------

[](#inspector)

The Inspector automatically detects common attack patterns in raw HTTP requests and fires signals into Sentry. It works directly with PHP superglobals — no framework or dependencies required.

```
use Joby\Smol\Sentry\Inspector;

// instantiate an Inspector linked to your Sentry and set up the default rules
$inspector = new Inspector($sentry);
$inspector->addDefaultRules();

// run inspections, ideally run this immediately after $sentry->resolve()
$inspector->inspect();
```

That's it. The Inspector examines the current request, and if it finds something malicious or suspicious, it fires the appropriate signal into Sentry, which may throw `BannedException` or `ChallengedException` depending on your rules.

### Default Inspection Rules

[](#default-inspection-rules)

`addDefaultRules()` enables three checks:

- **PathManipulation**: detects control characters, dotfile/dotdirectory access, path traversal in parameters, glob patterns, over-encoding, and abnormally long URLs. Most of these fire malicious signals; glob patterns and long URLs fire suspicious signals.
- **RestrictedFiles**: matches request URLs against known sensitive file paths (`.env`, `.git/`, `.aws/`, `wp-config.php`, AI tool config directories, etc.) using pattern data from the OWASP Core Rule Set. Matches in the URL path are malicious; matches in GET/POST/cookie values are suspicious.
- **MaliciousUserAgents**: matches the User-Agent header against known vulnerability scanner signatures (Nikto, SQLMap, Nessus, etc.) using OWASP CRS data. Any match is malicious.

### Custom Rules

[](#custom-rules)

You can add or replace individual inspection rules:

```
$inspector->addRule('restricted_files', new RestrictedFiles());
$inspector->addRule('my_custom_check', new MyCustomCheck());
```

Rules implement `InspectionRule` and return a `Severity` or `null`:

```
use Joby\Smol\Sentry\InspectionRules\InspectionRule;
use Joby\Smol\Sentry\InspectionRules\RequestData;
use Joby\Smol\Sentry\Severity;

class MyCustomCheck implements InspectionRule
{
    public function check(RequestData $request): Severity|null
    {
        // return Severity::Malicious, Severity::Suspicious, or null
    }
}
```

### Testing

[](#testing)

For testing, pass raw superglobal data directly instead of reading from the live request:

```
$inspector->inspect(
    SERVER: ['REQUEST_URI' => '/.env', 'HTTP_USER_AGENT' => 'curl/8.0'],
    GET: [], POST: [], FILES: [], COOKIE: []
);
```

Default Rules
-------------

[](#default-rules)

`Sentry::default()` configures a reasonable starting point:

- One malicious signal: immediate ban (5 minutes base, ramps up exponentially for repeat offenders)
- 5 suspicious signals in 10 minutes: challenge
- 20 suspicious signals in an hour: ban

Optionally also configured with AbuseIPDB lookups:

```
$sentry = Sentry::default($db, abuseipdb_key: 'your-api-key');
```

Signals
-------

[](#signals)

Signals are the core input — they record that something suspicious or malicious happened during a request from a given IP.

```
// Use the current request IP automatically
$sentry->signal('login_failure');
$sentry->signal('dangerous_url', Severity::Malicious);

// Or specify an IP explicitly
$sentry->signal('login_failure', Severity::Suspicious, '1.2.3.4');

// Log without throwing (useful for logging-only contexts)
$sentry->signal('login_failure', silent: true);

// Log without evaluating rules
$sentry->signal('login_failure', skip_rules: true);
```

Severity has two levels:

- `Severity::Suspicious` — accumulates toward rule thresholds
- `Severity::Malicious` — typically triggers an immediate ban

Signal types are arbitrary strings. Use whatever naming convention makes sense for your application.

Rules
-----

[](#rules)

Rules define when accumulated signals should result in a ban or challenge. They are evaluated whenever a new signal is logged.

```
use Joby\Smol\Sentry\Rule;
use Joby\Smol\Sentry\Outcome;
use Joby\Smol\Sentry\Severity;

$sentry->addRule(new Rule(
    outcome: Outcome::Ban,
    threshold: 5,           // number of signals required to trigger
    search_window: 3600,    // time window to search in seconds
    outcome_duration: 600,  // base duration of the outcome in seconds
    signal_type: 'login_failure', // null to match any type
    severity: Severity::Suspicious, // null to match any severity
));
```

When multiple rules trigger simultaneously, the worst outcome wins — bans beat challenges, and longer durations beat shorter ones. Only one verdict row is written per evaluation.

### Ban Ramp-Up

[](#ban-ramp-up)

Ban durations increase exponentially for repeat offenders. A client banned multiple times within the ramp-up window receives progressively longer bans, up to a configurable maximum.

```
$sentry = new Sentry(
    $db,
    ban_ramp_up_window: 86400 * 30, // look back 30 days for prior bans
    ban_ramp_up_rate: 1.5,           // multiply duration by 1.5x per prior ban
    ban_max_duration: 86400 * 30,    // cap at 30 days
);
```

Reputation Sources
------------------

[](#reputation-sources)

External reputation data can be checked for IPs with no local verdict. Reputation sources are consulted during `resolve()`, after local verdicts have been checked. Results are written as local verdicts so subsequent calls are fast.

```
use Joby\Smol\Sentry\AbuseIpDb;

$abuseipdb = new AbuseIpDb($db, api_key: 'your-api-key');
$abuseipdb->migrateDB();

$sentry->addReputationSource($abuseipdb);
```

External reputation data may also be used "reactively" if you want to, for example, conserve your AbuseIPDB API calls. In this mode a window in seconds can be specified as the second argument to `addReputationSource()` to only run that reputation source against IPs that have triggered some signal in that time window.

### AbuseIPDB

[](#abuseipdb)

The bundled `AbuseIpDb` source checks individual IPs and their /24 (IPv4) or /48 (IPv6) blocks. Results are cached locally to preserve API quota. Default values for optional parameters are designed to be appropriate for low-to-moderate traffic sites on free webmaster accounts.

```
$abuseipdb = new AbuseIpDb(
    db: $db,
    api_key: 'your-api-key'
);
```

Custom reputation sources can be added by implementing `ReputationSourceInterface`.

Return `Outcome::Ban`, `Outcome::Challenge`, or `null` to pass. The source's class name is used as the verdict reason, which allows verdicts to be automatically released when a re-check returns a clean score.

IP Normalization
----------------

[](#ip-normalization)

All IP addresses are normalized before storage:

- IPv4-mapped IPv6 addresses (`::ffff:1.2.3.4`) are normalized to plain IPv4
- IPv6 addresses are masked to their /64 block (last 64 bits zeroed) — a bot rotating through addresses in the same /64 block is treated as a single client

Database Setup
--------------

[](#database-setup)

smolSentry stores all state in SQLite. Call `migrateDB()` before first use. This will also call migrations on any reputation sources that need it.

```
$sentry->migrateDB();
```

Because all state is in a single database file, multiple applications on the same server can share a smolSentry database — signals and verdicts from one site count toward thresholds on all others.

Database Maintenance
--------------------

[](#database-maintenance)

Periodic database maintenance can be run by calling `cleanupDb()`. This will clear out old records and free up space for efficiency. This will also call maintenace calls on any reputation sources that need it. Ideally this should be called automatically in the background at least roughly once per day.

```
$sentry->cleanupDb();
```

Exception Handling
------------------

[](#exception-handling)

```
try {
    // always run $sentry->resolve() as early as possible
    $sentry->resolve();
    // your app logic goes here, including additional signals which may throw BannedExceptions or ChallengedExceptions
} catch (BannedException $e) {
    http_response_code(403);
    exit;
} catch (ChallengedException $e) {
    header('Location: /challenge');
    exit;
}
```

Requirements
------------

[](#requirements)

Fully tested on PHP 8.3+, static analysis for PHP 8.2+. Requires the `pdo_sqlite` and `sqlite3` PHP extensions (both enabled by default in most PHP installations). Depends on [smolQuery](https://github.com/joby-lol/smol-query).

License
-------

[](#license)

smolSentry is licensed under the MIT License. See [LICENSE](LICENSE) file for details.

### Third-party licensing

[](#third-party-licensing)

Inspector pattern data files in `src/InspectionRules/owasp-crs` are derived from the [OWASP CRS](https://github.com/coreruleset/coreruleset) under Apache Software License (ASL) version 2. Please see [LICENSE-CRS](LICENSE-CRS) for full details.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance88

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

16

Last Release

59d ago

Major Versions

v1.4.1 → v2.0.02026-06-02

v2.0.0 → v3.0.02026-06-03

PHP version history (2 changes)v1.0.0PHP &gt;=8.1

v1.3.0PHP &gt;=8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/f4e606aa41ebfd7d82c365c6dc4ec158fb1b0bfff5f8dcbfad4ab53e3092e6a6?d=identicon)[jobyone](/maintainers/jobyone)

---

Top Contributors

[![joby-lol](https://avatars.githubusercontent.com/u/856610?v=4)](https://github.com/joby-lol "joby-lol (5 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/joby-smol-sentry/health.svg)

```
[![Health](https://phpackages.com/badges/joby-smol-sentry/health.svg)](https://phpackages.com/packages/joby-smol-sentry)
```

###  Alternatives

[psr/log

Common interface for logging libraries

10.4k1.2B12.5k](/packages/psr-log)[open-telemetry/api

API for OpenTelemetry PHP.

2044.7M332](/packages/open-telemetry-api)[open-telemetry/sdk

SDK for OpenTelemetry PHP.

2430.8M413](/packages/open-telemetry-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
