Search by

trianity / laravel-ip-analyzer

trianity

Local IP country and ASN analysis with extensible observation-based rules for Laravel.

Package info

github.com/trianity/laravel-ip-analyzer

pkg:composer/trianity/laravel-ip-analyzer

Statistics

Installs: 16

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.3.0 2026-10-04 15:50 UTC

This package is auto-updated.

Last update: 2026-10-04 15:51:38 UTC


README

Documentation for 2.3.0 · Changelog · Magyar quickstart

Local Country and ASN facts with configurable observation rules for Laravel 12–13. PHP 8.4–8.5 is the tested range. The package reads manually installed MaxMind MMDB files. Runtime lookups, ip-data:status, ip-data:verify and about perform no HTTP, DNS, downloads or telemetry. The explicit ip-data:update command, including its read-only --check mode, can contact the configured source over HTTPS.

The package does not make signup decisions, block requests, assign a global risk score or ship an application blacklist. Country/ASN data is not bot evidence; an empty match list does not certify safety.

Installation and use

Requirements: PHP 8.4 or 8.5, Laravel 12 or 13, and the PHP cURL and zlib extensions. For the 2.3 release series:

composer require trianity/laravel-ip-analyzer:^2.3
php artisan vendor:publish --tag=ip-analyzer-config

Releases are available on GitHub and Packagist. For a local source checkout, use the local development installation.

The provider is autodiscovered. Configure local files outside the public web root:

IP_ANALYZER_COUNTRY_DB=/srv/private/ip-data/GeoLite2-Country.mmdb
IP_ANALYZER_ASN_DB=/srv/private/ip-data/GeoLite2-ASN.mmdb

The defaults are under storage/app/ip-analyzer/. Only the configuration file reads environment variables. After changing paths, rebuild your application's configuration cache and reload long-lived workers to pick up configuration changes. Replacing the file at an unchanged path does not require restarting a reader.

use Trianity\IpAnalyzer\Contracts\IpAnalyzer;
use Trianity\IpAnalyzer\Contracts\IpLookup;
use Trianity\IpAnalyzer\Enums\LookupStatus;

$result = app(IpAnalyzer::class)->analyze(request()->ip());
if ($result->facts->countryStatus === LookupStatus::Found) {
    $country = $result->facts->countryCode;
}
$observations = $result->matches; // list<RuleMatch>; no automatic decision
$factsOnly = app(IpLookup::class)->lookup('8.8.8.8');

The application owns trusted-proxy configuration and source-IP selection. Do not pass a raw forwarded-header list. DTOs are readonly and expose no MaxMind objects.

Input policy and source states

Input must be an exact IPv4 or IPv6 address: no hostname, URL, port, whitespace, zone ID or address list. Accepted input is canonicalized with inet_pton/inet_ntop. IPv4-mapped IPv6 is converted to IPv4 before classification and rule matching.

NonPublic is a conservative special-purpose exclusion policy, not a claim that every excluded address is unroutable. No database is opened for InvalidInput or NonPublic. Configuration is still validated. See the exact ranges in IP policy, based on the IANA IPv4 registry and IPv6 registry.

Each source is independent:

Status Meaning
found Requested data exists; only then is its value populated
not_found Address or requested field is absent
unavailable File cannot be read, is corrupt, has an invalid record or wrong type
invalid_input Input is not one literal IP
non_public Address is excluded by the policy

Country reads country.isoCode only, never registered-country fallback. Missing data remains null, never HU, ASN 0 or a safe assessment. ASN organization may be null even when the ASN number exists.

Stable source error codes are file_unreadable, invalid_database, database_type_mismatch, and invalid_record. Public errors do not include paths. Expected SDK data/IO errors become states; programming errors propagate from PHP services. Custom-rule errors also propagate.

Lookup, status and explicit verification accept exactly GeoLite2-Country or GeoIP2-Country for Country and GeoLite2-ASN for ASN. The built-in updater manages the configured GeoLite2-Country and GeoLite2-ASN editions only. A manually installed GeoIP2-Country remains readable and verifiable, but is edition-incompatible with updater freshness state; check reports unknown freshness and a normal update attempts to obtain a validated GeoLite2 replacement. Its build epoch still participates in downgrade protection. City/ISP databases are deliberately not substitutes. Metadata contains databaseType, buildEpoch, ageDays, stale, and futureBuild. Age is clamped to zero for a future build; futureBuild=true makes that clock anomaly explicit. Stale means age strictly greater than max_age_days (default 30); equality is fresh. The threshold must be a non-negative integer whose conversion to seconds fits a PHP integer. Stale data remains readable.

Readers exist only within a source operation, with close in finally. Status reads metadata without probing an IP. New operations reopen the files, so an atomic replacement is visible even to a retained lookup instance. There is no persistent reader, Redis cache or per-lookup full-file hash. A concurrent replacement of the two files is not a transaction across both sources.

Observation rules

In config/ip-analyzer.php:

'rules' => [
    'ip_cidr' => [
        [
            'id' => 'observed-test-network',
            'value' => '192.0.2.0/24',
            'reason_code' => 'operator_observation',
            'severity' => 'warning',
            'message' => 'Matches an operator-maintained observation.',
        ],
    ],
    'asn' => [],
    'country' => [],
],
'custom_rules' => [
    App\IpRules\ObservedNetworkRule::class,
],

All three groups use the same five required fields. value is a literal IP or CIDR, a positive integer ASN, or a two-letter country code (normalized uppercase). Severity is info, warning or high. IDs must be non-empty and unique across all configured and custom rules. Malformed entries, unknown fields/groups, duplicates and invalid custom classes raise InvalidArgumentException.

Order is IP/CIDR, ASN, country, then custom class list order. The lookup runs once; every rule is evaluated and all matches are retained. ASN/country rules require the corresponding Found status. IP rules also apply to normalized NonPublic addresses, but never invalid input. CIDR compares binary prefixes within one address family, including /0, /32 and /128. Host bits in the configured CIDR are ignored. Mapped single IP rules normalize to IPv4; mapped IPv6 CIDRs are rejected as ambiguous—use their IPv4 CIDR equivalent.

Custom classes implement Contracts\Rule, with id(): string and evaluate(IpFacts $facts): ?RuleMatch. The Laravel container resolves constructor dependencies. Null means no match. A match must use the rule's ID. See the custom-rule example. Keep custom rules local and side-effect free; the package cannot enforce arbitrary application code. Config contains class names, not closures or instantiated rules, so it can be cached.

Commands

php artisan ip-data:lookup 8.8.8.8
php artisan ip-data:lookup 2606:4700:4700::1111 --json
php artisan ip-data:status --json
php artisan ip-data:verify --database=country --json
php artisan about

Human output includes localized status/error labels and an indented structured result; --json emits one compact parseable document without decoration. Lookup includes facts, source states/metadata/errors and matches. Status reports each source's readability/structural metadata status, without opening records or making a geographic probe. Found in status means metadata was read successfully; it is not an exhaustive integrity scan of every record. ip-data:verify performs that full offline traversal and SHA-256 calculation for both configured files by default, or for the requested --database selections. Both configured sources are required for command availability.

Exit Lookup Status Verify
0 Usable check; includes NotFound, NonPublic, stale data and rule matches Both sources readable, right type, not stale Every selected database verified
1 At least one source Unavailable At least one source Unavailable or stale Failure, or busy mixed with a successful verification
2 Invalid input/configuration or rule failure Invalid configuration or unexpected failure Invalid database selection/configuration
3 — — Every selected operation was busy; no verification completed
130 — — Cooperative Ctrl-C interruption

At the lookup/status CLI boundary failures are sanitized to {"error":"invalid_configuration_or_rule"} and exit 2; exception details remain available to callers of the PHP services. Verify uses the update-domain errors documented in the updater guide. A future-build flag alone does not change the lookup/status exit code. Lookup, status and verify never download data or create directories/databases. about reads the installed Composer version, with a root-package/development fallback, and does not open MMDB files.

Database downloads and updates

Manual MMDB installation still works without credentials. For the built-in updater, obtain the following from your MaxMind account:

MaxMind setting Where to obtain it Host application setting
AccountID Account Information (instructions) .env: IP_ANALYZER_MAXMIND_ACCOUNT_ID; config: ip-analyzer.update.account_id
LicenseKey License Keys, create a key (instructions) .env: IP_ANALYZER_MAXMIND_LICENSE_KEY; config: ip-analyzer.update.license_key
EditionIDs Download Databases, available database editions and Get Permalink(s) Fixed mapping: country → GeoLite2-Country, asn → GeoLite2-ASN

Use a License Key, not your account login password. Put the values in the consuming Laravel application's .env, not in the package/vendor directory:

IP_ANALYZER_MAXMIND_ACCOUNT_ID=YOUR_ACCOUNT_ID
IP_ANALYZER_MAXMIND_LICENSE_KEY=YOUR_LICENSE_KEY
IP_ANALYZER_UPDATE_SCHEDULE=false
IP_ANALYZER_VALIDATION_WORKERS=1
IP_ANALYZER_VALIDATION_WORKER_TIMEOUT=1800

The host application's config/ip-analyzer.php maps these environment variables into the update section. Publish it with php artisan vendor:publish --tag=ip-analyzer-config if it does not exist; retain existing custom rules when upgrading. See the config example.

MaxMind's GeoIP.conf is for the separate geoipupdate program. This package neither reads that file nor requires that program: copy the AccountID/LicenseKey values into the settings above. There is no EditionIDs environment variable; the updater supports Country and ASN, both selected by default or individually with --database=country / --database=asn. GeoLite2-City from a MaxMind sample is not supported by this package.

php artisan ip-data:update --check --json
php artisan ip-data:verify --workers=2
php artisan ip-data:update --workers=2
php artisan ip-data:update --database=country
php artisan ip-data:update --force --json

The first normal update invocation downloads missing files. Later invocations use lightweight local metadata, persisted installation state and HEAD to avoid unnecessary GETs. Prepared Country and ASN candidates can be validated concurrently, then are renamed atomically per file. Older build epochs are rejected even with --force. --check performs only lightweight metadata reads and HEAD, without record traversal, full-file hashing, download, installation or state writes. Use ip-data:verify for a full offline integrity scan of the installed files.

Downloads use HTTPS, origin-scoped Basic Auth and a checked redirect allowlist. Successful lookups, provider boot, status/about and Composer installation never start downloads. Long Retry-After responses persist a cooldown for normal update runs; --force cannot bypass it.

See the updater guide for source URLs, configuration limits, scheduler opt-in, status/exit codes, credential handling, notices, recovery and filesystem/platform constraints. Upgrading from 1.x does not require overwriting a published config: new update options receive defaults. Add credentials to the environment used when building the config cache; republishing with --force would overwrite your custom rules and paths.

Update and verification progress

Human update and verification output shows Country/ASN, the current phase, measured work and elapsed time. A complete integrity scan can take several minutes; the lightweight --check no longer performs that scan.

php artisan ip-data:update --check               # default human progress
php artisan ip-data:verify --workers=2           # full installed-file verification
php artisan ip-data:update --no-progress        # final result and total duration only
php artisan ip-data:update --json               # one final JSON document on STDOUT
php artisan ip-data:update --json --progress     # progress and duration on STDERR

--quiet suppresses all output, including explicit --progress. --no-progress takes precedence over --progress. Since 2.2.1, an interactive ANSI terminal keeps one stable row per selected database in Country/ASN order. The compact block refreshes at most once per second, except that phase transitions, completion, failure and cancellation are immediate. Rows are truncated to the detected terminal width and completed rows remain visible while other work runs. Capability detection uses the stream that receives progress, including STDERR for --json --progress; the whole terminal is never cleared.

Redirected output, CI, unsupported output implementations and --no-ansi use plain lines without cursor controls. Periodic events are limited to one line every 15 seconds per database; initial state, phase changes and terminal states remain immediate. Human output includes a final total duration. The 2.2.1 rendering change did not alter the then-current JSON result schema; 2.3 adds the documented freshness, local-health and integrity fields and the separate verification result. Use --json for machine parsing.

MMDB validation reports the actual number of processed CIDR ranges. Real database traversal proved that metadata nodeCount + 1 is not a valid total for the SDK's CIDR iterations: the counter can exceed that value. Since 2.2.1 validation is therefore deliberately indeterminate, showing the processed count without a percentage or ETA. It does not perform a second full scan merely to obtain a total.

Hashing and downloads use bytes when total size is known. Missing Content-Length means no download percentage or ETA. Phase ETA uses a monotonic clock and smoothed speed, starts only after at least one second and two samples, and becomes unknown during a stall. It is not an estimate for the whole command. Since 2.2.1, elapsed time has whole-second precision and ETA is shown as approximate rounded seconds below one minute or rounded minutes thereafter. The 2.2.1 precision change did not alter numeric progress snapshots. Progress collection remains independent of rendering and does not add sleeps to update supervision. --check never shows download, extraction or installation as performed phases.

Where optional PHP PCNTL signal handling is available, Ctrl-C requests cooperative cancellation, reports interruption and exits 130. Readers, staging files and owned locks are released at safe checkpoints. An in-flight blocking call may delay cancellation until it returns or reaches a callback/timeout. Installation already in its atomic rename/state section is completed before cancellation is observed; a previously completed database is not rolled back. Inspect status and rerun to reconcile an interrupted command; its JSON error does not claim overall success. No cleanup guarantee is made for SIGKILL. PCNTL is not a package requirement.

See 2.1 verification, the 2.1.2 ETA verification, and the 2.2.1 compact-progress verification.

Freshness and parallel validation (2.3)

Freshness and integrity are separate operations:

php artisan ip-data:update --check
php artisan ip-data:verify --workers=2
php artisan ip-data:update --workers=2

The check reads basic MMDB metadata and existing update state, then performs HEAD. It reports freshness_unknown when compatible state or comparable remote metadata is unavailable. up_to_date means no remote change is known; neither status claims that the installed file passed a full integrity scan. Local metadata health is reported separately. A normal update conservatively downloads a replacement when freshness is unknown or local metadata is unusable.

Priority is the CLI workers option over ip-analyzer.validation.workers, whose default is 1. Effective concurrency is bounded by available tasks: Country + ASN verification or two prepared update candidates can use at most two workers. One task always runs directly in the main process; a value of 1 starts no subprocess. There is no CPU-count autodetection.

Each worker performs one complete MMDB traversal and hash using the same validator as sequential execution. For verify it reads installed files; for update it reads only prepared candidates, never unchanged or soon-to-be-replaced installed files. Workers cannot perform HTTP, extraction, installation or state writes. The main process retains locks, checks that the validated path was not replaced, and preserves deterministic requested result order. Worker IPC is bounded internal NDJSON and is not part of the public JSON schema; credentials are never passed to workers.

The ip-analyzer.validation.worker_timeout setting defaults to 1800 seconds. If proc_open, a readable Composer autoloader or a usable PHP CLI executable is unavailable before startup, the command reports a localized notice in progress mode and runs sequentially. A started worker crash, invalid protocol or timeout is a failure and is not silently retried. Supported Ctrl-C handling stops active children before returning exit 130; portable signal handling and SIGKILL cleanup are not promised. Subprocess termination and file-identity protection are verified on local Linux filesystems; Windows and network/distributed filesystems are not certified by this release.

Two workers may roughly double validation memory use and increase storage I/O. Speedup depends on CPU, filesystem/cache behavior and MMDB sizes and is not guaranteed. Download, HEAD, extraction, installation and state writes remain non-parallel. Rebuild Laravel's config cache after changing either environment setting. See the current updater guide for the complete 2.3 contract and 2.2 verification for the worker foundation.

Language and application overrides (2.1.1)

Package-owned human output supports English (en) and Hungarian (hu). Every rendering uses the application's current Laravel translator locale, including locale changes within the same application instance. The package never changes app.locale, app.fallback_locale or the translator's settings.

Resolution is per key: current locale (including application overrides), then explicit English (including English application overrides). The host fallback locale is not used for package messages. There is no regional-locale mapping: hu_HU and en_GB fall back to English unless the application supplies those exact locale's package translations.

Translations work immediately, without publishing. Optional publication:

php artisan vendor:publish --tag=ip-analyzer-translations

The destination is the host application's lang_path('vendor/ip-analyzer'). For a partial Hungarian override, create lang/vendor/ip-analyzer/hu/messages.php (under your configured language path):

<?php

return [
    'progress' => [
        'start' => 'Az adatbázisok ellenőrzése elindult; ez több percig tarthat.',
    ],
];

Omitted keys retain package translations. An equivalent en/messages.php override also applies when English is selected as fallback. Preserve the placeholders of the overridden key; for example progress.elapsed uses :elapsed and progress.summary uses :status and :elapsed. Their over-60-second counterparts are progress.elapsed_minutes (:minutes, :seconds) and progress.summary_minutes (:status, :minutes, :seconds). The compact ETA keys use :remaining for seconds and :minutes for minutes; wait keys retain the duration placeholders. Counted CIDR-range/byte messages use Laravel pluralization. Keep overrides in the host application, not in vendor/.

Progress, summaries, status labels, sanitized errors/warnings and package command descriptions are localized. Human results include localized source labels followed by the structured result; machine codes and custom rule messages remain intact. Framework-owned help text is left to Laravel/Symfony. There are no package tables or table headers to translate.

--json output is locale-independent, including existing message fields. With --json --progress, only STDERR human progress is localized; STDOUT remains exactly one JSON document. The localization layer does not itself change quiet, no-progress, timing, validation, installation or cancellation behavior; later releases may add documented result fields or workflow changes. See the 2.1.1 verification record for test results.

Manual data maintenance

Obtain Country and ASN databases manually from MaxMind, under the applicable terms. The package does not supply production databases or credentials.

  1. Download and extract in a private staging directory on the same filesystem as the destination. Keep credentials out of the application repository.
  2. Verify the source/checksum using the vendor's distribution information. Check file ownership and read permissions for the PHP worker account.
  3. Validate candidate metadata with the local SDK: supported database type, acceptable build date and no future-clock anomaly. In an isolated application process, point the two config paths at staged files and run ip-data:status --json. Ensure cached config is not still pointing at the installed files.
  4. Run ip-data:verify --json in that isolated process for a full traversal and SHA-256 calculation. Status validates metadata only; it does not inspect every reachable range. Do not overwrite/truncate a live MMDB in place.
  5. Rename each validated candidate over its destination atomically on that same filesystem. Retain a rollback copy under the applicable data terms, and run status with the application's normal config again.

An operator-controlled atomic rename preserves active readers and lets the next operation see the replacement. The built-in updater automates bounded download, archive extraction, exact downloaded-edition validation, full candidate verification and per-file atomic replacement without changing offline lookup behavior. It does not fetch vendor checksum files or authenticate the producer; source/checksum verification remains an operator responsibility for manual installation.

Development and compatibility

Run development checks from a Git source checkout. Distribution archives exclude the tests, fixtures and development configuration.

composer install
composer validate --strict
composer test
vendor/bin/pint --test
vendor/bin/phpstan analyse

The tests are Pest functions, including the original provider tests, with the Pest Laravel plugin and Orchestra Testbench. Synthetic MMDB fixtures are committed; tests never download data. Their original generator, license and records are in fixture documentation in the source repository. See V2 verification notes for RED/GREEN evidence and actual versions.

The CI workflow covers PHP 8.4/8.5 with Laravel 12 (Testbench 10, Pest 4, PHPUnit 12) and Laravel 13 (Testbench 11, Pest 5, PHPUnit 13), performs Composer validation and PHP lint, then runs Pest with networking entry points disabled. PHP 8.3 is deliberately outside this package's existing ^8.4 requirement and Pest 4/5 test baseline, even though Laravel 13 itself supports PHP 8.3. No Laravel/PHP version outside the tested matrix is claimed here.

Local development installation

Add the local directory as a path repository in the consuming Laravel application:

{
    "repositories": [
        {"type": "path", "url": "../packages/laravel-ip-analyzer"}
    ]
}

Then install the checkout and publish its configuration:

composer require trianity/laravel-ip-analyzer:@dev
php artisan vendor:publish --tag=ip-analyzer-config

Package versions come from Git tags; composer.json deliberately has no version field. See 2.0 verification for the local implementation checks and known limits. The 1.0 verification remains in the historical release record.

Licenses and limits

MIT applies to this project's code and its original synthetic fixtures. MaxMind SDK dependencies and downloaded databases have separate licenses; see THIRD-PARTY-NOTICES.md. The age threshold is an operational warning, not a guarantee of license compliance or data accuracy. Database acquisition, update/deletion obligations and attribution remain the operator's responsibility under the applicable terms.

Magyar quickstart.